//+------------------------------------------------------------------+ //| CalendarFilter.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ /* Filters: event ID country code currency code type sector importance date range (day/week/month/quarter in past/future) has (actual, forecast, previous, revised) event name full-text other properties May be applied multiple per property via OR/AND with EQUAL, NOT_EQUAL, GREATER conditions examples: CalendarFilter f; f.let(CALENDAR_IMPORTANCE_MODERATE, GREATER) // high priority news .let(CALENDAR_IMPACT_POSITIVE) // only with better actual value than expected .let(CALENDAR_TIMEMODE_DATETIME) // only events with exact timing .let("DE").let("FR") // collect 2 countries, OR... .let("USD").let("GBP") // ...collect 2 currencies (but do not combine these 2 conditions!) .let(TimeCurrent() - MONTH_LONG, TimeCurrent() + WEEK_LONG) // date range .let("farm"); // full-text search MqlCalendarValue records[]; f.select(records); */ #include "Defines.mqh" #include "IS.mqh" #include "CalendarDefines.mqh" #include "QuickSortStructT.mqh" #ifdef LOGGING #include "PRTF.mqh" #else #define PRTF #endif //+------------------------------------------------------------------+ //| Calendar filter class | //+------------------------------------------------------------------+ class CalendarFilter { protected: // initial (optional) selectors passed via ctor, invariants string context; datetime from, to; bool fixedDates; // true if 'from'/'to' assigned in ctor and can't be changed // common selectors for event fields, assigned via let-methods long selectors[][3]; // [0] - property, [1] - value, [2] - condition string stringCache[]; // cache of all strings in 'selectors' // specific selectors (country/currency/id/change) ulong change; // for last changes requests string country[], currency[]; ulong ids[]; // kinds of events //+------------------------------------------------------------------+ //| Block of virtual methods for calendar access | //+------------------------------------------------------------------+ virtual bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt) { return CalendarCountryById(country_id, cnt); } virtual bool calendarEventById(ulong event_id, MqlCalendarEvent &event) { return CalendarEventById(event_id, event); } virtual int calendarValueHistoryByEvent(ulong event_id, MqlCalendarValue &temp[], datetime _from, datetime _to = 0) { return CalendarValueHistoryByEvent(event_id, temp, _from, _to); } virtual int calendarValueHistory(MqlCalendarValue &temp[], datetime _from, datetime _to = 0, const string _code = NULL, const string _coin = NULL) { return CalendarValueHistory(temp, _from, _to, _code, _coin); } virtual int calendarValueLast(ulong &_change, MqlCalendarValue &result[], const string _code = NULL, const string _coin = NULL) { return CalendarValueLast(_change, result, _code, _coin); } virtual int calendarValueLastByEvent(ulong event_id, ulong &_change, MqlCalendarValue &result[]) { return CalendarValueLastByEvent(event_id, _change, result); } virtual void init() { fixedDates = from != 0 || to != 0; change = 0; if(StringLen(context) == 3) { PUSH(currency, context); } else { // even if context is NULL, we save it to request entire calendar PUSH(country, context); } } //+------------------------------------------------------------------+ //| Block of overloads to resolve specific ENUM into PROPERTY | //+------------------------------------------------------------------+ ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_TYPE e) { return CALENDAR_PROPERTY_EVENT_TYPE; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_SECTOR e) { return CALENDAR_PROPERTY_EVENT_SECTOR; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_FREQUENCY e) { return CALENDAR_PROPERTY_EVENT_FREQUENCY; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_TIMEMODE e) { return CALENDAR_PROPERTY_EVENT_TIMEMODE; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_UNIT e) { return CALENDAR_PROPERTY_EVENT_UNIT; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_IMPORTANCE e) { return CALENDAR_PROPERTY_EVENT_IMPORTANCE; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_MULTIPLIER e) { return CALENDAR_PROPERTY_EVENT_MULTIPLIER; } ENUM_CALENDAR_PROPERTY resolve(const ENUM_CALENDAR_EVENT_IMPACT e) { return CALENDAR_PROPERTY_RECORD_IMPACT; } //+------------------------------------------------------------------+ //| Comparison methods for scalars of different prop types | //+------------------------------------------------------------------+ template static bool equal(const V v1, const V v2) { return v1 == v2; } template static bool greater(const V v1, const V v2) { return v1 > v2; } static bool equal(const string v1, const string v2) { if(StringFind(v2, "*") > -1) { int previous = 0; string words[]; const int n = StringSplit(v2, '*', words); for(int i = 0; i < n; ++i) { if(StringLen(words[i]) == 0) continue; int index = StringFind(v1, words[i], previous); if(index == -1) { return false; } previous = index + StringLen(words[i]); } return true; } else if(v2[0] == '\'' && v2[StringLen(v2) - 1] == '\'') { return v1 == v2; } return StringFind(v1, v2) > -1; } //+------------------------------------------------------------------+ //| Internal filtering stuff (condition checks) | //+------------------------------------------------------------------+ void filter(MqlCalendarValue &result[]) { #ifdef LOGGING PrintFormat("Filtering %d records", ArraySize(result)); #endif for(int i = ArraySize(result) - 1; i >= 0; --i) { if(!match(result[i])) { ArrayRemove(result, i, 1); } } } bool match(const MqlCalendarValue &v) { int or_totals = 0, or_matches = 0; MqlCalendarEvent event; if(!calendarEventById(v.event_id, event)) return false; for(int j = 0; j < ArrayRange(selectors, 0); ++j) { // retrieve selector value long field = 0; string text = NULL; switch((int)selectors[j][0]) { // NB: multiple countries or currencies are supported by // multiple calls to Calendar-functions, so records presented to 'match' // are already selected according to the filter // CALENDAR_PROPERTY_COUNTRY_CODE, // +string (2 chars) // CALENDAR_PROPERTY_COUNTRY_CURRENCY, // +string (3 chars) case CALENDAR_PROPERTY_EVENT_ID: field = (long)event.id; break; case CALENDAR_PROPERTY_EVENT_TYPE: field = event.type; break; case CALENDAR_PROPERTY_EVENT_SECTOR: field = event.sector; break; case CALENDAR_PROPERTY_EVENT_FREQUENCY: field = event.frequency; break; case CALENDAR_PROPERTY_EVENT_TIMEMODE: field = event.time_mode; break; case CALENDAR_PROPERTY_EVENT_UNIT: field = event.unit; break; case CALENDAR_PROPERTY_EVENT_IMPORTANCE: field = event.importance; break; case CALENDAR_PROPERTY_EVENT_MULTIPLIER: field = event.multiplier; break; case CALENDAR_PROPERTY_EVENT_SOURCE: text = event.source_url; break; case CALENDAR_PROPERTY_EVENT_NAME: text = event.name; break; case CALENDAR_PROPERTY_RECORD_REVISION: field = v.revision; break; case CALENDAR_PROPERTY_RECORD_IMPACT: field = v.impact_type; break; case CALENDAR_PROPERTY_RECORD_ACTUAL: field = v.actual_value; break; case CALENDAR_PROPERTY_RECORD_PREVIOUS: field = v.prev_value; break; case CALENDAR_PROPERTY_RECORD_REVISED: field = v.revised_prev_value; break; case CALENDAR_PROPERTY_RECORD_PREVISED: field = v.revised_prev_value != LONG_MIN ? v.revised_prev_value : v.prev_value; break; case CALENDAR_PROPERTY_RECORD_FORECAST: field = v.forecast_value; break; case CALENDAR_PROPERTY_RECORD_PERIOD: field = v.period; break; } // compare the value with the record if(text == NULL) { switch((IS)selectors[j][2]) { case EQUAL: if(!equal(field, selectors[j][1])) return false; break; case NOT_EQUAL: if(equal(field, selectors[j][1])) return false; break; case GREATER: if(!greater(field, selectors[j][1])) return false; break; case LESS: if(greater(field, selectors[j][1])) return false; break; case OR_EQUAL: or_totals++; if(equal(field, selectors[j][1])) or_matches++; break; } } else { const string find = stringCache[(int)selectors[j][1]]; switch((IS)selectors[j][2]) { case EQUAL: if(!equal(text, find)) return false; break; case NOT_EQUAL: if(equal(text, find)) return false; break; case GREATER: if(!greater(text, find)) return false; break; case LESS: if(greater(text, find)) return false; break; case OR_EQUAL: or_totals++; if(equal(text, find)) or_matches++; break; } } } if(or_totals > 0) return or_matches > 0; return true; } int insertDelimiter(MqlCalendarValue &result[]) { static const MqlCalendarValue empty[1] = {}; for(int i = 1; i < ArraySize(result); ++i) { if(result[i].time > TimeTradeServer() && result[i - 1].time <= TimeTradeServer()) { ArrayInsert(result, empty, i); return i; } } return -1; } public: CalendarFilter(const string _context = NULL, const datetime _from = 0, const datetime _to = 0): context(_context), from(_from), to(_to) { init(); } virtual bool isLoaded() const { return true; // will be overriden in descendants } void describe() const { Print("-= Calendar filter description =-"); Print("Dates: ", from, "-", to); Print("Countries:"); ArrayPrint(country); Print("Currencies:"); ArrayPrint(currency); Print("Event IDs:"); ArrayPrint(ids); Print("Selectors:"); ArrayPrint(selectors); Print("Strings:"); ArrayPrint(stringCache); } template static string stringify(const E e) { string result = EnumToString(e); string words[]; if(StringSplit(result, '_', words) > 0) { return words[ArraySize(words) - 1]; } return result; } //+------------------------------------------------------------------+ //| Block of let-methods to setup conditions on event properties | //+------------------------------------------------------------------+ CalendarFilter *let(const int r, const IS c = EQUAL) { const int n = EXPAND(selectors); selectors[n][0] = CALENDAR_PROPERTY_RECORD_REVISION; selectors[n][1] = r; selectors[n][2] = c; return &this; } CalendarFilter *let(const ulong event) { PUSH(ids, event); return &this; } CalendarFilter *let(const long value, const ENUM_CALENDAR_PROPERTY property, const IS c = EQUAL) { /* fields covered by this overload: CALENDAR_PROPERTY_RECORD_PERIOD, CALENDAR_PROPERTY_RECORD_ACTUAL, CALENDAR_PROPERTY_RECORD_PREVIOUS, CALENDAR_PROPERTY_RECORD_REVISED, CALENDAR_PROPERTY_RECORD_FORECAST, */ const int n = EXPAND(selectors); selectors[n][0] = property; selectors[n][1] = value; selectors[n][2] = c; return &this; } template CalendarFilter *let(const E e, const IS c = EQUAL) { // enumerations are processed here const int n = EXPAND(selectors); selectors[n][0] = resolve(e); selectors[n][1] = e; selectors[n][2] = c; return &this; } CalendarFilter *let(const datetime _from, const datetime _to = 0) { if(!fixedDates) // we can narrow down complete date range, { // but do not change it if already specified in ctor from = _from; to = _to; } else { PrintFormat("A limited date range %s-%s was cached in filter constructor," " can't change it ad hoc", (string)from, (string)to); } return &this; } CalendarFilter *let(const string find, const IS c = EQUAL) { const int wildcard = (StringFind(find, "*") + 1) * 10; switch(StringLen(find) + wildcard) { case 0: case 1: break; case 2: // if initial context was other than country, we can mix it with a country if(StringLen(context) != 2) { if(ArraySize(country) == 1 && StringLen(country[0]) == 0) // narrowing all countries to specific one { country[0] = find; } else { PUSH(country, find); } } else { PrintFormat("Specific country '%s' was cached in filter constructor," " can't change it ad hoc", context); } break; case 3: if(StringLen(context) != 3) // initially selected currency can not be changed { PUSH(currency, find); } else { PrintFormat("Specific currency '%s' was cached in filter constructor," " can't change it ad hoc", context); } break; default: { const int n = EXPAND(selectors); PUSH(stringCache, find); if(StringFind(find, "http://") == 0 || StringFind(find, "https://") == 0) { selectors[n][0] = CALENDAR_PROPERTY_EVENT_SOURCE; } else { selectors[n][0] = CALENDAR_PROPERTY_EVENT_NAME; } selectors[n][1] = ArraySize(stringCache) - 1; selectors[n][2] = c; break; } } return &this; } void reset() { ArrayFree(selectors); ArrayFree(stringCache); ArrayFree(ids); ArrayFree(country); ArrayFree(currency); if(context != NULL) { if(StringLen(context) == 3) PUSH(currency, context); else PUSH(country, context); } if(!fixedDates) { from = to = 0; } change = 0; } //+------------------------------------------------------------------+ //| Main public methods | //+------------------------------------------------------------------+ bool select(MqlCalendarValue &result[], const bool delimiter = false, const int limit = -1) { int count = 0; #ifdef LOGGING Print("Selecting calendar records... (now:", TimeCurrent(), ", server:", TimeTradeServer(), ")"); #endif ArrayFree(result); if(ArraySize(ids)) // some kinds of events specified { for(int i = 0; i < ArraySize(ids); ++i) { #ifdef LOGGING PRTF(ids[i]); #endif MqlCalendarValue temp[]; if(PRTF(calendarValueHistoryByEvent(ids[i], temp, from, to))) { ArrayCopy(result, temp, ArraySize(result)); ++count; } } } else { // many countries or currencies if(ArraySize(country) > ArraySize(currency)) { const string c = ArraySize(currency) > 0 ? currency[0] : NULL; for(int i = 0; i < ArraySize(country); ++i) { #ifdef LOGGING PRTF(country[i]); #endif MqlCalendarValue temp[]; if(PRTF(calendarValueHistory(temp, from, to, country[i], c))) { ArrayCopy(result, temp, ArraySize(result)); ++count; } } } else { const string c = ArraySize(country) > 0 ? country[0] : NULL; for(int i = 0; i < ArraySize(currency); ++i) { #ifdef LOGGING PRTF(currency[i]); #endif MqlCalendarValue temp[]; if(PRTF(calendarValueHistory(temp, from, to, c, currency[i]))) { ArrayCopy(result, temp, ArraySize(result)); ++count; } } } } // get current change_id change = 0; MqlCalendarValue dummy[]; calendarValueLast(change, dummy); if(ArraySize(result) > 0) { filter(result); } if(count > 1 && ArraySize(result) > 1) { #ifdef LOGGING PrintFormat("Sorting %d records by time", ArraySize(result)); #endif SORT_STRUCT(MqlCalendarValue, result, time); } if(ArraySize(result) > 1) { int now = -1; if(delimiter) { now = insertDelimiter(result); } if(limit > 0 && limit < ArraySize(result)) { #ifdef LOGGING PrintFormat("Limit %d is exceeded with %d records ('now' is at index %d)", limit, ArraySize(result), now); #endif if(now == -1) { ArrayResize(result, limit); } else { int start = fmax(now - limit, 0); int stop = fmin(now + limit, ArraySize(result) - 1); if(stop - start > limit) { const int excess = (stop - start - limit) / 2; const int odd = (stop - start - limit) % 2; start += excess; stop -= excess + odd; } #ifdef LOGGING PrintFormat("Removing records outside %d to %d", start, stop); #endif if(stop < ArraySize(result)) ArrayRemove(result, stop); if(start > 0) ArrayRemove(result, 0, start); } } } #ifdef LOGGING PrintFormat("Got %d records", ArraySize(result)); #endif return ArraySize(result) > 0; } bool update(MqlCalendarValue &result[]) { ArrayFree(result); // 'update' is only applicable after 'select' call or previous 'update' if(change == 0) { calendarValueLast(change, result); return false; } int count = 0; // number of combined requests with results ulong _change = 0; // change placeholder during partial requests ulong new_change = 0; // final change id after all requests done if(ArraySize(ids)) { for(int i = 0; i < ArraySize(ids); ++i) { MqlCalendarValue temp[]; _change = change; if(calendarValueLastByEvent(ids[i], _change, temp)) { ArrayCopy(result, temp, ArraySize(result)); ++count; } new_change = fmax(new_change, _change); } } else { // many countries or currencies if(ArraySize(country) > ArraySize(currency)) { const string c = ArraySize(currency) > 0 ? currency[0] : NULL; for(int i = 0; i < ArraySize(country); ++i) { MqlCalendarValue temp[]; _change = change; if(calendarValueLast(_change, temp, country[i], c)) { ArrayCopy(result, temp, ArraySize(result)); ++count; } new_change = fmax(new_change, _change); } } else { const string c = ArraySize(country) > 0 ? country[0] : NULL; for(int i = 0; i < ArraySize(currency); ++i) { MqlCalendarValue temp[]; _change = change; if(calendarValueLast(_change, temp, c, currency[i])) { ArrayCopy(result, temp, ArraySize(result)); ++count; } new_change = fmax(new_change, _change); } } } change = new_change; if(ArraySize(result) > 0) { filter(result); } if(count > 1 && ArraySize(result) > 1) { #ifdef LOGGING PrintFormat("Sorting %d records by time", ArraySize(result)); #endif SORT_STRUCT(MqlCalendarValue, result, time); } return ArraySize(result) > 0; } ulong getChangeID() const { return change; } bool format(const MqlCalendarValue &data[], const ENUM_CALENDAR_PROPERTY &props[], string &result[], const bool padding = false, const bool header = false) { const int cols = ArraySize(props); const int rows = ArraySize(data); if(!cols || !(rows + header)) return false; if(ArrayResize(result, cols * (rows + header)) <= 0) return false; int c = 0; int widths[]; if(header) { for(int j = 0; j < cols; ++j) { result[c++] = stringify(props[j]); } ArrayResize(widths, cols); ArrayInitialize(widths, 0); } for(int i = 0; i < rows; ++i) { MqlCalendarValue v = data[i]; MqlCalendarEvent event = {}; MqlCalendarCountry cnt = {}; if(!v.event_id || !calendarEventById(v.event_id, event)) { for(int j = 0; j < cols; ++j) result[c++] = ShortToString(0x2014); continue; } if(!event.country_id || !calendarCountryById(event.country_id, cnt)) { for(int j = 0; j < cols; ++j) result[c++] = ShortToString(0x2014); continue; } for(int j = 0; j < cols; ++j) { // retrieve field value switch((int)props[j]) { case CALENDAR_PROPERTY_COUNTRY_CODE: result[c++] = cnt.code; break; case CALENDAR_PROPERTY_COUNTRY_CURRENCY: result[c++] = cnt.currency; break; case CALENDAR_PROPERTY_EVENT_TYPE: result[c++] = stringify(event.type); break; case CALENDAR_PROPERTY_EVENT_SECTOR: result[c++] = stringify(event.sector); break; case CALENDAR_PROPERTY_EVENT_FREQUENCY: result[c++] = stringify(event.frequency); break; case CALENDAR_PROPERTY_EVENT_TIMEMODE: result[c++] = stringify(event.time_mode); break; case CALENDAR_PROPERTY_EVENT_UNIT: result[c++] = stringify(event.unit); break; case CALENDAR_PROPERTY_EVENT_IMPORTANCE: result[c++] = stringify(event.importance); break; case CALENDAR_PROPERTY_EVENT_MULTIPLIER: result[c++] = stringify(event.multiplier); break; case CALENDAR_PROPERTY_EVENT_SOURCE: result[c++] = event.source_url; break; case CALENDAR_PROPERTY_EVENT_NAME: result[c++] = event.name; break; case CALENDAR_PROPERTY_EVENT_DIGITS: result[c++] = (string)event.digits; break; case CALENDAR_PROPERTY_RECORD_REVISION: result[c++] = (string)v.revision; break; case CALENDAR_PROPERTY_RECORD_IMPACT: result[c++] = stringify(v.impact_type); break; case CALENDAR_PROPERTY_RECORD_ACTUAL: result[c++] = StringFormat("%+.*f", event.digits, v.GetActualValue()); break; case CALENDAR_PROPERTY_RECORD_PREVIOUS: result[c++] = StringFormat("%+.*f", event.digits, v.GetPreviousValue()); break; case CALENDAR_PROPERTY_RECORD_REVISED: result[c++] = StringFormat("%+.*f", event.digits, v.GetRevisedValue()); break; case CALENDAR_PROPERTY_RECORD_PREVISED: result[c++] = StringFormat("%+.*f", event.digits, v.HasRevisedValue() ? v.GetRevisedValue() : v.GetPreviousValue()); break; case CALENDAR_PROPERTY_RECORD_FORECAST: result[c++] = StringFormat("%+.*f", event.digits, v.GetForecastValue()); break; case CALENDAR_PROPERTY_RECORD_TIME: result[c++] = TimeToString(v.time); break; case CALENDAR_PROPERTY_RECORD_PERIOD: result[c++] = TimeToString(v.period, TIME_DATE); break; } if(header) { widths[j] = fmax(widths[j], StringLen(result[c - 1])); } } } if(header && rows > 0) { for(int j = 0; j < cols; ++j) { if(widths[j] < StringLen(result[j]) - 1) { StringSetLength(result[j], widths[j]); // truncate result[j] += ShortToString(0x205E); // add ellipsis } } } if(padding) // for table pretty-printing in monotype logs { int width[]; ArrayResize(width, cols); ArrayInitialize(width, 0); for(int i = 0; i < rows + header; ++i) { for(int j = 0; j < cols; ++j) { width[j] = fmax(width[j], StringLen(result[i * cols + j])); } } for(int i = 0; i < rows + header; ++i) { for(int j = 0; j < cols; ++j) { const int pad = width[j] - StringLen(result[i * cols + j]); result[i * cols + j] = StringFormat("%*.*s%s", pad, pad, " ", result[i * cols + j]); } } } return true; } }; //+------------------------------------------------------------------+