OnTickMulti - с добавленными пересчетом прибыли в валюту депозита, свопами, комисссией в % за лот.

25 декабря 2023, 20:24
Forester
12
159

Текущий вариант 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&nbsp;&nbsp; и   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? Десятки тысяч ордеров могут оказаться активными.

Файлы:
Virtual.zip  57 kb