MQL5Book/Include/CalendarFilter.mqh

920 lines
28 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| 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<typename V>
static bool equal(const V v1, const V v2)
{
return v1 == v2;
}
template<typename V>
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<typename E>
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<typename E>
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;
}
};
//+------------------------------------------------------------------+