//+------------------------------------------------------------------+ //| UnityMartingaleDraft2.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\TradeReport.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); virtual bool statement(); }; //+------------------------------------------------------------------+ //| 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; } }; //+------------------------------------------------------------------+ //| Special PositionState with on-the-fly equity monitoring | //+------------------------------------------------------------------+ class PositionStateWithEquity: public PositionState { TradeReport *report; public: PositionStateWithEquity(const long t, TradeReport *r): PositionState(t), report(r) { } ~PositionStateWithEquity() { if(HistorySelectByPosition(get(POSITION_IDENTIFIER))) { double result = 0; DealFilter filter; int props[] = {DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE}; Tuple4 overheads[]; if(filter.select(props, overheads)) { for(int i = 0; i < ArraySize(overheads); ++i) { result += NormalizeDouble(overheads[i]._1, 2) + NormalizeDouble(overheads[i]._2, 2) + NormalizeDouble(overheads[i]._3, 2) + NormalizeDouble(overheads[i]._4, 2); } } if(CheckPointer(report) != POINTER_INVALID) report.addBalance(result); } } }; //+------------------------------------------------------------------+ //| Main class with trading strategy | //+------------------------------------------------------------------+ class UnityMartingale: public TradingStrategy { protected: Settings settings; SymbolMonitor symbol; AutoPtr position; AutoPtr trailing; AutoPtr command; TradeReport report; TradeReport::DrawDown equity; const double deposit; const datetime epoch; double lotsStep; double lotsLimit; double takeProfit, stopLoss; bool paused; datetime badConditions; public: UnityMartingale(const Settings &state, TradingSignal *signal): symbol(state.symbol), deposit(AccountInfoDouble(ACCOUNT_BALANCE)), epoch(TimeCurrent()) { settings = state; paused = false; badConditions = 0; command = signal; equity.calcDrawdown(deposit); // 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 = MQLInfoInteger(MQL_TESTER) ? new PositionStateWithEquity(tickets[0], &report) : new PositionState(tickets[0]); if(settings.stopLoss && settings.trailing) { trailing = new TrailingStop(tickets[0], settings.stopLoss, ((int)symbol.get(SYMBOL_SPREAD) + 1) * 2); } } } virtual bool statement() override { if(MQLInfoInteger(MQL_TESTER)) { Print("Separate trade report for ", settings.symbol); // equity drawdown should have been already calculated on-the-fly Print("Equity DD:"); equity.print(); // balance drawdown is calculated along with resulting report Print("Trade Statistics (with Balance DD):"); DealFilter filter; filter.let(DEAL_SYMBOL, settings.symbol) .let(DEAL_MAGIC, settings.magic, IS::EQUAL_OR_ZERO); // we need ZERO magic for last exit made by the tester HistorySelect(0, LONG_MAX); TradeReport::GenericStats stats = report.calcStatistics(filter, deposit, epoch); stats.print(); } return false; } 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; } if(MQLInfoInteger(MQL_TESTER)) { if(position[]) { report.resetFloatingPL(); // after reset we need to sum up all floating PLs // calling addFloatingPL for every position, // but in this strategy we have only 1 position at a time report.addFloatingPL(position[].get(POSITION_PROFIT) + position[].get(POSITION_SWAP)); equity.calcDrawdown(report.getCurrent()); // once all floating PL added - calculate DD } } // 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 = MQLInfoInteger(MQL_TESTER) ? new PositionStateWithEquity(ticket, &report) : 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 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; } virtual bool statement() override { bool result = false; for(int i = 0; i < ArraySize(pool); i++) { result = pool[i][].statement() || result; } return result; } }; //+------------------------------------------------------------------+ //| Global pointer for the pool of strategies | //+------------------------------------------------------------------+ AutoPtr pool; AutoPtr 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); if(pool[] != NULL) { pool[].statement(); // ask trading systems to output their results } 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 } } } } } //+------------------------------------------------------------------+