Article-22597-MQL5-Economic.../News Logic.mqh

635 lines
No EOL
27 KiB
MQL5

//+------------------------------------------------------------------+
//| News Logic.mqh |
//| Copyright 2026, Allan Munene Mutiiria. |
//| https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
//--- Include guard
#ifndef NEWS_LOGIC_MQH
#define NEWS_LOGIC_MQH
//--- Include MQL5 trading library
#include <Trade/Trade.mqh>
//--- Include core data definitions and state
#include "News Core.mqh"
//+------------------------------------------------------------------+
//| Forward Extern Declarations |
//+------------------------------------------------------------------+
#ifndef NEWS_COMPILED_FROM_MAIN
extern ENUM_TIMEFRAMES start_time; // Past time window for live mode
extern ENUM_TIMEFRAMES end_time; // Future time window for live mode
extern ENUM_TIMEFRAMES range_time; // Time filter range from now
extern bool updateServerTime; // Update server clock on header
extern bool debugLogging; // Print debug info to journal
extern datetime StartDate; // Tester window start date
extern datetime EndDate; // Tester window end date
extern double tradeLotSize; // Trade lot size
extern int tradeOffsetHours; // Trade offset hours
extern int tradeOffsetMinutes; // Trade offset minutes
extern int tradeOffsetSeconds; // Trade offset seconds
#endif
//+------------------------------------------------------------------+
//| Trade mode enumeration |
//+------------------------------------------------------------------+
//--- Declared here for standalone compile; skipped when compiled from main
#ifndef NEWS_COMPILED_FROM_MAIN
enum ENewsTradeMode
{
NEWS_TRADE_BEFORE, // Trade before news event
NEWS_TRADE_AFTER, // Trade after news event
NEWS_NO_TRADE, // Do not trade on news
NEWS_PAUSE_TRADING // Pause trading around news
};
extern ENewsTradeMode tradeMode; // Active trade mode input
#endif
//+------------------------------------------------------------------+
//| Trade Helper and Resource Declarations |
//+------------------------------------------------------------------+
CTrade g_news_trade; // Trade execution helper instance
//--- Embedded CSV resource handle declared by main .mq5
#ifndef NEWS_COMPILED_FROM_MAIN
extern string EconomicCalendarData; // Embedded CSV resource string
#endif
//+------------------------------------------------------------------+
//| Convert importance string to calendar importance enum |
//+------------------------------------------------------------------+
ENUM_CALENDAR_EVENT_IMPORTANCE News_ImpStrToEnum(string s)
{
//--- Map string label to corresponding enum value
if(s == "High") return CALENDAR_IMPORTANCE_HIGH;
if(s == "Medium") return CALENDAR_IMPORTANCE_MODERATE;
if(s == "Low") return CALENDAR_IMPORTANCE_LOW;
return CALENDAR_IMPORTANCE_NONE;
}
//+------------------------------------------------------------------+
//| Load events from embedded CSV resource string (tester mode) |
//+------------------------------------------------------------------+
bool News_LoadEventsFromCsv(string csvData)
{
//--- Abort if resource string is empty
if(StringLen(csvData) == 0)
{
Print("News_LoadEventsFromCsv: CSV resource is empty!");
return false;
}
//--- Split CSV into lines
string lines[];
const int lineCount = StringSplit(csvData, '\n', lines);
if(lineCount <= 1)
{
Print("News_LoadEventsFromCsv: no data lines found");
return false;
}
//--- Reset event array and process each data line
ArrayResize(g_news_allEvents, 0);
int eventIndex = 0;
//--- Skip header row at index 0
for(int i = 1; i < lineCount; i++)
{
if(StringLen(lines[i]) == 0) continue;
//--- Split line into comma-separated fields
string fields[];
const int fieldCount = StringSplit(lines[i], ',', fields);
if(fieldCount < 8)
{
if(debugLogging) Print("Skipping malformed line ", i, ": ", lines[i]);
continue;
}
//--- Extract fixed-position fields from start and end of field array
const string dateStr = fields[0];
const string timeStr = fields[1];
const string currency = fields[2];
//--- Reconstruct event name by joining all middle fields that may contain commas
string eventName = fields[3];
for(int j = 4; j < fieldCount - 4; j++)
eventName += "," + fields[j];
const string impactStr = fields[fieldCount - 4];
const string actualStr = fields[fieldCount - 3];
const string forecastStr = fields[fieldCount - 2];
const string previousStr = fields[fieldCount - 1];
//--- Parse event datetime; retry with dot separator if slash format fails
datetime eventDt = StringToTime(dateStr + " " + timeStr);
if(eventDt == 0)
{
string altDate = dateStr;
StringReplace(altDate, "/", ".");
eventDt = StringToTime(altDate + " " + timeStr);
if(eventDt == 0)
{
if(debugLogging) Print("Skipping line ", i, " - invalid datetime: ", dateStr, " ", timeStr);
continue;
}
}
//--- Populate event record and append to array
ArrayResize(g_news_allEvents, eventIndex + 1);
g_news_allEvents[eventIndex].eventDate = dateStr;
g_news_allEvents[eventIndex].eventTime = timeStr;
g_news_allEvents[eventIndex].currency = currency;
g_news_allEvents[eventIndex].event = eventName;
g_news_allEvents[eventIndex].importance = impactStr;
g_news_allEvents[eventIndex].actual = StringToDouble(actualStr);
g_news_allEvents[eventIndex].forecast = StringToDouble(forecastStr);
g_news_allEvents[eventIndex].previous = StringToDouble(previousStr);
g_news_allEvents[eventIndex].eventDateTime = eventDt;
//--- Use line index as stable unique ID for trade deduplication
g_news_allEvents[eventIndex].eventId = (long)i;
eventIndex++;
}
if(debugLogging) Print("News_LoadEventsFromCsv: loaded ", eventIndex, " events");
return eventIndex > 0;
}
//+------------------------------------------------------------------+
//| Load events from MetaTrader live calendar API |
//+------------------------------------------------------------------+
int News_LoadEventsFromLive(datetime startDt, datetime endDt)
{
//--- Reset event array before loading
ArrayResize(g_news_allEvents, 0);
//--- Query all calendar values in the requested time window
MqlCalendarValue values[];
const int total = CalendarValueHistory(values, startDt, endDt, NULL, NULL);
if(total <= 0) return 0;
//--- Process each returned calendar value
int eventIndex = 0;
for(int i = 0; i < total; i++)
{
//--- Fetch the associated event and country metadata
MqlCalendarEvent ev;
if(!CalendarEventById(values[i].event_id, ev)) continue;
MqlCalendarCountry ctry;
CalendarCountryById(ev.country_id, ctry);
MqlCalendarValue val;
CalendarValueById(values[i].id, val);
//--- Populate event record with time and identity fields
ArrayResize(g_news_allEvents, eventIndex + 1);
g_news_allEvents[eventIndex].eventDate = TimeToString(values[i].time, TIME_DATE);
g_news_allEvents[eventIndex].eventTime = TimeToString(values[i].time, TIME_MINUTES);
g_news_allEvents[eventIndex].currency = ctry.currency;
g_news_allEvents[eventIndex].event = ev.name;
g_news_allEvents[eventIndex].importance = News_GetImpactLabel(ev.importance);
//--- Store has-value flags alongside doubles to distinguish missing from 0.0
g_news_allEvents[eventIndex].hasActual = val.HasActualValue();
g_news_allEvents[eventIndex].hasForecast = val.HasForecastValue();
g_news_allEvents[eventIndex].hasPrevious = val.HasPreviousValue();
g_news_allEvents[eventIndex].hasRevised = val.HasRevisedValue();
//--- Store raw double values guarded by their has-value flags
g_news_allEvents[eventIndex].actual = val.HasActualValue() ? val.GetActualValue() : 0.0;
g_news_allEvents[eventIndex].forecast = val.HasForecastValue() ? val.GetForecastValue() : 0.0;
g_news_allEvents[eventIndex].previous = val.HasPreviousValue() ? val.GetPreviousValue() : 0.0;
g_news_allEvents[eventIndex].revisedPrevious = val.HasRevisedValue() ? val.GetRevisedValue() : 0.0;
//--- Store formatting metadata for unit-aware value display
g_news_allEvents[eventIndex].unit = (int)ev.unit;
g_news_allEvents[eventIndex].multiplier = (int)ev.multiplier;
g_news_allEvents[eventIndex].digits = (int)ev.digits;
g_news_allEvents[eventIndex].eventDateTime = values[i].time;
g_news_allEvents[eventIndex].eventId = (long)values[i].event_id;
eventIndex++;
}
return eventIndex;
}
//+------------------------------------------------------------------+
//| Test if event passes the currency filter |
//+------------------------------------------------------------------+
bool News_PassesCurrencyFilter(const NewsEvent &ev)
{
//--- Pass all events when currency filter master switch is off
if(!g_news_filterCurrencyOn) return true;
//--- Check currency against each selected slot
for(int i = 0; i < NEWS_CURR_COUNT; i++)
{
if(g_news_currSelected[i] && ev.currency == NEWS_CURRENCIES[i]) return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Test if event passes the impact filter |
//+------------------------------------------------------------------+
bool News_PassesImpactFilter(const NewsEvent &ev)
{
//--- Pass all events when impact filter master switch is off
if(!g_news_filterImpactOn) return true;
//--- Check importance against each selected impact level
for(int i = 0; i < NEWS_IMPACT_COUNT; i++)
{
if(g_news_impactSelected[i] && ev.importance == NEWS_IMPACT_LABELS[i]) return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Test if event passes the time-window filter |
//+------------------------------------------------------------------+
bool News_PassesTimeFilter(const NewsEvent &ev)
{
//--- Pass all events when time filter master switch is off
if(!g_news_filterTimeOn) return true;
//--- Compute window bounds around current server time
const datetime now = TimeTradeServer();
const datetime span = PeriodSeconds(range_time);
const datetime lower = now - span;
const datetime upper = now + span;
return (ev.eventDateTime >= lower && ev.eventDateTime <= upper);
}
//+------------------------------------------------------------------+
//| Test whether a date string is in the collapsed day set |
//+------------------------------------------------------------------+
bool News_IsDayCollapsed(string date)
{
//--- Linear scan of collapsed days array
const int n = ArraySize(g_news_collapsedDays);
for(int i = 0; i < n; i++)
if(g_news_collapsedDays[i] == date) return true;
return false;
}
//+------------------------------------------------------------------+
//| Toggle collapsed state for a date string |
//+------------------------------------------------------------------+
bool News_ToggleDayCollapsed(string date)
{
const int n = ArraySize(g_news_collapsedDays);
//--- Remove date from set if already present
for(int i = 0; i < n; i++)
{
if(g_news_collapsedDays[i] == date)
{
for(int j = i; j < n - 1; j++)
g_news_collapsedDays[j] = g_news_collapsedDays[j + 1];
ArrayResize(g_news_collapsedDays, n - 1);
return false;
}
}
//--- Append date to set if not present
ArrayResize(g_news_collapsedDays, n + 1);
g_news_collapsedDays[n] = date;
return true;
}
//+------------------------------------------------------------------+
//| Build row plan with day separators from displayable events |
//+------------------------------------------------------------------+
void News_BuildRowPlan()
{
//--- Reset row plan array
ArrayResize(g_news_rowPlan, 0);
const int n = ArraySize(g_news_displayableEvents);
int planIdx = 0;
string lastDate = "";
//--- Walk all displayable events and emit day headers and event rows
for(int i = 0; i < n; i++)
{
const string evDate = g_news_displayableEvents[i].eventDate;
//--- Emit a day separator row when the date changes
if(evDate != lastDate)
{
//--- Count events belonging to this date for the header label
int dayCount = 0;
for(int k = i; k < n; k++)
{
if(g_news_displayableEvents[k].eventDate != evDate) break;
dayCount++;
}
//--- Build friendly day label: "Monday, August 12, 2025 - N events"
const datetime evDt = g_news_displayableEvents[i].eventDateTime;
MqlDateTime mdt;
TimeToStruct(evDt, mdt);
const string daysOfWeek[] = {"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
const string monthNames[] = {"January","February","March","April","May","June",
"July","August","September","October","November","December"};
const int dow = mdt.day_of_week;
const int mo = mdt.mon - 1;
const string countSuffix = " - " + IntegerToString(dayCount) + (dayCount == 1 ? " event" : " events");
const string label = daysOfWeek[dow] + ", " + monthNames[mo] + " "
+ IntegerToString(mdt.day) + ", " + IntegerToString(mdt.year) + countSuffix;
//--- Append day separator row to plan
ArrayResize(g_news_rowPlan, planIdx + 1);
g_news_rowPlan[planIdx].kind = NEWS_ROW_KIND_DAY;
g_news_rowPlan[planIdx].eventIdx = -1;
g_news_rowPlan[planIdx].label = label;
g_news_rowPlan[planIdx].dateKey = evDate;
planIdx++;
lastDate = evDate;
}
//--- Skip event rows for collapsed day groups (header row still appears)
if(News_IsDayCollapsed(evDate)) continue;
//--- Append event row to plan
ArrayResize(g_news_rowPlan, planIdx + 1);
g_news_rowPlan[planIdx].kind = NEWS_ROW_KIND_EVENT;
g_news_rowPlan[planIdx].eventIdx = i;
g_news_rowPlan[planIdx].label = "";
g_news_rowPlan[planIdx].dateKey = evDate;
planIdx++;
}
}
//+------------------------------------------------------------------+
//| Apply all filters and build the displayable events array |
//+------------------------------------------------------------------+
void News_ApplyFilters()
{
//--- Reset displayable array and record total considered count
ArrayResize(g_news_displayableEvents, 0);
g_news_totalConsidered = ArraySize(g_news_allEvents);
g_news_totalFiltered = 0;
const bool isTester = MQLInfoInteger(MQL_TESTER);
int outIdx = 0;
//--- Filter each event through tester window, currency, impact, and time checks
for(int i = 0; i < g_news_totalConsidered; i++)
{
//--- Apply tester date window check in strategy tester mode
if(isTester)
{
if(g_news_allEvents[i].eventDateTime < StartDate
|| g_news_allEvents[i].eventDateTime > EndDate) continue;
}
if(!News_PassesCurrencyFilter(g_news_allEvents[i])) continue;
if(!News_PassesImpactFilter(g_news_allEvents[i])) continue;
if(!News_PassesTimeFilter(g_news_allEvents[i])) continue;
//--- Append passing event to displayable array
ArrayResize(g_news_displayableEvents, outIdx + 1);
g_news_displayableEvents[outIdx] = g_news_allEvents[i];
outIdx++;
}
g_news_totalFiltered = outIdx;
g_news_filtersChanged = false;
//--- Insertion-sort displayable events ascending by event datetime (oldest first)
for(int i = 1; i < outIdx; i++)
{
const NewsEvent key = g_news_displayableEvents[i];
int j = i - 1;
while(j >= 0 && g_news_displayableEvents[j].eventDateTime > key.eventDateTime)
{
g_news_displayableEvents[j + 1] = g_news_displayableEvents[j];
j--;
}
g_news_displayableEvents[j + 1] = key;
}
//--- Rebuild row plan with day separators from sorted events
News_BuildRowPlan();
//--- Update change-tracking signature: count + last-event time + first event id
const int n = ArraySize(g_news_displayableEvents);
g_news_lastEventCount = n;
datetime sig = 0;
if(n > 0)
{
sig = g_news_displayableEvents[n - 1].eventDateTime
+ (datetime)(g_news_displayableEvents[0].eventId & 0xFFFFFFFF);
}
g_news_lastEventStamp = sig;
}
//+------------------------------------------------------------------+
//| Refresh events from data source and reapply filters |
//+------------------------------------------------------------------+
void News_RefreshEvents()
{
//--- In tester mode reuse the loaded CSV; just rerun filters
if(MQLInfoInteger(MQL_TESTER))
{
News_ApplyFilters();
}
else
{
//--- Compute live date window from current server time
const datetime now = TimeTradeServer();
const datetime startDt = now - PeriodSeconds(start_time);
const datetime endDt = now + PeriodSeconds(end_time);
News_LoadEventsFromLive(startDt, endDt);
News_ApplyFilters();
}
}
//+------------------------------------------------------------------+
//| Test if an event ID is already in the triggered list |
//+------------------------------------------------------------------+
bool News_AlreadyTriggered(long evId)
{
//--- Linear scan of triggered IDs array
const int n = ArraySize(g_news_triggeredIds);
for(int i = 0; i < n; i++)
{
if(g_news_triggeredIds[i] == evId) return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Candidate Event Cache |
//+------------------------------------------------------------------+
datetime g_news_cachedCandStamp = (datetime)-1; // Sentinel value meaning uncached
datetime g_news_cachedCandTime = 0; // Cached candidate event time
string g_news_cachedCandName = ""; // Cached candidate event name
string g_news_cachedCandSide = ""; // Cached candidate trade side
long g_news_cachedCandId = 0; // Cached candidate event ID
bool g_news_cachedCandFound = false; // Cached result validity flag
//+------------------------------------------------------------------+
//| Append event ID to triggered list and invalidate candidate cache |
//+------------------------------------------------------------------+
void News_MarkTriggered(long evId)
{
//--- Append ID to triggered list
const int n = ArraySize(g_news_triggeredIds);
ArrayResize(g_news_triggeredIds, n + 1);
g_news_triggeredIds[n] = evId;
//--- Invalidate candidate cache so next FindCandidate recomputes
g_news_cachedCandStamp = (datetime)-1;
}
//+------------------------------------------------------------------+
//| Determine trade direction from forecast vs previous values |
//+------------------------------------------------------------------+
string News_DecideTradeSide(double forecast, double previous)
{
//--- Return empty string when either value is missing or equal
if(forecast == 0.0 || previous == 0.0) return "";
if(forecast == previous) return "";
//--- Buy when forecast beats previous; sell when forecast misses
return (forecast > previous) ? "BUY" : "SELL";
}
//+------------------------------------------------------------------+
//| Place a market order in the chosen direction |
//+------------------------------------------------------------------+
bool News_ExecuteTrade(string side, string comment)
{
//--- Send buy or sell order with configured lot size
bool ok = false;
if(side == "BUY")
ok = g_news_trade.Buy(tradeLotSize, _Symbol, 0, 0, 0, comment);
else if(side == "SELL")
ok = g_news_trade.Sell(tradeLotSize, _Symbol, 0, 0, 0, comment);
//--- Show success or failure toast
if(ok)
News_ShowToast("Trade " + side + ": " + comment, false);
else
News_ShowToast("Trade FAILED: " + g_news_trade.ResultRetcodeDescription(), true);
return ok;
}
//+------------------------------------------------------------------+
//| Find the next upcoming event eligible for pre-news trading |
//+------------------------------------------------------------------+
bool News_FindCandidateEvent(datetime &outEventTime, string &outEventName,
string &outTradeSide, long &outEventId)
{
const datetime now = TimeTradeServer();
//--- Return cached result when event set is unchanged and candidate has not elapsed
if(g_news_cachedCandStamp == g_news_lastEventStamp
&& g_news_cachedCandFound
&& now < g_news_cachedCandTime)
{
outEventTime = g_news_cachedCandTime;
outEventName = g_news_cachedCandName;
outTradeSide = g_news_cachedCandSide;
outEventId = g_news_cachedCandId;
return true;
}
//--- Return cached negative result when event set is unchanged and no candidate was found
if(g_news_cachedCandStamp == g_news_lastEventStamp && !g_news_cachedCandFound)
{
outEventTime = 0; outEventName = ""; outTradeSide = ""; outEventId = 0;
return false;
}
//--- Compute search window and offset window in seconds
const datetime lower = now - PeriodSeconds(start_time);
const datetime upper = now + PeriodSeconds(end_time);
const int offsetSec = tradeOffsetHours * 3600
+ tradeOffsetMinutes * 60
+ tradeOffsetSeconds;
//--- Reset output parameters
outEventTime = 0;
outEventName = "";
outTradeSide = "";
outEventId = 0;
//--- Scan displayable events for the earliest eligible candidate
for(int i = 0; i < ArraySize(g_news_displayableEvents); i++)
{
const NewsEvent ev = g_news_displayableEvents[i];
//--- Skip events outside the search window
if(ev.eventDateTime < lower || ev.eventDateTime > upper) continue;
//--- Skip events already traded
if(News_AlreadyTriggered(ev.eventId)) continue;
//--- Skip events outside the pre-news trade offset window
if(now < (ev.eventDateTime - offsetSec) || now >= ev.eventDateTime) continue;
//--- Skip events with no determinable trade direction
const string side = News_DecideTradeSide(ev.forecast, ev.previous);
if(side == "") continue;
//--- Keep earliest candidate by event time
if(outEventTime == 0 || ev.eventDateTime < outEventTime)
{
outEventTime = ev.eventDateTime;
outEventName = ev.event;
outTradeSide = side;
outEventId = ev.eventId;
}
}
//--- Update candidate cache with search result
g_news_cachedCandStamp = g_news_lastEventStamp;
g_news_cachedCandFound = (outEventTime > 0);
g_news_cachedCandTime = outEventTime;
g_news_cachedCandName = outEventName;
g_news_cachedCandSide = outTradeSide;
g_news_cachedCandId = outEventId;
return g_news_cachedCandFound;
}
//+------------------------------------------------------------------+
//| Run pre-news trade decision and place order if appropriate |
//+------------------------------------------------------------------+
void News_CheckForNewsTrade()
{
//--- Skip execution when trade mode is disabled or paused
if(tradeMode == NEWS_NO_TRADE || tradeMode == NEWS_PAUSE_TRADING) return;
//--- Reset trade flag 15 seconds after news time to allow subsequent trades
if(g_news_tradeExecuted)
{
const datetime now = TimeTradeServer();
if(now > g_news_tradedNewsTime + 15)
{
g_news_tradeExecuted = false;
g_news_tradedNewsTime = 0;
}
return;
}
//--- Search for a valid candidate event
datetime evTime = 0;
string evName = "";
string side = "";
long evId = 0;
if(!News_FindCandidateEvent(evTime, evName, side, evId)) return;
//--- Execute pre-news trade and record the triggered event
if(tradeMode == NEWS_TRADE_BEFORE)
{
const string comment = "News: " + evName;
if(News_ExecuteTrade(side, comment))
{
News_MarkTriggered(evId);
g_news_tradeExecuted = true;
g_news_tradedNewsTime = evTime;
}
}
}
//+------------------------------------------------------------------+
//| Compute seconds remaining to the next eligible news event |
//+------------------------------------------------------------------+
int News_SecondsToNextNews()
{
//--- Return 0 when no candidate event exists
datetime evTime = 0;
string evName = "";
string side = "";
long evId = 0;
if(!News_FindCandidateEvent(evTime, evName, side, evId)) return 0;
//--- Return positive remaining seconds or 0 if already past
const int remaining = (int)(evTime - TimeTradeServer());
return (remaining > 0) ? remaining : 0;
}
//+------------------------------------------------------------------+
//| Format total seconds as "Hh Mm Ss" countdown string |
//+------------------------------------------------------------------+
string News_FormatCountdown(int totalSec)
{
//--- Return "Now" for zero or negative remaining time
if(totalSec <= 0) return "Now";
//--- Decompose into hours, minutes, and seconds
const int hrs = totalSec / 3600;
const int mins = (totalSec % 3600) / 60;
const int secs = totalSec % 60;
//--- Format with only the two most significant units
if(hrs > 0)
return IntegerToString(hrs) + "h " + IntegerToString(mins) + "m " + IntegerToString(secs) + "s";
if(mins > 0)
return IntegerToString(mins) + "m " + IntegerToString(secs) + "s";
return IntegerToString(secs) + "s";
}
//+------------------------------------------------------------------+
//| Get the name of the next eligible news event |
//+------------------------------------------------------------------+
string News_NextNewsName()
{
//--- Return empty string when no candidate event exists
datetime evTime = 0;
string evName = "";
string side = "";
long evId = 0;
if(!News_FindCandidateEvent(evTime, evName, side, evId)) return "";
return evName;
}
#endif // NEWS_LOGIC_MQH