1060 lines
32 KiB
MQL5
1060 lines
32 KiB
MQL5
|
//+------------------------------------------------------------------+
|
||
|
//| UnityMartingaleDraft1.mq5 |
|
||
|
//| Copyright (c) 2022, MetaQuotes Ltd. |
|
||
|
//| https://www.mql5.com |
|
||
|
//+------------------------------------------------------------------+
|
||
|
#property copyright "Copyright (c) 2022, MetaQuotes Ltd."
|
||
|
#property link "https://www.mql5.com"
|
||
|
#property description "Multi-currency expert adviser based on Unity indicator and reversal strategy with martingale."
|
||
|
|
||
|
#property tester_set "UnityMartingale-eurusd.set"
|
||
|
#property tester_set "UnityMartingale-gbpchf.set"
|
||
|
#property tester_set "UnityMartingale-audjpy.set"
|
||
|
#property tester_set "UnityMartingale-combo.set"
|
||
|
#property tester_no_cache
|
||
|
|
||
|
#include "..\..\Include\DateTime.mqh"
|
||
|
#include "..\..\Include\SymbolMonitor.mqh"
|
||
|
#include "..\..\Include\PositionFilter.mqh"
|
||
|
#include "..\..\Include\DealFilter.mqh"
|
||
|
#include "..\..\Include\MqlTradeSync.mqh"
|
||
|
#include "..\..\Include\TradeState.mqh"
|
||
|
#include "..\..\Include\AutoPtr.mqh"
|
||
|
#include "..\..\Include\TrailingStop.mqh"
|
||
|
#include "..\..\Include\MultiSymbolMonitor.mqh"
|
||
|
#include "..\..\Include\Tuples.mqh"
|
||
|
#include "..\..\Include\RSquared.mqh"
|
||
|
#include "..\..\Include\TickModel.mqh"
|
||
|
|
||
|
enum ERROR_TIMEOUT
|
||
|
{
|
||
|
bt_NONE = 0, // none
|
||
|
bt_SECOND = 1, // second
|
||
|
bt_MINUTE = 60, // minute (M1)
|
||
|
bt_HOUR = 60 * 60, // hour (H1)
|
||
|
bt_SESSION = 60 * 60 * 4, // session (H4)
|
||
|
bt_DAY = 60 * 60 * 24, // day (D1)
|
||
|
bt_MONTH = 60 * 60 * 24 * 30, // month (MN)
|
||
|
bt_YEAR = 60 * 60 * 24 * 365, // year
|
||
|
bt_FOREVER = UINT_MAX // forever
|
||
|
};
|
||
|
|
||
|
enum SIGNAL_TYPE
|
||
|
{
|
||
|
BREAKOUT,
|
||
|
PULLBACK
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Inputs |
|
||
|
//+------------------------------------------------------------------+
|
||
|
|
||
|
input group "S Y M B O L S E T T I N G S"
|
||
|
input bool UseTime = true; // UseTime (HourStart and HourEnd)
|
||
|
input uint HourStart = 2; // HourStart (0...23)
|
||
|
input uint HourEnd = 22; // HourEnd (0...23)
|
||
|
input double Lots = 0.01; // Lots (initial)
|
||
|
input double Factor = 2.0; // Factor (lot multiplication)
|
||
|
input uint Limit = 5; // Limit (max number of multiplications)
|
||
|
input uint StopLoss = 500; // StopLoss (points)
|
||
|
input uint TakeProfit = 500; // TakeProfit (points)
|
||
|
input SIGNAL_TYPE StartType = 0; // SignalType
|
||
|
|
||
|
input group "C O M M O N S E T T I N G S"
|
||
|
sinput ulong Magic = 1234567890; // Magic
|
||
|
input ERROR_TIMEOUT SkipTimeOnError = bt_SECOND; // SkipTimeOnError
|
||
|
input bool Trailing = true; // Trailing
|
||
|
input string WorkSymbols = ""; // WorkSymbols (name±lots*factor^limit(sl,tp)[start,stop];...)
|
||
|
|
||
|
input group "U N I T Y S E T T I N G S"
|
||
|
input string UnitySymbols = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
|
||
|
input int UnityBarLimit = 10;
|
||
|
input ENUM_APPLIED_PRICE UnityPriceType = PRICE_CLOSE;
|
||
|
input ENUM_MA_METHOD UnityPriceMethod = MODE_EMA;
|
||
|
input int UnityPricePeriod = 1;
|
||
|
|
||
|
#define SLTP_DEFAULT 1000
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Complete set of settings for one symbol |
|
||
|
//+------------------------------------------------------------------+
|
||
|
struct Settings
|
||
|
{
|
||
|
bool useTime;
|
||
|
uint hourStart;
|
||
|
uint hourEnd;
|
||
|
double lots;
|
||
|
double factor;
|
||
|
uint limit;
|
||
|
uint stopLoss;
|
||
|
uint takeProfit;
|
||
|
SIGNAL_TYPE startType;
|
||
|
ulong magic;
|
||
|
ERROR_TIMEOUT skipTimeOnError;
|
||
|
bool trailing;
|
||
|
string symbol;
|
||
|
|
||
|
void defaults()
|
||
|
{
|
||
|
useTime = false;
|
||
|
hourStart = hourEnd = 0;
|
||
|
lots = 0.01;
|
||
|
factor = 1;
|
||
|
limit = 1;
|
||
|
stopLoss = SLTP_DEFAULT;
|
||
|
takeProfit = SLTP_DEFAULT;
|
||
|
startType = BREAKOUT;
|
||
|
magic = 0;
|
||
|
skipTimeOnError = bt_NONE;
|
||
|
symbol = _Symbol;
|
||
|
trailing = false;
|
||
|
}
|
||
|
|
||
|
int range(const string &line, const string opener, const string closer,
|
||
|
uint &min, uint &max)
|
||
|
{
|
||
|
int p, q;
|
||
|
q = StringFind(line, opener);
|
||
|
if(q == -1)
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
p = q + 1;
|
||
|
q = StringFind(line, closer, p);
|
||
|
if(q == -1)
|
||
|
{
|
||
|
PrintFormat("WARNING: Range has no closing brace %s for %s", closer, opener);
|
||
|
return -1; // no pair brace
|
||
|
}
|
||
|
|
||
|
string elements[];
|
||
|
const string substr = StringSubstr(line, p, q - p);
|
||
|
const int r = StringSplit(substr, ',', elements);
|
||
|
if(r == 2)
|
||
|
{
|
||
|
min = (int)elements[0];
|
||
|
max = (int)elements[1];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
PrintFormat("WARNING: Range within %s%s should contain 2 elements, %d given",
|
||
|
opener, closer, r);
|
||
|
}
|
||
|
|
||
|
return r;
|
||
|
}
|
||
|
|
||
|
// syntax: name±lots*factor^limit(sl,tp)[start,stop];...
|
||
|
// parentheses and brackets are optional
|
||
|
// examples: EURUSD+0.01*2^5
|
||
|
// EURUSD+0.01*2^5(500,1000)[2,22]
|
||
|
// EURUSD+0.01*2.0^7(500,500)[2,22];AUDJPY+0.01*2.0^8(300,500)[2,22];GBPCHF+0.01*1.7^8(1000,2000)[2,22]
|
||
|
|
||
|
bool parse(const string &line)
|
||
|
{
|
||
|
defaults();
|
||
|
|
||
|
// obligatory part
|
||
|
int p = StringFind(line, "+");
|
||
|
if(p == -1) p = StringFind(line, "-");
|
||
|
if(p == -1) return false;
|
||
|
|
||
|
symbol = StringSubstr(line, 0, p);
|
||
|
startType = line[p] == '+' ? BREAKOUT : PULLBACK;
|
||
|
int q = StringFind(line, "*", ++p);
|
||
|
if(q == -1) return false;
|
||
|
lots = (double)StringSubstr(line, p, q - p);
|
||
|
p = q + 1;
|
||
|
q = StringFind(line, "^", p);
|
||
|
if(q == -1) return false;
|
||
|
factor = (double)StringSubstr(line, p, q - p);
|
||
|
p = q + 1;
|
||
|
limit = (int)StringSubstr(line, p);
|
||
|
|
||
|
// optional part
|
||
|
|
||
|
if(range(line, "(", ")", stopLoss, takeProfit) == -1)
|
||
|
{
|
||
|
return false; // error
|
||
|
}
|
||
|
const int plan = range(line, "[", "]", hourStart, hourEnd);
|
||
|
if(plan == 2)
|
||
|
{
|
||
|
useTime = true;
|
||
|
}
|
||
|
else if(plan == -1)
|
||
|
{
|
||
|
return false; // error
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool static parseAll(const string &line, Settings &settings[])
|
||
|
{
|
||
|
string symbols[];
|
||
|
int n = StringSplit(line, ';', symbols);
|
||
|
ArrayResize(settings, n);
|
||
|
|
||
|
string hash = "";
|
||
|
|
||
|
for(int i = 0; i < n; i++)
|
||
|
{
|
||
|
if(!settings[i].parse(symbols[i]))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
string signature = "^" + settings[i].symbol + "$";
|
||
|
if(StringFind(hash, signature) > -1)
|
||
|
{
|
||
|
Print("WARNING: Duplicate of symbol ", settings[i].symbol, " found");
|
||
|
}
|
||
|
|
||
|
settings[i].print();
|
||
|
if(settings[i].validate())
|
||
|
{
|
||
|
hash += signature;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Print("Invalid settings: trade system will be ignored");
|
||
|
settings[i].symbol = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return StringLen(hash) > 0; // some settings are correct
|
||
|
}
|
||
|
|
||
|
bool validate()
|
||
|
{
|
||
|
SymbolMonitor s(symbol);
|
||
|
|
||
|
if(takeProfit == 0 && !trailing)
|
||
|
{
|
||
|
Print("Either TakeProfit or Trailing should be applied");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(stopLoss == 0 && trailing)
|
||
|
{
|
||
|
Print("StopLoss required for Trailing");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
const double minLot = s.get(SYMBOL_VOLUME_MIN);
|
||
|
if(lots < minLot)
|
||
|
{
|
||
|
lots = minLot;
|
||
|
Print("Minimal lot ", (float)minLot, " is applied for ", symbol);
|
||
|
}
|
||
|
|
||
|
const double maxLot = s.get(SYMBOL_VOLUME_MAX);
|
||
|
if(lots > maxLot)
|
||
|
{
|
||
|
lots = maxLot;
|
||
|
Print("Maximal lot ", (float)maxLot, " is applied for ", symbol);
|
||
|
}
|
||
|
|
||
|
if(hourStart == hourEnd && hourStart != 0)
|
||
|
{
|
||
|
Print("For 24-hour schedule use 0-0 hours or disable UseTime");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// check if specified symbol exists
|
||
|
// AND initiate its history loading into the tester (when running in the tester)
|
||
|
// without this the tester loads only one symbol selected for the chart
|
||
|
double rates[1];
|
||
|
const bool success = CopyClose(symbol, PERIOD_CURRENT, 0, 1, rates) > -1;
|
||
|
if(!success)
|
||
|
{
|
||
|
Print("Unknown symbol: ", symbol, " ", E2S(_LastError));
|
||
|
}
|
||
|
return success;
|
||
|
}
|
||
|
|
||
|
void print() const
|
||
|
{
|
||
|
Print(symbol, (startType == BREAKOUT ? "+" : "-"), (float)lots,
|
||
|
"*", (float)factor,
|
||
|
"^", limit,
|
||
|
"(", stopLoss, ",", takeProfit, ")",
|
||
|
useTime ? "[" + (string)hourStart + "," + (string)hourEnd + "]": "");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Simple common interface for trading |
|
||
|
//+------------------------------------------------------------------+
|
||
|
interface TradingStrategy
|
||
|
{
|
||
|
virtual bool trade(void);
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Simple common interface for trading signals |
|
||
|
//+------------------------------------------------------------------+
|
||
|
interface TradingSignal
|
||
|
{
|
||
|
virtual int signal(void);
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Helper class to manage and read data from Unity indicator |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class UnityController
|
||
|
{
|
||
|
int handle; // indicator handle
|
||
|
int buffers; // number of buffers, registered in the indicator
|
||
|
const int bar; // bar number where to read values
|
||
|
double data[]; // current values read from the buffers
|
||
|
datetime lastRead; // last time values were read
|
||
|
const bool tickwise; // work by ticks (true) or by bars (false)
|
||
|
MultiSymbolMonitor sync;
|
||
|
|
||
|
public:
|
||
|
UnityController(const string symbolList, const int offset, const int limit,
|
||
|
const ENUM_APPLIED_PRICE type, const ENUM_MA_METHOD method, const int period):
|
||
|
bar(offset), tickwise(!offset)
|
||
|
{
|
||
|
handle = iCustom(_Symbol, _Period, "MQL5Book/p6/UnityPercentEvent",
|
||
|
symbolList, limit, type, method, period);
|
||
|
lastRead = 0;
|
||
|
|
||
|
string symbols[];
|
||
|
const int n = StringSplit(symbolList, ',', symbols);
|
||
|
for(int i = 0; i < n; ++i)
|
||
|
{
|
||
|
sync.attach(symbols[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
~UnityController()
|
||
|
{
|
||
|
IndicatorRelease(handle);
|
||
|
}
|
||
|
|
||
|
void attached(const int b)
|
||
|
{
|
||
|
buffers = b;
|
||
|
ArrayResize(data, buffers);
|
||
|
}
|
||
|
|
||
|
bool isReady()
|
||
|
{
|
||
|
return sync.check(true) == 0;
|
||
|
}
|
||
|
|
||
|
bool isNewTime() const
|
||
|
{
|
||
|
return lastRead != lastTime();
|
||
|
}
|
||
|
|
||
|
datetime lastTime() const
|
||
|
{
|
||
|
// when in sync, all symbols have the same 0-th bar time
|
||
|
return tickwise ? TimeTradeServer() : iTime(_Symbol, _Period, 0);
|
||
|
}
|
||
|
|
||
|
bool getOuterIndices(int &min, int &max)
|
||
|
{
|
||
|
if(isNewTime())
|
||
|
{
|
||
|
if(!read()) return false;
|
||
|
}
|
||
|
max = ArrayMaximum(data);
|
||
|
min = ArrayMinimum(data);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
double operator[](const int buffer)
|
||
|
{
|
||
|
if(isNewTime())
|
||
|
{
|
||
|
if(!read())
|
||
|
{
|
||
|
return EMPTY_VALUE;
|
||
|
}
|
||
|
}
|
||
|
return data[buffer];
|
||
|
}
|
||
|
|
||
|
bool read()
|
||
|
{
|
||
|
if(!buffers) return false;
|
||
|
for(int i = 0; i < buffers; ++i)
|
||
|
{
|
||
|
double temp[1];
|
||
|
if(CopyBuffer(handle, i, bar, 1, temp) == 1)
|
||
|
{
|
||
|
data[i] = temp[0];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
lastRead = lastTime();
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Concrete trading signal based on Unity indicator |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class UnitySignal: public TradingSignal
|
||
|
{
|
||
|
UnityController *controller;
|
||
|
const int currency1;
|
||
|
const int currency2;
|
||
|
|
||
|
public:
|
||
|
UnitySignal(UnityController *parent, const int c1, const int c2):
|
||
|
controller(parent), currency1(c1), currency2(c2) { }
|
||
|
|
||
|
virtual int signal(void) override
|
||
|
{
|
||
|
if(!controller.isReady()) return 0; // wait until go out of sync
|
||
|
if(!controller.isNewTime()) return 0;
|
||
|
|
||
|
int min, max;
|
||
|
if(!controller.getOuterIndices(min, max)) return 0;
|
||
|
|
||
|
// overbought - can be breakout or pullback
|
||
|
if(currency1 == max && currency2 == min) return +1;
|
||
|
// oversold - can be breakout or pullback
|
||
|
if(currency2 == max && currency1 == min) return -1;
|
||
|
|
||
|
// can emit early exit signals as well
|
||
|
// if(controller[currency1] > controller[currency2]) return +2; // exit sells
|
||
|
// if(controller[currency1] < controller[currency2]) return -2; // exit buys
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Main class with trading strategy |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class UnityMartingale: public TradingStrategy
|
||
|
{
|
||
|
protected:
|
||
|
Settings settings;
|
||
|
SymbolMonitor symbol;
|
||
|
AutoPtr<PositionState> position;
|
||
|
AutoPtr<TrailingStop> trailing;
|
||
|
AutoPtr<TradingSignal> command;
|
||
|
|
||
|
double lotsStep;
|
||
|
double lotsLimit;
|
||
|
double takeProfit, stopLoss;
|
||
|
|
||
|
bool paused;
|
||
|
datetime badConditions;
|
||
|
|
||
|
public:
|
||
|
UnityMartingale(const Settings &state, TradingSignal *signal):
|
||
|
symbol(state.symbol)
|
||
|
{
|
||
|
settings = state;
|
||
|
paused = false;
|
||
|
badConditions = 0;
|
||
|
command = signal;
|
||
|
|
||
|
// assign member variables
|
||
|
const double point = symbol.get(SYMBOL_POINT);
|
||
|
takeProfit = settings.takeProfit * point;
|
||
|
stopLoss = settings.stopLoss * point;
|
||
|
lotsLimit = settings.lots;
|
||
|
lotsStep = symbol.get(SYMBOL_VOLUME_STEP);
|
||
|
|
||
|
// calculate maximal lot after predefined number of multiplications
|
||
|
for(int pos = 0; pos < (int)settings.limit; pos++)
|
||
|
{
|
||
|
lotsLimit = MathFloor((lotsLimit * settings.factor) / lotsStep) * lotsStep;
|
||
|
}
|
||
|
|
||
|
double maxLot = symbol.get(SYMBOL_VOLUME_MAX);
|
||
|
if(lotsLimit > maxLot)
|
||
|
{
|
||
|
lotsLimit = maxLot;
|
||
|
}
|
||
|
|
||
|
// pick up existing positions (if any)
|
||
|
PositionFilter positions;
|
||
|
ulong tickets[];
|
||
|
positions.let(POSITION_MAGIC, settings.magic).let(POSITION_SYMBOL, settings.symbol)
|
||
|
.select(tickets);
|
||
|
const int n = ArraySize(tickets);
|
||
|
if(n > 1)
|
||
|
{
|
||
|
Alert(StringFormat("Too many positions: %d", n));
|
||
|
// TODO: close old positions
|
||
|
}
|
||
|
else if(n > 0)
|
||
|
{
|
||
|
position = new PositionState(tickets[0]);
|
||
|
if(settings.stopLoss && settings.trailing)
|
||
|
{
|
||
|
trailing = new TrailingStop(tickets[0], settings.stopLoss,
|
||
|
((int)symbol.get(SYMBOL_SPREAD) + 1) * 2);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool scheduled(const datetime now)
|
||
|
{
|
||
|
const long hour = (now % 86400) / 3600;
|
||
|
if(settings.hourStart < settings.hourEnd)
|
||
|
{
|
||
|
return hour >= settings.hourStart && hour < settings.hourEnd;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return hour >= settings.hourStart || hour < settings.hourEnd;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
virtual bool trade() override
|
||
|
{
|
||
|
// if an error occured in the recent past, wait a predefined period
|
||
|
if(settings.skipTimeOnError > 0 && badConditions ==
|
||
|
TimeCurrent() / settings.skipTimeOnError * settings.skipTimeOnError)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// work hours
|
||
|
if(settings.useTime && !scheduled(TimeCurrent()))
|
||
|
{
|
||
|
// if position is open - close it
|
||
|
if(position[] && position[].isReady())
|
||
|
{
|
||
|
if(close(position[].get(POSITION_TICKET)))
|
||
|
{
|
||
|
// NB: we could keep position in cache to find new direction
|
||
|
// and continue series of lot multiplication between schedules,
|
||
|
// then do not do "NULLifing" on the next line
|
||
|
position = NULL;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// errors are handled inside 'close', i.e. trading is paused for a while
|
||
|
position[].refresh(); // make sure 'ready' flag is dropped or kept by actual state
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
int s = command[].signal();
|
||
|
if(s != 0)
|
||
|
{
|
||
|
if(settings.startType == PULLBACK) s *= -1; // reverse logic
|
||
|
}
|
||
|
|
||
|
ulong ticket = 0;
|
||
|
|
||
|
if(position[] == NULL) // fresh start - no position existing or existed
|
||
|
{
|
||
|
if(s == +1)
|
||
|
{
|
||
|
ticket = openBuy(settings.lots);
|
||
|
}
|
||
|
else if(s == -1)
|
||
|
{
|
||
|
ticket = openSell(settings.lots);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(position[].refresh()) // position still exists
|
||
|
{
|
||
|
if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s == -1)
|
||
|
|| (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s == +1))
|
||
|
{
|
||
|
PrintFormat("Opposite signal: %d for position %d %lld",
|
||
|
s, position[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
|
||
|
if(close(position[].get(POSITION_TICKET)))
|
||
|
{
|
||
|
// position = NULL; - keep position info in the cache
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// errors are handled inside 'close', i.e. trading is paused for a while
|
||
|
position[].refresh(); // make sure 'ready' flag is dropped if closed anyway
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
position[].update();
|
||
|
if(trailing[]) trailing[].trail();
|
||
|
}
|
||
|
}
|
||
|
else // position closed - let open next one
|
||
|
{
|
||
|
if(s == 0) // while no signals keep martingale
|
||
|
{
|
||
|
// use cached object to read former position properties: profit and lot
|
||
|
if(position[].get(POSITION_PROFIT) >= 0.0)
|
||
|
{
|
||
|
// keep previous trade direction
|
||
|
if(position[].get(POSITION_TYPE) == POSITION_TYPE_BUY)
|
||
|
ticket = openBuy(settings.lots); // BUY in case of previous profitable BUY
|
||
|
else
|
||
|
ticket = openSell(settings.lots); // SELL in case of previous profitable SELL
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
double lots = MathFloor((position[].get(POSITION_VOLUME) * settings.factor) / lotsStep) * lotsStep;
|
||
|
|
||
|
if(lotsLimit < lots)
|
||
|
{
|
||
|
lots = settings.lots;
|
||
|
}
|
||
|
|
||
|
// change trade direction
|
||
|
if(position[].get(POSITION_TYPE) == POSITION_TYPE_BUY)
|
||
|
ticket = openSell(lots); // SELL in case of previous lossy BUY
|
||
|
else
|
||
|
ticket = openBuy(lots); // BUY in case of previous lossy SELL
|
||
|
}
|
||
|
}
|
||
|
else // if a signal exists do multiplied lots after a loss
|
||
|
{
|
||
|
double lots;
|
||
|
if(position[].get(POSITION_PROFIT) >= 0.0)
|
||
|
{
|
||
|
lots = settings.lots;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
lots = MathFloor((position[].get(POSITION_VOLUME) * settings.factor) / lotsStep) * lotsStep;
|
||
|
|
||
|
if(lotsLimit < lots)
|
||
|
{
|
||
|
lots = settings.lots;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ticket = (s == +1) ? openBuy(lots) : openSell(lots);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(ticket > 0) // new position is just opened
|
||
|
{
|
||
|
position = new PositionState(ticket);
|
||
|
if(settings.stopLoss && settings.trailing)
|
||
|
{
|
||
|
trailing = new TrailingStop(ticket, settings.stopLoss,
|
||
|
((int)symbol.get(SYMBOL_SPREAD) + 1) * 2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
protected:
|
||
|
bool checkFreeMargin(const ENUM_ORDER_TYPE type, const double price, double &lots) const
|
||
|
{
|
||
|
double margin;
|
||
|
if(OrderCalcMargin(type, settings.symbol, lots, price, margin))
|
||
|
{
|
||
|
if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) > margin)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// fallback to starting lot due to insufficient margin
|
||
|
lots = settings.lots;
|
||
|
|
||
|
if(OrderCalcMargin(type, settings.symbol, lots, price, margin))
|
||
|
{
|
||
|
return AccountInfoDouble(ACCOUNT_MARGIN_FREE) > margin;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
void prepare(MqlTradeRequestSync &request)
|
||
|
{
|
||
|
request.deviation = (int)(symbol.get(SYMBOL_SPREAD) + 1) * 2;
|
||
|
request.magic = settings.magic;
|
||
|
}
|
||
|
|
||
|
ulong postprocess(MqlTradeRequestSync &request)
|
||
|
{
|
||
|
if(request.result.order == 0)
|
||
|
{
|
||
|
badConditions = TimeCurrent() / settings.skipTimeOnError * settings.skipTimeOnError;
|
||
|
if(!paused)
|
||
|
{
|
||
|
Print("Pausing due to bad conditions: ", badConditions);
|
||
|
paused = true;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(request.completed())
|
||
|
{
|
||
|
paused = false;
|
||
|
return request.result.position;
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
ulong openBuy(double lots)
|
||
|
{
|
||
|
const double price = symbol.get(SYMBOL_ASK);
|
||
|
|
||
|
if(!checkFreeMargin(ORDER_TYPE_BUY, price, lots)) return 0;
|
||
|
|
||
|
MqlTradeRequestSync request;
|
||
|
prepare(request);
|
||
|
if(request.buy(settings.symbol, lots, price,
|
||
|
stopLoss ? price - stopLoss : 0,
|
||
|
takeProfit ? price + takeProfit : 0))
|
||
|
{
|
||
|
return postprocess(request);
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
ulong openSell(double lots)
|
||
|
{
|
||
|
const double price = symbol.get(SYMBOL_BID);
|
||
|
|
||
|
if(!checkFreeMargin(ORDER_TYPE_SELL, price, lots)) return 0;
|
||
|
|
||
|
MqlTradeRequestSync request;
|
||
|
prepare(request);
|
||
|
if(request.sell(settings.symbol, lots, price,
|
||
|
stopLoss ? price + stopLoss : 0,
|
||
|
takeProfit ? price - takeProfit : 0))
|
||
|
{
|
||
|
return postprocess(request);
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
bool close(const ulong ticket)
|
||
|
{
|
||
|
MqlTradeRequestSync request;
|
||
|
prepare(request);
|
||
|
return request.close(ticket) && postprocess(request);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Manager for a set of TradingStrategy instances |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class TradingStrategyPool: public TradingStrategy
|
||
|
{
|
||
|
private:
|
||
|
AutoPtr<TradingStrategy> pool[];
|
||
|
|
||
|
public:
|
||
|
TradingStrategyPool(const int reserve = 0)
|
||
|
{
|
||
|
ArrayResize(pool, 0, reserve);
|
||
|
}
|
||
|
|
||
|
TradingStrategyPool(TradingStrategy *instance)
|
||
|
{
|
||
|
push(instance);
|
||
|
}
|
||
|
|
||
|
void push(TradingStrategy *instance)
|
||
|
{
|
||
|
int n = ArraySize(pool);
|
||
|
ArrayResize(pool, n + 1);
|
||
|
pool[n] = instance;
|
||
|
}
|
||
|
|
||
|
virtual bool trade() override
|
||
|
{
|
||
|
for(int i = 0; i < ArraySize(pool); i++)
|
||
|
{
|
||
|
pool[i][].trade();
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Global pointer for the pool of strategies |
|
||
|
//+------------------------------------------------------------------+
|
||
|
AutoPtr<TradingStrategyPool> pool;
|
||
|
AutoPtr<UnityController> controller;
|
||
|
|
||
|
int currenciesCount;
|
||
|
string currencies[];
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Expert initialization function |
|
||
|
//+------------------------------------------------------------------+
|
||
|
int OnInit()
|
||
|
{
|
||
|
currenciesCount = 0;
|
||
|
ArrayResize(currencies, 0);
|
||
|
|
||
|
if(!StartUp(true)) return INIT_PARAMETERS_INCORRECT;
|
||
|
|
||
|
const bool barwise = UnityPriceType == PRICE_CLOSE && UnityPricePeriod == 1;
|
||
|
controller = new UnityController(UnitySymbols, barwise,
|
||
|
UnityBarLimit, UnityPriceType, UnityPriceMethod, UnityPricePeriod);
|
||
|
// now waiting for indicator messages about buffers/currencies
|
||
|
return INIT_SUCCEEDED;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Get indices of 2 currencies building up given forex pair |
|
||
|
//+------------------------------------------------------------------+
|
||
|
bool SplitSymbolToCurrencyIndices(const string symbol, int &first, int &second)
|
||
|
{
|
||
|
const string s1 = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
|
||
|
const string s2 = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
|
||
|
first = second = -1;
|
||
|
for(int i = 0; i < ArraySize(currencies); ++i)
|
||
|
{
|
||
|
if(currencies[i] == s1) first = i;
|
||
|
else if(currencies[i] == s2) second = i;
|
||
|
}
|
||
|
|
||
|
return first != -1 && second != -1;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Applied initialization function (called on different events) |
|
||
|
//+------------------------------------------------------------------+
|
||
|
bool StartUp(const bool init = false)
|
||
|
{
|
||
|
if(WorkSymbols == "")
|
||
|
{
|
||
|
Settings settings =
|
||
|
{
|
||
|
UseTime, HourStart, HourEnd,
|
||
|
Lots, Factor, Limit,
|
||
|
StopLoss, TakeProfit,
|
||
|
StartType, Magic, SkipTimeOnError, Trailing, _Symbol
|
||
|
};
|
||
|
|
||
|
if(settings.validate())
|
||
|
{
|
||
|
if(init)
|
||
|
{
|
||
|
Print("Input settings:");
|
||
|
settings.print();
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(init) Print("Wrong settings, please fix");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(!init)
|
||
|
{
|
||
|
controller[].attached(currenciesCount);
|
||
|
// split _Symbol to 2 of currencies[]
|
||
|
int first, second;
|
||
|
if(!SplitSymbolToCurrencyIndices(_Symbol, first, second))
|
||
|
{
|
||
|
PrintFormat("Can't find currencies (%s %s) for %s",
|
||
|
(first == -1 ? "base" : ""), (second == -1 ? "profit" : ""), _Symbol);
|
||
|
return false;
|
||
|
}
|
||
|
pool = new TradingStrategyPool(new UnityMartingale(settings,
|
||
|
new UnitySignal(controller[], first, second)));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Print("Parsed settings:");
|
||
|
Settings settings[];
|
||
|
|
||
|
if(!Settings::parseAll(WorkSymbols, settings))
|
||
|
{
|
||
|
if(init) Print("Settings are incorrect, can't start up");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if(!init)
|
||
|
{
|
||
|
controller[].attached(currenciesCount);
|
||
|
|
||
|
const int n = ArraySize(settings);
|
||
|
pool = new TradingStrategyPool(n);
|
||
|
for(int i = 0; i < n; i++)
|
||
|
{
|
||
|
if(settings[i].symbol == NULL) continue; // skip incorrect settings
|
||
|
|
||
|
settings[i].skipTimeOnError = SkipTimeOnError;
|
||
|
settings[i].trailing = Trailing;
|
||
|
// support many subsystems on the same symbol on a hedge account
|
||
|
settings[i].magic = Magic + i; // different magic for every subsystem
|
||
|
|
||
|
// split settings[i].symbol to 2 of currencies[]
|
||
|
int first, second;
|
||
|
if(!SplitSymbolToCurrencyIndices(settings[i].symbol, first, second))
|
||
|
{
|
||
|
PrintFormat("Can't find currencies (%s %s) for %s",
|
||
|
(first == -1 ? "base" : ""), (second == -1 ? "profit" : ""), settings[i].symbol);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
pool[].push(new UnityMartingale(settings[i],
|
||
|
new UnitySignal(controller[], first, second)));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Chart event handler |
|
||
|
//+------------------------------------------------------------------+
|
||
|
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
||
|
{
|
||
|
if(id == CHARTEVENT_CUSTOM + UnityBarLimit)
|
||
|
{
|
||
|
PrintFormat("%lld %f '%s'", lparam, dparam, sparam);
|
||
|
if(lparam == 0) ArrayResize(currencies, 0);
|
||
|
currenciesCount = (int)MathRound(dparam);
|
||
|
PUSH(currencies, sparam);
|
||
|
|
||
|
if(ArraySize(currencies) == currenciesCount)
|
||
|
{
|
||
|
if(pool[] == NULL)
|
||
|
{
|
||
|
StartUp(); // confirms that indicator is ready, we can start trading
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Alert("Repeated initialization!");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Tick event handler |
|
||
|
//+------------------------------------------------------------------+
|
||
|
void OnTick()
|
||
|
{
|
||
|
CheckTickModel();
|
||
|
if(pool[] != NULL)
|
||
|
{
|
||
|
pool[].trade();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Tester event handler |
|
||
|
//+------------------------------------------------------------------+
|
||
|
|
||
|
#define STAT_PROPS 5
|
||
|
|
||
|
double OnTester()
|
||
|
{
|
||
|
if(IsStopped())
|
||
|
{
|
||
|
FrameAdd("status", 1, 0, NULL);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
HistorySelect(0, LONG_MAX);
|
||
|
|
||
|
const ENUM_DEAL_PROPERTY_DOUBLE props[STAT_PROPS] =
|
||
|
{
|
||
|
DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, DEAL_VOLUME
|
||
|
};
|
||
|
double expenses[][STAT_PROPS];
|
||
|
ulong tickets[]; // used here only to match 'select' prototype, but helpful for debug
|
||
|
|
||
|
DealFilter filter;
|
||
|
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)
|
||
|
.select(props, tickets, expenses);
|
||
|
|
||
|
const int n = ArraySize(tickets);
|
||
|
|
||
|
double balance[]; // adjusted by trade volumes for using R2 criterion
|
||
|
|
||
|
ArrayResize(balance, n + 1);
|
||
|
balance[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
|
||
|
|
||
|
for(int i = 0; i < n; ++i)
|
||
|
{
|
||
|
double result = 0;
|
||
|
for(int j = 0; j < STAT_PROPS - 1; ++j)
|
||
|
{
|
||
|
result += expenses[i][j];
|
||
|
}
|
||
|
result /= expenses[i][STAT_PROPS - 1]; // normalize by volume
|
||
|
balance[i + 1] = result + balance[i];
|
||
|
}
|
||
|
|
||
|
const double r2 = RSquaredTest(balance);
|
||
|
|
||
|
return r2 * 100;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Optimization pass (frame), used for error detection here |
|
||
|
//+------------------------------------------------------------------+
|
||
|
void OnTesterPass()
|
||
|
{
|
||
|
ulong pass;
|
||
|
string name;
|
||
|
long id;
|
||
|
double value;
|
||
|
uchar data[];
|
||
|
while(FrameNext(pass, name, id, value, data))
|
||
|
{
|
||
|
if(name == "status" && id == 1)
|
||
|
{
|
||
|
Alert("Please stop optimization!");
|
||
|
Alert("Tick model is incorrect: OHLC M1 or better is required");
|
||
|
// could be great if this call would stop optimization itself,
|
||
|
// but it is not
|
||
|
ExpertRemove();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Proper tick model guard |
|
||
|
//+------------------------------------------------------------------+
|
||
|
void CheckTickModel()
|
||
|
{
|
||
|
if(MQLInfoInteger(MQL_TESTER))
|
||
|
{
|
||
|
static const TICK_MODEL minimalRequiredQuality = TICK_MODEL_OHLC_M1;
|
||
|
|
||
|
static ulong count = 0;
|
||
|
static const ulong detector = 3;
|
||
|
if(count++ < detector)
|
||
|
{
|
||
|
const TICK_MODEL model = getTickModel();
|
||
|
if(count >= 2)
|
||
|
{
|
||
|
if(minimalRequiredQuality < model)
|
||
|
{
|
||
|
PrintFormat("Tick model is incorrect (%s %sis required), terminating",
|
||
|
EnumToString(minimalRequiredQuality),
|
||
|
(minimalRequiredQuality != TICK_MODEL_REAL ? "or better " : ""));
|
||
|
ExpertRemove(); // will set _StopFlag flag
|
||
|
// TesterStop(); // will NOT set _StopFlag flag
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
//+------------------------------------------------------------------+
|