Warrior_EA/Scripts/CalendarHistoryExport.mq5
AnimateDread dc6daa2ff9 fix(news): probe the calendar call instead of declaring the feed absent
The operator checked the Toolbox: the Calendar tab is FULL. Calendar data comes
from MetaQuotes' servers; the News tab is the broker's own feed and is separately
empty. So my conclusion this morning - "this broker delivers no calendar" - was
wrong, and it was built on proxies: an empty news.dat, which is the unrelated
news-headline subsystem, and the absence of any file named *calendar* on disk,
which only means the store is not named what I guessed. One glance at the tab
settled what three filesystem checks could not.

The real fact is narrower and stranger: CalendarValueHistory over 2010..now with
NULL country and NULL currency returns zero rows and error 0 - success, no data -
while the terminal plainly holds the data. So the script now probes before it
exports, reporting CalendarCountries() and then trying the call four ways with n
and error printed for each: (NULL,NULL), ("",""), a narrow recent range, and a
per-country loop over CalendarCountries(). It exports the first variant that
returns rows, preferring the widest, and if all four are empty it says so rather
than offering a cause.

Compile-verified in _claude_stage: 0 errors, 0 warnings. Deployed to the fleet
terminal's MQL5\Scripts so it can be dragged onto a chart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:31:06 -04:00

249 lines
12 KiB
MQL5

//+------------------------------------------------------------------+
//| CalendarHistoryExport.mq5 |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
#property version "1.00"
#property script_show_inputs
#property description "Dumps the terminal's whole economic calendar to Common\\Files, once."
#property description "Run it on ANY terminal that HAS a calendar - the folder is shared."
//--- WHY THIS EXISTS AND WHEN TO REACH FOR IT
//---
//--- Measured 2026-09-06 on the fleet terminal: CalendarValueHistory over 2010..now with a
//--- NULL country and a NULL currency returns ZERO rows and error 0 - success, no data -
//--- WHILE THE TOOLBOX CALENDAR TAB IS FULL. The data is present; the call is being refused.
//--- (I first read that as "this broker has no calendar", from an empty news.dat - which is
//--- the unrelated news-headline feed - and from no file named *calendar* on disk. The
//--- operator corrected it by looking at the tab. Absence of a filename I guessed at is not
//--- absence of data.)
//---
//--- So this script PROBES before it exports. It reports what CalendarCountries() knows, then
//--- tries the history call four ways and prints n and the error for each:
//--- A (NULL, NULL) the documented "everything", and the one that returns nothing
//--- B ("", "") in case this build wants empty strings rather than NULL
//--- C narrow range last InpProbeDays days, to separate "range too wide" from "no data"
//--- D per country CalendarCountries() then one call per country code, accumulated
//--- It exports the first variant that returns rows, preferring the widest. If they all come
//--- back empty the printout says which, and that is the fact to act on rather than a guess.
//---
//--- It is ALSO the way to use a different terminal: FILE_COMMON resolves to the SAME
//--- Common\Files folder for every standard MT5 install of one Windows user, so a terminal
//--- that can read its calendar writes the file the research already reads, and nothing about
//--- the fleet terminal has to change. A SCRIPT and not the EA because such a terminal is a
//--- borrowed one: it should not have to host a trading system, its symbols and its models
//--- just to hand over a CSV.
//---
//--- WHAT IT IS NOT. It is not Scripts/CalendarRecorder.mq5 and does not replace it. That
//--- one records releases AT release time, write-once, because actual_value is the
//--- POST-REVISION figure and a surprise built from history is contaminated in the
//--- flattering direction - strongest on exactly the events that were revised most. This
//--- dumps history, which is honest for precisely the columns that are never restated: the
//--- release TIME and the event's importance. Those are what the books' news blackout
//--- (T-5/T+15) and the two news setups actually need. research/newsdata.py keeps the
//--- surprise behind allow_surprise=False for the same reason.
//---
//--- Deliberately NO includes from the EA tree, the same argument CalendarRecorder makes:
//--- it has to compile on a machine that has none of it.
input datetime InpFrom = D'2010.01.01 00:00'; // History from
input int InpMinImportance = 0; // 0=all 1=low 2=moderate 3=high
input int InpProbeDays = 30; // Narrow-range probe, in days back
input string InpFileName = "Warrior_EA\\News\\calendar.csv";
#define CAL_SCALE 1000000.0
//--- Must match System/NewsExport.mqh column for column: research/newsdata.py reads whichever
//--- of the two wrote the file and cannot be asked to tell them apart.
#define NEWS_HEADER "time;country;currency;importance;event_id;value_id;name;actual;forecast;previous;revised;actual_raw;forecast_raw;previous_raw;impact;unit;multiplier;digits;sector;event_type"
//--- ';' is the separator and event names contain them, so they are replaced rather than
//--- quoted: a quoted field would need an escape convention the reader does not have.
string Esc(const string s)
{
string o = s;
StringReplace(o, ";", ",");
StringReplace(o, "\n", " ");
StringReplace(o, "\r", " ");
return(o);
}
//--- LONG_MIN is the terminal's "no value" sentinel. It becomes EMPTY, not zero: a figure
//--- reported as 0 is an impossible reading rather than a missing one, and the research has
//--- to be able to tell the two apart.
string Scaled(const long v, const int digits)
{
if(v == LONG_MIN)
return("");
return(DoubleToString(v / CAL_SCALE, (digits >= 0 && digits <= 8) ? digits : 6));
}
string Raw(const long v)
{
if(v == LONG_MIN)
return("");
return(IntegerToString(v));
}
//--- One attempt, reported. Returns the row count and leaves the rows in `out`.
int Try(MqlCalendarValue &out[], const string label, const datetime from, const datetime to,
const string country, const string ccy)
{
ArrayFree(out);
ResetLastError();
const int n = CalendarValueHistory(out, from, to, country, ccy);
const int err = GetLastError();
PrintFormat(" %-28s n=%d error=%d", label, n, err);
return(n > 0 ? n : 0);
}
void OnStart()
{
if(MQLInfoInteger(MQL_TESTER))
{
Print("CalendarHistoryExport: the calendar API returns nothing in the tester. "
"Run this on a live chart.");
return;
}
const datetime now = TimeCurrent();
//--- WHAT THE TERMINAL ADMITS TO KNOWING. If this is 0 the calendar base really is absent and
//--- no variant below can help; if it is ~30 the base is loaded and the fault is in the query.
MqlCalendarCountry countries[];
ResetLastError();
const int nc = CalendarCountries(countries);
PrintFormat("CalendarHistoryExport: CalendarCountries() = %d (error %d). Probing the history "
"call - the Toolbox Calendar tab being full means the data is here somewhere.",
nc, GetLastError());
MqlCalendarValue values[];
int n = Try(values, "A (NULL, NULL) full range", InpFrom, now, NULL, NULL);
if(n == 0)
n = Try(values, "B (\"\", \"\") full range", InpFrom, now, "", "");
if(n == 0 && InpProbeDays > 0)
n = Try(values, "C narrow range", now - (datetime)InpProbeDays * 86400, now, NULL, NULL);
if(n == 0 && nc > 0)
{
//--- D: one call per country, accumulated. Slower, but it is the variant that does not
//--- depend on NULL meaning "all" - and if THIS is the one that works, that is the answer.
Print(" D per country - accumulating:");
MqlCalendarValue part[];
for(int i = 0; i < nc; i++)
{
ArrayFree(part);
ResetLastError();
const int m = CalendarValueHistory(part, InpFrom, now, countries[i].code, NULL);
if(m <= 0)
continue;
const int at = ArraySize(values);
ArrayResize(values, at + m);
for(int j = 0; j < m; j++)
values[at + j] = part[j];
}
n = ArraySize(values);
PrintFormat(" %-28s n=%d", "D per country total", n);
}
if(n <= 0)
{
PrintFormat("CalendarHistoryExport: every variant returned nothing (countries=%d). The "
"Calendar TAB reads the terminal's own store directly, so a full tab with an "
"empty API is a terminal-side refusal, not a missing feed - report the four "
"lines above rather than assuming which cause it is.", nc);
return;
}
//--- Written under a temp name and moved into place, so no reader can ever see half a file.
const string tmp = InpFileName + ".tmp";
const int h = FileOpen(tmp, FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON |
FILE_SHARE_READ | FILE_SHARE_WRITE);
if(h == INVALID_HANDLE)
{
PrintFormat("CalendarHistoryExport: cannot open %s (%d)", tmp, GetLastError());
return;
}
FileWriteString(h, NEWS_HEADER + "\n");
//--- event and country lookups repeat heavily and are per-id, so they are cached by id
ulong evIds[];
string evName[], evCountry[], evCurrency[];
int evImp[], evUnit[], evMult[], evDig[], evSector[], evType[];
int written = 0, skipped = 0;
datetime lo = 0, hi = 0;
for(int i = 0; i < n; i++)
{
MqlCalendarValue v = values[i];
int k = -1;
for(int j = ArraySize(evIds) - 1; j >= 0; j--)
if(evIds[j] == v.event_id)
{
k = j;
break;
}
if(k < 0)
{
MqlCalendarEvent ev;
if(!CalendarEventById(v.event_id, ev))
{
skipped++;
continue;
}
MqlCalendarCountry co;
const bool okc = CalendarCountryById(ev.country_id, co);
k = ArraySize(evIds);
ArrayResize(evIds, k + 1); ArrayResize(evName, k + 1);
ArrayResize(evImp, k + 1); ArrayResize(evUnit, k + 1);
ArrayResize(evMult, k + 1); ArrayResize(evDig, k + 1);
ArrayResize(evSector, k + 1); ArrayResize(evType, k + 1);
ArrayResize(evCountry, k + 1); ArrayResize(evCurrency, k + 1);
evIds[k] = v.event_id;
evName[k] = Esc(ev.name);
evImp[k] = (int)ev.importance;
evUnit[k] = (int)ev.unit;
evMult[k] = (int)ev.multiplier;
evDig[k] = (int)ev.digits;
evSector[k] = (int)ev.sector;
evType[k] = (int)ev.type;
evCountry[k] = okc ? Esc(co.code) : "";
evCurrency[k] = okc ? Esc(co.currency) : "";
}
if(evImp[k] < InpMinImportance)
{
skipped++;
continue;
}
const int dg = evDig[k];
FileWriteString(h,
TimeToString(v.time, TIME_DATE | TIME_MINUTES) + ";" +
evCountry[k] + ";" + evCurrency[k] + ";" +
IntegerToString(evImp[k]) + ";" +
IntegerToString((long)v.event_id) + ";" +
IntegerToString((long)v.id) + ";" +
evName[k] + ";" +
Scaled(v.actual_value, dg) + ";" +
Scaled(v.forecast_value, dg) + ";" +
Scaled(v.prev_value, dg) + ";" +
Scaled(v.revised_prev_value, dg) + ";" +
Raw(v.actual_value) + ";" +
Raw(v.forecast_value) + ";" +
Raw(v.prev_value) + ";" +
IntegerToString((int)v.impact_type) + ";" +
IntegerToString(evUnit[k]) + ";" +
IntegerToString(evMult[k]) + ";" +
IntegerToString(dg) + ";" +
IntegerToString(evSector[k]) + ";" +
IntegerToString(evType[k]) + "\n");
if(lo == 0 || v.time < lo)
lo = v.time;
if(v.time > hi)
hi = v.time;
written++;
}
FileClose(h);
if(!FileMove(tmp, FILE_COMMON, InpFileName, FILE_COMMON | FILE_REWRITE))
{
PrintFormat("CalendarHistoryExport: wrote %s but could not move it into place (%d)",
tmp, GetLastError());
return;
}
PrintFormat("CalendarHistoryExport: %s - %d releases of %d distinct events, %s to %s "
"(%d skipped). If this is not the research machine, copy the file across.",
InpFileName, written, ArraySize(evIds),
TimeToString(lo, TIME_DATE), TimeToString(hi, TIME_DATE), skipped);
}
//+------------------------------------------------------------------+