Текущий вариант OnTickMulti https://www.mql5.com/ru/code/47647 считает прибыль в валюте каждого символа или можно получить в пипсах. Но на общий баланс они влияют в другой пропорции, согласно текущему курсу каждого из символов.
Решил дополнить код пересчетом в валюту депозита, добавить комиссию в % за лот, как у моего ДЦ, расчет свопов на каждом ролловере.
В результате доработки прибыль и итоговые балансы стали абсолютно точно совпадать с тестером MQ при торговле на 1 символе (кроме пары сложных пересчетов вроде EURCHF депозит в GBP - сбой чаще по сделкам на первом тике, отличия общего профита не более $1 за 40000+ сделок ). Если торговать одновременно на нескольких символах, то итоговые балансы иногда совпадают, но чаще отличаются в 6м знаке, например профит в MQ = -65076948.20 , а в виртуальном тестере -65076968.58. Такое отличие в $20.38 набежало за 272378 сделок. См. скриншот этого примера ниже.
Провел исследование причины расхождения. Оказалось, что в очень редких случаях, когда в одной миллисекунде несколько тиков (видел до 6), и СЛ или ТП мог сработать на любом из них, а при пересчете прибыли в валюту депозита происходит запрос кросс-курса на эту миллисекунду, курс возвращается по последнему тику, а ТП/СЛ мог сработать по другому тику, с другим курсом на тот момент времени внутри одной и той же миллисекунды. Полагаю, что эти $20 набежали именно в эти моменты. Исправить эту погрешность не представляется возможным. Тестировал специально на 10 лотах, чтобы было видно эти мелкие неточности.
Для тестирования дополнил советник из поста https://www.mql5.com/ru/blogs/post/755348 так, чтобы он покрывал больше вариантов сделок. Добавил:
- стоповые лимитные ордера по аналогии с тем что было в исходнике,
- просто открытие сделок с фиксированным ТП/СЛ и срабатыванию по ним,
- с ТП/СЛ = Ask и Bid для проверки крайнего случая их установки,
- вариант для закрытия по OrderClose().
Возможно есть предложения что-то еще добавить и проверить?
Полный код советника для тестов:
// из https://www.mql5.com/ru/blogs/post/755348 #include <MT4Orders.mqh> // https://www.mql5.com/ru/code/16006 #define MAX_ORDERS 20000 // предлагаю сделать динамический массив для активных ордеров, как для истории. В данном тесте 2000 было мало. //#define ORDER_COMMISSION 1 // Задание комиссии (OrderCommission() = OrderLots() * (ORDER_COMMISSION)), включая динамический вариант. #define ORDER_COMMISSION_PERCENT 0.0016 // 0.0016% за лот. Будет работать, если ORDER_COMMISSION в пунктах не объявлен #define ORDER_COMMISSION_PERCENT_NUMS 2 // 1 - комиссия берется при входе, 2 - комиссия берется 2 раза и при входе и при выходе #define CALC_SWAP 0 // расчитывать своп в указанное время ролловера в секундах от 0:00. Например ролловер в 3:00:00 = 3*60*60 = 10800. Значения свопов для каждого инструмента берутся из спецификации символа на текущий момент. Свопы регулярно меняются, вчера они модли быть другими, т.е. результат может не совпадать с вчерашним тестированием. #define TO_ACCOUNT_CURRENCY //- пересчитать прибыль в валюту депозита. Иначе, считает в валюте профита инструмента #define CONTROL_TRADE_SESSION //будет проверять каждый тик на открытость рынка, чтобы не было сделок, если рынок закрыт, а тики есть. Можно не использовать, если котировочная и торговая сессии совпадают. //#define CONTROL_EQUITY // расчитывать эквити на каждом тике, замедление от 10% до 400% при пересчете в валюту аккаунта, если много сделок и живут они долго #define VIRTUAL_TESTER_MULTI // Запуск в виртуальном торговом окружении OnTickMulti #define VIRTUAL_LIMITS_TP_SLIPPAGE // Лимитники и TP исполняются по первой цене акцепта - положительные проскальзывания #define VIRTUAL_CLOSEALL_BYEND // Закрывает принудительно все ордера в конце тестирования #define VIRTUAL_ALTERNATIVE // Альтернативная скорость расчетов #include <fxsaber\OnTickMulti\OnTickMulti.mqh> // https://www.mql5.com/ru/code/47647 #define REPORT_TESTER // В тестере будут автоматически записываться отчеты #define REPORT_BROWSER // Создание отчета с запуском браузера - требует разрешения DLL. #include <Report.mqh> // https://www.mql5.com/ru/code/18801 // https://www.mql5.com/ru/forum/282062/page46#comment_50705106 input group "EA's inputs" input int inAmount = 1; input int inOffset = 5; input int inRange = 0; bool OrdersBuy[]; bool OrdersSell[]; bool OrdersBuyStop[]; bool OrdersSellStop[]; void OnInit() { ArrayResize(OrdersBuy, inAmount + 1); ArrayResize(OrdersSell, inAmount + 1); ArrayResize(OrdersBuyStop, inAmount + 1); ArrayResize(OrdersSellStop, inAmount + 1); } // Мультисимвольный OnTick. void OnTickMulti( const string &Symb, const int &Index ) { MqlTick Tick; if (SymbolInfoTick(Symb, Tick)) { double sl, tp, point = SymbolInfoDouble(Symb, SYMBOL_POINT); const double Offset = inOffset * point; ArrayInitialize(OrdersBuy, false); ArrayInitialize(OrdersSell, false); ArrayInitialize(OrdersBuyStop, false); ArrayInitialize(OrdersSellStop, false); for (uint i = OrdersTotal(); (bool)i--;) if (OrderSelect(i, SELECT_BY_POS) && (OnTickMulti.IsOnlySymbolBase || ( VIRTUAL::GetHandle() ? (OrderSymbolID() == Index) : (OrderSymbol() == Symb))))// https://www.mql5.com/ru/forum/282062/page52#comment_51051261 { ulong Magic = OrderMagicNumber(); if(Magic > 0 && Magic < 100 ){ switch (OrderType()) { case OP_BUY: OrderModify(OrderTicket(), OrderOpenPrice(), 0, Tick.bid + Magic * Offset, 0); OrdersBuy[Magic] = true; break; case OP_SELL: OrderModify(OrderTicket(), OrderOpenPrice(), 0, Tick.ask - Magic * Offset, 0); OrdersSell[Magic] = true; break; case OP_BUYLIMIT: OrderModify(OrderTicket(), Tick.ask - Magic * Offset, 0, 0, 0); OrdersBuy[Magic] = true; break; case OP_SELLLIMIT: OrderModify(OrderTicket(), Tick.bid + Magic * Offset, 0, 0, 0); OrdersSell[Magic] = true; break; } } if(Magic > 100 && Magic < 200 ){ Magic = Magic - 100; switch (OrderType()) { case OP_BUY: OrderModify(OrderTicket(), OrderOpenPrice(), 0, Tick.bid + Magic * Offset, 0); OrdersBuyStop[Magic] = true; break; case OP_SELL: OrderModify(OrderTicket(), OrderOpenPrice(), 0, Tick.ask - Magic * Offset, 0); OrdersSellStop[Magic] = true; break; case OP_BUYSTOP: OrderModify(OrderTicket(), Tick.ask + Magic * Offset, 0, 0, 0); OrdersBuyStop[Magic] = true; break; case OP_SELLSTOP: OrderModify(OrderTicket(), Tick.bid - Magic * Offset, 0, 0, 0); OrdersSellStop[Magic] = true; break; } } if(Magic == 1001 ){//проверка полного закрытия OrderClose(OrderTicket(), OrderLots(), (OrderType()==OP_BUY ? Tick.bid : Tick.ask), 0 ) ;//проверка полного закрытия - работает } //проверка частичного закрытия на разных тиках - работает правильно if(Magic == 1002 ){ double Lots = OrderLots(); if(Lots==10){//первичный лот - закрыть 25% double LotsDel1=NormalizeDouble(Lots/4, 2);// закрыть 25% от первоначального лота OrderClose(OrderTicket(), LotsDel1, (OrderType()==OP_BUY ? Tick.bid : Tick.ask), 0 ) ;//проверка частичного закрытия }else{ //остаток закрыть на следущем тике OrderClose(OrderTicket(), Lots, (OrderType()==OP_BUY ? Tick.bid : Tick.ask), 0 ) ;//проверка полного закрытия } } /* //проверка частичного закрытия OrderClose() на том же самом тике. В вирт. тестере не работает - продит множество ордеров с 1/2 лота. if(Magic == 1003 ){ double Lots = OrderLots(); double LotsDel1=NormalizeDouble(Lots/3, 2);// закрыть 33% от первоначального лота OrderClose(OrderTicket(), LotsDel1, (OrderType()==OP_BUY ? Tick.bid : Tick.ask), 0 ) ;//проверка частичного закрытия OrderClose(OrderTicket(), NormalizeDouble(Lots-LotsDel1,2), (OrderType()==OP_BUY ? Tick.bid : Tick.ask), 0 ) ;//проверка полного закрытия остатка на том же тике - тут сбой } if(Magic == 1004 ){ //OrderCloseBy(); //проверить бы } } if(TimeHour(TimeCurrent())<23 && TimeHour(TimeCurrent())>0 ){return;} //совершаем операции с 0 до 1 и с 23 до 0 for (int i = 1; i <= inAmount; i++) { if (!OrdersBuy[i]) OrderSend(Symb, OP_BUYLIMIT, 10, Tick.ask - i * Offset, 0, 0, 0, NULL, i); if (!OrdersSell[i]) OrderSend(Symb, OP_SELLLIMIT, 10, Tick.bid + i * Offset, 0, 0, 0, NULL, i); // if (!OrdersBuyStop[i]) OrderSend(Symb, OP_BUYSTOP, 10, Tick.ask + i * Offset, 0, 0, 0, NULL, i+100); if (!OrdersSellStop[i]) OrderSend(Symb, OP_SELLSTOP, 10, Tick.bid - i * Offset, 0, 0, 0, NULL, i+100); } //обычные ордера с ТП/СЛ = Offset Не будут модифицироваться, сработают по ТП/СЛ sl=Tick.bid-Offset; tp=Tick.ask+Offset; OrderSend(Symb, OP_BUY, 10, Tick.ask, 0, sl, tp, NULL, 1000);// sl=Tick.ask+Offset; tp=Tick.bid-Offset; OrderSend(Symb, OP_SELL, 10, Tick.bid, 0, sl, tp, NULL, 1000);// //проверка ТП/СЛ на границе спреда. В виртуальном тестере - ок. У тестера MQ бывают сбои на 1-х сделках теста https://www.mql5.com/ru/forum/455977/page36#comment_51246904 и https://www.mql5.com/ru/forum/455977/page36#comment_51248196 OrderSend(Symb, OP_BUY, 10, Tick.ask, 0, Tick.bid, Tick.ask, NULL, 1000);//ТП/СЛ на границе спреда OrderSend(Symb, OP_SELL, 10, Tick.bid, 0, Tick.ask, Tick.bid, NULL, 1000);//ТП/СЛ на границе спреда //проверка полного закрытия OrderClose() OrderSend(Symb, OP_BUY, 10, Tick.ask, 0, 0, 0, NULL, 1001); OrderSend(Symb, OP_SELL, 10, Tick.bid, 0, 0, 0, NULL, 1001); //проверка частичного закрытия OrderClose() на разных тиках в вирт. не работает sl=Tick.bid-Offset; tp=Tick.ask+Offset; OrderSend(Symb, OP_BUY, 10, Tick.ask, 0, 0, 0, NULL, 1002); sl=Tick.ask+Offset; tp=Tick.bid-Offset; OrderSend(Symb, OP_SELL, 10, Tick.bid, 0, 0, 0, NULL, 1002);// /* //проверка частичного закрытия OrderClose() на том же самом тике // в вирт. тестере не работает - продит множество ордеров с 1/2 лота sl=Tick.bid-Offset; tp=Tick.ask+Offset; OrderSend(Symb, OP_BUY, 10, Tick.ask, 0, 0, 0, NULL, 1003); sl=Tick.ask+Offset; tp=Tick.bid-Offset; OrderSend(Symb, OP_SELL, 10, Tick.bid, 0, 0, 0, NULL, 1003);// */ } } double OnTester() { return(AccountInfoDouble(ACCOUNT_BALANCE)); } void OnDeinit(const int reason ){ Print("OnDeinit main"); for (int v = 0 ; v <= VIRTUAL::Total(); v++){ if (VIRTUAL::SelectByIndex(v)){ Print("AccountBalance = ",AccountBalance(), " AccountEquity = ",AccountEquity()); }} } int TimeHour ( datetime time ){return((int)((time / 3600) % 24));}//current hour in day. 3600 sec in hour
Новые функции вклчаются/выключаются командами
#define ORDER_COMMISSION_PERCENT 0.0016 // 0.0016% за лот. Будет работать, если ORDER_COMMISSION в пунктах не объявлен #define ORDER_COMMISSION_PERCENT_NUMS 2 // 1 - комиссия берется при входе, 2 - комиссия берется 2 раза и при входе и при выходе #define CALC_SWAP 0 // расчитывать своп в указанное время ролловера в секундах от 0:00. Например ролловер в 3:00:00 = 3*60*60 = 10800. Значения свопов для каждого инструмента берутся из спецификации символа на текущий момент. Свопы регулярно меняются, вчера они модли быть другими, т.е. результат может не совпадать с вчерашним тестированием. #define TO_ACCOUNT_CURRENCY //- пересчитать прибыль в валюту депозита. Иначе, считает в валюте профита инструмента
Вот сравнения расчетов в тестере MQ и в виртуальном тестере.
Проверку всех различных формул пересчета в валюту депозита и комиссии в % можно провести используя 3 инструмента: USDCHF, EURUSD, EURCHF на валютах депозита в USD, EUR, CHF. Вот один из вариантов:
Gif отчета:
Как писал выше, отличие балансов в $20.38 набежало за 272378 сделок.
Время расчета в тестере MQ 217.170 s. , а в виртуальном тестере 30.560 s. - в 7 раз быстрее.
На данном ДЦ была комиссия 0.0016% за лот. Но свопы = 0. Поэтому столбец не рассчитан.
На другом ДЦ комиссий нет, но есть свопы. Отключаю расчет комиссии
//#define ORDER_COMMISSION_PERCENT 0.0016 // 0.0016% за лот. Будет работать, если ORDER_COMMISSION в пунктах не объявлен
и запускаю расчет:
При подготовке анимации перпутал подписи: Virtual и MQ надо поеменять местами.
Свопы совпали точно, профит и баланс отличаются на $8,08.
Сделок и тиков у этого ДЦ меньше, поэтому время расчетов в тестере MQ 105.759 s, в виртуальном тестере 16.626 s. - быстрее в 6,4 раза.
Особенность: некоторые строки сделок закрывшихся на одной миллисекунде могут быть переставлены местами, но на итоговый профит это не влияет.Это видно ниже.
ДЦ со свопами и комиссией 0.0016% за лот:
Так же добавлены опции:
#define CONTROL_TRADE_SESSION //будет проверять каждый тик на открытость рынка, чтобы не было сделок, если рынок закрыт, а тики есть. Можно не использовать, если котировочная и торговая сессии совпадают. //#define CONTROL_EQUITY // расчитывать эквити на каждом тике, замедление от 10% до 400% при пересчете в валюту аккаунта, если много сделок и живут они долго
Первая будет отклонять сделки вне торговой сессии, вторая может значительно ускорить расчеты, если они идут с пересчетом в валюту депозита. Расчет эквити может понадобиться, если вам надо её считывать и контролировать прямо во время тестирования. Если вам нужно просто получить итоговый баланс, то расчет эквити можно отключить.
Все правки кода были в библиотеке Virtual. Библиотека OnTickMulti - без изменений. Архив в новым Virtual приложен.
Надеюсь, fxsaber добавит новые функции в свою библиотеку.
И дополнительное пожелание: для теста использовалось
#define MAX_ORDERS 20000
По умолчанию стоит 200.
2000 оказалось мало. Чем подбирать число ячеек статического массива вручную в коде и перекомпиляцией, лучше сделать его динамическим и менять размер, если вдруг задача оказалась с большим часлом активных ордеров, например установили ТП/СЛ большими. Я установил inOffset = 15, а из него и ТП/СЛ = 15 пт. А если установить 200 или 1000? Десятки тысяч ордеров могут оказаться активными.