MQL5Book/Include/CalendarCache.mqh

824 lines
24 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| CalendarCache.mqh |
//| Copyright 2022, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#ifndef PRTF
#include "PRTF.mqh"
#endif
#include "Defines.mqh"
#include "QuickSortStructT.mqh"
//+------------------------------------------------------------------+
//| Helper struct with indexed storage of strings |
//+------------------------------------------------------------------+
template<typename T>
struct StringRef
{
static string cache[];
int index;
StringRef(): index(-1) { }
void operator=(const string s)
{
if(index == -1)
{
PUSH(cache, s);
index = ArraySize(cache) - 1;
}
else
{
cache[index] = s;
}
}
string operator[](int x = 0) const
{
if(index != -1)
{
return cache[index];
}
return NULL;
}
static bool save(const int handle)
{
FileWriteInteger(handle, ArraySize(cache));
for(int i = 0; i < ArraySize(cache); ++i)
{
FileWriteInteger(handle, StringLen(cache[i]));
FileWriteString(handle, cache[i]);
}
return true;
}
static bool load(const int handle)
{
const int n = FileReadInteger(handle);
for(int i = 0; i < n; ++i)
{
PUSH(cache, FileReadString(handle, FileReadInteger(handle)));
}
return true;
}
};
template<typename T>
static string StringRef::cache[];
//+------------------------------------------------------------------+
//| Main class for calendar cache |
//+------------------------------------------------------------------+
class CalendarCache
{
string context;
datetime from, to;
datetime t;
ulong eventId;
MqlCalendarValue values[];
MqlCalendarEvent events[];
MqlCalendarCountry countries[];
// indices of values by event_id, country, currency, time - ArraySort()-ed
ulong value2event[][2]; // [0] - event_id, [1] - value_id
ulong value2country[][2]; // [0] - country_id, [1] - value_id
ulong value2currency[][2]; // [0] - currency ushort[4]<->long, [1] - value_id
ulong value2time[][2]; // [0] - time, [1] - value_id
// mapping of ids into indices in MqlCalendar-arrays
int id4country[];
int id4event[];
int id4value[];
int collisions;
int worse;
int collided;
//+------------------------------------------------------------------+
//| Identificator hash support |
//+------------------------------------------------------------------+
static int size2prime(const int size)
{
static int primes[] =
{
17, 53, 97, 193, 389,
769, 1543, 3079, 6151,
12289, 24593, 49157, 98317,
196613, 393241, 786433, 1572869,
3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189,
805306457, 1610612741
};
const int pmax = ArraySize(primes);
for(int p = 0; p < pmax; ++p)
{
if(primes[p] >= 2 * size)
{
return primes[p];
}
}
return size;
}
int place(const ulong id, const int index, int &array[])
{
const int n = ArraySize(array);
int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hash-function
int attempt = 0;
while(array[p] != -1)
{
if(++attempt > n / 10)
{
return -1; // not added
}
#ifdef DEBUG_LOG
Print("Collision ", attempt, ": ", id, "=", index, " / ", array[p]);
#endif
p = (p + attempt) % n;
}
collisions += attempt;
worse = fmax(worse, attempt);
if(attempt) collided++;
array[p] = index;
return p;
}
template<typename S>
int find(const ulong id, const int &array[], const S &structs[])
{
const int n = ArraySize(array);
if(!n) return false;
int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hash-function
int attempt = 0;
while(structs[array[p]].id != id)
{
if(++attempt > n / 10)
{
return -1; // not found
}
p = (p + attempt) % n;
}
return array[p];
}
//+------------------------------------------------------------------+
//| Calendar data collecting, hashing and relational binding |
//+------------------------------------------------------------------+
bool update()
{
string country = NULL, currency = NULL;
if(StringLen(context) == 3)
{
currency = context;
}
else if(StringLen(context) == 2)
{
country = context;
}
Print("Reading online calendar base...");
reset();
if(!PRTF(CalendarValueHistory(values, from, to, country, currency))
|| (currency != NULL ?
!PRTF(CalendarEventByCurrency(currency, events)) :
!PRTF(CalendarEventByCountry(country, events)))
|| !PRTF(CalendarCountries(countries)))
{
// not ready, t = 0
}
else
{
t = TimeTradeServer();
}
return (bool)t;
}
bool hash()
{
Print("Hashing calendar...");
collisions = 0;
worse = 0;
collided = 0;
const int c = PRTF(ArraySize(countries));
PRTF(ArrayResize(id4country, size2prime(c)));
ArrayInitialize(id4country, -1);
for(int i = 0; i < c; ++i)
{
if(place(countries[i].id, i, id4country) == -1)
{
return false;
}
}
Print("Total collisions: ", collisions, ", worse:", worse,
", average: ", (float)collisions / collided, " in ", collided);
collisions = 0;
worse = 0;
collided = 0;
const int e = PRTF(ArraySize(events));
PRTF(ArrayResize(id4event, size2prime(e)));
ArrayInitialize(id4event, -1);
for(int i = 0; i < e && !IsStopped(); ++i)
{
if(place(events[i].id, i, id4event) == -1)
{
return false;
}
}
Print("Total collisions: ", collisions, ", worse:", worse,
", average: ", (float)collisions / collided, " in ", collided);
collisions = 0;
worse = 0;
collided = 0;
const int v = PRTF(ArraySize(values));
PRTF(ArrayResize(id4value, size2prime(v)));
ArrayInitialize(id4value, -1);
for(int i = 0; i < v && !IsStopped(); ++i)
{
if(place(values[i].id, i, id4value) == -1)
{
return false;
}
}
Print("Total collisions: ", collisions, ", worse:", worse,
", average: ", (float)collisions / collided, " in ", collided);
return true;
}
bool bind()
{
Print("Binding calendar tables...");
const int n = ArraySize(values);
ArrayResize(value2event, n);
ArrayResize(value2country, n);
ArrayResize(value2currency, n);
ArrayResize(value2time, n);
for(int i = 0; i < n; ++i)
{
value2event[i][0] = values[i].event_id;
value2event[i][1] = values[i].id;
const int e = find(values[i].event_id, id4event, events);
if(e == -1) return false;
value2country[i][0] = events[e].country_id;
value2country[i][1] = values[i].id;
const int c = find(events[e].country_id, id4country, countries);
if(c == -1) return false;
value2currency[i][0] = currencyId(countries[c].currency);
value2currency[i][1] = values[i].id;
value2time[i][0] = values[i].time;
value2time[i][1] = values[i].id;
}
ArraySort(value2event);
ArraySort(value2country);
ArraySort(value2currency);
ArraySort(value2time); // should it be already in chronologic order?
return true;
}
void reset()
{
ArrayFree(values);
ArrayFree(events);
ArrayFree(countries);
ArrayFree(value2country);
ArrayFree(value2currency);
ArrayFree(value2event);
ArrayFree(value2time);
ArrayFree(id4country);
ArrayFree(id4event);
ArrayFree(id4value);
}
public:
const static string CALENDAR_CACHE_HEADER;
//+------------------------------------------------------------------+
//| Public interface |
//+------------------------------------------------------------------+
CalendarCache(const string _context = NULL,
const datetime _from = 0, const datetime _to = 0):
context(_context), from(_from), to(_to), t(0), eventId(0)
{
if(from > to) // context is a filename 'flag'
{
if(!load(_context))
{
load(_context, FILE_COMMON);
}
}
else
{
if(!update() || !hash() || !bind())
{
t = 0;
}
}
}
string getContext() const
{
return context;
}
datetime getFrom() const
{
return from;
}
datetime getTo() const
{
return to;
}
bool isLoaded() const
{
return t != 0;
}
static ulong currencyId(const string s)
{
union CRNC4
{
ushort word[4];
ulong ul;
} v;
StringToShortArray(s, v.word);
return v.ul;
}
template<typename T1,typename T2>
void static store(T1 &array[], T2 &origin[])
{
ArrayResize(array, ArraySize(origin));
for(int i = 0; i < ArraySize(origin); ++i)
{
array[i] = origin[i];
}
}
template<typename T1,typename T2>
void static restore(T1 &array[], T2 &origin[])
{
ArrayResize(array, ArraySize(origin));
for(int i = 0; i < ArraySize(origin); ++i)
{
array[i] = origin[i][];
}
}
struct MqlCalendarCountryRef
{
ulong id;
StringRef<MqlCalendarCountry> name;
StringRef<MqlCalendarCountry> code;
StringRef<MqlCalendarCountry> currency;
StringRef<MqlCalendarCountry> currency_symbol;
StringRef<MqlCalendarCountry> url_name;
void operator=(const MqlCalendarCountry &c)
{
id = c.id;
name = c.name;
code = c.code;
currency = c.currency;
currency_symbol = c.currency_symbol;
url_name = c.url_name;
}
MqlCalendarCountry operator[](int x = 0) const
{
MqlCalendarCountry r;
r.id = id;
r.name = name[];
r.code = code[];
r.currency = currency[];
r.currency_symbol = currency_symbol[];
r.url_name = url_name[];
return r;
}
};
struct MqlCalendarEventRef
{
ulong id;
ENUM_CALENDAR_EVENT_TYPE type;
ENUM_CALENDAR_EVENT_SECTOR sector;
ENUM_CALENDAR_EVENT_FREQUENCY frequency;
ENUM_CALENDAR_EVENT_TIMEMODE time_mode;
ulong country_id;
ENUM_CALENDAR_EVENT_UNIT unit;
ENUM_CALENDAR_EVENT_IMPORTANCE importance;
ENUM_CALENDAR_EVENT_MULTIPLIER multiplier;
uint digits;
StringRef<MqlCalendarEvent> source_url;
StringRef<MqlCalendarEvent> event_code;
StringRef<MqlCalendarEvent> name;
void operator=(const MqlCalendarEvent &e)
{
id = e.id;
type = e.type;
sector = e.sector;
frequency = e.frequency;
time_mode = e.time_mode;
country_id = e.country_id;
unit = e.unit;
importance = e.importance;
multiplier = e.multiplier;
digits = e.digits;
source_url = e.source_url;
event_code = e.event_code;
name = e.name;
}
MqlCalendarEvent operator[](int x = 0) const
{
MqlCalendarEvent r;
r.id = id;
r.type = type;
r.sector = sector;
r.frequency = frequency;
r.time_mode = time_mode;
r.country_id = country_id;
r.unit = unit;
r.importance = importance;
r.multiplier = multiplier;
r.digits = digits;
r.source_url = source_url[];
r.event_code = event_code[];
r.name = name[];
return r;
}
};
bool save(string filename = NULL, const int flags = 0)
{
if(!t) return false;
MqlDateTime mdt;
TimeToStruct(t, mdt);
if(filename == NULL) filename = "calendar-" +
StringFormat("%04d-%02d-%02d-%02d-%02d.cal",
mdt.year, mdt.mon, mdt.day, mdt.hour, mdt.min);
int handle = PRTF(FileOpen(filename, FILE_WRITE | FILE_BIN | flags));
if(handle == INVALID_HANDLE) return false;
FileWriteString(handle, CALENDAR_CACHE_HEADER);
FileWriteString(handle, context, 4);
FileWriteLong(handle, from);
FileWriteLong(handle, to);
FileWriteLong(handle, t);
const int n = ArraySize(values);
FileWriteInteger(handle, n);
if(n > 0)
{
FileWriteArray(handle, values);
Print("First and last records: ", values[0].time, "-", values[n - 1].time);
}
MqlCalendarEventRef erefs[];
store(erefs, events);
FileWriteInteger(handle, ArraySize(erefs));
FileWriteArray(handle, erefs);
StringRef<MqlCalendarEvent>::save(handle);
MqlCalendarCountryRef crefs[];
store(crefs, countries);
FileWriteInteger(handle, ArraySize(crefs));
FileWriteArray(handle, crefs);
StringRef<MqlCalendarCountry>::save(handle);
FileClose(handle);
return true;
}
bool load(const string filename, const int flags = 0)
{
Print("Loading calendar cache ", filename);
t = 0;
int handle = PRTF(FileOpen(filename, FILE_READ | FILE_BIN | flags));
if(handle == INVALID_HANDLE) return false;
const string header = FileReadString(handle, StringLen(CALENDAR_CACHE_HEADER));
if(header != CALENDAR_CACHE_HEADER) return false;
reset();
ResetLastError();
context = FileReadString(handle, 4);
if(!StringLen(context)) context = NULL;
from = (datetime)FileReadLong(handle);
to = (datetime)FileReadLong(handle);
t = (datetime)FileReadLong(handle);
Print("Calendar cache interval: ", from, "-", to);
Print("Calendar cache saved at: ", t);
int n = FileReadInteger(handle);
if(n > 0)
{
FileReadArray(handle, values, 0, n);
Print("First and last records: ", values[0].time, "-", values[n - 1].time);
}
MqlCalendarEventRef erefs[];
n = FileReadInteger(handle);
FileReadArray(handle, erefs, 0, n);
StringRef<MqlCalendarEvent>::load(handle);
restore(events, erefs);
MqlCalendarCountryRef crefs[];
n = FileReadInteger(handle);
FileReadArray(handle, crefs, 0, n);
StringRef<MqlCalendarCountry>::load(handle);
restore(countries, crefs);
FileClose(handle);
if(_LastError) Print("Error in load: ", _LastError);
const bool result = hash() && bind();
if(!result) t = 0;
return result;
}
//+------------------------------------------------------------------+
//| Methods to emulate standard calendar API |
//+------------------------------------------------------------------+
bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt)
{
const int index = find(country_id, id4country, countries);
if(index == -1) return false;
cnt = countries[index];
return true;
}
bool calendarEventById(ulong event_id, MqlCalendarEvent &event)
{
const int index = find(event_id, id4event, events);
if(index == -1) return false;
event = events[index];
return true;
}
static int ArrayBlowerBound(const ulong &array[][2], const ulong value, const int index)
{
if(index >= ArrayRange(array, 0)) return false;
if(array[index][0] != value) return index; // no exact match
for(int i = index - 1; i >= 0; --i)
{
if(array[i][0] != value) return i + 1;
}
return 0;
}
int calendarValueHistoryByEvent(ulong event_id, MqlCalendarValue &temp[],
datetime _from, datetime _to = 0)
{
if(_to == 0) _to = LONG_MAX;
ArrayFree(temp);
const int index = ArrayBsearch(value2event, event_id);
if(index < 0 || index >= ArrayRange(value2event, 0)) return 0;
int i = ArrayBlowerBound(value2event, event_id, index);
while(value2event[i][0] == event_id)
{
const ulong value_id = value2event[i][1];
const int p = find(value_id, id4value, values);
if(p != -1 && values[p].time >= _from && values[p].time < _to)
{
PUSH(temp, values[p]);
}
i++;
}
if(ArraySize(temp) > 0)
{
SORT_STRUCT(MqlCalendarValue, temp, time);
}
return ArraySize(temp);
}
int calendarValueHistory(MqlCalendarValue &temp[],
datetime _from, datetime _to = 0,
const string _code = NULL, const string _coin = NULL)
{
if(_to == 0) _to = LONG_MAX;
ArrayFree(temp);
ulong country_id = 0;
ulong currency_id = _coin != NULL ? currencyId(_coin) : 0;
if(_code != NULL)
{
for(int i = 0; i < ArraySize(countries); ++i)
{
if(countries[i].code == _code)
{
country_id = countries[i].id;
break;
}
}
}
// NB1: country and currency are considered more narrow filters than time,
// hence try to apply them in first place. This is debatable.
// NB2: if we manage to load actual times of changes into the cache,
// then selection by from/to should be applied to these times,
// instead of values[p].time
if(country_id)
{
const int index = ArrayBsearch(value2country, country_id);
if(index < 0 || index >= ArrayRange(value2country, 0)) return 0;
if(value2country[index][0] != country_id) return 0;
int i = ArrayBlowerBound(value2country, country_id, index);
while(value2country[i][0] == country_id)
{
const ulong value_id = value2country[i][1];
const int p = find(value_id, id4value, values);
if(p != -1 && values[p].time >= _from && values[p].time < _to)
{
PUSH(temp, values[p]);
}
i++;
}
}
else if(currency_id)
{
const int index = ArrayBsearch(value2currency, currency_id);
if(index < 0 || index >= ArrayRange(value2currency, 0)) return 0;
if(value2currency[index][0] != currency_id) return 0;
int i = ArrayBlowerBound(value2currency, currency_id, index);
while(value2currency[i][0] == currency_id)
{
const ulong value_id = value2currency[i][1];
const int p = find(value_id, id4value, values);
if(p != -1 && values[p].time >= _from && values[p].time < _to)
{
PUSH(temp, values[p]);
}
i++;
}
}
else if(_from) // no filters, only start and end time (optional)
{
const int index = ArrayBsearch(value2time, _from);
if(index < 0 || index >= ArrayRange(value2time, 0)) return 0;
int i = ArrayBlowerBound(value2time, value2time[index][0], index);
while(value2time[i][0] < (ulong)_from && i < ArrayRange(value2time, 0)) ++i;
if(i >= ArrayRange(value2time, 0)) return 0;
for(int j = i; j < ArrayRange(value2time, 0) && value2time[j][0] < (ulong)_to; ++j)
{
const int p = find(value2time[j][1], id4value, values);
if(p != -1)
{
PUSH(temp, values[p]);
}
}
}
else if(_to != LONG_MAX) // no filters, only end time
{
const int index = ArrayBsearch(value2time, _to);
if(index < 0 || index >= ArrayRange(value2time, 0)) return 0;
int i = ArrayBlowerBound(value2time, value2time[index][0], index);
while(value2time[i][0] >= (ulong)_to && i > 0) --i;
for(int j = 0; j <= i; ++j)
{
const int p = find(value2time[j][1], id4value, values);
if(p != -1)
{
PUSH(temp, values[p]);
}
}
}
else
{
ArrayCopy(temp, values);
}
if(ArraySize(temp) > 0)
{
SORT_STRUCT(MqlCalendarValue, temp, time);
}
return ArraySize(temp);
}
int calendarValueLast(ulong &change, MqlCalendarValue &result[],
const string code = NULL, const string currency = NULL)
{
/*
straightforward equivalent is shown in this comment, but
it's too slow for requests on short latest periods of time
const int n = change ? calendarValueHistory(result,
change, TimeTradeServer(), code, currency) : 0;
change = TimeTradeServer();
return n;
*/
if(!change)
{
change = TimeTradeServer();
return 0;
}
ulong country_id = 0;
ulong currency_id = currency != NULL ? currencyId(currency) : 0;
if(code != NULL)
{
for(int i = 0; i < ArraySize(countries); ++i)
{
if(countries[i].code == code)
{
country_id = countries[i].id;
break;
}
}
}
const ulong past = change;
const int index = ArrayBsearch(value2time, past);
if(index < 0 || index >= ArrayRange(value2time, 0)) return 0;
int i = ArrayBlowerBound(value2time, value2time[index][0], index);
while(value2time[i][0] <= (ulong)past && i < ArrayRange(value2time, 0)) ++i;
if(i >= ArrayRange(value2time, 0)) return 0;
for(int j = i; j < ArrayRange(value2time, 0) && value2time[j][0] <= (ulong)TimeTradeServer(); ++j)
{
const int p = find(value2time[j][1], id4value, values);
if(p != -1)
{
change = TimeTradeServer();
if(country_id != 0 || currency_id != 0)
{
const int q = find(values[p].event_id, id4event, events);
if(country_id != 0 && country_id != events[q].country_id) continue;
if(currency_id != 0)
{
const int m = find(events[q].country_id, id4country, countries);
if(countries[m].currency != currency) continue;
}
}
if(!eventId || eventId == values[p].event_id)
{
PUSH(result, values[p]);
}
}
}
return ArraySize(result);
}
int calendarValueLastByEvent(ulong event_id, ulong &change,
MqlCalendarValue &result[])
{
eventId = event_id; // enable internal filtering by event id for the next call
const int n = calendarValueLast(change, result);
change = TimeTradeServer();
eventId = 0;
return n;
}
};
const static string CalendarCache::CALENDAR_CACHE_HEADER = "MQL5 Calendar Cache\r\nv.1.0\r\n";
//+------------------------------------------------------------------+