//+------------------------------------------------------------------+ //| 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 //--- 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