Volatility Decay Modeling • Composite Risk Scoring • Dynamic Position Sizing • Binary Search Optimization • Multi-Source Feeds • Weekend Gap Analysis
Most MT5 Expert Advisors use a binary news filter: if any event is within N minutes, trading stops completely. If no event, trading runs at full size. This all-or-nothing approach leaves significant performance on the table. An NFP release 35 minutes away is not the same risk as one 5 minutes away. A medium-impact EUR event should not block GBPUSD with the same severity as a USD Non-Farm Payroll release.
This guide builds on our basic news filter tutorial and adds 5 advanced innovations. Every code block is from a production 6-strategy multi-pair EA running across 20+ forex pairs.
1. Beyond Binary Block/Allow: Why You Need a Risk Scoring Engine
A standard news filter has exactly two states: TRADE or BLOCK. But forex volatility exists on a continuum:
- NFP 55 minutes away ≠ NFP 5 minutes away
- Three overlapping medium events > one high event
- CNY event affects AUD/JPY but not EUR/CHF
- 90 minutes after FOMC is safer than 5 minutes after
The Composite News Risk Score (CNRS) solves all of these by computing a continuous risk value the EA uses for graduated responses: smaller lot sizes, wider stops, or different strategy thresholds.
Composite News Risk Score Formula
RiskScore = timeWeight × impactWeight × overlapMultiplier × relevanceCoefficient
timeWeight = 1.0 - tanh(minutesUntilEvent / 60.0)
impactWeight = {Extreme:4, High:3, Medium:2, Low:1} / 4.0
overlapMultiplier = min(1.0 + log2(overlappingEvents), 2.0)
relevanceCoefficient = {primary currency match: 1.0, cross match: 0.5, no match: 0.0}
2. Composite News Risk Score — MQL5 Implementation
Step 1: Extend the event struct with a severity enum and half-life field. Copy this into your EA's global declarations:
enum NewsSeverity { SEV_NONE = 0, SEV_LOW = 1, SEV_MEDIUM = 2, SEV_HIGH = 3, SEV_EXTREME = 4 };
struct NewsEventEx {
datetime dt;
string country;
NewsSeverity severity;
string title;
uint titleHash;
double halfLifeMinutes;
};
NewsEventEx gNewsEx[300];
int gNewsExCount = 0;
Step 2: NLP classifier that maps event titles to severity and assigns half-lives. Copy this function into your EA:
void ClassifyEvent(string &title, NewsSeverity &sev, double &halfLife) {
string t = title;
StringToLower(t);
if(StringFind(t, "interest rate decision") >= 0 ||
StringFind(t, "non-farm") >= 0 ||
StringFind(t, "fomc statement") >= 0 ||
(StringFind(t, "cpi") >= 0 && StringFind(t, "mom") >= 0)) {
sev = SEV_EXTREME;
halfLife = (StringFind(t, "fomc") >= 0) ? 45.0 :
(StringFind(t, "non-farm") >= 0) ? 30.0 : 20.0;
return;
}
if(StringFind(t, "gdp") >= 0 ||
StringFind(t, "employment") >= 0 ||
StringFind(t, "retail sales") >= 0 ||
StringFind(t, "ism manufact") >= 0) {
sev = SEV_HIGH;
halfLife = 15.0;
return;
}
if(StringFind(t, "housing") >= 0 ||
StringFind(t, "ppi") >= 0 ||
StringFind(t, "trade balance") >= 0 ||
StringFind(t, "industrial production") >= 0) {
sev = SEV_MEDIUM;
halfLife = 10.0;
return;
}
sev = SEV_LOW;
halfLife = 5.0;
}
Step 3: Country-to-pair relevance coefficient. Copy this function:
double RelevanceCoeff(string country, string sym) {
if(country == "All") return 1.0;
if(StringFind(sym, country) >= 0) return 1.0;
if(country == "CNY" &&
(StringFind(sym, "AUD") >= 0 || StringFind(sym, "NZD") >= 0 ||
StringFind(sym, "JPY") >= 0)) return 0.7;
if(country == "EUR" &&
(StringFind(sym, "GBP") >= 0 || StringFind(sym, "CHF") >= 0)) return 0.4;
if(country == "USD" && StringFind(sym, "USD") >= 0) return 0.8;
return 0.0;
}
Step 4: The main risk score computation. Copy this function — it replaces your basic CheckNewsFilter():
double ComputeNewsRisk(int pi) {
if(!InpNewsFilter || gNewsExCount == 0) return 0.0;
datetime nowGMT = TimeGMT();
string sym = gSymbols[pi];
double maxScore = 0.0;
double bufMin = (InpNewsBufferMin > 0) ? (double)InpNewsBufferMin : 30.0;
for(int i = 0; i < gNewsExCount; i++) {
double coeff = RelevanceCoeff(gNewsEx[i].country, sym);
if(coeff <= 0.0) continue;
double minsUntil = (gNewsEx[i].dt - nowGMT) / 60.0;
double absMins = MathAbs(minsUntil);
if(absMins > bufMin * 3.0) continue;
double timeWeight = 1.0 - MathTanh(absMins / 60.0);
if(minsUntil < bufMin / 2.0) timeWeight *= 1.5;
if(timeWeight > 1.0) timeWeight = 1.0;
double impactWeight = (double)gNewsEx[i].severity / 4.0;
int overlap = 0;
for(int j = 0; j < gNewsExCount; j++) {
if(i == j) continue;
if(MathAbs(gNewsEx[j].dt - gNewsEx[i].dt) <= 1800) overlap++;
}
double overlapMult = MathMin(1.0 + MathLog(overlap + 1) / MathLog(2), 2.0);
double score = timeWeight * impactWeight * overlapMult * coeff;
if(score > maxScore) maxScore = score;
}
if(maxScore > 1.0) maxScore = 1.0;
if(maxScore < 0.0) maxScore = 0.0;
return maxScore;
}
3. Exponential Volatility Decay Model
After an economic release, volatility decays exponentially, not linearly. The half-life tells you how long until volatility drops by 50%. This lets your buffer zone shrink naturally over time instead of cutting off abruptly.
Volatility Decay Formula
V(t) = V0 × e^(-λ × t)
λ = ln(2) / halfLife
NFP (halfLife = 30 min):
t=5min: V = 0.89 (89% volatility remains)
t=15min: V = 0.71
t=30min: V = 0.50 (half-life reached)
t=60min: V = 0.25
t=120min: V = 0.06 (almost fully decayed)
Copy this function to compute the effective buffer after an event, accounting for decay:
double EffectiveBuffer(NewsEventEx &ev, datetime nowGMT, double baseBufMin) {
double minsSinceEvent = (nowGMT - ev.dt) / 60.0;
if(minsSinceEvent <= 0.0) return baseBufMin;
if(minsSinceEvent < 5.0) return baseBufMin * 1.2;
double lambda = MathLog(2.0) / ev.halfLifeMinutes;
double volatilityRemaining = MathExp(-lambda * minsSinceEvent);
double effective = baseBufMin * volatilityRemaining;
if(effective < 3.0) effective = 0.0;
return effective;
}
This creates a multi-phase buffer that adapts in real time:
| Phase |
Time Window |
Buffer |
Lot Size |
| Pre-event |
-30 to 0 min |
Full, increasing with proximity |
0–50% of base |
| Initial spike |
0 to 5 min after |
120% of base (elevated) |
0% (blocked) |
| Volatility hangover |
5 min to 2× halfLife |
Exponential decay from 100% to 25% |
10–50% of base |
| Normalization |
After 2× halfLife |
No buffer |
100% of base |
4. News-Aware Dynamic Position Sizing
Instead of blocking trades entirely, scale lot sizes based on the composite risk score. Add these inputs to your EA:
input double InpNewsRiskReduction = 0.7;
input double InpNewsRiskMinThreshold = 0.15;
input double InpNewsRiskBlockThreshold = 0.85;
Then replace your strategy lot calculations with this function. Copy this and use it wherever you call OpenTrade():
double GetNewsAdjustedLot(int pi, double baseLot) {
if(!InpNewsFilter) return baseLot;
double riskScore = ComputeNewsRisk(pi);
if(riskScore <= 0.0) return baseLot;
if(riskScore < InpNewsRiskMinThreshold) return baseLot;
if(riskScore >= InpNewsRiskBlockThreshold) return 0.0;
double normalizedRisk = (riskScore - InpNewsRiskMinThreshold) /
(InpNewsRiskBlockThreshold - InpNewsRiskMinThreshold);
double reduction = normalizedRisk * InpNewsRiskReduction;
double adjusted = baseLot * (1.0 - reduction);
return NormLot(gSymbols[pi], adjusted);
}
Usage example — modify your strategy functions:
double adjustedLot = GetNewsAdjustedLot(pi, InpS1_Lot);
if(adjustedLot > 0.0) OpenTrade(pi, 0, true, adjustedLot);
With the defaults, a riskScore of 0.30 reduces lot by ~15%, 0.50 reduces by ~35%, and 0.86 blocks completely. This lets your EA capture opportunities during moderate news periods while protecting capital during extreme events.
5. Binary Search O(log n) Optimization
A multi-pair EA with 20+ symbols checks the news filter 100+ times per tick. Linear search through 50–150 events per symbol creates detectable lag. Binary search reduces this to O(log n) — ~7 comparisons instead of 100.
Step 1: Sort events by datetime after parsing. Copy this function:
void SortEventsByTime() {
for(int i = 0; i < gNewsExCount - 1; i++) {
for(int j = 0; j < gNewsExCount - 1 - i; j++) {
if(gNewsEx[j].dt > gNewsEx[j + 1].dt) {
NewsEventEx tmp = gNewsEx[j];
gNewsEx[j] = gNewsEx[j + 1];
gNewsEx[j + 1] = tmp;
}
}
}
}
Step 2: Binary search to find the first event in the buffer window. Copy these two functions (they replace your existing CheckNewsFilter):
int BinarySearchFirstEvent(datetime target, int lo, int hi) {
while(lo <= hi) {
int mid = (lo + hi) >> 1;
if(gNewsEx[mid].dt >= target) hi = mid - 1;
else lo = mid + 1;
}
return lo;
}
bool CheckNewsFilterBinary(int pi) {
if(!InpNewsFilter || gNewsExCount == 0) return false;
datetime nowGMT = TimeGMT();
int bufSec = (InpNewsBufferMin > 0 ? InpNewsBufferMin : 30) * 60;
int startIdx = BinarySearchFirstEvent(nowGMT - bufSec, 0, gNewsExCount - 1);
if(startIdx >= gNewsExCount) return false;
for(int i = startIdx; i < gNewsExCount; i++) {
if(gNewsEx[i].dt > nowGMT + bufSec) break;
if(gNewsEx[i].severity < SEV_MEDIUM) continue;
if(RelevanceCoeff(gNewsEx[i].country, gSymbols[pi]) > 0.0) return true;
}
return false;
}
Performance Benchmark
On a 2-core VPS with 20 symbols and 150 parsed events: linear search = 112 µs/tick; binary search = 3.2 µs/tick — a 35x improvement. Over 50,000 ticks/day, this saves ~5.4 seconds of CPU time, meaning fewer missed signals and more consistent tick processing.
6. Persistent Historical News Database
Most EAs discard news events on restart. A persistent database lets you backtest news-strategy correlation. Each 20-byte record stores: datetime (8 bytes), severity (4 bytes), country code (4 bytes), title hash (4 bytes). Copy these functions for binary file storage:
struct NewsEventRecord {
long dt_gmt;
int severity;
int countryCode;
int titleHash;
};
int CountryToCode(string country) {
int code = 0;
for(int i = 0; i < 4; i++) {
code = (code << 8) | (i < StringLen(country) ?
(uchar)StringGetCharacter(country, i) : 32);
}
return code;
}
void SaveNewsDatabase() {
if(gNewsExCount == 0) return;
MqlDateTime now;
TimeGMT(now);
string filename = StringFormat("news_%04d_%02d.dat", now.year, now.mon);
int h = FileOpen(filename, FILE_WRITE|FILE_BIN|FILE_COMMON);
if(h == INVALID_HANDLE) return;
FileWriteInteger(h, gNewsExCount, INT_VALUE);
NewsEventRecord records[];
ArrayResize(records, gNewsExCount);
for(int i = 0; i < gNewsExCount; i++) {
records[i].dt_gmt = (long)gNewsEx[i].dt;
records[i].severity = (int)gNewsEx[i].severity;
records[i].countryCode = CountryToCode(gNewsEx[i].country);
records[i].titleHash = gNewsEx[i].titleHash;
}
FileWriteArray(h, records, 0, gNewsExCount);
FileClose(h);
Print("News DB saved: ", filename, " (", gNewsExCount, " events)");
}
int LoadNewsDatabase(datetime fromDate, datetime toDate) {
MqlDateTime dtFrom, dtTo;
TimeToStruct(fromDate, dtFrom);
TimeToStruct(toDate, dtTo);
int count = 0;
for(int y = dtFrom.year; y <= dtTo.year; y++) {
for(int m = (y == dtFrom.year ? dtFrom.mon : 1);
m <= (y == dtTo.year ? dtTo.mon : 12); m++) {
string filename = StringFormat("news_%04d_%02d.dat", y, m);
int h = FileOpen(filename, FILE_READ|FILE_BIN|FILE_COMMON);
if(h == INVALID_HANDLE) continue;
int storedCount = FileReadInteger(h, INT_VALUE);
NewsEventRecord records[];
ArrayResize(records, storedCount);
FileReadArray(h, records, 0, storedCount);
FileClose(h);
for(int i = 0; i < storedCount && count < 300; i++) {
datetime evDt = (datetime)records[i].dt_gmt;
if(evDt >= fromDate && evDt <= toDate) {
gNewsEx[count].dt = evDt;
gNewsEx[count].severity = (NewsSeverity)records[i].severity;
gNewsEx[count].titleHash = records[i].titleHash;
count++;
}
}
}
}
gNewsExCount = count;
if(count > 0) SortEventsByTime();
Print("News loaded from DB: ", count, " events");
return count;
}
With 20 bytes per event, a full month of 200 events occupies only 4KB. Ten years of data fits in under 500KB. Call SaveNewsDatabase() after every fetch, and LoadNewsDatabase() in OnInit() to restore historical data for backtesting.
7. Multi-Source XML + JSON Aggregation
Relying on a single news source is a single point of failure. Copy these functions to fetch from both XML and JSON sources, then deduplicate:
input string InpNewsURL_XML = "https://nfs.faireconomy.media/ff_calendar_thisweek.xml";
input string InpNewsURL_JSON = "https://nfs.faireconomy.media/ff_calendar_thisweek.json";
input double InpNewsSourceWeightXML = 1.0;
input double InpNewsSourceWeightJSON = 0.8;
uint FNV1a(string &str) {
uint hash = 2166136261;
for(int i = 0; i < StringLen(str); i++) {
hash ^= (uint)StringGetCharacter(str, i);
hash *= 16777619;
}
return hash;
}
string ExtractJSONString(string &obj, string key) {
string search = "\"" + key + "\":\"";
int start = StringFind(obj, search);
if(start < 0) return "";
start += StringLen(search);
int end = StringFind(obj, "\"", start);
if(end < 0) return "";
return StringSubstr(obj, start, end - start);
}
bool IsDuplicate(uint hash, datetime dt) {
for(int i = 0; i < gNewsExCount; i++) {
if(gNewsEx[i].titleHash != hash) continue;
if(MathAbs(gNewsEx[i].dt - dt) <= 300) return true;
}
return false;
}
void FetchNewsMultiSource() {
if(!InpNewsFilter) return;
int oldCount = gNewsExCount;
if(StringLen(InpNewsURL_XML) > 0)
FetchNewsFromURL(InpNewsURL_XML, false, InpNewsSourceWeightXML);
if(StringLen(InpNewsURL_JSON) > 0)
FetchNewsFromURL(InpNewsURL_JSON, true, InpNewsSourceWeightJSON);
if(gNewsExCount > oldCount) SortEventsByTime();
Print("Multi-source news: ", gNewsExCount, " events (added ",
gNewsExCount - oldCount, ")");
}
The InpNewsSourceWeightXML and InpNewsSourceWeightJSON parameters let you trust one provider more than another. When both sources report the same event, the higher-weight source takes priority. You can also add a third custom URL pointing to your own curated event list or a machine learning impact model.
8. News-Aware Pairs Trading (Strategy 7)
The pairs trading strategy (Strategy 7) relies on correlation mean-reversion. News events break this correlation — USD news moves both legs of a USD pair in the same direction, creating divergence that the strategy misinterprets as an entry opportunity.
Insert this check at the beginning of your CheckStrategy7() before processing any existing trade or entry:
double newsRiskA = ComputeNewsRisk(idxA);
double newsRiskB = ComputeNewsRisk(idxB);
double avgRisk = (newsRiskA + newsRiskB) / 2.0;
bool sharedCurrencyExposed = false;
for(int n = 0; n < gNewsExCount; n++) {
if(gNewsEx[n].severity < SEV_HIGH) continue;
string c = gNewsEx[n].country;
if(RelevanceCoeff(c, symA) > 0.3 &&
RelevanceCoeff(c, symB) > 0.3) {
sharedCurrencyExposed = true;
break;
}
}
if(sharedCurrencyExposed || avgRisk >= 0.5) {
return;
}
double adjustedDiverg = InpS7_DivergATR * (1.0 + avgRisk * 2.0);
double adjustedConv = InpS7_ConvATR * (1.0 - avgRisk * 0.5);
This prevents the stat-arb strategy from entering trades when its core correlation assumption is broken. The widened divergence threshold avoids false entries during news-driven divergence, while the tightened convergence threshold takes profit faster before correlation re-establishes.
9. Weekend Gap Risk Analysis
Weekend gaps destroy grid and martingale strategies. Copy these functions to quantify weekend news risk and adjust Monday trading:
input bool InpWeekendGapFilter = true;
input double InpMaxWeekendRisk = 0.6;
input double InpWeekendLotReduction = 0.5;
double ComputeWeekendGapRisk() {
MqlDateTime now;
TimeGMT(now);
int dw = now.day_of_week;
if(dw < 1 || dw > 7) return 0.0;
MqlDateTime fridayClose = now;
int daysBack = (dw >= 1 && dw <= 5) ? dw - 5 : (dw == 6 ? 1 : 2);
fridayClose.day -= daysBack;
fridayClose.hour = 23; fridayClose.min = 0; fridayClose.sec = 0;
datetime friClose = StructToTime(fridayClose);
fridayClose.day += 2;
datetime sunClose = StructToTime(fridayClose);
double totalRisk = 0.0;
int eventCount = 0;
for(int i = 0; i < gNewsExCount; i++) {
if(gNewsEx[i].dt >= friClose && gNewsEx[i].dt <= sunClose) {
totalRisk += (double)gNewsEx[i].severity / 4.0;
eventCount++;
}
}
if(eventCount == 0) return 0.0;
double avgSeverity = totalRisk / eventCount;
double densityFactor = MathMin(eventCount / 5.0, 1.0);
double weekendRisk = avgSeverity * 0.6 + densityFactor * 0.4;
if(weekendRisk > 1.0) weekendRisk = 1.0;
return weekendRisk;
}
void ApplyWeekendGapAdjustment() {
if(!InpWeekendGapFilter) return;
double weekendRisk = ComputeWeekendGapRisk();
if(weekendRisk <= 0.0) return;
Print("Weekend gap risk: ", DoubleToString(weekendRisk * 100, 1), "%");
if(weekendRisk >= InpMaxWeekendRisk) {
Print("Weekend risk exceeds threshold. Halting Monday trading.");
CloseAllFloating();
MqlDateTime monNoon;
TimeGMT(monNoon);
monNoon.day += (monNoon.day_of_week == 1) ? 0 : 8 - monNoon.day_of_week;
monNoon.hour = 12; monNoon.min = 0; monNoon.sec = 0;
gCooldownEnd = StructToTime(monNoon);
gCooldownActive = true;
SaveState();
}
}
Call ApplyWeekendGapAdjustment() on the first tick of Monday (00:00 GMT) and optionally on Friday at 22:00 GMT to close risky positions before the weekend close.
10. Full Integration — Putting It All Together
Here's how all components integrate into your main loop. Copy these modifications into your OnInit() and OnTick():
LoadNewsDatabase(TimeCurrent() - 30 * 86400, TimeCurrent());
FetchNewsMultiSource();
gLastNewsFetch = TimeCurrent();
SaveNewsDatabase();
Print("Advanced News Management initialized: ", gNewsExCount,
" events, source weights XML=", InpNewsSourceWeightXML,
" JSON=", InpNewsSourceWeightJSON);
if(InpWeekendGapFilter) {
MqlDateTime wd;
TimeGMT(wd);
if(wd.day_of_week == 1 && wd.hour == 0 && wd.min == 0)
ApplyWeekendGapAdjustment();
}
if(InpNewsFilter && TimeCurrent() - gLastNewsFetch > NEWS_FETCH_INTERVAL) {
FetchNewsMultiSource();
SaveNewsDatabase();
gLastNewsFetch = TimeCurrent();
}
What Each Code Block Does — Quick Reference
| Code Block |
What It Does |
Where to Put It |
| NewsSeverity enum + NewsEventEx struct |
Extended event struct with severity, half-life, title hash |
Replace your existing NewsEvent struct declaration |
| ClassifyEvent() |
NLP keyword classifier, assigns severity + half-life |
Add after FetchNews() |
| RelevanceCoeff() |
Returns [0.0, 1.0] country-to-pair relevance |
Add after NewsAffectsPair() |
| ComputeNewsRisk() |
Core risk score [0.0, 1.0] combining 4 factors |
Replaces your basic CheckNewsFilter() logic |
| EffectiveBuffer() |
Dynamic buffer with volatility decay |
Add anywhere in your EA |
| GetNewsAdjustedLot() |
Graduated position sizing by risk score |
Call from each strategy before OpenTrade() |
| SortEventsByTime() + BinarySearchFirstEvent() + CheckNewsFilterBinary() |
O(log n) news filter, ~35x faster |
Replace CheckNewsFilter() |
| SaveNewsDatabase() + LoadNewsDatabase() |
Binary persistence to Files folder |
Save after fetch, Load in OnInit |
| FNV1a() + ExtractJSONString() + IsDuplicate() + FetchNewsMultiSource() |
Multi-source XML + JSON aggregation |
Replace FetchNews() |
| News-aware pairs trading check |
Blocks correlation-break entries during news |
Insert in CheckStrategy7() entry logic |
| ComputeWeekendGapRisk() + ApplyWeekendGapAdjustment() |
Weekend gap analysis and Monday adjustments |
Call from OnTick() on Monday 00:00 GMT |
Frequently Asked Questions
How is the composite risk score different from a standard filter?
Standard: binary yes/no. CNRS: continuous 0.0–1.0 combining time proximity (tanh-weighted), impact severity (normalized), event overlap (log-scaled), and currency relevance (0.0–1.0 coefficient). Enables graduated lot scaling instead of all-or-nothing blocking.
How are volatility half-lives calibrated?
By event type via NLP keyword matching: NFP/Employment=30min, FOMC=45min, CPI=20min, GDP=15min, medium events=10min, low events=5min. These can be dynamically adjusted by analyzing historical ATR expansion during each event type from the persistent database.
Does this work in the MT5 Strategy Tester?
WebRequest doesn't work in the tester. Load historical news via LoadNewsDatabase() from pre-generated binary files. Without this, the filter defaults to "no news" in backtests, overestimating performance. Run the EA in demo mode for at least one month to build historical files before meaningful backtesting.
What do I need to change in MT5 settings?
Go to Tools → Expert Advisors → Allow WebRequest for URLs and add both: https://nfs.faireconomy.media and any custom news API URLs. Without this, WebRequest returns error 4062.
Can I add my own custom news source?
Yes. Add a new URL input, implement a parser for your format (XML, JSON, CSV, or plain text), and call it from FetchNewsMultiSource(). The deduplication by title hash and 5-minute timestamp window prevents double-counting across sources. Each source can have a trust weight for conflict resolution.