Article-22608-News-Calendar.../News Logic.mqh

536 lines
No EOL
23 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"
//--- 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