Warrior_EA/System/NewsRelevance.mqh
AnimateDread d0e89a6fc7 fix(SignalNewsFilter): scope calendar veto to the traded symbol's own currencies
CalendarValueHistory() was called with no country filter at all, so
ANY country's economic calendar event vetoed a trade regardless of
relevance - a JPY release blocked a EURUSD trade just as readily as a
USD one, making NF_MinImpact's fine-tuning far noisier than intended.

Adds System/NewsRelevance.mqh (GetRelevantCountryCodes/
ImpactWeightedProximity), a shared utility that cross-references
CalendarCountries() against the symbol's base/quote currency to get
the actually-relevant ISO country codes, then uses
CalendarValueHistory()'s country_code-filtering overload. Shared so
the upcoming NN news-input feature reuses the same relevance logic
rather than duplicating it.

Compiled clean (MetaEditor, 0 errors/0 warnings).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 17:22:33 -04:00

87 lines
4.3 KiB
MQL5

//+------------------------------------------------------------------+
//| NewsRelevance.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property link "https://www.mql5.com"
//--- Module-scope cache: a symbol's currency composition never changes at runtime, and
//--- CalendarCountries() enumerating every country in the terminal's calendar database on
//--- every call (both the live NewsFilter veto and, per-bar, the NN news feature) would be
//--- pure waste. Single-symbol cache (this codebase runs one symbol per EA instance/chart) -
//--- rebuilt if a different symbol is ever requested.
string g_newsRelevanceCachedSymbol = "";
string g_newsRelevanceCountryCodes[];
//+------------------------------------------------------------------+
//| Builds the ISO country codes relevant to symbol's base+quote |
//| currency (e.g. EURUSD -> every Eurozone country + the US) by |
//| cross-referencing CalendarCountries() against |
//| SYMBOL_CURRENCY_BASE/SYMBOL_CURRENCY_PROFIT. This is what |
//| CalendarValueHistory()'s country_code filter needs to actually |
//| scope news to the traded symbol, instead of vetoing/scoring on |
//| ANY country's events regardless of relevance. |
//+------------------------------------------------------------------+
void GetRelevantCountryCodes(string symbol, string &countryCodes[])
{
if(symbol == g_newsRelevanceCachedSymbol)
{
ArrayCopy(countryCodes, g_newsRelevanceCountryCodes);
return;
}
ArrayResize(countryCodes, 0);
string baseCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
string profitCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
MqlCalendarCountry countries[];
int total = CalendarCountries(countries);
for(int i = 0; i < total; i++)
{
if(countries[i].currency == baseCcy || countries[i].currency == profitCcy)
{
int n = ArraySize(countryCodes);
ArrayResize(countryCodes, n + 1);
countryCodes[n] = countries[i].code;
}
}
g_newsRelevanceCachedSymbol = symbol;
ArrayCopy(g_newsRelevanceCountryCodes, countryCodes);
}
//+------------------------------------------------------------------+
//| Impact-weighted proximity, in [0,1], to the CLOSEST relevant |
//| (symbol-currency-matched) calendar event on one side of pivotTime: |
//| searchForward=false looks backward (recency - "how close was the |
//| last relevant release"), true looks forward (proximity to the |
//| next SCHEDULED relevant release - legitimate to use even for a |
//| historical training bar, since release times are publicly known |
//| in advance; this is NOT the same as knowing the release's actual |
//| outcome ahead of time, which would be real lookahead bias and is |
//| deliberately not exposed anywhere here). |
//| Returns 0.0 if no relevant event falls within windowMinutes on the |
//| requested side, else (impact_type/3.0) * (1 - minutesAway/window) |
//| for whichever qualifying event is closest to pivotTime. |
//+------------------------------------------------------------------+
double ImpactWeightedProximity(string symbol, datetime pivotTime, int windowMinutes, bool searchForward)
{
if(windowMinutes <= 0)
return 0.0;
string countryCodes[];
GetRelevantCountryCodes(symbol, countryCodes);
if(ArraySize(countryCodes) == 0)
return 0.0;
datetime windowStart = searchForward ? pivotTime : pivotTime - windowMinutes * 60;
datetime windowEnd = searchForward ? pivotTime + windowMinutes * 60 : pivotTime;
double best = 0.0;
for(int c = 0; c < ArraySize(countryCodes); c++)
{
MqlCalendarValue values[];
if(!CalendarValueHistory(values, windowStart, windowEnd, countryCodes[c]))
continue;
for(int i = 0; i < ArraySize(values); i++)
{
double minutesAway = MathAbs((double)(values[i].time - pivotTime)) / 60.0;
double weight = (values[i].impact_type / 3.0) * (1.0 - minutesAway / windowMinutes);
if(weight > best)
best = weight;
}
}
return best;
}