MQL5Book/Experts/p7/CalendarTrading.mq5
super.admin 1c8e83ce31 convert
2025-05-30 16:09:41 +02:00

275 lines
9.3 KiB
MQL5

//+------------------------------------------------------------------+
//| 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<CalendarFilter> fptr;
AutoPtr<CalendarCache> cache;
AutoPtr<TrailingStop> 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<double, double, double, ulong, ulong> trades[];
MapArray<ulong,double> profits;
MapArray<ulong,double> losses;
MapArray<ulong,int> 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
}
}
}
}
//+------------------------------------------------------------------+