536 lines
23 KiB
MQL5
536 lines
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
|