forked from rosh/MQL5Book
275 lines
9.3 KiB
MQL5
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
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
|