//+------------------------------------------------------------------+ //| CalendarTrading.mq5 | //| Copyright 2022, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2022, MetaQuotes Ltd." #property link "https://www.mql5.com" #property description "Trade by calendar events in the tester or online." #property tester_file "xyz.cal" #define SHOW_WARNINGS // output extended info into the log, with changes in data state #define WARNING Print // use simple Print for warnings (instead of a built-in format with line numbers etc.) #define LOGGING // calendar detailed logs #include "..\..\Include\MqlTradeSync.mqh" #include "..\..\Include\PositionFilter.mqh" #include "..\..\Include\TrailingStop.mqh" #include "..\..\Include\CalendarFilterCached.mqh" #include "..\..\Include\StringUtils.mqh" #include "..\..\Include\DealFilter.mqh" #include "..\..\Include\Tuples.mqh" input double Volume; // Volume (0 = minimal lot) input int Distance2SLTP = 500; // Distance to SL/TP in points (0 = no) input uint MultiplePositions = 25; sinput ulong EventID; sinput string Text; AutoPtr fptr; AutoPtr cache; AutoPtr trailing[]; double Lot; bool Hedging; string Base; string Profit; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(AccountInfoInteger(ACCOUNT_TRADE_MODE) != ACCOUNT_TRADE_MODE_DEMO) { Alert("This is a test EA! Run it on a DEMO account only!"); return INIT_FAILED; } Lot = Volume == 0 ? SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) : Volume; Hedging = AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING; Base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE); Profit = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT); cache = new CalendarCache("xyz.cal", true); if(cache[].isLoaded()) { fptr = new CalendarFilterCached(cache[]); } else { if(!MQLInfoInteger(MQL_TESTER)) { Print("Calendar cache file not found, fall back to online mode"); fptr = new CalendarFilter(); } else { Print("Can't proceed in the tester without calendar cache file"); return INIT_FAILED; } } CalendarFilter *f = fptr[]; if(!f.isLoaded()) return INIT_FAILED; // if a specific event is given, use it if(EventID > 0) f.let(EventID); else { // otherwise track news for current chart currencies only f.let(Base); if(Base != Profit) { f.let(Profit); } // monitor high impact economic indicators with available forecasts f.let(CALENDAR_TYPE_INDICATOR); f.let(LONG_MIN, CALENDAR_PROPERTY_RECORD_FORECAST, NOT_EQUAL); f.let(CALENDAR_IMPORTANCE_HIGH); if(StringLen(Text)) f.let(Text); } f.describe(); if(Distance2SLTP) { ArrayResize(trailing, Hedging && MultiplePositions ? MultiplePositions : 1); } // setup timer for periodic trade by news EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Custom tester event handler | //+------------------------------------------------------------------+ double OnTester() { Print("Trade profits by calendar events:"); HistorySelect(0, LONG_MAX); DealFilter filter; int props[] = {DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_MAGIC, DEAL_TIME}; filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE) .let(DEAL_ENTRY, (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY), IS::OR_BITWISE); Tuple5 trades[]; MapArray profits; MapArray losses; MapArray counts; if(filter.select(props, trades)) { for(int i = 0; i < ArraySize(trades); ++i) { counts.inc((ulong)trades[i]._4); const double payout = trades[i]._1 + trades[i]._2 + trades[i]._3; if(payout >= 0) { profits.inc((ulong)trades[i]._4, payout); losses.inc((ulong)trades[i]._4, 0); } else { profits.inc((ulong)trades[i]._4, 0); losses.inc((ulong)trades[i]._4, payout); } } for(int i = 0; i < profits.getSize(); ++i) { MqlCalendarEvent event; MqlCalendarCountry country; const ulong keyId = profits.getKey(i); if(cache[].calendarEventById(keyId, event) && cache[].calendarCountryById(event.country_id, country)) { PrintFormat("%lld %s %s %+.2f [%d] (PF:%.2f) %s", event.id, country.code, country.currency, profits[keyId] + losses[keyId], counts[keyId], profits[keyId] / (losses[keyId] != 0 ? -losses[keyId] : DBL_MIN), event.name); } else { Print("undefined ", DoubleToString(profits.getValue(i), 2)); } } } return 0; } //+------------------------------------------------------------------+ //| Timer event handler | //+------------------------------------------------------------------+ void OnTimer() { CalendarFilter *f = fptr[]; MqlCalendarValue records[]; f.let(TimeTradeServer() - SCOPE_DAY, TimeTradeServer() + SCOPE_DAY); if(f.update(records)) // find changes that match filters { // print changes to log static const ENUM_CALENDAR_PROPERTY props[] = { CALENDAR_PROPERTY_RECORD_TIME, CALENDAR_PROPERTY_COUNTRY_CURRENCY, CALENDAR_PROPERTY_COUNTRY_CODE, CALENDAR_PROPERTY_EVENT_NAME, CALENDAR_PROPERTY_EVENT_IMPORTANCE, CALENDAR_PROPERTY_RECORD_ACTUAL, CALENDAR_PROPERTY_RECORD_FORECAST, CALENDAR_PROPERTY_RECORD_PREVISED, CALENDAR_PROPERTY_RECORD_IMPACT, }; static const int p = ArraySize(props); string result[]; f.format(records, props, result); for(int i = 0; i < ArraySize(result) / p; ++i) { Print(SubArrayCombine(result, " | ", i * p, p)); } // calculate news impact static const int impacts[3] = {0, +1, -1}; int impact = 0; string about = ""; ulong lasteventid = 0; for(int i = 0; i < ArraySize(records); ++i) { const int sign = result[i * p + 1] == Profit ? -1 : +1; impact += sign * impacts[records[i].impact_type]; about += StringFormat("%+lld ", sign * (long)records[i].event_id); lasteventid = records[i].event_id; } if(impact == 0) return; // no signal // close existing positions if needed PositionFilter positions; ulong tickets[]; positions.let(POSITION_SYMBOL, _Symbol).select(tickets); const int n = ArraySize(tickets); if(n >= (int)(Hedging ? MultiplePositions : 1)) { MqlTradeRequestSync position; position.close(_Symbol) && position.completed(); } // open new position according to the signal direction MqlTradeRequestSync request; request.magic = lasteventid; request.comment = about; const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); ulong ticket = 0; if(impact > 0) { ticket = request.buy(Lot, 0, Distance2SLTP ? ask - point * Distance2SLTP : 0, Distance2SLTP ? ask + point * Distance2SLTP : 0); } else if(impact < 0) { ticket = request.sell(Lot, 0, Distance2SLTP ? bid + point * Distance2SLTP : 0, Distance2SLTP ? bid - point * Distance2SLTP : 0); } if(ticket && request.completed() && Distance2SLTP) { for(int i = 0; i < ArraySize(trailing); ++i) { if(trailing[i][] == NULL) // find free slot, create trailing object { trailing[i] = new TrailingStop(ticket, Distance2SLTP, Distance2SLTP / 50); break; } } } } } //+------------------------------------------------------------------+ //| Tick event handler | //+------------------------------------------------------------------+ void OnTick() { for(int i = 0; i < ArraySize(trailing); ++i) { if(trailing[i][]) { if(!trailing[i][].trail()) // position was closed { trailing[i] = NULL; // free the slot, delete object } } } } //+------------------------------------------------------------------+