#include #include #include "FormatTime.h" #include "ConfigWindow.h" #include "../shared/MiscSupport.h" #include "../shared/ReplyToClient.h" #include "../shared/XmlSupport.h" #include "../shared/LogFile.h" #include "UserInfo.h" #include "Types.h" #include "AlertConfig.h" #include "TopListConfig.h" #include "../shared/GlobalConfigFile.h" #include "CloudSupport.h" #include "MiscRODatabase.h" /* SymbolLists.C and MiscRODatabase.C each handle a lot of miscelaneous * requests. SymbolLists.C gives access to the master database, and all * requests are subject to the tar pit. MiscRODatabase.C only has access to * a read only database, so it doesn't need the tar pit. * * WARNING: This file is partially obsolete. The newest 4.x clients now * point to ../misc_ms/MiscRODatabase.C instead of this. We will probably * keep some version of this around in ax_alert_server forever, for older * clients. But don't add any new features here. If you have an emergency * bug fix, you probably need to make it in both places. */ static void sendNullResponse(ExternalRequest *request) { ExternalRequest::MessageId messageId = request->getResponseMessageId(); if (messageId.present()) { addToOutputQueue(request->getSocketInfo(), XmlNode().asString("API"), messageId); } } enum { mtQuit, mtEditConfig, /* Populate an old config window. */ mtEditConfigNew, /* Populate a new alert config window. */ mtGeneralInfo, /* A description of alerts and filters. */ mtAllAlertTypes, /* A list of all alert types. */ mtDisconnect, /* The client is asking us to close the socket. Only used * for testing. Normally the client will close the * connection, unless the server detects an error. */ mtEcho, /* Used for testing. Outputs anything the client requests. Can * output large or multiple messages from a single small request, * depeing on settings. */ mtAllAlertInfo, /* This was a request from E*TRADE. Given an alert * id we return the values of all corresponding filters. * It's a little like the filters section of * http://www.trade-ideas.com/StockInfo/_StocksLikeThis.html */ mtCloudList, /* Moved to ../misc_ms/MiscNFS.[Ch] */ mtCloudLoad, /* Moved to ../misc_ms/MiscNFS.[Ch] */ mtCloudImport, /* Moved to ../misc_ms/MiscNFS.[Ch] */ mtGetProfile, /* Grab company profile for detail view window */ mtTopListConfig, mtQuickSymbolListInfo /* Aimed at Scottrade. "Quick" is the key word. */ }; ///////////////////////////////////////////////////////////////////// // GetProfileRequest ///////////////////////////////////////////////////////////////////// class GetProfileRequest : public MiscRODatabaseHandler::Deferred { private: const std::string _symbol; public: GetProfileRequest(ExternalRequest *original) : MiscRODatabaseHandler::Deferred(userInfoGetInfo(original->getSocketInfo()).userId, original), _symbol(original->getProperty("symbol")) { } void doRequest(DatabaseWithRetry &database); }; void GetProfileRequest::doRequest(DatabaseWithRetry &database) { std::string sql = "SELECT summary " "FROM company_profile " "WHERE symbol=\"" + mysqlEscapeString(_symbol) + "\" ORDER BY protected DESC LIMIT 1"; MysqlResultRef result = database.tryQueryUntilSuccess(sql); XmlNode response; XmlNode &profile = response["PROFILE"]; profile.useCdata = true; profile.properties["SYMBOL"] = _symbol; profile.text = result->getStringField(0); addToOutputQueue(getSocketInfo(), response.asString("API"), _returnMessageId); } ///////////////////////////////////////////////////////////////////// // TopListConfigRequest ///////////////////////////////////////////////////////////////////// class TopListConfigRequest : public MiscRODatabaseHandler::Deferred { private: const std::string _settings; const std::string _language; const std::string _whichSamples; const bool _easternTime; const bool _allowSymbolListFolders; const bool _skipStrategies; static void addTimeFrame(XmlNode &strategy, std::string const &html_help); XmlNode &addStrategy(UserId userId, DatabaseWithRetry &database, XmlNode &parent, std::string settings, std::string icon = "!", std::string description = "", std::string name = "", std::string htmlHelp = "") const; void addPrimaryStrategies(XmlNode &parent, std::string whichSamples, DatabaseWithRetry &database) const; void addPrimaryStrategyList(XmlNode &parent, std::string listName, UserId userId, DatabaseWithRetry &database, std::set< std::string > &added) const; typedef std::pair< time_t, std::string > Item; void addOneRecentStrategy(DatabaseWithRetry &database, XmlNode &parent, Item const &item) const; typedef std::set< Item > SimilarItems; typedef std::map< std::string, SimilarItems > AllItems; struct SimilarItemsWithName { std::string name; SimilarItems items; }; typedef std::multimap< int, SimilarItemsWithName > AllItemsByAge; void addRecentSettings(XmlNode &parent, DatabaseWithRetry &database) const; void addCurrentSettings(XmlNode &node, DatabaseWithRetry &database) const; public: TopListConfigRequest(ExternalRequest *original) : MiscRODatabaseHandler::Deferred(userInfoGetInfo(original->getSocketInfo()).userId, original), _settings(original->getProperty("settings")), _language(original->getProperty("language")), _whichSamples(original->getProperty("which_samples")), _easternTime(userInfoUseEasternTime(original->getSocketInfo())), _allowSymbolListFolders(original->getProperty("symbol_list_folders") == "1"), _skipStrategies(original->getProperty("skip_strategies") == "1") { } void doRequest(DatabaseWithRetry &database); }; void TopListConfigRequest::addPrimaryStrategies(XmlNode &parent, std::string whichSamples, DatabaseWithRetry &database ) const { // This is mostly copied from ConfigWindow.C. We look at tables with // different names to file the top list items, but the tables have the // same structure and purpose. // // This table exists mostly to allow us some control over what is exported. // a person cannot ask for a part of an existing tree. They can only // take the trees that we export. // // We start with the white_label. MysqlResultRef result = database.tryQueryUntilSuccess("SELECT list_name " "FROM top_level_strategies_tl, users " "WHERE id=" + ntoa(_userId) + " " "AND source='white_label' " "AND external_name=wl_include"); if (result->fieldIsEmpty(0) && !whichSamples.empty()) { // If we couldn't find anything using the whitelabel, we wil see if the // client requested anything. This is used by the new E*TRADE client. // We wanted to seperate the new E*TRADE client from the old E*TRADE // client because the old client is so brittle. Sometimes changing a // strategy would cause their entire program to crash! result = database.tryQueryUntilSuccess("SELECT list_name " "FROM top_level_strategies_tl " "WHERE external_name ='" + mysqlEscapeString(whichSamples) + "' " + "AND source = 'client_request'"); } if (result->fieldIsEmpty(0)) { // If if there is still a problem, then we try the default result = database.tryQueryUntilSuccess("SELECT list_name " "FROM top_level_strategies_tl " "WHERE external_name ='' " "AND source = 'client_request'"); } if (!result->fieldIsEmpty(0)) { // It's possible that there is a problem still. This would be a server // problem. In that case we display nothing, but we don't crash. std::set< std::string > uniqueList; addPrimaryStrategyList(parent, result->getStringField(0), _userId, database, uniqueList); } } void TopListConfigRequest::addPrimaryStrategyList(XmlNode &parent, std::string listName, UserId userId, DatabaseWithRetry &database, std::set< std::string > &added ) const { if (!added.insert(listName).second) { // This node has already been added to the tree. To avoid infinite // recursion, we do not allow the same node to be added more than once. // The first insertion will act as expected, and all future attempts will // be ingored. return; } MysqlResultRef result = database.tryQueryUntilSuccess("SELECT text_help, " "html_help, " "settings, " "name, " "icon, " "user_must_modify, " "sub_list " "FROM strategies_tl " "WHERE list_name='" + mysqlEscapeString(listName) + "' ORDER BY id"); while (result->rowIsValid()) { if (result->fieldIsEmpty("sub_list")) { // This is an individual strategy. XmlNode &strategy = addStrategy(userId, database, parent, result->getStringField("settings"), result->getStringField("icon"), result->getStringField("text_help"), result->getStringField("name"), result->getStringField("html_help")); if (result->getStringField("user_must_modify") == "Y") { strategy.properties["USER_MUST_MODIFY"] = "1"; } } else { // This is a list of strategies. XmlNode &node = parent[-1]; node.name = "FOLDER"; node.properties["NAME"] = result->getStringField("name"); node.properties["DESCRIPTION"] = result->getStringField("text_help"); addPrimaryStrategyList(node, result->getStringField("sub_list"), userId, database, added); } result->nextRow(); } } void TopListConfigRequest::addOneRecentStrategy(DatabaseWithRetry &database, XmlNode &parent, Item const &item) const { int seconds = item.first; std::string const &settings = item.second; std::string description; if (seconds < 2) { description = "within the last second."; } else if (seconds < 100) { description = ntoa(seconds) + " seconds ago."; } else { int minutes = (seconds + 30) / 60; if (minutes < 100) { description = ntoa(minutes) + " minutes ago."; } else { int hours = (seconds + 1800) / 3600; if (hours <= 36) { description = ntoa(hours) + " hours ago."; } else { int days = (seconds + 43200) / 86400; description = ntoa(days) + " days ago."; } } } description = "You last started this window " + description; XmlNode &strategy = addStrategy(_userId, database, parent, settings, ":)", description); // Forcing this to be at least 0 will make things easier for the // client. strategy.properties["AGE"] = ntoa(std::max(0, seconds)); } void TopListConfigRequest::addRecentSettings(XmlNode &parent, DatabaseWithRetry &database) const { MysqlResultRef result = database.tryQueryUntilSuccess ("SELECT UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(start_time) AS age, settings" " FROM view_mru_tl " "WHERE user_id=" + ntoa(_userId)); if (!result->rowIsValid()) return; XmlNode &allRecentSettings = parent[-1]; allRecentSettings.name = "FOLDER"; allRecentSettings.properties["NAME"] = "Recent Settings"; // Collect all items, grouping by name. Within a name, sort by age. AllItems allItems; while (result->rowIsValid()) { Item item; item.first = result->getIntegerField("age", 0); TopListConfig config; item.second = result->getStringField("settings"); config.load(item.second, _userId, database, true); const std::string name = config.getWindowName(); allItems[name].insert(item); result->nextRow(); } // Sort the groups. Use the minimum age of any item in the group as the // sort key. The name is no longer a key, but we still need to keep it. AllItemsByAge allItemsByAge; for (AllItems::const_iterator it = allItems.begin(); it != allItems.end(); it++) { SimilarItems const &items = it->second; // The list of items sorted by age. const int key = items.begin()->first; // Smallest age of any member. SimilarItemsWithName value; value.name = it->first; value.items = items; allItemsByAge.insert(AllItemsByAge::value_type(key, value)); } for (AllItemsByAge::const_iterator it = allItemsByAge.begin(); it != allItemsByAge.end(); it++) { SimilarItemsWithName itemsWithName = it->second; SimilarItems const &items = itemsWithName.items; if (items.size() == 1) { Item const &item = *items.begin(); addOneRecentStrategy(database, allRecentSettings, item); } else { XmlNode &group = allRecentSettings[-1]; group.name = "FOLDER"; std::string name = itemsWithName.name; name += " ("; name += ntoa(items.size()); name += ')'; group.properties["NAME"] = name; for (SimilarItems::const_iterator similarItemsIt = items.begin(); similarItemsIt != items.end(); similarItemsIt++) { Item const &item = *similarItemsIt; addOneRecentStrategy(database, group, item); } } } } void TopListConfigRequest::addCurrentSettings(XmlNode &node, DatabaseWithRetry &database) const { if (!_settings.empty()) { XmlNode &strategy = addStrategy(_userId, database, node, _settings, ":)", "These were your settings before you requested " "the configuration window.", "Current Settings"); strategy.properties["CURRENT"] = "1"; strategy.properties["USER_MUST_MODIFY"] = "1"; } } void TopListConfigRequest::addTimeFrame(XmlNode &strategy, std::string const &html_help) { // For the E*TRADE wizard. const std::vector< std::string > pieces = explode("TIME_FRAME:", html_help); if ((pieces.size() == 2) && (pieces[0].empty())) strategy.properties["TIME_FRAME"] = pieces[1]; } XmlNode &TopListConfigRequest::addStrategy(UserId userId, DatabaseWithRetry &database, XmlNode &parent, std::string settings, std::string icon, std::string description, std::string name, std::string htmlHelp) const { XmlNode &strategy = parent[-1]; strategy.name = "STRATEGY"; TopListConfig config; config.load(settings, userId, database, true); if (name == "") { strategy.properties["NAME"] = config.getWindowName(); } else { strategy.properties["NAME"] = name; } // SETTINGS is slightly redundant because we have the same info in CONFIG. strategy.properties["SETTINGS"] = settings; strategy.properties["ICON"] = icon; strategy.properties["DESCRIPTION"] = description; config.getSettingsForEditor(strategy["CONFIG"], _easternTime); addTimeFrame(strategy, htmlHelp); return strategy; } void TopListConfigRequest::doRequest(DatabaseWithRetry &database) { // The output should look a lot like the message for an alert config window. // In the C# client one class reads both types of messages becase they have // so much in common. This is based on populateConfigWindow(). XmlNode reply; XmlNode &strategies = reply["STRATEGIES"]; //LogFile::primary().sendString(TclList()<getSocketInfo()).userId, originalRequest), _originalRequest(originalRequest) { assert(originalRequest); } void WrapperListRequest::doRequest(DatabaseWithRetry &database) { ThreadMonitor &tm = ThreadMonitor::find(); // Currently we only have one database connection. readOnlyDatabase exists // only for documentation. That will be useful if we every add a connection to // a slave database. DatabaseWithRetry &readOnlyDatabase = database; switch (_originalRequest->callbackId) { case mtEditConfigNew: { ThreadMonitor::SetState setState("mtEditConfigNew"); tm.increment("mtEditConfigNew"); populateConfigWindow(*_originalRequest, readOnlyDatabase); break; } case mtGeneralInfo: { ThreadMonitor::SetState setState("mtGeneralInfo"); tm.increment("mtGeneralInfo"); SocketInfo *socket = _originalRequest->getSocketInfo(); XmlNode reply; AlertConfig::generalInfo(reply, _userId, readOnlyDatabase); std::string sql = "SELECT UNIX_TIMESTAMP(CONCAT((SELECT date FROM alert_shards WHERE live='Y' ORDER BY date limit 1), ' 04:20:00'))"; MysqlResultRef result = readOnlyDatabase.tryQueryUntilSuccess(sql); const time_t oldestAlert = result->getIntegerField(0, 0); if (oldestAlert > 0) reply["HISTORY"].properties["OLDEST_ALERT"] = exportTime(oldestAlert, userInfoUseEasternTime(socket)); sql = "SELECT COUNT(*) FROM holidays WHERE (NOT find_in_set('US', closed)) AND EXISTS (SELECT * FROM candles_d WHERE day=date)"; result = readOnlyDatabase.tryQueryUntilSuccess(sql); const int usCount = result->getIntegerField(0, 0); XmlNode &usCountNode = reply["OM_DAY_COUNT"][-1]; // Note: The first child of OM_DAY_COUNT is the default. This is // what the E*TRADE client will use. Currently we only send one // answer, the US answer. Whenever we start using this in TI Pro, // we should add "Canada" as the second answer. usCountNode.name = "COUNT"; usCountNode.properties["LOCATION"] = "US"; usCountNode.properties["DAYS"] = ntoa(usCount); addToOutputQueue(socket, reply.asString("API"), _returnMessageId); break; } case mtAllAlertTypes: { // This replaces http://www.trade-ideas.com/API2/AX_Types.html // This provides a list of the short names of each valid // alert type, in the standard order. ThreadMonitor::SetState setState("mtAllAlertTypes"); tm.increment("mtAllAlertTypes"); SocketInfo *socket = _originalRequest->getSocketInfo(); XmlNode reply; AllConfigInfo::instance().getAllAlertTypes(reply); addToOutputQueue(socket, reply.asString("API"), _returnMessageId); break; } case mtEditConfig: { // The logs suggest that someone still calls this, but it's very rare. ThreadMonitor::SetState setState("mtEditConfig"); tm.increment("mtEditConfig"); SocketInfo *socket = _originalRequest->getSocketInfo(); AlertConfig config; config.load(makeUrlSafe(_originalRequest->getProperty("config")), _userId, readOnlyDatabase, // This is an obsolete call. Only very old clients will // use this. So do not allow custom columns here. false, false); XmlNode reply; config.getForEditor(reply["CONFIG"], _userId, readOnlyDatabase); reply["STATUS"].properties["SUCCESS"] = "1"; // Why does the client still need this? addToOutputQueue(socket, reply.asString("API"), _returnMessageId); break; } case mtAllAlertInfo: { ThreadMonitor::SetState setState("mtAllAlertInfo"); tm.increment("mtAllAlertInfo"); allAlertInfo(readOnlyDatabase); break; } default: assert("unknown message type"&&false); } } WrapperListRequest::~WrapperListRequest() { delete _originalRequest; } void WrapperListRequest::allAlertInfo(DatabaseWithRetry &readOnlyDatabase) { XmlNode reply; // This test is a little oversimplified. We should do the same test // as we do in the top list. That looks at the exchange of the alert, // and the user's permissions. (It was very tempting to grab use even // more of that code.) At the moment this is only aimed at E*TRADE, so // it's not an issue. TODO if (_userId) { static const std::string PRICE = "price"; // We are saying that we only want the filters. Really it would make // more sense to give them other types of columns, too. But this // is only aimed at E*TRADE and we want to make as few changes as // possible. Since they didn't get other columns in test, don't give // them the new columns now. PairedFilterList filters(_userId, readOnlyDatabase, false, true); const PairedFilterList::Iterator begin = filters.begin(); const PairedFilterList::Iterator end = filters.end(); std::string sql = "SELECT price "; for (PairedFilterList::Iterator it = begin; it != end; it++) { sql += ", "; sql += PairedFilterList::fixPrice((*it)->sql, PRICE); sql += " as x"; } sql += " FROM alerts LEFT JOIN alerts_daily ON symbol=d_symbol AND date=DATE(timestamp) WHERE id='"; sql += mysqlEscapeString(_originalRequest->getProperty("id")); sql += '\''; MysqlResultRef result = readOnlyDatabase.tryQueryUntilSuccess(sql); const double price = result->getDoubleField(0, 10.0); const int pDigits = ((price > 0) && (price < 1))?4:2; XmlNode &filterList = reply["FILTERS"]; int index = 1; for (PairedFilterList::Iterator it = begin; it != end; it++, index++) if (!result->fieldIsEmpty(index)) { XmlNode &filter = filterList[-1]; filter.properties["CODE"] = (*it)->baseName; const int digits = strtolDefault((*it)->format, pDigits); filter.properties["VALUE"] = dtoaFixed(result->getDoubleField(index, 0.0), digits); } } addToOutputQueue(_originalRequest->getSocketInfo(), reply.asString("API"), _returnMessageId); } ///////////////////////////////////////////////////////////////////// // MiscRODatabaseHandler ///////////////////////////////////////////////////////////////////// void MiscRODatabaseHandler::threadFunction() { while(true) { while (Request *current = _incomingRequests.getRequest()) { switch(current->callbackId) { case mtQuit: { delete current; return; } case mtCloudList: { // Moved to ../misc_ms/MiscNFS.[Ch] ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); TclList msg; msg<(current); SocketInfo *socket = request->getSocketInfo(); TclList msg; msg<(current); SocketInfo *socket = request->getSocketInfo(); TclList msg; msg<(current); _requests.push(new GetProfileRequest(request)); break; } case mtTopListConfig: { ExternalRequest *request = dynamic_cast(current); _requests.push(new TopListConfigRequest(request)); break; } case mtEditConfigNew: case mtGeneralInfo: case mtAllAlertTypes: case mtEditConfig: case mtAllAlertInfo: { ExternalRequest *request = dynamic_cast(current); _requests.push(new WrapperListRequest(request)); current = NULL; break; } case mtDisconnect: { //m.setState(S_mtDisconnect); //m.increment(S_mtDisconnect); DeleteSocketThread::deleteSocket(current->getSocketInfo()); break; } case mtQuickSymbolListInfo: { ThreadMonitor::SetState tm("mtQuickSymbolListsInfo"); tm.increment("mtQuickSymbolListsInfo"); ExternalRequest *request = dynamic_cast(current); XmlNode reply; AlertConfig::quickSymbolListInfo(request->getProperty("config"), reply); addToOutputQueue(request->getSocketInfo(), reply.asString("API"), request->getResponseMessageId()); break; } case mtEcho: { //m.setState(S_mtEcho); //m.increment(S_mtEcho); ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); XmlNode reply; reply["SOCKET"].properties["REMOTE_ADDR"] = socket->remoteAddr(); for (PropertyList::const_iterator it = request->getProperties().begin(); it != request->getProperties().end(); it++) { XmlNode newProperty; newProperty.properties["NAME"] = it->first; newProperty.properties["VALUE"] = it->second; newProperty.name = "PROPERTY"; reply["ECHO"].orderedChildren.push_back(newProperty); } std::string repeatText = request->getProperty("repeat_text"); int repeatCount = strtolDefault(request->getProperty("repeat_count"), 5); for (int i = 0; i < repeatCount; i++) { reply["ECHO"].text += repeatText; } int randomCount = strtolDefault(request->getProperty("random_count"), 0); for (int i = 0; i < randomCount; i++) { reply["ECHO"].text += (char)(((rand()^i)%96)+32); } int message_count = strtolDefault(request->getProperty("message_count"), 1); for (int i = 0; i < message_count; i++) { addToOutputQueue(request->getSocketInfo(), reply.asString("API"), request->getResponseMessageId()); } break; } case DeleteSocketThread::callbackId: { _requests.remove(current->getSocketInfo()); break; } } delete current; } if (_requests.empty()) _incomingRequests.waitForRequest(); else { MiscRODatabaseHandler::Deferred *request = dynamic_cast< MiscRODatabaseHandler::Deferred * >(_requests.pop()); request->doRequest(_database); delete request; } } } MiscRODatabaseHandler::MiscRODatabaseHandler() : ThreadClass("MiscRODatabaseHandler"), _incomingRequests(getName()), _database(true, getName()) { CommandDispatcher *c = CommandDispatcher::getInstance(); c->listenForCommand("edit_config_new", &_incomingRequests, mtEditConfigNew); c->listenForCommand("general_info", &_incomingRequests, mtGeneralInfo); c->listenForCommand("all_alert_types", &_incomingRequests, mtAllAlertTypes); c->listenForCommand("edit_config", &_incomingRequests, mtEditConfig); c->listenForCommand("disconnect", &_incomingRequests, mtDisconnect); c->listenForCommand("edit_top_list_config", &_incomingRequests, mtTopListConfig); c->listenForCommand("all_alert_info", &_incomingRequests, mtAllAlertInfo); c->listenForCommand("cloud_list", &_incomingRequests, mtCloudList); c->listenForCommand("cloud_load", &_incomingRequests, mtCloudLoad); c->listenForCommand("cloud_import", &_incomingRequests, mtCloudImport); c->listenForCommand("get_profile", &_incomingRequests, mtGetProfile); c->listenForCommand("quick_symbol_lists_info", &_incomingRequests, mtQuickSymbolListInfo); if (getConfigItem("allow_echo") == "1") { // This command is helpful in development, but possibly dangerous on a // production system. c->listenForCommand("echo", &_incomingRequests, mtEcho); } startThread(); } MiscRODatabaseHandler::~MiscRODatabaseHandler() { Request *r = new Request(NULL); r->callbackId = mtQuit; _incomingRequests.newRequest(r); waitForThread(); }