Warrior_EA/Scripts/CalendarRecorder.mq5

350 lines
14 KiB
MQL5
Raw Permalink Normal View History

feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
//+------------------------------------------------------------------+
//| CalendarRecorder.mq5 |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
#property version "1.00"
#property description "Records economic calendar releases AT RELEASE TIME, write-once."
#property description "Standalone: attach to any spare chart. Does not touch the trading EA."
//--- WHY THIS EXISTS, AND WHY IT CANNOT BE A BACKTEST
//---
//--- The calendar is the only non-price data source this project found that MQL5 actually
//--- carries with content (swap has no history, TICK_FLAG_BUY/SELL are empty on FX, DOM is
//--- absent on retail FX and never in the tester). But it CANNOT be researched from
//--- history, because MqlCalendarValue.actual_value returns the CURRENT figure - the one
//--- after every subsequent revision. Reading 2019's NFP today gives a number that nobody
//--- could have known in 2019. Any "surprise = actual - forecast" feature built from
//--- history is therefore contaminated by lookahead, and it is the flattering kind: it
//--- makes the model look prescient exactly on the events that were revised most.
//---
//--- The only sound way to get an honest surprise series is to write down what the
//--- terminal reported at the moment of release and never touch that row again. That is
//--- all this does. It buys a dataset that starts empty and grows in real time - months
//--- before it is usable, which is precisely why it should start running now rather than
//--- when someone wants it.
//---
//--- WRITE-ONCE IS THE WHOLE CONTRACT. A row is appended the first time a value_id is seen
//--- with an actual figure attached, and is never rewritten. Re-recording on a later poll
//--- would silently import the revision this file exists to avoid.
//--- Deliberately NO includes from the EA tree. This is a data-collection side-task that
//--- may run for months on a spare chart; coupling it to the trading system's headers
//--- would mean a refactor there can stop the recorder, and a silent gap in a write-once
//--- series cannot be backfilled by definition. The one helper it needs is eight lines.
input int InpPollSeconds = 30; // Poll interval (seconds)
input int InpLookbackHours = 48; // How far back each poll looks for newly-filled values
input int InpLookaheadHours = 4; // Upcoming window (captures scheduled-but-unreleased)
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
input int InpLiveLagSeconds = 600; // Max release->observation lag still counted LIVE
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
input bool InpAllCountries = true; // Record every country (false = this symbol's only)
input int InpMinImportance = 1; // 0=none 1=low 2=moderate 3=high
input string InpFileName = "Warrior_EA/Research/calendar_live.csv";
//--- LONG_MIN is the terminal's "this field has no value" sentinel on MqlCalendarValue.
#define CAL_NO_VALUE LONG_MIN
#define CAL_SCALE 1000000.0
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
//--- CAPTURE CLASS - the single most important column in this file.
//---
//--- Every poll looks BACK as well as forward, so it necessarily re-reads events that were
//--- released before the recorder was watching: the first poll after attaching picked up 83
//--- of them. Those rows carry whatever the terminal holds NOW, which is the revised figure
//--- - the exact contamination this recorder exists to avoid. The very first batch proved
//--- it was not hypothetical: a "Retail Sales y/y" row came back with previous 3.5 and
//--- revised_prev 3.4, and a "Core CPI m/m" row already had revision=1.
//---
//--- Backfill is still worth keeping (it is a fine snapshot of the revised series, and it
//--- carries the event metadata), but it must never be silently mixed with release-time
//--- observations. So the lag is written explicitly and classified, rather than left to be
//--- re-derived by whoever loads the CSV later and may not think to.
//---
//--- LIVE observed within InpLiveLagSeconds of the release -> usable as a surprise
//--- BACKFILL seen long after the fact -> MUST NOT be used for surprise research
#define CSV_HEADER "observed_time,capture,lag_sec,value_id,event_id,event_time,country," \
"currency,importance,name,actual,forecast,previous,revised_prev," \
"revision,impact,unit,digits,multiplier,symbol,bid,ask,spread_pts"
ulong g_seen[]; // value_ids already written, kept SORTED for binary search
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
string g_countries[];
datetime g_lastPoll = 0;
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
int g_live = 0; // release-time captures this session (the only usable rows)
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
//+------------------------------------------------------------------+
//| Sorted insert into the seen-set |
//+------------------------------------------------------------------+
bool SeenContains(const ulong id)
{
int n = ArraySize(g_seen);
if(n == 0)
return false;
int lo = 0, hi = n - 1;
while(lo <= hi)
{
int mid = (lo + hi) >> 1;
if(g_seen[mid] == id)
return true;
if(g_seen[mid] < id)
lo = mid + 1;
else
hi = mid - 1;
}
return false;
}
void SeenInsert(const ulong id)
{
int n = ArraySize(g_seen);
int pos = n;
for(int i = 0; i < n; i++)
if(g_seen[i] > id)
{
pos = i;
break;
}
ArrayResize(g_seen, n + 1);
for(int i = n; i > pos; i--)
g_seen[i] = g_seen[i - 1];
g_seen[pos] = id;
}
//+------------------------------------------------------------------+
//| Rebuild the seen-set from the file so a restart never duplicates |
//| or, worse, re-records a value that has since been revised. |
//+------------------------------------------------------------------+
void LoadSeen()
{
ArrayResize(g_seen, 0);
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
//--- Read whole LINES and split, rather than walking FILE_CSV field by field with a
//--- counter. The counter version had an off-by-one and was keying the seen-set on
//--- event_id instead of value_id - and because event_id identifies the event TYPE, not
//--- the release, that would have made every restart permanently skip all future
//--- releases of every event already in the file. Indexing a split array by name-checked
//--- position cannot drift like that when the schema changes.
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
//--- FILE_SHARE_READ|FILE_SHARE_WRITE is mandatory in this codebase: an exclusive open
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
//--- fails 5004 whenever anything else holds the file, and looks like "no history yet".
int h = FileOpen(InpFileName, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON |
FILE_SHARE_READ | FILE_SHARE_WRITE);
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
if(h == INVALID_HANDLE)
{
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
PrintFormat("%s: no existing file (%s) - starting fresh", __FUNCTION__, InpFileName);
return;
}
string header = FileIsEnding(h) ? "" : FileReadString(h);
StringTrimRight(header);
StringTrimLeft(header);
if(StringLen(header) > 0 && header != CSV_HEADER)
{
//--- Old schema. Rotate rather than append: mixing layouts silently mis-parses every
//--- old row, and the existing rows are backfill that will be re-derived anyway.
FileClose(h);
string bak = InpFileName + "." + IntegerToString((int)TimeCurrent()) + ".old";
if(FileMove(InpFileName, FILE_COMMON, bak, FILE_COMMON))
PrintFormat("%s: schema changed - previous file rotated to %s", __FUNCTION__, bak);
else
PrintFormat("%s: schema changed but rotate FAILED (error %d) - "
"move %s aside by hand", __FUNCTION__, GetLastError(), InpFileName);
return;
}
int vcol = -1;
string hf[];
if(StringSplit(header, ',', hf) > 0)
for(int i = 0; i < ArraySize(hf); i++)
if(hf[i] == "value_id")
vcol = i;
if(vcol < 0)
{
FileClose(h);
Print(__FUNCTION__ + ": header has no value_id column - refusing to guess");
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
return;
}
while(!FileIsEnding(h))
{
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
string line = FileReadString(h);
if(StringLen(line) < 5)
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
continue;
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
string f[];
if(StringSplit(line, ',', f) > vcol)
SeenInsert((ulong)StringToInteger(f[vcol]));
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
}
FileClose(h);
PrintFormat("%s: loaded %d previously recorded values from %s",
__FUNCTION__, ArraySize(g_seen), InpFileName);
}
//+------------------------------------------------------------------+
//| Append one row. Header written only when the file is created. |
//+------------------------------------------------------------------+
bool AppendRow(const string row)
{
int h = FileOpen(InpFileName, FILE_READ | FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON |
FILE_SHARE_READ | FILE_SHARE_WRITE);
if(h == INVALID_HANDLE)
{
PrintFormat("%s: cannot open %s, error %d", __FUNCTION__, InpFileName, GetLastError());
return false;
}
if(FileSize(h) == 0)
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
FileWriteString(h, CSV_HEADER + "\r\n");
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
FileSeek(h, 0, SEEK_END);
FileWriteString(h, row + "\r\n");
FileFlush(h);
FileClose(h);
return true;
}
//+------------------------------------------------------------------+
double CalValue(const long v)
{
return (v == CAL_NO_VALUE) ? EMPTY_VALUE : (double)v / CAL_SCALE;
}
string CalField(const long v)
{
return (v == CAL_NO_VALUE) ? "" : DoubleToString((double)v / CAL_SCALE, 6);
}
//+------------------------------------------------------------------+
int OnInit()
{
if(InpAllCountries)
{
MqlCalendarCountry countries[];
int total = CalendarCountries(countries);
ArrayResize(g_countries, 0);
for(int i = 0; i < total; i++)
{
int n = ArraySize(g_countries);
ArrayResize(g_countries, n + 1);
g_countries[n] = countries[i].code;
}
}
else
{
//--- this symbol's base+quote currencies only
string baseCcy = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE);
string profCcy = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);
MqlCalendarCountry countries[];
int total = CalendarCountries(countries);
ArrayResize(g_countries, 0);
for(int i = 0; i < total; i++)
if(countries[i].currency == baseCcy || countries[i].currency == profCcy)
{
int n = ArraySize(g_countries);
ArrayResize(g_countries, n + 1);
g_countries[n] = countries[i].code;
}
}
if(ArraySize(g_countries) == 0)
{
Print(__FUNCTION__ + ": no countries resolved - calendar unavailable on this terminal?");
return INIT_FAILED;
}
LoadSeen();
EventSetTimer(MathMax(5, InpPollSeconds));
PrintFormat("CalendarRecorder: watching %d countries, poll %ds, file %s",
ArraySize(g_countries), InpPollSeconds, InpFileName);
Poll();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
}
void OnTimer()
{
Poll();
}
//+------------------------------------------------------------------+
//| One sweep. Cheap enough to run every 30s: the terminal serves the |
//| calendar from a local cache, and the window is a couple of days. |
//+------------------------------------------------------------------+
void Poll()
{
datetime now = TimeCurrent();
datetime from = now - (datetime)InpLookbackHours * 3600;
datetime to = now + (datetime)InpLookaheadHours * 3600;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
long spr = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
int added = 0;
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
int liveBefore = g_live;
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
for(int c = 0; c < ArraySize(g_countries); c++)
{
MqlCalendarValue values[];
if(!CalendarValueHistory(values, from, to, g_countries[c]))
continue; // country simply has nothing in the window
for(int i = 0; i < ArraySize(values); i++)
{
//--- Only record once the ACTUAL figure exists. A scheduled-but-unreleased row has
//--- no surprise to measure, and recording it early would burn the value_id in the
//--- seen-set so the real release is never captured.
if(values[i].actual_value == CAL_NO_VALUE)
continue;
if(SeenContains(values[i].id))
continue;
MqlCalendarEvent ev;
if(!CalendarEventById(values[i].event_id, ev))
continue;
if((int)ev.importance < InpMinImportance)
{
SeenInsert(values[i].id); // remember, so it is not re-examined every poll
continue;
}
MqlCalendarCountry ctry;
string currency = "";
if(CalendarCountryById(ev.country_id, ctry))
currency = ctry.currency;
string name = ev.name;
StringReplace(name, ",", ";"); // keep the CSV parseable
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
long lag = (long)now - (long)values[i].time;
string cap = (lag >= 0 && lag <= InpLiveLagSeconds) ? "LIVE" : "BACKFILL";
if(cap == "LIVE")
g_live++;
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
string row = StringFormat(
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
"%s,%s,%I64d,%I64u,%I64u,%s,%s,%s,%d,%s,%s,%s,%s,%s,%d,%d,%d,%d,%.6f,%s,%.5f,%.5f,%d",
TimeToString(now, TIME_DATE | TIME_SECONDS), cap, lag,
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
values[i].id, values[i].event_id,
TimeToString(values[i].time, TIME_DATE | TIME_SECONDS),
g_countries[c], currency, (int)ev.importance, name,
CalField(values[i].actual_value),
CalField(values[i].forecast_value),
CalField(values[i].prev_value),
CalField(values[i].revised_prev_value),
values[i].revision, (int)values[i].impact_type,
(int)ev.unit, (int)ev.digits, (double)ev.multiplier,
_Symbol, bid, ask, (int)spr);
if(AppendRow(row))
{
SeenInsert(values[i].id);
added++;
}
}
}
if(added > 0)
fix(research): calendar recorder - separate LIVE from BACKFILL, fix seen-set key First live run exposed both problems at once. It logged "+83 release(s) recorded", and every one of those rows shared a single observed_time up to 30 hours after its event_time: they were the startup backfill, not release-time observations. Their actual figures are whatever the terminal holds NOW - the post-revision values this recorder exists to avoid - and the very first batch proved that is not hypothetical: a Retail Sales row came back previous 3.5 / revised_prev 3.4, and a Core CPI row already carried revision=1. Backfill is still worth keeping (a fine snapshot of the revised series, and it carries the event metadata) but must never be silently mixed with release-time observations. Every row now records lag_sec and a capture class, so the distinction cannot be lost by whoever loads the CSV later: LIVE observed within InpLiveLagSeconds (default 600s) of release BACKFILL seen long after the fact - MUST NOT be used for surprise research The log now reports the split per poll and says so explicitly when a poll is entirely backfill. Second and worse, in LoadSeen: the FILE_CSV field walk was off by one and keyed the seen-set on event_id instead of value_id. event_id identifies the event TYPE, not the release, so after any restart every future release of every event already in the file would have been skipped - permanently, and silently, exactly for the recurring high-importance events (NFP, CPI) that matter most. Now reads whole lines and indexes a split array by a NAME-CHECKED column position, which cannot drift when the schema changes. Refuses to guess if value_id is absent. Schema change is handled by rotating any file with a non-matching header to <name>.<timestamp>.old rather than appending, since mixing layouts mis-parses every old row. Compiles 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:12 -04:00
{
int live = g_live - liveBefore;
PrintFormat("CalendarRecorder: +%d row(s): %d LIVE, %d BACKFILL (usable this session: "
"%d, file total: %d)", added, live, added - live, g_live, ArraySize(g_seen));
if(live == 0)
Print("CalendarRecorder: all BACKFILL - post-revision figures, "
"NOT usable for surprise research. Only LIVE rows are.");
}
feat(research): write-once live calendar recorder The calendar is the only non-price source MQL5 carries with real content, and it is the one this project cannot research from history: MqlCalendarValue.actual_value returns the CURRENT figure, i.e. after every later revision. Reading 2019's NFP today returns a number nobody could have known in 2019, so any surprise = actual - forecast feature built from history carries lookahead - and the flattering kind, since it makes a model look most prescient exactly on the events that were revised most. The only sound fix is to write down what the terminal reported at the moment of release and never touch that row again. Write-once is the entire contract here: a row is appended the first time a value_id is seen carrying an actual figure, and is never rewritten, because re-recording on a later poll would silently import the revision this file exists to avoid. The seen-set is rebuilt from the file on init so a restart cannot duplicate or re-import either. Deliberately standalone - no includes from the EA tree, and not wired into Warrior_EA. It may run for months on a spare chart, and coupling it to the trading system would mean a refactor there can stop the recorder; a gap in a write-once series cannot be backfilled by definition. It also keeps a data-collection task from adding any failure mode to a system about to trade a prop account. Records actual/forecast/previous/revised_previous, revision number, impact, importance, units, plus observation time and the quote at observation. Every FileOpen carries FILE_SHARE_READ|FILE_SHARE_WRITE per the rule this codebase learned the hard way (exclusive opens fail 5004 and look like "no data"). Compiles 0 errors, 0 warnings. Value starts at zero and accrues with time, which is the argument for starting it now rather than when it is wanted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:14:48 -04:00
g_lastPoll = now;
}
//+------------------------------------------------------------------+