635 lines
No EOL
27 KiB
MQL5
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 |