#include #include #include #include #include #include #include #include "../shared/GlobalConfigFile.h" #include "../shared/SimpleLogFile.h" #include "../shared/MiscSupport.h" #include "../shared/DatabaseSupport.h" #include "../shared/MiscSQL.h" #include "../fast_alert_search/FieldLists.h" #include "libjson.h" #include "AlertConfig.h" ///////////////////////////////////////////////////////////////////// // AllConfigInfo ///////////////////////////////////////////////////////////////////// static JSONNode find(JSONNode const &parent, std::string const &name) { JSONNode::const_iterator it = parent.find(name); if (it == parent.end()) // If you try to dereference this value, sometimes it will work, but // sometimes it will dump core. The documentation for [] also says // that the reuslt will be undefined if name is not found. return JSONNode(JSON_NULL); else return *it; } static PropertyList getPropertyList(JSONNode const &node) { // This is used mostly when we have a list of translations. Typically // English will be listed as "", German will be listed as "de", etc. PropertyList result; if (node.type() == JSON_NODE) { for (JSONNode::const_iterator it = node.begin(); it != node.end(); it++) result[it->name()] = it->as_string(); } else if (node.type() == JSON_STRING) // In the case where only a string is in the file, we assume that's the // English version. We allow that mostly to make the JSON more readable, // but it does allow a little flexibility. result[""] = node.as_string(); else if (node.type() != JSON_NULL) { // It's tempting to throw an exception here, but it would be hard to // track it down. So we normally report it to the log, and move on. // If you want more details you can use your debugger and set a // breakpoint here. ThreadMonitor::find().increment("JSON parse error PropertyList"); // If we do see a null here, we assume that find() could not find // the value, and then we expect to get an empty result. If we see // something else, then this seems like as reasonable a default as // we can do. } return result; } static bool getBool(JSONNode const &node, bool defaultValue = false) { if (node.type() == JSON_NULL) // More than likely this means that we could not find() the node. return defaultValue; if (node.type() == JSON_BOOL) return node.as_bool(); ThreadMonitor::find().increment("JSON parse error PropertyList"); return defaultValue; } // We are trying to parse a symbol out of the SQL. I mean the name of a field, // not a stock symbol. This function returns true if this character could be // the first character in a symbol. We are including the special $$$ code // as a symbol. static bool symbolFirstChar(char ch) { return ((ch >= 'A') && (ch <= 'Z')) || (ch == '$') || ((ch >= 'a') && (ch <= 'z')) || (ch == '_'); } // This could be part of a symbol, not necessarily the first character. static bool symbolRestChar(char ch) { return ((ch >= '0') && (ch <= '9')) || symbolFirstChar(ch); } bool AllConfigInfo::_fixFloats; // MySQL has some quirks. If you store a field as a float, and then you // try to do any math on it, even comparing it to another number or adding // 0 to it, you get some strange rounding issues. This might show up as // a number like 24.11999923 instead of 24.12. That would look ugly if you // tried to print it, and if the user was looking for values of exactly 24.12 // this would not match. Also, the results from the SQL query would not // exactly match the results from ../fast_alert_search/. This code will // modify a query, or part of a query, to fix this issues. We first convert // the field from a float to a string, then we convert from a string to a // double. std::string AllConfigInfo::fixSql(std::string const &original) { static std::unordered_set< std::string > needFixing = []() { // A field "needs fixing" if it is a float. std::unordered_set< std::string > result; TclList fields; if (_fixFloats) { for (DatabaseFieldInfo const &field : DatabaseFieldInfo::getAlertFields()) if (field.type() == DatabaseFieldInfo::Float) { result.insert(field.databaseName()); fields< activeExchanges; for (BitSet::const_iterator it = exchanges.begin(); it != exchanges.end(); it++) { if (ExchangeInfo const *exchange = getExchange(*it)) activeExchanges.insert ("'" + mysqlEscapeString(exchange->shortName) + "'"); // It is certainly possible that an exchange will not exist. So that // if statement is required. For example, if the database and // AlertConfigAuto.C do not agree. This happened to us when we // had the xetra exchanges in the latter and the North American // exchanges in the former. } return sqlIn("list_exch", activeExchanges); } } std::string AllConfigInfo::removeCommas(std::string s) { // Replace every comma with the empty string. For the sake of optimization, // assume that many strings will have no commas, and that returning the // source, without modification, is fast. unsigned limit = s.size(); unsigned source; for (source = 0; source < limit; source++) { if (s[source] == ',') break; } if (source < limit) { unsigned dest; for (dest = source; source < limit; source++) { if (s[source] != ',') { s[dest] = s[source]; dest++; } } s.resize(dest); } return s; } void AllConfigInfo::init(bool fixFloats) { _fixFloats = fixFloats; assert(!_instance); _instance = new AllConfigInfo; } AllConfigInfo *AllConfigInfo::_instance; void AllConfigInfo::addAlert(AlertInfo const &info, bool addToDefault) { assert(!(_alertByShortName.count(info.shortName) || _alertByBit.count(info.asBit))); AlertInfo &save = _alertByShortName[info.shortName]; save = info; _legalAlerts.set(info.asBit); _alertByBit[info.asBit] = &save; _alertsUserOrder.push_back(&save); if (addToDefault) { _defaultAlerts.set(info.asBit); } } void AllConfigInfo::addFilter(std::string shortName, std::string description, std::string units, std::string flip, std::string keywords) { assert(!_filterByShortName.count(shortName)); FilterInfo &info = _filterByShortName[shortName]; info.shortName = shortName; info.description = description; info.units = units; info.flip = flip; info.keywords = keywords; _filtersUserOrder.push_back(&info); } void AllConfigInfo::addFilterInfo(PairedFilterInfo const &info) { assert(!_pairedFilterByBaseName.count(info.baseName)); PairedFilterInfo &save = _pairedFilterByBaseName[info.baseName]; save = info; _pairedFiltersUserOrder.push_back(&save); } void AllConfigInfo::addExchange(ExchangeInfo const &info) { assert(!(_exchangeByFormName.count(info.formName) || _exchangeByBit.count(info.asBit))); ExchangeInfo &save = _exchangeByFormName[info.formName]; save = info; _legalExchanges.set(info.asBit); if (info.userVisible()) { _realExchanges.set(info.asBit); } _exchangeByBit[info.asBit] = &save; _exchangesUserOrder.push_back(&save); } AllConfigInfo::AllConfigInfo() { InitConfigInfo(); } AllConfigInfo::AlertIterator AllConfigInfo::firstAlert() const { return _alertsUserOrder.begin(); } AllConfigInfo::AlertIterator AllConfigInfo::endAlert() const { return _alertsUserOrder.end(); } AllConfigInfo::AlertInfo const *AllConfigInfo::getAlert(std::string shortName) const { return getProperty(_alertByShortName, shortName); } AllConfigInfo::AlertInfo const *AllConfigInfo::getAlert(unsigned bit) const { return getPropertyDefault(_alertByBit, bit); } AllConfigInfo::FilterIterator AllConfigInfo::firstFilter() const { return _filtersUserOrder.begin(); } AllConfigInfo::FilterIterator AllConfigInfo::endFilter() const { return _filtersUserOrder.end(); } AllConfigInfo::FilterInfo const *AllConfigInfo::getFilter(std::string shortName) const { return getProperty(_filterByShortName, shortName); } AllConfigInfo::PairedFilterIterator AllConfigInfo::firstPairedFilter() const { return _pairedFiltersUserOrder.begin(); } AllConfigInfo::PairedFilterIterator AllConfigInfo::endPairedFilter() const { return _pairedFiltersUserOrder.end(); } AllConfigInfo::PairedFilterInfo const *AllConfigInfo::getPairedFilter(std::string baseName) const { return getProperty(_pairedFilterByBaseName, baseName); } AllConfigInfo::ExchangeIterator AllConfigInfo::firstExchange() const { return _exchangesUserOrder.begin(); } AllConfigInfo::ExchangeIterator AllConfigInfo::endExchange() const { return _exchangesUserOrder.end(); } AllConfigInfo::ExchangeInfo const *AllConfigInfo::getExchange(std::string shortName) const { return getProperty(_exchangeByFormName, shortName); } AllConfigInfo::ExchangeInfo const *AllConfigInfo::getExchange(unsigned bit) const { return getPropertyDefault(_exchangeByBit, bit); } void AllConfigInfo::getAllAlertTypes(XmlNode &parent) const { XmlNode &main = parent["ALERT_TYPES"]; for (AlertIterator it = _alertsUserOrder.begin(); it != _alertsUserOrder.end(); it++) { main[-1].name = (*it)->shortName; } } AllConfigInfo const &AllConfigInfo::instance() { return *_instance; } ///////////////////////////////////////////////////////////////////// // PairedFilterList ///////////////////////////////////////////////////////////////////// // For some reason "it" was not available in the debugger! //static volatile AllConfigInfo::PairedFilterInfo const *debugMe; PairedFilterList::PairedFilterList(UserId userId, DatabaseWithRetry &database, bool topList, bool onlyFilters) : _topList(topList), _onlyFilters(onlyFilters) { // Start with the standard filters. AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); for (AllConfigInfo::PairedFilterIterator it = allConfigInfo.firstPairedFilter(); it != allConfigInfo.endPairedFilter(); it++) { //debugMe = *it; if (((*it)->topList || !topList) && ((*it)->isFilter || !onlyFilters)) _userOrder.push_back(*it); } /* // Add filters based on a group that this user belongs to. bool loadRestictedFilters = false; if (userId == ALLOW_RESTRICTED) loadRestictedFilters = true; else if (userId > 0) { const std::string className = database.tryQueryUntilSuccess("SELECT class FROM users WHERE id=" + ntoa(userId))->getStringField(0); // This test is currently hard coded. We could do a lot more with // this. But for now we want to give options data to a few specific // groups of people. if ((className == "InternalTesting") || (className == "Scottrade") || (className == "E*TRADE")) loadRestictedFilters = true; } if (loadRestictedFilters) { std::string query = "SELECT internal_code, sql_code, description, units, flip, format, " "top_list, graphics, always_show, keywords " "FROM restricted_filters " "WHERE filter_group=1 "; if (topList) query += " AND top_list = 'Y'"; query += " ORDER BY order_by"; for (MysqlResultRef customFilters = database.tryQueryUntilSuccess(query); customFilters->rowIsValid(); customFilters->nextRow()) { std::string baseName = customFilters->getStringField("internal_code"); AllConfigInfo::PairedFilterInfo &info = _byBaseName[baseName]; info.baseName = baseName; info.sql = customFilters->getStringField("sql_code"); info.description[""] = customFilters->getStringField("description"); info.units[""] = customFilters->getStringField("units"); info.flip = customFilters->getStringField("flip"); info.format = customFilters->getStringField("format"); info.topList = customFilters->getStringField("top_list") == "Y"; info.graphics = customFilters->getStringField("graphics"); info.isFilter = true; info.alwaysShow = customFilters->getStringField("always_show") == "Y"; info.keywords[""] = customFilters->getStringField("keywords"); _userOrder.push_back(&info); } } */ if (userId > 0) { // Add filters in this user's account. std::string query = "SELECT internal_code, sql_code, description, units, format, graphics " "FROM user_filters " "WHERE user_id=" + ntoa(userId); if (topList) query += " AND top_list = 'Y'"; for (MysqlResultRef customFilters = database.tryQueryUntilSuccess(query); customFilters->rowIsValid(); customFilters->nextRow()) { std::string baseName = customFilters->getStringField(0); AllConfigInfo::PairedFilterInfo &info = _byBaseName[baseName]; info.baseName = baseName; info.sql = AllConfigInfo::fixSql(customFilters->getStringField(1)); info.description[""] = customFilters->getStringField(2); info.units[""] = customFilters->getStringField(3); info.format = customFilters->getStringField(4); info.graphics = customFilters->getStringField(5); info.isFilter = true; // For now, alwaysShow is false for user defined filters. We could // tag individual fields with this property, just like we do for // alerts only, and propagate that through the formula. info.alwaysShow = false; // Note: info.topList is undefinded. It doesn't seem to be used. _userOrder.push_back(&info); } } } PairedFilterList::Iterator PairedFilterList::begin() const { return _userOrder.begin(); } PairedFilterList::Iterator PairedFilterList::end() const { return _userOrder.end(); } AllConfigInfo::PairedFilterInfo const * PairedFilterList::get(std::string baseName) const { AllConfigInfo::PairedFilterInfo const *result = getProperty(_byBaseName, baseName); if (result) return result; result = AllConfigInfo::instance().getPairedFilter(baseName); if (!result) // Filter not found anywhere return NULL; if ((result->topList || !_topList) && (result->isFilter || !_onlyFilters)) // We found one and it matched our criteria. return result; // We found something, but it didn't match all of the criteria. return NULL; } /* This will replace every occurance of $$$ in the original string with * the given value of priceField. * * Originally I started using $$$ for the price of the stock in * CommonConfig.php to support the overnight scanner. You could often use the * same formula during the day as overnight, if you used the closing price of * the stock instead of the current price. * * Much later I added the "last" field. This is the official last print. * That is to say it ignores form-t and similar prints. So we have 3 possible * versions of the price. * * Then I added the top lists on the web. Most of the time they use "price" * for the price, to be consistent with the alerts. (There are good arguments * for using last, instead, but I made a decision.) But if you ask for * information at the close, then we give you data from the last 5 minutes * after the close, and we replace $$$ with "last". That will give you a good * chance of seeing the real closing price of the stock. (Of course we can't * be sure since the official closing price is not set for hours.) * * The alerts always use price for $$$. If there is a strange print, it's very * likely that that print caused the alert. And we want to use the price that * caused the alert. * * Originally $$$ was replaced with "price" when the sql codes were exported * from CommonConfig.php to the C++ code. The C++ code never had to deal * with $$$ directly. However, now that we have top lists, we need to see * the original $$$ formulas, so we can be consistent with the web. So the * top list code needs to do the smart replacement. The alerts code also needs * to do the replacement, although it will always replace the $$$ with "price". */ std::string PairedFilterList::fixPrice(std::string const &original, std::string const &priceField) { static const std::string placeholder = "$$$"; std::string result = original; std::string::size_type start = 0; while (true) { start = result.find(placeholder, start); if (start == std::string::npos) break; result.replace(start, 3, priceField); start += 3; } return result; } ///////////////////////////////////////////////////////////////////// // ColumnListConfig::Mapping ///////////////////////////////////////////////////////////////////// TclList ColumnListConfig::Mapping::debugDump() const { TclList result; result<<"internalCode"<isFilter) { AllConfigInfo::PairedFilterInfo const &info = **it; XmlNode &filterNode = parent["DISPLAY_ONLY_FIELDS"][-1]; filterNode.properties["BASE"] = info.baseName; copyLanguageValue(filterNode, "DESCRIPTION", info.description, language); copyLanguageValue(filterNode, "UNITS", info.units, language); //if (!info.flip.empty()) // filterNode.properties["FLIP"] = info.flip; copyLanguageValue(filterNode, "KEYWORDS", info.keywords, language); } } ColumnListConfig::ColumnListConfig() { clear(); } void ColumnListConfig::clear() { _mappings.clear(); // If you forget to ask which data is legal, we assume none of it is legal. // That is different from the other types of data, where the user's request // is stored until we remove the illegal parts of the request. _fullExchangeData.clear(); // Add some defaults or else it looks really bad on the client side. Mapping mapping; mapping.alwaysShow = true; mapping.internalCode = "D_Symbol"; mapping.altName = "c_" + mapping.internalCode; _mappings.push_back(mapping); } void ColumnListConfig::removeIllegalData(UserId userId, DatabaseWithRetry &database) { //std::cout<<"ColumnListConfig::removeIllegalData("<internalCode; } if (_allowNonFilterColumns) { result += "&col_ver=1"; } return result; } void ColumnListConfig::addOneColumn(std::set< std::string > &used, std::string const &internalCode, PairedFilterList const &filters) { AllConfigInfo::PairedFilterInfo const *filter = filters.get(internalCode); if (filter && !used.count(internalCode)) { // Get rid of items that are not legit. As much as anything, // that's just to make sure that these are all valid strings so // we never need to quote them. Get rid of duplicates as they // probably were not intentional. used.insert(internalCode); Mapping mapping; mapping.alwaysShow = filter->alwaysShow; mapping.internalCode = internalCode; // This is the name that we use in the database query and in the XML. // Not all internal codes would be legal in that context because some // start with a number. // // We guarantee to the client that all of the names in the XML will be // consistent. So if two different strategies both ask for the same // column, they will both see the results tagged with the same name. // // At this point we are not expecting the client to guess what the // name is based on the internal code. It would not be hard to // guess! At the moment we want some flexibility. (We could not // have changed the names if the client was not flexible!) mapping.altName = "c_" + internalCode; _mappings.push_back(mapping); } } void ColumnListConfig::getFromConfigWindow(PropertyList const &config, PairedFilterList const &filters, bool allowNonFilterColumns) { _mappings.clear(); _allowNonFilterColumns = allowNonFilterColumns; std::set< std::string > used; int inId = 0; bool colVer = getPropertyDefault(config, "col_ver") == "1"; if (!colVer) { std::set< std::string > skipped; while (true) { std::string skipCode = getPropertyDefault(config, "skip" + ntoa(inId)); if (skipCode.empty()) break; skipped.insert(skipCode); inId++; } if (!skipped.count("Symbol")) addOneColumn(used, "D_Symbol", filters); if (!skipped.count("Type")) addOneColumn(used, "D_Type", filters); if (!skipped.count("Time")) addOneColumn(used, "D_Time", filters); if (!skipped.count("Description")) addOneColumn(used, "D_Desc", filters); if (getPropertyDefault(config, "add_quality") == "1") addOneColumn(used,"D_Quality", filters); } inId = 0; while (true) { std::string internalCode = getPropertyDefault(config, "show" + ntoa(inId)); if (internalCode.empty()) break; addOneColumn(used, internalCode, filters); inId++; } if (_mappings.empty()) // Defaults. The web version always had exactly 3 columns. We allow // more or fewer. We do not allow 0. We assume that was a mistake and // fill in the defaults. That's similar to the way we do defaults for // the exchanges. clear(); } // Includes a comma *before* each item. This should be appended to a list // of items. This will be the empty string if the column list is empty. std::string ColumnListConfig::selectSql(PairedFilterList const &filters) const { if (_mappings.empty()) return ""; std::string result = ", price < 1.0 AS four_digits, " "(timestamp < NOW() - INTERVAL 900 SECOND) OR "; result += AllConfigInfo::instance().exchangeExpression(_fullExchangeData); result += " AS can_copy"; for (Mappings::const_iterator it = _mappings.begin(); it != _mappings.end(); it++) { result += ','; if (AllConfigInfo::PairedFilterInfo const *info = filters.get(it->internalCode)) result += info->sql; else result += "NULL"; result += " AS "; result += it->altName; } return result; } void ColumnListConfig::getInitialDescription(XmlNode &parent, PairedFilterList const &filters ) const { XmlNode &columns = parent["COLUMNS"]; for (Mappings::const_iterator it = _mappings.begin(); it != _mappings.end(); it++) { if (AllConfigInfo::PairedFilterInfo const *info = filters.get(it->internalCode)) { XmlNode &column = columns[-1]; column.name = it->altName; column.properties["CODE"] = info->baseName; copyLanguages(column, "DESCRIPTION", info->description); column.properties["FORMAT"] = info->format; copyLanguages(column, "UNITS", info->units); if (!info->graphics.empty()) column.properties["GRAPHICS"] = info->graphics; if (!info->isFilter) column.properties["TEXT_HEADER"] = "1"; if (!info->isFilter) column.properties["TEXT_FIELD"] = "1"; } } } void ColumnListConfig::getCsvNames(PropertyList &headers, PairedFilterList const &filters) const { headers.clear(); for (Mappings::const_iterator it = _mappings.begin(); it != _mappings.end(); it++) { if (AllConfigInfo::PairedFilterInfo const *info = filters.get(it->internalCode)) { const std::string englishDescription = getPropertyDefault(info->description, ""); const std::string englishUnits = getPropertyDefault(info->units, ""); std::string fullTitle = englishDescription; if (!englishUnits.empty()) { fullTitle += " ("; fullTitle += englishUnits; fullTitle += ')'; } fullTitle += " ["; fullTitle += info->baseName; fullTitle += ']'; headers[it->altName] = fullTitle; } } } void ColumnListConfig::getValuesForEditor(XmlNode &parent) const { XmlNode &columns = parent["COLUMNS"]; for (Mappings::const_iterator it = _mappings.begin(); it != _mappings.end(); it++) columns[-1].properties["BASE"] = it->internalCode; } void ColumnListConfig::copyResult(PropertyList &destination, MysqlResultRef &source) const { if (_mappings.size()) { if (source->getBooleanField("four_digits")) destination["four_digits"] = "1"; const bool canCopy = source->getBooleanField("can_copy"); for (Mappings::const_iterator it = _mappings.begin(); it != _mappings.end(); it++) if (canCopy || it->alwaysShow) copyData(destination, source, it->altName); } } void ColumnListConfig::copyData(PropertyList &dest, MysqlResultRef &source, std::string const &fieldName) { const std::string value = source->getStringField(fieldName); if (value.empty()) return; if (value.find('.') != std::string::npos) { // Not an integer. const double asDouble = source->getDoubleField(fieldName, NAN); if (finite(asDouble)) { // The value is a floating point value (and not an integer.) char buffer[30]; sprintf(buffer, "%.6g", asDouble); // We store most numbers in the database as floats. But somewhere // they get converted to doubles. So we see a lot of things like // 0.48900032043457 and 0.27999997138977. So mysql is giving us // things with about twice as much precision as it should. By // cutting the precision in half, I typically make the numbers much // shorter, not just half. Those should appear as 0.489 and 0.28, // respectively. dest[fieldName] = buffer; // This is not perfect, but it's close. Sometimes we should cut // off one more digit. return; } } dest[fieldName] = value; } ///////////////////////////////////////////////////////////////////// // ClientCookie ///////////////////////////////////////////////////////////////////// std::string ClientCookie::asSaveConfig() const { if (_value.empty()) return ""; return "&ccookie=" + urlEncode(_value); } void ClientCookie::getFromConfigWindow(PropertyList const &config) { _value = getPropertyDefault(config, "ccookie"); } void ClientCookie::getInitialDescription(XmlNode &parent) const { if (!_value.empty()) parent["CLIENT_COOKIE"].properties["VALUE"] = _value; } ///////////////////////////////////////////////////////////////////// // SymbolListDesciption ///////////////////////////////////////////////////////////////////// std::string SymbolListDesciption::asSaveConfig() const { if (ownerId) { return ntoa(ownerId) + "o" + ntoa(listId); } else { return ntoa(listId); } } bool SymbolListDesciption::operator <(SymbolListDesciption const &other) const { // I sort first by owner id because it makes it easier in debugging. It's // also helpful on the configuration screen. if (ownerId < other.ownerId) { return true; } else if (ownerId > other.ownerId) { return false; } else { return listId < other.listId; } } ///////////////////////////////////////////////////////////////////// // SymbolLists ///////////////////////////////////////////////////////////////////// void SymbolLists::normalize() { switch (_symbolListType) { case sltAll: break; case sltOnly: case sltExclude: if (_symbolLists.empty()) { _symbolListType = sltAll; } break; case sltSingle: _singleSymbol = trim(_singleSymbol); if (_singleSymbol.empty()) { _symbolListType = sltAll; } else { _singleSymbol = strtoupper(_singleSymbol); } break; } } void SymbolLists::clear() { _symbolLists.clear(); _singleSymbol.clear(); _symbolListType = sltAll; } void SymbolLists::removeIllegalLists(UserId userId, DatabaseWithRetry &database) { std::set< UserId > sharingFrom; // Fill sharingFrom with the owners whose lists we are trying to read. for (std::set< SymbolListDesciption >::const_iterator it = _symbolLists.begin(); it != _symbolLists.end(); it++) { UserId ownerId = it->ownerId; if (ownerId) { sharingFrom.insert(ownerId); } } if (!sharingFrom.empty()) { std::string sql = "SELECT owner_id FROM list_permissions " "WHERE user_id IN (" + ntoa(userId) + ", 0) AND owner_id IN (" + // We have to write itoa here because UserId is really an "int". // We use ntoa everywhere else, but the compiler can't figure that // out here. implode(",", sharingFrom.begin(), sharingFrom.end(), itoa) + ")"; MysqlResultRef result = database.tryQueryUntilSuccess(sql); if (sharingFrom.size() != (unsigned)result->numRows()) { // Now fill sharingFrom with the owners whose lists we are // allowed to read. sharingFrom.clear(); sharingFrom.insert(0); while (result->rowIsValid()) { sharingFrom.insert(result->getIntegerField(0, 0)); result->nextRow(); } std::set< SymbolListDesciption > newLists; for (std::set< SymbolListDesciption >::const_iterator it = _symbolLists.begin(); it != _symbolLists.end(); it++) { SymbolListDesciption const &list = *it; if (sharingFrom.count(list.ownerId)) { newLists.insert(list); } } _symbolLists = newLists; normalize(); } } } std::string SymbolLists::asSaveConfigUrl() const { std::string symbolListDescription = asSaveConfig(); if (!symbolListDescription.empty()) return "&SL=" + symbolListDescription; else return ""; } std::string SymbolLists::asSaveConfig() const { std::string result; switch (_symbolListType) { case sltSingle: result = 'x' + urlEncode(_singleSymbol); break; case sltExclude: result = 'X'; // fall through! case sltOnly: { bool first = true; for (std::set< SymbolListDesciption >::const_iterator it = _symbolLists.begin(); it != _symbolLists.end(); it++) { if (first) { first = false; } else { result += "a"; } result += it->asSaveConfig(); } } case sltAll: break; } return result; } void SymbolLists::getFromConfigWindow(PropertyList config) { clear(); std::string sl = getPropertyDefault(config, "SL"); if (sl.empty()) { // The configuration is in the long form, or it does not contain // any symbol list information. std::string entireUniverse = getPropertyDefault(config, "EntireUniverse"); // This input format makes more sense in a historical context. // Originally there were two choices. By default we'd try to read the // symbol lists. If we found some, we'd use them, otherwise we'd have to // use the entire universe. Someone using the HTML config form could // click on a single button to select the entire universe rather than // unchecking each of the symbol lists one at a time. The EntireUniverse // item was required in that case. When it wasn't required, we didn't // list it at all, hence we use the empty string to say, look at the // symbol lists. // // Now we have more options so we really do need to use this input more. // We kept the original two values of "" and "1" for backward // compatibility. if (entireUniverse == "") { _symbolListType = sltOnly; } else if (entireUniverse == "2") { _symbolListType = sltExclude; } else if (entireUniverse == "3") { _symbolListType = sltSingle; } else { // The prefered input is "1". But this is also the default case, // so any other input will also work. _symbolListType = sltAll; } switch (_symbolListType) { case sltOnly: case sltExclude: for (PropertyList::const_iterator it = config.begin(); it != config.end(); it++) { std::vector< std::string >pieces = explode("_", it->first); if ((pieces.size() == 3) && (pieces[0] == "SL")) { UserId ownerId = strtolDefault(pieces[1], -1); if (ownerId >= 0) { SymbolListId listId = strtolDefault(pieces[2], 0); if (listId) { SymbolListDesciption newList; newList.ownerId = ownerId; newList.listId = listId; _symbolLists.insert(newList); } } } } break; case sltSingle: _symbolListType = sltSingle; _singleSymbol = getPropertyDefault(config, "SingleSymbol"); break; case sltAll: break; } } else { // The configuration is in the short form. // sl != "" if (sl[0] == 'x') { _symbolListType = sltSingle; _singleSymbol.assign(sl, 1, std::string::npos); } else { std::string encodedSymbolLists; if (sl[0] == 'X') { _symbolListType = sltExclude; encodedSymbolLists.assign(sl, 1, std::string::npos); } else { _symbolListType = sltOnly; encodedSymbolLists = sl; } std::vector< std::string > symbolLists = explode("a", encodedSymbolLists); for (std::vector< std::string >::iterator it = symbolLists.begin(); it != symbolLists.end(); it++) { std::vector< std::string > pieces = explode("o", *it); switch (pieces.size()) { case 1: { // Just a single number. SymbolListDesciption list; list.ownerId = 0; // Owner by the current user. list.listId = strtolDefault(pieces[0], 0); if (list.listId != 0) { _symbolLists.insert(list); } break; } case 2: { // 4o7 means list #7 of user #4. SymbolListDesciption list; list.ownerId = strtolDefault(pieces[0], -1); list.listId = strtolDefault(pieces[1], 0); if ((list.listId != 0) && (list.ownerId > 0)) { _symbolLists.insert(list); } break; } default: // Anything else is a syntax error and is silently discarded. break; } } } } normalize(); } std::string SymbolLists::inListsSql(UserId userId, std::string const &table, bool exclude) const { /* The goal is to make something like this. By creating these seperate subqueries we can make better use of the indexes. Of course this will become simipler if the user's selections are simpler. In that case the query would not be hard for MySQL to optimize. But the example shown here comes from a real user. This was part of a top list query that could run for several seconds under the old system. The old system used a join, which isn't as flexible as a subquery. Once I split the query up and unioned the pieces, it got a whole lot faster. (NOT EXISTS ( (SELECT * FROM symbols_in_lists WHERE (list_id=20 AND user_id=448683) AND top_list.symbol=symbols_in_lists.symbol) UNION (SELECT * FROM symbols_in_lists WHERE (list_id=4 AND user_id=1) AND top_list.symbol=symbols_in_lists.symbol) UNION (SELECT * FROM symbols_in_lists WHERE (user_id=3542798) AND (list_id IN (23,24,25,27,28,29,30,31)) AND (top_list.symbol=symbols_in_lists.symbol)) )) */ std::map< UserId, std::set< SymbolListId > > lists; for (std::set< SymbolListDesciption >::const_iterator it = _symbolLists.begin(); it != _symbolLists.end(); it++) { const SymbolListId listId = it->listId; //if (listId <= 0) continue; UserId ownerId = it->ownerId; if (!ownerId) ownerId = userId; lists[ownerId].insert(listId); } std::vector< std::string > queries; const std::string symbolMatches = table + ".symbol=symbols_in_lists.symbol"; for (std::map< UserId, std::set< SymbolListId > >::const_iterator it = lists.begin(); it != lists.end(); it++) { // For each user ... const UserId ownerId = it->first; std::set< SymbolListId > const &listsAsNumbers = it->second; std::vector< std::string > expressions; expressions.reserve(3); expressions.push_back("user_id=" + ntoa(ownerId)); expressions.push_back(symbolMatches); std::set< std::string > listsAsStrings; for (std::set< SymbolListId >::const_iterator listIdIt = listsAsNumbers.begin(); listIdIt != listsAsNumbers.end(); listIdIt++) listsAsStrings.insert(ntoa(*listIdIt)); assert(!listsAsStrings.empty()); expressions.push_back(sqlIn("list_id", listsAsStrings)); const std::string query = "(SELECT * FROM symbols_in_lists WHERE " + sqlAnd(expressions) + ')'; queries.push_back(query); } std::string allSymbolsQuery; assert(!queries.empty()); // normalize() should cover this case! if (queries.size() == 1) allSymbolsQuery = *queries.begin(); else { for (std::vector< std::string >::const_iterator it = queries.begin(); it != queries.end(); it++) { if (allSymbolsQuery.empty()) // First one. allSymbolsQuery = '(' + *it; else { allSymbolsQuery += " UNION "; allSymbolsQuery += *it; } } allSymbolsQuery += ')'; } std::string result; if (exclude) result = "NOT "; result += "EXISTS "; result += allSymbolsQuery; return result; } std::string SymbolLists::whereSql(UserId userId, std::string table) const { switch (_symbolListType) { case sltOnly: return inListsSql(userId, table, false); case sltExclude: return inListsSql(userId, table, true); case sltSingle: return table + ".symbol='" + mysqlEscapeString(_singleSymbol) + "'"; case sltAll: return sqlTrue; } // This is required to avoid a comipler warning. assert(0); return ""; } // This lists out all of the lists from which the user can choose. // This also says which lists are active in the current configuration. This // feature is only used by people with old versions of the client, so we // only fill it in if the configuration was in "only" mode. If the // configuration was in "single" or "exclude" mode, we assume the client is // not smart enough to understand, so we just leave everything blank, // effectively converting the configuration into "all" mode. For the new // clients this is not a problem because they are only looking at the // structure. They will always call this function from "all" mode. void SymbolLists::getStructureForEditor(XmlNode &parent, UserId userId, bool allowFolders, bool allowNegativeListIds, DatabaseWithRetry &database) const { /* sendToLogFile(TclList()<rowIsValid(); result->nextRow()) { const SymbolListId listId = result->getIntegerField("id", 0); if (!allowNegativeListIds) // Filter out the lists with negative id numbers. This could have been // done in the SQL, but it would have been more complicated there. It // wouldn't have saved much at run time because we expect very few lists // to be filtered out. if (listId < 1) continue; XmlNode &listNode = parent["SYMBOL_LISTS"][-1]; listNode.name = "SL_" + result->getStringField("owner_id") + "_" + result->getStringField("id"); listNode.properties["NAME"] = result->getStringField("name"); SymbolListDesciption description; description.ownerId = result->getIntegerField("owner_id", 0); description.listId = listId; /* sendToLogFile(TclList()<::iterator it = _symbolLists.begin(); it != _symbolLists.end(); it++) { SymbolListDesciption const &description = *it; XmlNode &listNode = listInfo[-1]; listNode.name = "SL_" + ntoa(description.ownerId) + "_" + ntoa(description.listId); } } } } SymbolLists::SymbolListType SymbolLists::getSymbolListType() const { return _symbolListType; } ///////////////////////////////////////////////////////////////////// // AlertConfig::CustomSql ///////////////////////////////////////////////////////////////////// inline std::string AlertConfig::CustomSql::formatLimit(int limit) { // Perhaps we should assert that limit >= 1. return itoa(std::max(limit, 1)); } // By default we let the database pick the best index for the alerts table // on its own. static std::string aiNoneString = " "; // This was in the code for a while as the default. I'm not sure it does // much. I had to take it out when I added the ability to look for a specific // symbol in various places. Now maybe it makes sense as an option. static std::string aiIgnoreSymbolString = " IGNORE INDEX (symbol) "; // This is completely crazy. It seems to be a workaround for a bug in some // versions of MySql. // // Of course you don't want to do this always. Sometimes mysql can use the // type index to make things faster. In fact, there is some commented out // code in the OddsMaker which tried using aiUsePrimaryString every time we // looked for an OM entry condition. That helped in some cases but I took it // out because it slowed other things down. // // When I looked at some slow OM runs I found something interesting. 67% of // the entry queries took 1 - 10ms. 32% took 10 - 100ms. But a handful took // longer, including some which took over 10 seconds! // // Further investigation showed that the slow queries were very repeatable on // that machine. Moving them to a different mysql server made them much // faster. In the good cases mysql was using the primary index to jump // directly to the range of IDs that we care about. In the bad cases, mysql // was still using the primary index, but it was scanning that entire index // for some reason. (DESCRIBE calls the good case "range" and the bad case // "index".) // // If I tell MySQL to use the primary index, that fixes things. That makes // no sense. It was already using the primary index! This statement just // makes MySQL use the index better. I tried several alternatives. Anything // that avoids the by_type index makes things better. E.g. use index (PRIMARY, // symbol) or use index (PRIMARY). static std::string aiIgnoreTypeString = " IGNORE INDEX (by_type) "; // This is used in certain cases to make things faster. This is not great. // Mostly I'm trying to avoid going to disk. By always using the primary // index, in theory we're using less of the table, and the table may be // able to fit into memory better. static std::string aiUsePrimaryString = " USE INDEX (primary) "; std::string AlertConfig::CustomSql::alertIndexString() const { switch (_alertIndex) { case aiIgnoreSymbol: return aiIgnoreSymbolString; case aiUsePrimary: return aiUsePrimaryString; case aiIgnoreType: return aiIgnoreTypeString; default: return aiNoneString; } } static const std::string DEFAULT_TABLE_NAME = "alerts"; std::string AlertConfig::CustomSql::get(std::string where) const { return _part1 + DEFAULT_TABLE_NAME + alertIndexString() + _part2 + where + _part3 + _limit; } std::string AlertConfig::CustomSql::get(std::string where, int limit) const { return get(where, limit, DEFAULT_TABLE_NAME); } std::string AlertConfig::CustomSql::get(std::string where, int limit, std::string tableName) const { static const std::string asAlerts = " as alerts"; return _part1 + tableName + asAlerts + alertIndexString() + _part2 + where + _part3 + formatLimit(limit); } std::string AlertConfig::CustomSql::getCommon(AlertId startAfter, AlertId continueThrough, int limit) const { return getCommon(startAfter, continueThrough, limit, DEFAULT_TABLE_NAME); } std::string AlertConfig::CustomSql::getCommon(AlertId startAfter, AlertId continueThrough, int limit, std::string tableName) const { return get("(id > " + ntoa(startAfter) + ") AND (id <= " + ntoa(continueThrough) + ")", limit, tableName); } //#include //std::string AlertConfig::CustomSql::getCommon(AlertId startAfter, // AlertId continueThrough, // int limit) const //{ // std::cout<<"startAfter="< " + ntoa(startAfter) + ") AND (id <= " + // ntoa(continueThrough) + ")", // limit); // std::cout<<"\nresult="<getStringField(sourceName, value, found); if (found) { destination.properties[destName] = value; } } void AlertConfig::CustomSql::copyAlert(XmlNode &destination, MysqlResultRef &source, AlertHistoryType historical) const { switch (historical) { case ahtShortHistory: destination.properties["HISTORICAL"] = "1"; break; case ahtHistory: destination.properties["HISTORICAL"] = "2"; break; default: break; } for (int i = 0; i < STANDARD_FIELD_COUNT; i++) { copyField(destination, fields[i].dest, source, fields[i].src); } _customColumns.copyResult(destination.properties, source); } ///////////////////////////////////////////////////////////////////// // AlertConfig::SimpleExpression // // This is a quick and dirty way to add more functionality for // advanced users, and minimize the impact on normal users. Normally // the user types a number, and the field is compared to that number. // However, the user can enter a special form, like "**p5". That // means to divide the field by the most recent price before // comparing it to 5. This is a quick and dirty percent (or more // precisely a ratio). Alternatively the field could start with // "**v" to mean that we divide the field by the average daily volume // before the comparison. // // **P and **V go in the other direction. They convert a ratio back // into a fixed number of dollars or shares. // // The actual implemention will be to multiply the constant by the // price or volume, rather than dividing the field by that value. // The result will be the same. But the previous paragraph gives // a better description for the end user. ///////////////////////////////////////////////////////////////////// bool AlertConfig::SimpleExpression::isValid() const { return _type != invalid; } std::string AlertConfig::SimpleExpression::toDatabase() const { switch (_type) { case invalid: // There isn't a good answer for this. assert(false) might work, too. return "NULL"; case simpleNumber: return ntoa(_value); case divideByPrice: // We assume that this will be used in a <. or a between, or something // similar where we don't have to worry about parentheses. return "price*" + ntoa(_value); case divideByVolume: return "advol*" + ntoa(_value); case multiplyByPrice: return ntoa(_value) + "/price"; case multiplyByVolume: return ntoa(_value) + "/advol"; } // We should never get here, but the compiler complains anyway. return ""; } std::string AlertConfig::SimpleExpression::toEncodedConfig() const { return urlEncode(toGUI(), false); } std::string AlertConfig::SimpleExpression::toGUI() const { switch (_type) { case invalid: // There isn't a great answer for this, but this is correct. return ""; case simpleNumber: return ntoa(_value); case divideByPrice: return "**p" + ntoa(_value); case divideByVolume: return "**v" + ntoa(_value); case multiplyByPrice: return "**P" + ntoa(_value); case multiplyByVolume: return "**V" + ntoa(_value); } // We should never get here, but the compiler complains anyway. return ""; } // It's better to avoid this function. Use one of the higher level functions // in this case to make sure we handle the type correctly. double AlertConfig::SimpleExpression::getValue() const { return _value; } AlertConfig::SimpleExpression::SimpleExpression() : _type(invalid), _value(NAN) { } AlertConfig::SimpleExpression::SimpleExpression(std::string encodedConfig) { if ((encodedConfig.size() >= 3) && (encodedConfig[0] == '*') && (encodedConfig[1] == '*')) { switch (encodedConfig[2]) { case 'p': // Note: divide by price means to divide the data field, i.e. the // thing that's stored in the database, by the price stored in the // database. Equivelantly, it means to multiply the user's input by // the price. _type = divideByPrice; break; case 'v': _type = divideByVolume; break; case 'P': _type = multiplyByPrice; break; case 'V': _type = multiplyByVolume; break; default: // Force an invalid value. encodedConfig.clear(); } encodedConfig.erase(0, 3); } else _type = simpleNumber; _value = strtodDefault(AllConfigInfo::removeCommas(encodedConfig), NAN); if (!finite(_value)) _type = invalid; } AlertConfig::SimpleExpression::SimpleExpression(double value) { if (!finite(value)) { _type = simpleNumber; _value = value; } else { _type = invalid; _value = NAN; } } ///////////////////////////////////////////////////////////////////// // AlertConfig ///////////////////////////////////////////////////////////////////// void AlertConfig::clear() { _activeAlerts.clear(); _alertQuality.clear(); _windowFilter.clear(); _activeExchanges.clear(); _symbolLists.clear(); _columnListConfig.clear(); _windowName.clear(); _sound.clear(); _clientCookie.clear(); } bool AlertConfig::hasConfigInfo(PropertyList const &rawConfig) { return rawConfig.count("O") || rawConfig.count("form"); } void AlertConfig::quickSymbolListInfo(std::string const &rawConfig, XmlNode &parent) { PropertyList propertyList; parseUrlRequest(propertyList, rawConfig); SymbolLists symbolLists; symbolLists.getFromConfigWindow(propertyList); symbolLists.getValuesForEditor(parent); } void AlertConfig::load(std::string rawConfig, UserId userId, DatabaseWithRetry &database, bool allowCustomColumns, bool allowNonFilterColumns) { PropertyList propertyList; parseUrlRequest(propertyList, rawConfig); load(propertyList, userId, database, allowCustomColumns, allowNonFilterColumns); } // Some filters have been renamed. We use this function to read in a value // using the old name, but store it internally as if it came to us with the // new name. When we export the configuration, we always use the new name. void AlertConfig::loadOldFilter(PropertyList const &rawConfig, std::string const &oldName, std::string const &newName, bool negate) { double filter = strtodDefault(AllConfigInfo::removeCommas(getPropertyDefault(rawConfig, oldName)), NAN); if (finite(filter)) { if (negate) { filter = -filter; } _windowFilter[newName] = SimpleExpression(filter); } } void AlertConfig::loadWSFPair(std::string const &baseName, PropertyList const &rawConfig) { loadWSF("Min" + baseName, rawConfig); loadWSF("Max" + baseName, rawConfig); } void AlertConfig::loadWSF(std::string const &fullName, PropertyList const &rawConfig) { SimpleExpression filter(getPropertyDefault(rawConfig, fullName)); if (filter.isValid()) _windowFilter[fullName] = filter; } void AlertConfig::load(PropertyList const &rawConfig, UserId userId, DatabaseWithRetry &database, bool allowCustomColumns, bool allowNonFilterColumns) { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); clear(); bool fromConfigWindow = false; if (rawConfig.count("O")) { std::vector< std::string > pieces = explode("_", getPropertyDefault(rawConfig, std::string("O"))); while (pieces.size() < 3) { pieces.push_back(""); } _activeAlerts.loadFromHex(pieces[0]); _activeAlerts &= allConfigInfo.getLegalAlerts(); _activeExchanges.loadFromHex(pieces[1]); _activeExchanges &= allConfigInfo.getLegalExchanges(); } else { fromConfigWindow = true; for (AllConfigInfo::AlertIterator it = allConfigInfo.firstAlert(); it != allConfigInfo.endAlert(); it++) { AllConfigInfo::AlertInfo const &info = **it; if (rawConfig.count("Sh_" + info.shortName)) { _activeAlerts.set(info.asBit); } } for (AllConfigInfo::ExchangeIterator it = allConfigInfo.firstExchange(); it != allConfigInfo.endExchange(); it++) { AllConfigInfo::ExchangeInfo const &info = **it; if (rawConfig.count(info.formName)) { _activeExchanges.set(info.asBit); } } } AllConfigInfo::ExchangeInfo const *listed = allConfigInfo.getExchange("XL"); if (listed && _activeExchanges.get(listed->asBit)) { _activeExchanges.clear(listed->asBit); if (AllConfigInfo::ExchangeInfo const *NYSE = allConfigInfo.getExchange("X_NYSE")) { _activeExchanges.set(NYSE->asBit); } if (AllConfigInfo::ExchangeInfo const *AMEX = allConfigInfo.getExchange("X_AMEX")) { _activeExchanges.set(AMEX->asBit); } } AllConfigInfo::ExchangeInfo const *OTC = allConfigInfo.getExchange("X_OTC"); if (OTC && _activeExchanges.get(OTC->asBit)) { _activeExchanges.clear(listed->asBit); if (AllConfigInfo::ExchangeInfo const *QB = allConfigInfo.getExchange("X_OTCQB")) { _activeExchanges.set(QB->asBit); } if (AllConfigInfo::ExchangeInfo const *QX = allConfigInfo.getExchange("X_OTCQX")) { _activeExchanges.set(QX->asBit); } } for (AllConfigInfo::AlertIterator it = allConfigInfo.firstAlert(); it != allConfigInfo.endAlert(); it++) { AllConfigInfo::AlertInfo const &info = **it; if (_activeAlerts.get(info.asBit)) { SimpleExpression filter = SimpleExpression(getPropertyDefault(rawConfig, "Q" + info.shortName)); if (filter.isValid()) { _alertQuality[info.asBit] = filter; } } } loadOldFilter(rawConfig, "PF1", "MinDec", false); loadOldFilter(rawConfig, "PF2", "MaxDec", false); loadOldFilter(rawConfig, "MinGDR", "MaxGUR", true); loadOldFilter(rawConfig, "MinGDP", "MaxGUP", true); loadOldFilter(rawConfig, "MinGDD", "MaxGUD", true); PairedFilterList filters(userId, database, false, true); for (PairedFilterList::Iterator it = filters.begin(); it != filters.end(); it++) loadWSFPair((*it)->baseName, rawConfig); _symbolLists.getFromConfigWindow(rawConfig); if (allowCustomColumns) { PairedFilterList pairedFilterList(userId, database, false, !allowNonFilterColumns); _columnListConfig.getFromConfigWindow(rawConfig, pairedFilterList, allowNonFilterColumns); } // else column list is empty. _sound = getPropertyDefault(rawConfig, "S"); if (_sound == "-1") { _sound = getPropertyDefault(rawConfig, "S_OTHER"); } _windowName = getPropertyDefault(rawConfig, "WN"); // We always fix things. This used to be a choice. if (_activeExchanges.empty()) { _activeExchanges = allConfigInfo.getDefaultExchanges(); } // This is a nasty hack. We always turn on all the exchanges. But we // only turn on the heartbeat if we are coming from the config window. // This has to satisfy a lot things. This allows us to have the empty // setting on the list of sample settings, without any special magic. // Scottrade has their own empty setting, so it's not enough for us to // change our own settings. But the chinesefn.com people have a very // old setting that looks like it came from the config window, and it // doesn't have any exchanges, and we want to turn on the exchanges for // them. if (fromConfigWindow && _activeAlerts.empty()) { _activeAlerts = allConfigInfo.getDefaultAlerts(); } _clientCookie.getFromConfigWindow(rawConfig); } std::string AlertConfig::save() const { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); std::string result = "O=" + _activeAlerts.asHex() + "_" + _activeExchanges.asHex() + "_0"; for (std::map< unsigned, SimpleExpression >::const_iterator it = _alertQuality.begin(); it != _alertQuality.end(); it++) result += "&Q" + allConfigInfo.getAlert(it->first)->shortName + "=" + it->second.toEncodedConfig(); for (std::map< std::string, SimpleExpression >::const_iterator it = _windowFilter.begin(); it != _windowFilter.end(); it++) result += "&" + it->first + "=" + it->second.toEncodedConfig(); if (!_windowName.empty()) result += "&WN=" + urlEncode(_windowName, false); std::string symbolListDescription = _symbolLists.asSaveConfig(); if (!symbolListDescription.empty()) result += "&SL=" + symbolListDescription; result += _columnListConfig.asSaveConfig(); if (!(_sound.empty() || (_sound == "0"))) { // 0 and "" are both thrown out, to be consistant with PHP. result += "&S=" + urlEncode(_sound, false); } result += _clientCookie.asSaveConfig(); return result; } std::string AlertConfig::alertTypeExpression() const { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); std::set< std::string > simpleAlerts; std::vector< std::string > expressions; for (BitSet::const_iterator it = _activeAlerts.begin(); it != _activeAlerts.end(); it++) { AllConfigInfo::AlertInfo const *info = allConfigInfo.getAlert(*it); SimpleExpression const *quality = getProperty(_alertQuality, *it); if (quality && info) { expressions.push_back("alert_type='" + info->shortName + "' AND (CONVERT(quality,CHAR)+0)>=" + quality->toDatabase()); } else { simpleAlerts.insert("'" + info->shortName + "'"); } } expressions.push_back(sqlIn("alert_type", simpleAlerts)); return sqlOr(expressions); } std::string AlertConfig::rangeExpression(std::string dbField, std::string minName, std::string maxName) const { dbField = '(' + dbField + ')'; SimpleExpression const *minValue = getProperty(_windowFilter, minName); SimpleExpression const *maxValue = getProperty(_windowFilter, maxName); if (minValue) { if (maxValue) { if (minValue->getValue() <= maxValue->getValue()) { // The value must fall within a certain range. // This is only guaranteed to be correct if both values are of // the same type. i.e. both simple numbers, or both ratios of // price. If they are of different types, I don't know what to // do anyway, so I take the simple path. return dbField + " BETWEEN " + minValue->toDatabase() + " AND " + maxValue->toDatabase(); } else { // The value must be outside of the range. return "(" + dbField + " >= " + minValue->toDatabase() + " OR " + dbField + " <= " + maxValue->toDatabase() + ")"; } } else return dbField + " >= " + minValue->toDatabase(); } else if (maxValue) return dbField + " <= " + maxValue->toDatabase(); else return sqlTrue; } std::string AlertConfig::exchangeExpression() const { return AllConfigInfo::instance().exchangeExpression(_activeExchanges); } std::string AlertConfig::allFiltersExpression(UserId userId, DatabaseWithRetry &database ) const { PairedFilterList filters(userId, database, false, true); std::vector< std::string > expressions; expressions.push_back(alertTypeExpression()); for (PairedFilterList::Iterator it = filters.begin(); it != filters.end(); it++) { AllConfigInfo::PairedFilterInfo const *info = *it; expressions.push_back(rangeExpression(info->sql, "Min" + info->baseName, "Max" + info->baseName)); } expressions.push_back(exchangeExpression()); return "(" + sqlAnd(expressions) + ")"; } void AlertConfig::customSql(UserId userId, DatabaseWithRetry &database, std::string selectFields, bool ascending, CustomSql &generator) const { if (!_columnListConfig.empty()) { // Add custom columns PairedFilterList pairedFilterList(userId, database, false, false); selectFields += _columnListConfig.selectSql(pairedFilterList); } generator._customColumns = _columnListConfig; const std::string priceField = "price"; generator._part1 = "SELECT " + PairedFilterList::fixPrice(selectFields, priceField) + " FROM "; // Insert table name here. // Insert alertIndexString() here. generator._part2 = " LEFT JOIN alerts_daily" " ON alerts.symbol=d_symbol AND date=DATE(timestamp)" " WHERE ("; // Insert where here. generator._part3 = ") AND " + PairedFilterList::fixPrice(allFiltersExpression(userId, database), priceField) + " AND " + _symbolLists.whereSql(userId) + " ORDER BY id" + (ascending?"":" DESC") + " LIMIT "; // Insert _limit here. //std::cout< 1) generator.setAlertIndex(CustomSql::aiIgnoreType); } BitSet AlertConfig::getValidExchanges(UserId userId, DatabaseWithRetry &database) { return (BitSet)database.tryQueryUntilSuccess("SELECT HEX(valid_exchanges) FROM users WHERE id=" + ntoa(userId))->getStringField(0, "0"); } bool AlertConfig::useHiddenSettings(UserId userId, DatabaseWithRetry &database) { if (userId) { std::string sql = "SELECT settings FROM users, hidden_settings " "WHERE users.wl_include = hidden_settings.wl_include " "AND name = '" + mysqlEscapeString(_windowName) + "' " "AND id=" + ntoa(userId); MysqlResultRef result = database.tryQueryUntilSuccess(sql); std::string newSettings = result->getStringField(0); if (!newSettings.empty()) { // TODO -- We are always assuming these are using the old client... // no custom columns. :( load(newSettings, userId, database, false, false); return true; } } return false; } void AlertConfig::removeIllegalData(UserId userId, DatabaseWithRetry &database) { useHiddenSettings(userId, database); if (userId) { _activeExchanges &= getValidExchanges(userId, database); } _symbolLists.removeIllegalLists(userId, database); _columnListConfig.removeIllegalData(userId, database); } void copyLanguages(XmlNode &dest, std::string const &baseName, PropertyList const &values) { for (PropertyList::const_iterator it = values.begin(); it != values.end(); it++) { const std::string value = it->second; if (!value.empty()) { std::string key; if (it->first.empty()) key = baseName; else key = it->first + '_' + baseName; dest.properties[key] = value; } } } void AlertConfig::generalInfo(XmlNode &node, UserId userId, DatabaseWithRetry &database) { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); for (AllConfigInfo::AlertIterator it = allConfigInfo.firstAlert(); it != allConfigInfo.endAlert(); it++) { AllConfigInfo::AlertInfo const &info = **it; XmlNode &alertNode = node["ALERT_TYPES"][-1]; alertNode.name = info.shortName; copyLanguages(alertNode, "DESCRIPTION", info.description); copyLanguages(alertNode, "QUALITY_NAME", info.qualityFilter); if (!info.direction.empty()) alertNode.properties["DIRECTION"] = info.direction; if (!info.qualityFormat.empty()) alertNode.properties["QUALITY_FORMAT"] = info.qualityFormat; } for (AllConfigInfo::ExchangeIterator it = allConfigInfo.firstExchange(); it != allConfigInfo.endExchange(); it++) if ((*it)->userVisible()) { XmlNode &exchangeNode = node["EXCHANGES"][-1]; exchangeNode.name = (*it)->formName; exchangeNode.properties["DESCRIPTION"] = (*it)->description; exchangeNode.properties["CODE"] = (*it)->shortName; } } void copyLanguageValue(XmlNode &dest, std::string const &propertyName, PropertyList const &values, std::string const &language) { // Look up the request as is. std::string const *value = getProperty(values, language); if ((!value) && (!language.empty())) // If we didn't find a value for a specific language, then try the generic // version. value = getProperty(values, std::string()); if (value && !value->empty()) // Only copy the value if we found something. It would be wasteful to // send "". It would also add a lot of useless stuff to the diffs, // as was the case in /home/phil/cpp_alert_server/live_server/ax_alert_server/Downloads/Misc/ETrade.Config.1.xml dest.properties[propertyName] = *value; } void AlertConfig::getForEditor(XmlNode &node, UserId userId, DatabaseWithRetry &database, bool disabledSupported, bool useFilterPairs, std::string const &language, bool allowSymbolListFolders, bool allowNegativeListIds) const { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); if (!_windowName.empty()) { node.properties["WINDOW_NAME"] = _windowName; } for (AllConfigInfo::AlertIterator it = allConfigInfo.firstAlert(); it != allConfigInfo.endAlert(); it++) { AllConfigInfo::AlertInfo const &info = **it; XmlNode &alertNode = node["ALERT_TYPES"][-1]; alertNode.name = info.shortName; copyLanguageValue(alertNode, "DESCRIPTION", info.description, language); if (_activeAlerts.get(info.asBit)) { alertNode.properties["SELECTED"] = "1"; } if (SimpleExpression const *quality = getProperty(_alertQuality, info.asBit)) { alertNode.properties["QUALITY_VALUE"] = quality->toGUI(); } copyLanguageValue(alertNode, "QUALITY_NAME", info.qualityFilter, language); copyLanguageValue(alertNode, "KEYWORDS", info.keywords, language); std::string &extendedKeywords = alertNode.properties["KEYWORDS"]; if (!extendedKeywords.empty()) { extendedKeywords += ' '; } if (info.direction == "+") { extendedKeywords += "bullish"; extendedKeywords += ' '; } else if (info.direction == "-") { extendedKeywords += "bearish"; extendedKeywords += ' '; } else if (info.direction == "") { extendedKeywords += "neutral"; extendedKeywords += ' '; } extendedKeywords +=".alert."; // The direction field works, but the client doesn't use it yet. //if (!info.direction.empty()) //{ // alertNode.properties["DIRECTION"] = info.direction; //} if (!info.flip.empty()) { alertNode.properties["FLIP"] = info.flip; } } if (useFilterPairs) { // Newer mode. Report MinPrice and MaxPrice together. // Get rid of the type and the current values. Those were only needed // for older clients who will never use this option. Notice that the // name of the tag is not the base name of the filter. Some base names // are not valid because they start with a number. This was not a // problem in the other version because we always added the "Min" or // "Max" prefix. PairedFilterList filterList(userId, database, false, true); for (AllConfigInfo::PairedFilterIterator it = filterList.begin(); it != filterList.end(); it++) { AllConfigInfo::PairedFilterInfo const &info = **it; XmlNode &filterNode = node["WINDOW_SPECIFIC_FILTERS"][-1]; filterNode.properties["BASE"] = info.baseName; copyLanguageValue(filterNode, "DESCRIPTION", info.description, language); copyLanguageValue(filterNode, "UNITS", info.units, language); if (!info.flip.empty()) filterNode.properties["FLIP"] = info.flip; copyLanguageValue(filterNode, "KEYWORDS", info.keywords, language); std::string &keywords = filterNode.properties["KEYWORDS"]; if (keywords.empty()) keywords = ".filter."; else keywords += " .filter."; // We should also add the word "custom" if it's a custom filter. // We used to do it that way. } } else { // Traditional mode. MinPrice is on one line, and MaxPrice is on the // next. for (AllConfigInfo::FilterIterator it = allConfigInfo.firstFilter(); it != allConfigInfo.endFilter(); it++) { AllConfigInfo::FilterInfo const &info = **it; XmlNode &filterNode = node["WINDOW_SPECIFIC_FILTERS"][-1]; filterNode.name = info.shortName; filterNode.properties["TYPE"] = "D"; filterNode.properties["DESCRIPTION"] = info.description; filterNode.properties["UNITS"] = info.units; if (SimpleExpression const *quality = getProperty(_windowFilter, info.shortName)) filterNode.properties["VALUE"] = quality->toGUI(); if (!info.flip.empty()) filterNode.properties["FLIP"] = info.flip; if (info.keywords.empty()) filterNode.properties["KEYWORDS"] = ".filter."; else filterNode.properties["KEYWORDS"] = info.keywords + " .filter."; } // echo "B,", // rawurlencode("Show only optionable symbols"), // ",,", // AX_write_bool($config_info['optionable']), // ",opt\n"; } BitSet validExchanges = getValidExchanges(userId, database); for (AllConfigInfo::ExchangeIterator it = allConfigInfo.firstExchange(); it != allConfigInfo.endExchange(); it++) { AllConfigInfo::ExchangeInfo const &info = **it; if (info.userVisible()) { // If a person is not logged in, or is logged in a DEMO, show // them all of the exchanges. Let them play with it. They can't // get real data, so it doesn't hurt us. bool entitled = (!userId) || validExchanges.get(info.asBit); if (entitled || disabledSupported) { XmlNode &exchangeNode = node["EXCHANGES"][-1]; exchangeNode.name = info.formName; exchangeNode.properties["DESCRIPTION"] = info.description; if (!entitled) { exchangeNode.properties["DISABLED"] = "1"; } else if (_activeExchanges.get(info.asBit)) { exchangeNode.properties["SELECTED"] = "1"; } } } } _symbolLists.getStructureForEditor(node, userId, allowSymbolListFolders, allowNegativeListIds, database); PairedFilterList columns(userId, database, false, false); ColumnListConfig::getStructureForEditor(node, userId, database, columns, language); } void AlertConfig::getSettingsForEditor(XmlNode &node) const { AllConfigInfo const &allConfigInfo = AllConfigInfo::instance(); if (!_windowName.empty()) { node.properties["WINDOW_NAME"] = _windowName; } for (AllConfigInfo::AlertIterator it = allConfigInfo.firstAlert(); it != allConfigInfo.endAlert(); it++) { AllConfigInfo::AlertInfo const &info = **it; if (_activeAlerts.get(info.asBit)) { XmlNode &alertNode = node["ALERT_TYPES"][-1]; alertNode.name = info.shortName; if (SimpleExpression const *quality = getProperty(_alertQuality, info.asBit)) { alertNode.properties["QUALITY"] = quality->toGUI(); } } } for (std::map< std::string, SimpleExpression >::const_iterator it = _windowFilter.begin(); it != _windowFilter.end(); it++) { XmlNode &filterNode = node["WINDOW_SPECIFIC_FILTERS"][-1]; filterNode.name = it->first; filterNode.properties["VALUE"] = it->second.toGUI(); } for (AllConfigInfo::ExchangeIterator it = allConfigInfo.firstExchange(); it != allConfigInfo.endExchange(); it++) { AllConfigInfo::ExchangeInfo const &info = **it; if (info.userVisible() && _activeExchanges.get(info.asBit)) { XmlNode &exchangeNode = node["EXCHANGES"][-1]; exchangeNode.name = info.formName; } } _symbolLists.getValuesForEditor(node); _columnListConfig.getValuesForEditor(node); } void AlertConfig::saveToMru(UserId userId, DatabaseWithRetry &database) { if (!userId) { return; } const std::string userIdStr = ntoa(userId); const std::string options = save(); std::string sql = "REPLACE INTO view_mru VALUES (" + userIdStr + ", now(), '" + mysqlEscapeString(options) + "', md5('" + mysqlEscapeString(options) + "'))"; database.tryQueryUntilSuccess(sql); sql = "SELECT start_time FROM view_mru WHERE user_id=" + userIdStr + " ORDER BY start_time DESC LIMIT 99,1"; MysqlResultRef result = database.tryQueryUntilSuccess(sql); const std::string date = result->getStringField(0); if (!date.empty()) { sql = "DELETE FROM view_mru WHERE user_id=" + userIdStr + " AND start_time < '" + date + "'"; database.tryQueryUntilSuccess(sql); } } ///////////////////////////////////////////////////////////////////// // Unit Test ///////////////////////////////////////////////////////////////////// #ifdef UNIT_TEST_ALERT_CONFIG // g++ -Wall -ggdb -DUNIT_TEST_ALERT_CONFIG -L/usr/lib64/mysql -lmysqlclient -lpthread AlertConfig.C AlertConfigAuto.C ../shared/MiscSupport.C BitSet.C ../shared/DatabaseWithRetry.C ../shared/LogFile.C ../shared/FixedMalloc.C ../shared/PipeConditionVar.C ../shared/ThreadClass.C ../shared/Messages.C ../shared/SocketInfo.C ../shared/GlobalConfigFile.C XmlSupport.C ../shared/IPollSet.C ../shared/ThreadMonitor.C #include #include #include "../shared/GlobalConfigFile.h" std::string customSql(AlertConfig const &alertConfig, UserId userId, DatabaseWithRetry &database, std::string selectFields, bool ascending, std::string where, int limit) { // Typically the program will create a generator and reuse it a lot. This // function was moved to the test code because this is the only place where // we go directly from an AlertConfig to sql code. AlertConfig::CustomSql generator; alertConfig.customSql(userId, database, selectFields, ascending, generator); return generator.get(where, limit); } int main(int argc, char *argv[]) { const int userId=4; // This is the "phil" account! DatabaseWithRetry database("pablo", "only database"); // Use a specific name here so we don't need to use a config file. configItemsComplete(); // The log subsystem also looks at this, but the default is acceptable. AllConfigInfo::init(); while (std::cin) { std::string line; std::cin>>line; if (!std::cin) // Presumably someone hit control-D. break; AlertConfig alertConfig; alertConfig.load(line, userId, database); std::cout<