Эмуляция пополнения депозита и снятия средств

Тестер MetaTrader 5 позволяет эмулировать операции пополнения счета и снятия с него средств. Это позволяет проводить эксперименты с некоторыми системами управления капиталом.

bool TesterDeposit(double money)

Функция TesterDeposit пополняет счет в процессе тестирования на размер вносимой суммы в параметре money. Сумма указывается в валюте тестового депозита.

bool TesterWithdrawal(double money)

Функция TesterWithdrawal производит снятие средств в размере money.

Обе функции возвращают true как признака успеха.

В качестве примера рассмотрим эксперт на основе стратегии "carry trade". Для неё нам потребуется выбрать символ с большими положительными свопами в одном из торговых направлений, например, покупка AUDUSD. Эксперт будет открывать одну или более позиций в указанном направлении. Убыточные позиции будут удерживаться ради накопления по ним свопов. Прибыльные позиции будут закрываться по достижении предопределенного размера прибыли в расчете на лот. Заработанные свопы будут сниматься со счета.

Исходный код доступен в файле CrazyCarryTrade.mq5. Название выбрано не случайно, так как пересиживание убытков является рискованной практикой.

Во входных параметрах пользователь может выбрать направление торговли, размер одной сделки (по умолчанию 0, что означает минимальный лот) и минимальную прибыль на лот, при которой прибыльная позиция закроется.

enum ENUM_ORDER_TYPE_MARKET
{
   MARKET_BUY = ORDER_TYPE_BUY,
   MARKET_SELL = ORDER_TYPE_SELL
};
   
input ENUM_ORDER_TYPE_MARKET Type;
input double Volume;
input double MinProfitPerLot = 1000;

Для начала протестируем в обработчике OnInit работу функций TesterWithdrawal и TesterDeposit. В частности, попытка снять двойной баланс приведет к ошибке 10019.

int OnInit()
{
   PRTF(TesterWithdrawal(AccountInfoDouble(ACCOUNT_BALANCE) * 2));
   /*
   not enough money for 20 000.00 withdrawal (free margin: 10 000.00)
   TesterWithdrawal(AccountInfoDouble(ACCOUNT_BALANCE)*2)=false / MQL_ERROR::10019(10019)
   */
   ...

Зато последующие снятие и зачисление обратно по 100 единиц валюты счета пройдут успешно.

   PRTF(TesterWithdrawal(100));
   /*
   deal #2 balance -100.00 [withdrawal] done
   TesterWithdrawal(100)=true / ok
   */
   PRTF(TesterDeposit(100)); // вернем деньги
   /*
   deal #3 balance 100.00 [deposit] done
   TesterDeposit(100)=true / ok
   */
   return INIT_SUCCEEDED;
}

В обработчике OnTick проверим наличие позиций с помощью PositionFilter и заполним массив values значениями их текущих прибылей/убытков и накопленных свопов.

void OnTick()
{
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   ENUM_POSITION_PROPERTY_DOUBLE props[] = {POSITION_PROFITPOSITION_SWAP};
   double values[][2];
   ulong tickets[];
   PositionFilter pf;
   pf.select(propsticketsvaluestrue);
   ...

Когда позиций нет, откроем одну в предопределенном направлении.

   if(ArraySize(tickets) == 0// позиций нет
   {
      MqlTradeRequestSync request1;
      (Type == MARKET_BUY ? request1.buy(volume) : request1.sell(volume));
   }
   else
   {
      ... // позиции есть - см. следующую врезку
   }

Когда позиции есть, проходимся по ним в цикле и закрываем те, по которым появилась достаточная прибыль (с поправкой на свопы). Попутно суммируем свопы закрытых позиций и общие убытки. Поскольку свопы растут пропорционально времени, мы используем их как усиливающий коэффициент на закрытие "древних" позиций. Таким образом, возможно закрытие и с убытком.

      double loss = 0swaps = 0;
      for(int i = 0i < ArraySize(tickets); ++i)
      {
         if(values[i][0] + values[i][1] * values[i][1] >= MinProfitPerLot * volume)
         {
            MqlTradeRequestSync request0;
            if(request0.close(tickets[i]) && request0.completed())
            {
               swaps += values[i][1];
            }
         }
         else
         {
            loss += values[i][0];
         }
      }
      ...

Если общие убытки нарастают, периодически открываем дополнительные позиции, но тем реже, чем больше позиций, чтобы хоть как-то контролировать риски.

      if(loss / ArraySize(tickets) <= -MinProfitPerLot * volume * sqrt(ArraySize(tickets)))
      {
         MqlTradeRequestSync request1;
         (Type == MARKET_BUY ? request1.buy(volume) : request1.sell(volume));
      }
      ...

Наконец, снимаем со счета свопы.

      if(swaps >= 0)
      {
         TesterWithdrawal(swaps);
      }

В обработчике OnDeinit выведем статистику по отчислениям.

void OnDeinit(const int)
{
   PrintFormat("Deposit: %.2f Withdrawals: %.2f",
      TesterStatistics(STAT_INITIAL_DEPOSIT),
      TesterStatistics(STAT_WITHDRAWAL));
}

Например, при запуске эксперта с настройками по умолчанию на периоде 2021-начало 2022-го года получим для AUDUSD такой результат:

   final balance 10091.19 USD
   Deposit: 10000.00 Withdrawals: 197.42

А вот как выглядит отчет и график.

Отчет эксперта со снятием средств со счета

Отчет эксперта со снятием средств со счета

Таким образом, при торговле минимальным лотом и загрузкой депозита не более 1% за год с небольшим удалось снять примерно 200 USD.