Мы быстро создадим и протестируем торгового бота — для валют. Это будет (в некоторой степени) весело, вы узнаете кое-что о психологии трейдинга и, самое главное, принесет прибыль.

Торговля на валютном рынке отличается от торговли акциями. До появления Robinhood, например, о транзакциях с нулевой стоимостью не было и речи. На фондовом рынке, то есть потому, что на валютных рынках (мне не нравится термин FOREX) всегда было так: вы просто платите спред (разницу между покупкой и продажей).

Эта небольшая стоимость работы на валютных рынках позволила рано появиться автоматизированным стратегиям. С такими инструментами, как mt4 (платформа) и mq4 (соответствующий язык), нужно лишь потратить время и усердие, чтобы создать работающее и эффективное программное обеспечение для работы. Деньги никогда не тратятся впустую на оплату комиссий, и это позволяет нам открывать много позиций одновременно.

Основные отличия валютных рынков от фондовых рынков

Отсутствие комиссий при работе на валютных рынках — не единственное отличие от фондовых рынков:

  • Валютные рынки открыты круглосуточно с воскресенья по пятницу.
  • Огромный объем торгов, этот класс активов имеет самую большую ликвидность в мире.
  • Вы можете торговать каждый день много раз, не опасаясь быть отмеченным
  • Очень низкий минимальный депозит, необходимый для открытия счета
  • Вы можете работать в очень короткий промежуток времени, а можете делать это в очень долгосрочной перспективе.
  • Общепринятой практикой является одновременное открытие позиций на покупку и продажу, что позволяет использовать хеджирование без необходимости коротких продаж.

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

Платформа — мт4

Мы будем использовать всемирно признанную и общепризнанную торговую платформу под названием mt4. Это аккуратное программное обеспечение, которое вы загружаете на свой ноутбук (среда Windows). Есть также соответствующие приложения для iOS и Google Play Store, но мы не можем запускать через них нашего бота, поэтому нам нужно загрузить и установить программное обеспечение (кстати, совершенно БЕСПЛАТНО).

Язык программирования -mql4

Созданный на основе C++, язык программирования mql4 разработан специально для платформы mt4. С его помощью вы можете кодировать и тестировать свои торговые идеи. Но его кривая обучения не является легкой. Мне потребовались часы за часами, чтобы овладеть им. Немного сложно доминировать, но когда / если вы это сделаете, время будет потрачено с пользой.

Не беспокойтесь, потому что я включу полный код в эту же статью.

Мусор на входе, мусор на выходе

Одна из основных вычислительных аксиом гласит: «мусор на входе, мусор на выходе». Не очень полезно знать, как программировать, если наши торговые идеи не приносят прибыли. После многих отброшенных торговых идей теперь у меня есть несколько, которые приносят прибыль. Здесь мы обсудим только один из них.

Торговая идея, которую мы будем тестировать

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

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

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

Итак, чтобы переломить эту ситуацию, мы будем открывать сделку, когда цена ушла слишком далеко от, скажем так, «центра экрана». Это субъективный термин, но для его кодирования мы будем использовать скользящее среднее. Когда цены слишком высоки или слишком низки (взяв за основу эту скользящую среднюю), мы открываем встречную сделку.

Уже есть индикатор, который хорошо помогает нам в этом начинании. Он называется Полосы Боллинджера, и в моем наборе инструментов я использую его чаще всего. Однако этот торговый бот, который мы будем создавать, не ограничивается указанием, когда открывать сделку и когда ее закрывать.

Он также выполняет заказы.

Для этого, конечно же, у вас должен быть активный торговый счет у любого респектабельного брокера, который также должен разрешать торговлю через платформу mt4. Есть десятки на выбор. Мой личный фаворит — OANDA, они базируются в Лондоне и имеют офисы в Австралии. Следуйте их инструкциям, чтобы загрузить и установить платформу. Это не так сложно.

Список правил

Когда мы что-то кодируем, мы просто говорим платформе следовать последовательному порядку: если это произойдет, то сделайте то. Далее, если это произойдет, то сделайте это другое дело. И так за.

Таким образом, наш набор правил будет выражать желание открывать сделки, когда цены слишком далеки от скользящей средней. В двух словах:

  1. Создайте простую скользящую среднюю
  2. Расчет верхней и нижней полосы
  3. Открывайте позицию на покупку, когда цена достигает нижней полосы.
  4. Открывайте позицию на продажу, когда цена достигает верхней полосы.
  5. При открытии позиции на покупку закройте все открытые позиции на продажу
  6. При открытии позиции на продажу закройте все открытые позиции на покупку

После кодирования и компиляции мы можем изменить значения параметров с помощью интерфейса mt4. После завершения или код выглядит следующим образом:

Полный код

//+------------------------------------------------------------------+
//|                                              Strategy: BB120.mq4 |
//|                                       Created by Luiggi Trejo   |
//|                                  https://luiggitrejo.medium.com/                                                                          |
//+------------------------------------------------------------------+
#property copyright "Created with EABuilder.com"
#property link      "https://www.eabuilder.com"
#property version   "1.00"
#property description ""

#include <stdlib.mqh>
#include <stderror.mqh>

int LotDigits; //initialized in OnInit
int MagicNumber = 475403;
extern double TradeSize = 0.1;
int MaxSlippage = 3; //adjusted in OnInit
bool crossed[4]; //initialized to true, used in function Cross
extern int MaxOpenTrades = 10;
int MaxLongTrades = 1000;
int MaxShortTrades = 1000;
int MaxPendingOrders = 1000;
int MaxLongPendingOrders = 1000;
int MaxShortPendingOrders = 1000;
bool Hedging = true;
int OrderRetry = 5; //# of retries if sending order returns error
int OrderWait = 5; //# of seconds to wait if sending order returns error
double myPoint; //initialized in OnInit

bool Cross(int i, bool condition) //returns true if "condition" is true and was false in the previous call
  {
   bool ret = condition && !crossed[i];
   crossed[i] = condition;
   return(ret);
  }

void myAlert(string type, string message)
  {
   if(type == "print")
      Print(message);
   else if(type == "error")
     {
      Print(type+" | BB120 @ "+Symbol()+","+IntegerToString(Period())+" | "+message);
     }
   else if(type == "order")
     {
     }
   else if(type == "modify")
     {
     }
  }

int TradesCount(int type) //returns # of open trades for order type, current symbol and magic number
  {
   int result = 0;
   int total = OrdersTotal();
   for(int i = 0; i < total; i++)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false) continue;
      if(OrderMagicNumber() != MagicNumber || OrderSymbol() != Symbol() || OrderType() != type) continue;
      result++;
     }
   return(result);
  }

int myOrderSend(int type, double price, double volume, string ordername) //send order, return ticket ("price" is irrelevant for market orders)
  {
   if(!IsTradeAllowed()) return(-1);
   int ticket = -1;
   int retries = 0;
   int err = 0;
   int long_trades = TradesCount(OP_BUY);
   int short_trades = TradesCount(OP_SELL);
   int long_pending = TradesCount(OP_BUYLIMIT) + TradesCount(OP_BUYSTOP);
   int short_pending = TradesCount(OP_SELLLIMIT) + TradesCount(OP_SELLSTOP);
   string ordername_ = ordername;
   if(ordername != "")
      ordername_ = "("+ordername+")";
   //test Hedging
   if(!Hedging && ((type % 2 == 0 && short_trades + short_pending > 0) || (type % 2 == 1 && long_trades + long_pending > 0)))
     {
      myAlert("print", "Order"+ordername_+" not sent, hedging not allowed");
      return(-1);
     }
   //test maximum trades
   if((type % 2 == 0 && long_trades >= MaxLongTrades)
   || (type % 2 == 1 && short_trades >= MaxShortTrades)
   || (long_trades + short_trades >= MaxOpenTrades)
   || (type > 1 && type % 2 == 0 && long_pending >= MaxLongPendingOrders)
   || (type > 1 && type % 2 == 1 && short_pending >= MaxShortPendingOrders)
   || (type > 1 && long_pending + short_pending >= MaxPendingOrders)
   )
     {
      myAlert("print", "Order"+ordername_+" not sent, maximum reached");
      return(-1);
     }
   //prepare to send order
   while(IsTradeContextBusy()) Sleep(100);
   RefreshRates();
   if(type == OP_BUY)
      price = Ask;
   else if(type == OP_SELL)
      price = Bid;
   else if(price < 0) //invalid price for pending order
     {
      myAlert("order", "Order"+ordername_+" not sent, invalid price for pending order");
	  return(-1);
     }
   int clr = (type % 2 == 1) ? clrRed : clrBlue;
   while(ticket < 0 && retries < OrderRetry+1)
     {
      ticket = OrderSend(Symbol(), type, NormalizeDouble(volume, LotDigits), NormalizeDouble(price, Digits()), MaxSlippage, 0, 0, ordername, MagicNumber, 0, clr);
      if(ticket < 0)
        {
         err = GetLastError();
         myAlert("print", "OrderSend"+ordername_+" error #"+IntegerToString(err)+" "+ErrorDescription(err));
         Sleep(OrderWait*1000);
        }
      retries++;
     }
   if(ticket < 0)
     {
      myAlert("error", "OrderSend"+ordername_+" failed "+IntegerToString(OrderRetry+1)+" times; error #"+IntegerToString(err)+" "+ErrorDescription(err));
      return(-1);
     }
   string typestr[6] = {"Buy", "Sell", "Buy Limit", "Sell Limit", "Buy Stop", "Sell Stop"};
   myAlert("order", "Order sent"+ordername_+": "+typestr[type]+" "+Symbol()+" Magic #"+IntegerToString(MagicNumber));
   return(ticket);
  }

void myOrderClose(int type, double volumepercent, string ordername) //close open orders for current symbol, magic number and "type" (OP_BUY or OP_SELL)
  {
   if(!IsTradeAllowed()) return;
   if (type > 1)
     {
      myAlert("error", "Invalid type in myOrderClose");
      return;
     }
   bool success = false;
   int err = 0;
   string ordername_ = ordername;
   if(ordername != "")
      ordername_ = "("+ordername+")";
   int total = OrdersTotal();
   int orderList[][2];
   int orderCount = 0;
   int i;
   for(i = 0; i < total; i++)
     {
      while(IsTradeContextBusy()) Sleep(100);
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderMagicNumber() != MagicNumber || OrderSymbol() != Symbol() || OrderType() != type) continue;
      orderCount++;
      ArrayResize(orderList, orderCount);
      orderList[orderCount - 1][0] = OrderOpenTime();
      orderList[orderCount - 1][1] = OrderTicket();
     }
   if(orderCount > 0)
      ArraySort(orderList, WHOLE_ARRAY, 0, MODE_ASCEND);
   for(i = 0; i < orderCount; i++)
     {
      if(!OrderSelect(orderList[i][1], SELECT_BY_TICKET, MODE_TRADES)) continue;
      while(IsTradeContextBusy()) Sleep(100);
      RefreshRates();
      double price = (type == OP_SELL) ? Ask : Bid;
      double volume = NormalizeDouble(OrderLots()*volumepercent * 1.0 / 100, LotDigits);
      if (NormalizeDouble(volume, LotDigits) == 0) continue;
      success = OrderClose(OrderTicket(), volume, NormalizeDouble(price, Digits()), MaxSlippage, clrWhite);
      if(!success)
        {
         err = GetLastError();
         myAlert("error", "OrderClose"+ordername_+" failed; error #"+IntegerToString(err)+" "+ErrorDescription(err));
        }
     }
   string typestr[6] = {"Buy", "Sell", "Buy Limit", "Sell Limit", "Buy Stop", "Sell Stop"};
   if(success) myAlert("order", "Orders closed"+ordername_+": "+typestr[type]+" "+Symbol()+" Magic #"+IntegerToString(MagicNumber));
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {   
   //initialize myPoint
   myPoint = Point();
   if(Digits() == 5 || Digits() == 3)
     {
      myPoint *= 10;
      MaxSlippage *= 10;
     }
   //initialize LotDigits
   double LotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
   if(LotStep >= 1) LotDigits = 0;
   else if(LotStep >= 0.1) LotDigits = 1;
   else if(LotStep >= 0.01) LotDigits = 2;
   else LotDigits = 3;
   int i;
   //initialize crossed
   for (i = 0; i < ArraySize(crossed); i++)
      crossed[i] = true;
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   int ticket = -1;
   double price;   
   
   
   //Close Long Positions, instant signal is tested first
   RefreshRates();
   if(Cross(1, Bid > iBands(NULL, PERIOD_CURRENT, 120, 2, 0, PRICE_CLOSE, MODE_UPPER, 0)) //Price crosses above Bollinger Bands
   )
     {   
      if(IsTradeAllowed())
         myOrderClose(OP_BUY, 100, "");
      else //not autotrading => only send alert
         myAlert("order", "");
     }
   
   //Close Short Positions, instant signal is tested first
   RefreshRates();
   if(Cross(0, Bid < iBands(NULL, PERIOD_CURRENT, 120, 2, 0, PRICE_CLOSE, MODE_LOWER, 0)) //Price crosses below Bollinger Bands
   )
     {   
      if(IsTradeAllowed())
         myOrderClose(OP_SELL, 100, "");
      else //not autotrading => only send alert
         myAlert("order", "");
     }
   
   //Open Buy Order, instant signal is tested first
   RefreshRates();
   if(Cross(2, Bid < iBands(NULL, PERIOD_CURRENT, 120, 2, 0, PRICE_CLOSE, MODE_LOWER, 0)) //Price crosses below Bollinger Bands
   )
     {
      RefreshRates();
      price = Ask;   
      if(IsTradeAllowed())
        {
         ticket = myOrderSend(OP_BUY, price, TradeSize, "");
         if(ticket <= 0) return;
        }
      else //not autotrading => only send alert
         myAlert("order", "");
     }
   
   //Open Sell Order, instant signal is tested first
   RefreshRates();
   if(Cross(3, Bid > iBands(NULL, PERIOD_CURRENT, 120, 2, 0, PRICE_CLOSE, MODE_UPPER, 0)) //Price crosses above Bollinger Bands
   )
     {
      RefreshRates();
      price = Bid;   
      if(IsTradeAllowed())
        {
         ticket = myOrderSend(OP_SELL, price, TradeSize, "");
         if(ticket <= 0) return;
        }
      else //not autotrading => only send alert
         myAlert("order", "");
     }
  }
//+------------------------------------------------------------------+

После завершения кода приступаем к его тестированию. Мы ищем:

  • Прибыль. Это очевидная цель. Да, мы хотим прибыли, но мы также должны учитывать наши альтернативные издержки. Лучше ли инвестировать в валютный рынок по этому алгоритму? Или наши деньги зарабатывают больше в другом месте?
  • Просадки. Когда происходит большая просадка, наши ногти, как правило, становятся короче (мы часто их кусаем). Это не второстепенная проблема. Часто в середине сделки мы выбираем выход из боязни продолжающихся и потенциально больших убытков. Когда нет больших просадок, все спокойно и хорошо.
  • Надежность. Хорошо ли работает этот алгоритм только на одной валютной паре? Или в нескольких из них? Чем больше валютных пар может извлекать прибыль наш алгоритм, тем он надежнее.

Тестирование алгоритма

Мы запустим наш код сначала в течение одного месяца (январь 2019 г.) и, если будут положительные результаты, то продлим период тестирования до октября 2020 г. Мы будем загружать и использовать данные для пары eur/usd.

Использование этой валютной пары не случайно. Это самая торгуемая пара валют, она имеет самый высокий объем и предлагает нам большую ликвидность и низкие спреды.

Параметры, которые мы будем использовать, следующие:

  • Срок: 4 часа
  • Скользящее среднее: 120 периодов, простое и вычисляемое при закрытии
  • Количество стандартных отклонений, необходимых для запуска сделки: 2
  • Размер сделки: 0,01 лота
  • Максимально допустимое количество одновременных открытых сделок: 20
  • Первоначальные инвестиции: 1000 долларов США
  • Маржа 1:50
  • Разрешено хеджирование: да

Первый запуск: с 1 января 2019 г. по 30 января 2019 г.

Сначала мы тестируем этот единственный месяц, и если мы найдем удовлетворительные результаты, мы будем тестировать больший период времени.

У нас есть прибыль!

Поэтому мы продлеваем наше тестирование на исторических данных до 30 октября 2020 года. Вот результаты:

Этот второй прогон приносит еще больше прибыли, но, как мы видим, в марте 2020 года наблюдается большая просадка. Это соответствует началу пандемии COVID. Это одно из так называемых событий «Черный лебедь». Несмотря на это, наш алгоритм прибыльный. Сколько? Вот соответствующий отчет, который также генерируется платформой mt4:

Наш алгоритм (или торговый бот) принес нам чистую прибыль в размере 1629 долларов США. Но при просадке 59% этот код мог бы быть и лучше. Мы можем изменить его в будущем, чтобы снизить эту цифру.

А это наш собственный торговый бот, жив и здоров. Это лишь одна из идей, которые вы можете проверить сами. Не стесняйтесь копировать приведенный здесь код и играть с ним.

Спасибо, что дочитали до сих пор. Пожалуйста, следуйте за мной для других историй, как эта.