//+------------------------------------------------------------------+ //| 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" //--- Include SQLite database layer for event persistence #include "News Database.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; // Backtest window start date extern datetime EndDate; // Backtest window end date extern bool inp_DownloadDataInLive; // Download NEWS data for offline testing extern datetime inp_DownloadStartDate; // Download range start date extern datetime inp_DownloadEndDate; // Download range 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 Instance | //+------------------------------------------------------------------+ CTrade g_news_trade; // Trade execution helper instance //+------------------------------------------------------------------+ //| Load events from MetaTrader live calendar API and persist to DB | //+------------------------------------------------------------------+ 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; //--- Wrap all DB upserts in a transaction for ~10x speed over auto-commit const bool hasTx = DatabaseTransactionBegin(g_news_db); const datetime updatedAt = TimeCurrent(); int eventIndex = 0; //--- Process each returned calendar value for(int i = 0; i < total; i++) { //--- Fetch associated event metadata and country info 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 NewsEvent struct from calendar API data NewsEvent ne; ne.eventDate = TimeToString(values[i].time, TIME_DATE); ne.eventTime = TimeToString(values[i].time, TIME_MINUTES); ne.currency = ctry.currency; ne.event = ev.name; ne.importance = News_GetImpactLabel(ev.importance); ne.hasActual = val.HasActualValue(); ne.hasForecast = val.HasForecastValue(); ne.hasPrevious = val.HasPreviousValue(); ne.hasRevised = val.HasRevisedValue(); ne.actual = val.HasActualValue() ? val.GetActualValue() : 0.0; ne.forecast = val.HasForecastValue() ? val.GetForecastValue() : 0.0; ne.previous = val.HasPreviousValue() ? val.GetPreviousValue() : 0.0; ne.revisedPrevious = val.HasRevisedValue() ? val.GetRevisedValue() : 0.0; ne.unit = (int)ev.unit; ne.multiplier = (int)ev.multiplier; ne.digits = (int)ev.digits; ne.eventDateTime = values[i].time; ne.eventId = (long)values[i].event_id; //--- Append event to memory array and upsert to database ArrayResize(g_news_allEvents, eventIndex + 1); g_news_allEvents[eventIndex] = ne; News_DbUpsertEvent(values[i].id, ne, updatedAt); eventIndex++; } //--- Commit the transaction if one was started if(hasTx) DatabaseTransactionCommit(g_news_db); 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 string countSuffix = " - " + IntegerToString(dayCount) + (dayCount == 1 ? " event" : " events"); const string label = daysOfWeek[mdt.day_of_week] + ", " + monthNames[mdt.mon - 1] + " " + 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; int outIdx = 0; //--- Filter each event through currency, impact, and time checks for(int i = 0; i < g_news_totalConsidered; i++) { 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]; } 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 DB-loaded events in memory; just rerun filters if(MQLInfoInteger(MQL_TESTER)) { News_ApplyFilters(); } else { //--- Fetch fresh from MT5 calendar and persist to DB const datetime now = TimeTradeServer(); News_LoadEventsFromLive(now - PeriodSeconds(start_time), now + PeriodSeconds(end_time)); News_ApplyFilters(); } } //+------------------------------------------------------------------+ //| Test if an event ID is already in the triggered list | //+------------------------------------------------------------------+ bool News_AlreadyTriggered(long evId) { //--- Delegate to database module's in-memory triggered list check return News_DbAlreadyTriggered(evId); } //+------------------------------------------------------------------+ //| 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 persist to database | //+------------------------------------------------------------------+ void News_MarkTriggered(long evId) { //--- Add ID to the in-memory triggered array const int n = ArraySize(g_news_triggeredIds); ArrayResize(g_news_triggeredIds, n + 1); g_news_triggeredIds[n] = evId; //--- Persist triggered ID to database for restart safety News_DbMarkTriggered(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 found if(g_news_cachedCandStamp == g_news_lastEventStamp && !g_news_cachedCandFound) { outEventTime = 0; outEventName = ""; outTradeSide = ""; outEventId = 0; return false; } //--- Compute search window and offset 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) { if(TimeTradeServer() > 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) { if(News_ExecuteTrade(side, "News: " + evName)) { 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