Особенности создания мультисимвольных экспертов

До сих пор в рамках книги мы в основном разбирали примеры экспертов, торгующих на текущем рабочем символе графика. Однако MQL5 позволяет формировать торговые приказы для любых символов Обзора рынка, вне зависимости от рабочего символа графика.

На самом деле, многие примеры предыдущих разделов имели входной параметр Symbol, в котором можно задать произвольный символ. По умолчанию, там находится пустая строка, что трактуется как текущий символ графика. Напомним эти примеры и их разделы:

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

Более того, как мы видели в описании событий OnBookEven и OnTradeTransaction, они являются универсальными, сообщающими об изменениях торгового окружения, касающихся произвольных символов. Но этого нельзя сказать о событии OnTick — оно генерируется только по факту изменения новых цен текущего символа. Как правило, это не является проблемой, но для высокочастотной мультивалютной торговли требуется предпринять какие-либо дополнительные технические приемы, например, подписаться на события OnBookEvent для "чужих" символов или установить высокочастотный таймер. Еще один вариант обхода этого ограничения в виде индикатора-шпиона EventTickSpy.mq5 был представлен в разделе Генерация пользовательских событий.

В контексте разговора о поддержке мультисимвольной торговли следует отметить, что похожее понятие мультитаймфреймовых экспертов не совсем корректно. Дело в том, что торговля по открытию баров представляет собой лишь частный случай группировки тиков по произвольным периодам, не обязательно стандартным. Конечно, анализ появления нового бара на конкретном таймфрейме упрощен ядром системы за счет функций вроде iTime(_Symbol, PERIOD_XX, 0), но этот анализ в любом случае отталкивается от тиков.

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

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

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

Продемонстрируем данный подход на примере эксперта по известной стратегии мартингейла. Как известно, это довольно рискованная стратегия из-за удвоения лотов после каждого убыточного трейда в надежде компенсировать прошлые потери. И один из самых простых способов нивелирования этого риска — ведение торговли сразу по нескольким, желательно слабо коррелированным, символам. Тогда временные просадки на одном инструменте могут компенсироваться доходами на других.

Чем больше различных инструментов (или разных настроек одной торговой системы, или даже разных торговых систем) мы используем в эксперте, тем меньше будет зависимость общего результата от отдельных неудач её составных частей.

Новый эксперт назовем MultiMartingale.mq5. Настройки торгового алгоритма включают:

  • UseTime — логический флаг включения/отключения торговли по расписанию;
  • HourStart и HourEnd — диапазон часов, в пределах которого разрешена торговля, если UseTime равно true;
  • Lots — объем первой сделки в серии;
  • Factor — коэффициент увеличения объема для последующих сделок после убытка;
  • Limit — максимальное количество сделок в убыточной серии с умножением объемов (после него возврат к начальному лоту);
  • StopLoss и TakeProfit — дистанция до защитных уровней в пунктах;
  • StartType — тип первой сделки (покупка или продажа);
  • Trailing — признак сопровождения стоп-лосса.

В исходном коде они описаны таким образом.

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 ENUM_POSITION_TYPE StartType = 0; // StartType (first order type: BUY or SELL)
input bool Trailing = true;     // Trailing

В принципе, защитные уровни имеет смысл задавать не в пунктах, а в долях ATR (индикатор Average True Range), но сейчас это не приоритетная задача.

Помимо прочего, в эксперте предусмотрен механизм заморозки торговых операций на заданный пользователем период (параметр SkipTimeOnError) в случае ошибок. Здесь мы опустим данный аспект — с ним можно ознакомиться в исходных кодах.

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

struct Settings
{
   bool useTime;
   uint hourStart;
   uint hourEnd;
   double lots;
   double factor;
   uint limit;
   uint stopLoss;
   uint takeProfit;
   ENUM_POSITION_TYPE startType;
   ulong magic;
   bool trailing;
   string symbol;
   ...
};

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

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

struct Settings
{
   ...
   bool validate()
   {
      ... // проверки размера лота и защитных уровней (см. исходный код)
      
      double rates[1];
      const bool success = CopyClose(symbolPERIOD_CURRENT01rates) > -1;
      if(!success)
      {
         Print("Unknown symbol: "symbol);
      }
      return success;
   }
   ...
};

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

struct Settings
{
   ...
   void print() const
   {
      Print(symbol, (startType == POSITION_TYPE_BUY ? "+" : "-"), (float)lots,
        "*", (float)factor,
        "^"limit,
        "("stopLoss","takeProfit")",
        useTime ? "[" + (string)hourStart + "," + (string)hourEnd + "]""");
   }
};

Метод print в сокращенном виде одной строкой выводит в журнал совокупность всех полей. Например,

EURUSD+0.01*2.0^5(500,1000)[2,22]
|     | |   |   |  |    |   |  |
|     | |   |   |  |    |   |  `до этого часа торговля разрешена
|     | |   |   |  |    |   `от этого часа торговля разрешена
|     | |   |   |  |    `тейк-профит в пунктах
|     | |   |   |  `стоп-лосс в пунктах
|     | |   |   `максимальный размер серии убыточных сделок (после '^')
|     | |   `фактор умножения лотов (после '*')
|     | `начальный лот в серии
|     `+ начинаем с покупки
|     `- начинаем с продажи
`инструмент

Другие методы в структуре Settings потребуются нам, когда мы перейдем к мультивалютности. Пока представим себе упрощенный вариант того, как мог бы выглядеть обработчик OnInit эксперта, торгующего на одном символе.

int OnInit()
{
   Settings settings =
   {
      UseTimeHourStartHourEnd,
      LotsFactorLimit,
      StopLossTakeProfit,
      StartTypeMagicSkipTimeOnErrorTrailing_Symbol
   };
   
   if(settings.validate())
   {
      settings.print();
      ...
      // здесь нужно будет инициализировать торговый алгоритм с этими настройками
   }
   ...
}

Придерживаясь ООП, торговую систему в обобщенном виде следует описать как программный интерфейс. Опять же в целях упрощения примера ограничимся единственным методом в этом интерфейсе — trade.

interface TradingStrategy
{
   virtual bool trade(void);
};

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

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

На базе интерфейса начнем создавать класс конкретной торговой системы. В нашем случае все экземпляры будут одного класса SimpleMartingale, но в перспективе никто не мешает внутри одного эксперта реализовать множество различных классов-наследников интерфейса и затем единообразным образом пускать их в дело в произвольном сочетании. Портфель стратегий (желательно, сильно различающихся по своей сути), как правило, характеризуется повышенной устойчивостью финансовых показателей.

class SimpleMartingalepublic TradingStrategy
{
protected:
   Settings settings;
   SymbolMonitor symbol;
   AutoPtr<PositionStateposition;
   AutoPtr<TrailingStoptrailing;
   ...
};

Внутри класса мы видим знакомую структуру с настройками Settings и монитор для рабочего символа SymbolMonitor. Кроме того нам потребуется контролировать наличие позиций и выполнять для них сопровождение уровня стоп-лосса, для чего заведены переменные с авто-указателями на объекты PositionState и TrailingStop. Авто-указатели позволяют нам в своем коде не заботиться о явном удалении объектов — это будет сделано автоматически при выходе управления из области определения или когда авто-указателю присваивается новый указатель.

Класс TrailingStop является базовым, с наиболее простой реализацией сопровождения цены, от которого можно унаследовать массу более сложных алгоритмов, пример чего мы рассматривали в виде производного TrailingStopByMA. Поэтому для придания программе гибкости в перспективе желательно предусмотреть, чтобы вызывающий код мог передавать в стратегию свой специфический, настроенный объект "трала", производный от TrailingStop. Это можно сделать, например, передачей указателя в конструктор или сделав SimpleMartingale шаблонным (тогда класс "трала" будет задаваться параметром шаблона).
 
Данный принцип ООП называется внедрением зависимости (dependency injection) и широко применяется наравне со многими другими, которые мы вскользь упомянули в разделе Теоретические основы ООП: композиция.

Настройки передаются в класс стратегии как параметр конструктора. Исходя из них, присваиваем все внутренние переменные.

class SimpleMartingalepublic TradingStrategy
{
   ...
   double lotsStep;
   double lotsLimit;
   double takeProfitstopLoss;
public:
   SimpleMartingale(const Settings &state) : symbol(state.symbol)
   {
      settings = state;
      const double point = symbol.get(SYMBOL_POINT);
      takeProfit = settings.takeProfit * point;
      stopLoss = settings.stopLoss * point;
      lotsLimit = settings.lots;
      lotsStep = symbol.get(SYMBOL_VOLUME_STEP);
      
      // вычисляем максимальный лот в серии (после заданного количества умножений)
      for(int pos = 0pos < (int)settings.limitpos++)
      {
         lotsLimit = MathFloor((lotsLimit * settings.factor) / lotsStep) * lotsStep;
      }
      
      double maxLot = symbol.get(SYMBOL_VOLUME_MAX);
      if(lotsLimit > maxLot)
      {
         lotsLimit = maxLot;
      }
      ...

Далее используем объект PositionFilter для поиска существующих "своих" позиций (по магику и символу). Если такая находится, создаем для неё объект PositionState и при необходимости объект TrailingStop.

      PositionFilter positions;
      ulong tickets[];
      positions.let(POSITION_MAGICsettings.magic).let(POSITION_SYMBOLsettings.symbol)
         .select(tickets);
      const int n = ArraySize(tickets);
      if(n > 1)
      {
         Alert(StringFormat("Too many positions: %d"n));
      }
      else if(n > 0)
      {
         position = new PositionState(tickets[0]);
         if(settings.stopLoss && settings.trailing)
         {
           trailing = new TrailingStop(tickets[0], settings.stopLoss,
              ((int)symbol.get(SYMBOL_SPREAD) + 1) * 2);
         }
      }
   }

В методе trade оставим пока "за кадром" работу по расписанию (поля настроек useTime, hourStart, hourEnd) и обратимся непосредственно к торговому алгоритму.

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

   virtual bool trade() override
   {
      ...
      ulong ticket = 0;
      
      if(position[] == NULL)
      {
         if(settings.startType == POSITION_TYPE_BUY)
         {
            ticket = openBuy(settings.lots);
         }
         else
         {
            ticket = openSell(settings.lots);
         }
      }
      ...

Здесь используются вспомогательные методы openBuy и openSell — мы до них доберемся через пару абзацев. Пока достаточно знать, что они возвращают номер тикета при успешном открытии или 0 в случае ошибки.

Если в объекте position уже есть информация о подопечной позиции, проверяем "жива" ли она с помощью вызова refresh, и в случае успеха (true) обновляем информацию о позиции вызовом update, а также сопровождаем стоп-лосс, если это было запрошено настройками.

      else // position[] != NULL
      {
         if(position[].refresh()) // позиция все еще существует?
         {
            position[].update();
            if(trailing[]) trailing[].trail();
         }
         ...

Если позиция закрылась, refresh вернет false, и мы окажемся в другой ветке if, где требуется открыть новую позицию: либо в прежнем направлении, если была зафиксирована прибыль, либо в противоположном, если получился убыток. Обратите внимание, что в кэше у нас остался слепок прежней позиции.

         else // позиция закрыта - нужно открыть новую
         {
            if(position[].get(POSITION_PROFIT) >= 0.0
            {
               // сохраняем прежнее направление:
               // BUY в случае прибыльного предыдущего BUY
               // SELL в случае прибыльного предыдущего SELL
               if(position[].get(POSITION_TYPE) == POSITION_TYPE_BUY)
                  ticket = openBuy(settings.lots);
               else
                  ticket = openSell(settings.lots);
            }
            else
            {
               // увеличиваем лот в оговоренных пределах
               double lots = MathFloor((position[].get(POSITION_VOLUME) * settings.factor) / lotsStep) * lotsStep;
   
               if(lotsLimit < lots)
               {
                  lots = settings.lots;
               }
             
               // меняем направление торговли:
               // SELL в случае предыдущего убыточного BUY
               // BUY в случае предыдущего убыточного SELL
               if(position[].get(POSITION_TYPE) == POSITION_TYPE_BUY)
                  ticket = openSell(lots);
               else
                  ticket = openBuy(lots);
            }
         }
      }
      ...

Наличие ненулевого тикета на данной завершающей стадии означает, что мы должны начать его контролировать с помощью новых объектов PositionState и TrailingStop.

      if(ticket > 0)
      {
         position = new PositionState(ticket);
         if(settings.stopLoss && settings.trailing)
         {
            trailing = new TrailingStop(ticketsettings.stopLoss,
               ((int)symbol.get(SYMBOL_SPREAD) + 1) * 2);
         }
      }
  
      return true;
    }

Теперь представим с некоторыми сокращениями метод openBuy (openSell во всем аналогичен). Его суть заключается в трех шагах:

  • подготовка структуры MqlTradeRequestSync с помощью метода prepare (здесь не показан, в нем заполняются deviation и magic);
  • отправка приказа с помощью вызова метода request.buy;
  • проверка результата с помощью метода postprocess (здесь не показан, в нем вызывается request.completed и в случае ошибки начинается отсчет периода приостановки торговли в ожидании лучших условий);

   ulong openBuy(double lots)
   {
      const double price = symbol.get(SYMBOL_ASK);
      
      MqlTradeRequestSync request;
      prepare(request);
      if(request.buy(settings.symbollotsprice,
         stopLoss ? price - stopLoss : 0,
         takeProfit ? price + takeProfit : 0))
      {
         return postprocess(request);
      }
      return 0;
   }

Обычно позиции будут закрываться по стоп-лоссу или тейк-профиту. Однако у нас поддерживается и работа по расписанию, которая может привести к закрытию. Вернемся в начало метода trade для знакомства с работой по расписанию.

   virtual bool trade() override
   {
      if(settings.useTime && !scheduled(TimeCurrent())) // время вне расписания?
      {
         // если есть открытая позиция - закроем её
         if(position[] && position[].isReady())
         {
            if(close(position[].get(POSITION_TICKET)))
            {
                                // по желанию проектировщика:
               position = NULL// затираем кэш или можно было бы...
               // не делать это обнуление, то есть сохранить позицию в кэше,
               // чтобы перенести направление и лот следующего трейда в новую серию
            }
            else
            {
               position[].refresh(); // гарантируем сброс флага 'ready'
            }
         }
         return false;
      }
      ... // открытие позиций (было приведено выше)
   }

Рабочий метод close во многом схож с openBuy — нет смысла его представлять. Еще один метод scheduled просто возвращает true или false, в зависимости от того, попадает ли текущее время в заданный диапазон рабочих часов (hourStart, hourEnd).

Итак, торговый класс готов. Но для мультивалютной работы потребуется создать несколько его экземпляров. Управлять ими будет класс TradingStrategyPool, в котором опишем массив указателей на TradingStrategy и методы для его пополнения: параметрический конструктор и push.

class TradingStrategyPoolpublic TradingStrategy
{
private:
   AutoPtr<TradingStrategypool[];
public:
   TradingStrategyPool(const int reserve = 0)
   {
      ArrayResize(pool0reserve);
   }
   
   TradingStrategyPool(TradingStrategy *instance)
   {
      push(instance);
   }
   
   void push(TradingStrategy *instance)
   {
      int n = ArraySize(pool);
      ArrayResize(pooln + 1);
      pool[n] = instance;
   }
   
   virtual bool trade() override
   {
      for(int i = 0i < ArraySize(pool); i++)
      {
         pool[i][].trade();
      }
      return true;
   }
};

То, что пул сделан производным от интерфейса TradingStrategy, в принципе не обязательно, но позволяет в будущем упаковывать пулы стратегий в другие более крупные пулы стратегий и так далее. Метод trade просто вызывает аналогичный метод у всех объектов массива.

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

AutoPtr<TradingStrategyPoolpool;
   
int OnInit()
{
   ... // инициализация настроек была приведена ранее
   if(settings.validate())
   {
      settings.print();
      pool = new TradingStrategyPool(new SimpleMartingale(settings));
      return INIT_SUCCEEDED;
   }
   else
   {
      return INIT_FAILED;
   }
   ...
}

Нетрудно догадаться, что для запуска торговли нам достаточно написать следующий небольшой обработчик OnTick.

void OnTick()
{
   if(pool[] != NULL)
   {
      pool[].trade();
   }
}

А как же быть с поддержкой мультивалютности?

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

В данном случае применим наиболее простое решение. Чуть выше была представлена строка с описанием настроек, которую формирует метод print структуры Settings. Реализуем в структуре метод parse, который делает обратную операцию: по строке с описанием восстанавливает состояние полей. Кроме того, поскольку нам потребуется объединить несколько настроек для разных символов, договоримся, что они могут быть состыкованы в общую длинную строку через специальный символ разделитель, скажем ';'. Тогда для чтения объединенного набора настроек легко написать статический метод parseAll, который с помощью вызовов parse заполнит переданный по ссылке массив структур Settings. С полным исходным кодом методов можно ознакомиться в прилагаемом файле.

struct Settings
{
   ...
   bool parse(const string &line);
   void static parseAll(const string &lineSettings &settings[])
   ...
};  

Например, следующая объединенная строка содержит настройки для трех символов.

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]

Именно строки такого вида сможет парсить метод parseAll. Для ввода такой строки в эксперт опишем входную переменную WorkSymbols.

input string WorkSymbols = ""// WorkSymbols (name±lots*factor^limit(sl,tp)[start,stop];...)

Если она пуста, эксперт будет работать с настройками из отдельных входных переменных, представленных ранее. Если же строка указана, обработчик OnInit заполнит пул торговых систем по результатам разбора этой строки.

int OnInit()
{
   if(WorkSymbols == "")
   {
      ... // работа с текущим одним символом, как ранее
   }
   else
   {
      Print("Parsed settings:");
      Settings settings[];
      Settings::parseAll(WorkSymbolssettings);
      const int n = ArraySize(settings);
      pool = new TradingStrategyPool(n);
      for(int i = 0i < ni++)
      {
         settings[i].trailing = Trailing;
         // поддержим несколько систем на одном символе для счетов с хеджингом
         settings[i].magic = Magic + i;  // разный магик для каждой подсистемы
         pool[].push(new SimpleMartingale(settings[i]));
      }
   }
   return INIT_SUCCEEDED;
}

Следует иметь в виду, что длина входной строки ограничена в MQL5 250-ю символами. Более того, при оптимизации в тестере строки урезаются еще сильнее — до 63 символов. Поэтому для оптимизации одновременной торговли на большом количестве символов потребуется реализовать альтернативный вариант загрузки настроек, например, из текстового файла. Это легко сделать на основе той же входной переменной, если в ней задано имя файла, а не строка с настройками.

Этот подход реализован в упомянутом методе Settings::parseAll. Имя текстового файла для предоставления эксперту входной строки без ограничения длины решено устанавливать по универсальному принципу, походящему для всех аналогичных случаев: имя файла начинается с имени эксперта и далее, после дефиса, должно идти имя переменной, данные для которой содержит файл. Например, в нашем случае во входной переменной WorkSymbols можно опционально указать имя файла "MultiMartingale-WorkSymbols.txt". Тогда метод parseAll попробует прочитать текст из файла (тот должен быть в стандартной "песочнице" MQL5/Files).

Передача имен файлов во входных параметрах требует предпринять дополнительные действия для последующего тестирования и оптимизации такого эксперта: в исходный код следует добавить директиву #property tester_file "MultiMartingale-WorkSymbols.txt". Подробно про это будет рассказано в разделе Директивы препроцессора для тестера. Когда данная директива добавлена, эксперт будет требовать наличия файла и не запустится без него в тестере!

Эксперт готов. Мы можем протестировать его на разных символах по отдельности, подобрать удачные настройки для каждого и собрать торговый портфель. В следующей Главе мы займемся изучением API тестера, включая оптимизацию, и наличие такого "подопытного кролика" придется весьма кстати. А пока проверим его мультивалютную работу на настройках, которые определились как бы сами собой.

WorkSymbols=EURUSD+0.01*1.2^4(300,600)[9,11];GBPCHF+0.01*2.0^7(300,400)[14,16];AUDJPY+0.01*2.0^6(500,800)[18,16]

На первом квартале 2022 года получим следующий отчет (в отчетах MetaTrader 5 не предусмотрены показатели в разбивке по символам, поэтому отличить одновалютный отчет от многовалютного возможно только по таблице сделок/ордеров/позиций).

Отчет тестера для мультивалютного эксперта по стратегии Мартингейла

Отчет тестера для мультивалютного эксперта по стратегии Мартингейла

Следует отметить, что из-за того, что стратегия запускается из обработчика OnTick, её прогоны на разных основных символах (то есть тех, что выбираются в выпадающем списке настроек тестера) будут давать слегка отличающиеся результаты. В нашем тесте мы просто использовали EURUSD, как наиболее ликвидный инструмент, по которому тики происходят наиболее часто, и для большинства применений этого достаточно. Однако если требуется реагировать на тики всех инструментов, можно использовать индикатор вроде EventTickSpy.mq5. Или можно запускать торговую логику по таймеру, не привязываясь к тикам конкретного инструмента.

А вот как торговая стратегия выглядит для отдельно взятого символа, в данном случае AUDJPY.

График с тестом мультивалютного эксперта по стратегии Мартингейла

График с тестом мультивалютного эксперта по стратегии Мартингейла

Кстати говоря, для всех мультивалютных экспертов актуален другой важный вопрос, оставленный здесь без внимания. Речь о способе подбора размера лота, например, исходя из загрузки депозита или риска. Ранее мы показали примеры подобных расчетов в неторгующем эксперте LotMarginExposureTable.mq5. В эксперте MultiMartingale.mq5 мы упростили задачу, выбрав фиксированный лот и выведя его в настройки для каждого символа. Однако в рабочих мультивалютных экспертах имеет смысл выбирать лоты пропорционально стоимости инструментов (по марже или волатильности).

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