preview
Мультибот в MetaTrader (Часть II): улучшенный динамический шаблон

Мультибот в MetaTrader (Часть II): улучшенный динамический шаблон

MetaTrader 5Примеры | 29 февраля 2024, 14:40
870 0
Evgeniy Ilin
Evgeniy Ilin

Содержание


Введение

В прошлой статье я вдохновлялся некоторыми наиболее популярными решениями из Маркета, и у меня получилось создать свой аналог такого шаблона. Но, учитывая некоторые проекты, которыми я занимаюсь, оказалось, что такое решение не является оптимальным. Кроме того, оно все еще имеет ряд ограничений и неудобств, связанных с общей архитектурой такого шаблона. Да, такой шаблон может быть достаточным для подавляющего числа среднестатистических решений, но только не для моего. Вторым очень важным моментом является факт того, что с точки зрения потенциального покупателя и конечного пользователя данного советника, лично мне бы хотелось увидеть максимальную простоту и минимальное количество настроек. А в идеале — полное отсутствие необходимости переоптимизации и прочих манипуляций с моей стороны. Ведь я плачу деньги не только за рабочее решение, но, что гораздо более важное, — за максимально дружелюбный и понятный даже самому простому пользователю интерфейс.


Идея динамического шаблона

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

Плюсы статического шаблона(базового):

  1. Индивидуальная настройка каждого инструмента - периода (виртуального советника на виртуальном графике).
  2. Более простой и быстрый код (будет выполняться быстрее).
  3. Распространенность подхода (обширно представлен в маркете и доказал свою жизнеспособность).

    Минусы статического шаблона(базового):

    1. Сложность настройки и большая вероятность ошибки (длинные строковые цепи в настройках).
    2. Ограниченные возможности для расширения функционала.
    3. Невозможность добавления или удаления виртуальных роботов без ручной перенастройки.

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

      схема использования нового шаблона

      Здесь прошу обратить внимание на нижний левый элемент, содержащий "CLOSING". В шаблоне предусмотрено сохранение настроек, при которых была открыта последняя позиция на соответствующем виртуальном графике. Это специальный защитный механизм от частой смены настроек и поломки стратегии. Если мы открыли позиции по какой-то логике, мы заинтересованы закрыть ее по той же логике, и только после этого можем сменить настройку на более свежую. В противном случае, мы можем получить абсолютную кашу. Да, это не всегда так, но лучше такие моменты сразу блокировать на уровне общей логики.

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

      Плюсы динамического шаблона(нового):

      1. Настройка каждого инструмента-периода независимо от торгового терминала происходит с помощью текстовых файлов.
      2. Динамическое считывание настроек из папок и автоматическое открытие или закрытие виртуальных графиков происходит вместе с их советниками.
      3. Есть возможность автоматической синхронизации с Web API (обязательно через 443 порт).
      4. Настройка происходит с помощью общей папки терминала (*\Common\Files).
      5. Ведущий советник нужен для синхронизации с API всех терминалов на одной машине. Также он потенциально может работать на нескольких через общую папку внутри локальной сети.
      6. Имеется возможность интеграции с внешними программами, которые также могут управлять этими файлами, например, создавать их, удалять или менять настройки.
      7. Возможна ситуация, когда создается пара платный-бесплатный шаблон. Если вырезать связь с API, мы можем оформить это как демо-версию, которая требует наличия платной для эффективной работы.

                Минусы динамического шаблона(нового):

                1. Работа через файловую систему

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

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

                упрощенная схема работы с шаблоном

                Работает вся эта схема достаточно просто, с помощью двух независимых таймеров, которые отвечают каждый за свои задачи. Первый таймер грузит настройки с API, а второй считывает эти же настройки в память советника. Дополнительно изображены две стрелки, которые символизируют возможность ручного создания и изменения этих настроек, либо автоматизацию этого процесса с помощью стороннего приложения или иного кода. Чисто теоретически этим шаблоном можно управлять из любого иного кода, как например и из другого советника или скрипта для MetaTrader 4 и MetaTrader 5.

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


                Основные настройки динамического шаблона

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

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

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

                //+------------------------------------------------------------------+
                //|                         main variables                           |
                //+------------------------------------------------------------------+
                input bool bToLowerE=false;//To Lower Symbol
                input string SymbolPrefixE="";//Symbol Prefix
                input string SymbolPostfixE="";//Symbol Postfix
                
                input string SubfolderE="folder1";//Subfolder In Files Folder
                input bool bCommonReadE = true;//Read From Common Directory
                
                input bool bWebSyncE = false;//Sync with API
                input string SignalDirectoryE = "folder1";//Signal Name(Folder)
                input string ApiDomen = "https://yourdomen.us";//API DOMEN (add in terminal settings!)
                
                input bool bInitLotControl=true;//Auto Lot
                input double DeltaBarPercent=1.5;//Middle % of Delta Equity Per M1 Bar (For ONE! Bot)
                input double DepositDeltaEquityE=100.0;//Deposit For ONE! Bot
                
                input bool bParallelTradingE=true;//Parallel Trading
                
                input int SLE=0;//Stop Loss Points
                input int TPE=0;//Take Profit Points

                Я в данном примере разделил переменные на разные блоки по сходным признакам. Как видно, переменных не так много. Первый блок предназначен для обработки различных стилей именования инструментов у разных брокеров. Например, если на вашем брокере "EURUSD", то все переменные из данного блока должны быть такие, как есть сейчас, но возможны и иные варианты, например:

                • eurusd
                • EURUSDt
                • tEURUSD
                • _EURUSD
                • eurusd_

                И так далее. Не буду далее продолжать, думаю, вы додумаете сами. Само собой, в хорошем шаблоне должна быть обработка большинства таких вариаций.

                Второй подблок говорит о том, из какой папки мы считываем файлы, и в эту же папку мы загружаем данные с API. Если указать корректное имя папки, MetaTrader создаст соответствующую поддиректорию и будет работать именно с ней. Если же не указывать, все файлы будут лежать в корне. Интересной особенностью обладает общая папка терминалов: если включить эту опцию, мы можем объединить не только несколько советников, работающих внутри MetaTrader 5, но и все терминалы, независимо от того, работают ли они на MetaTrader 4 или MetaTrader 5. Таким образом, оба шаблона смогут работать одинаково по одним и тем же настройкам, обеспечивая максимальную интеграцию и синхронизацию.

                Третий подблок, как я предполагаю, все поняли, включает и выключает таймер синхронизации с вашим API, если он имеется. Важно учесть, что связь с API осуществляется благодаря единственной функции "WebRequest", которая работает как в MQL4, так и в MQL5. Единственное ограничение здесь в том, что ваш API должно обязательно работать на 443 порте. Однако, в MQL5 этот метод расширили, и он имеет возможность подключения через другой порт. Тем не менее, я отказался от этой затеи в своем API, чтобы обеспечить кросс-платформенность моих шаблонов и решений, построенных на его основе.

                API я построил так, чтобы имя сигнала одновременно было и директорией с файлами. Таким образом, я как бы могу подключаться к разным сигналам, зная их имена. В любой момент можно отключиться от старого сигнала и подключиться к новому. Да, конечно, сами файлы вы так не скачаете, но я сделал это немного иначе. Я получаю JSON с содержимым этого файла, а потом создаю его самостоятельно из кода самого шаблона. Да, это требует дополнительных методов для извлечения этих данных из вашей JSON-строки, но лично у меня это никаких проблем не создало, ведь MQL5 это позволяет легко сделать без особых проблем. Ну и, конечно же, перед использованием вашего API вам потребуется любой домен и добавление его в список разрешенных соединений в настройках MetaTrader.

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

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

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


                Правила именования файлов с настройками и директивы, которые помогут в этом

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

                //+------------------------------------------------------------------+
                //|                     your bot unique name                         |
                //+------------------------------------------------------------------+
                #define BotNick "dynamictemplate" //bot

                Эта директива, как бы, задает псевдоним нашему боту. По данному псевдониму происходит абсолютно все:

                1. Именование вновь созданных файлов,
                2. Чтение файлов,
                3. Создание глобальных переменных терминала,
                4. Чтение глобальных переменных терминала,
                5. Другое.

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

                Рядом находится еще одна похожая директива, которая обеспечивает то же самое:

                //+------------------------------------------------------------------+
                //|               unique shift for difference of EA                  |
                //+------------------------------------------------------------------+
                #define MagicHelp 0 //bot magic shift

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

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

                //+------------------------------------------------------------------+
                //|                        applied symbols                           |
                //+------------------------------------------------------------------+
                string III[] = { 
                   "EURUSD",
                   "GBPUSD",
                   "USDJPY",
                   "USDCHF",
                   "USDCAD",
                   "AUDUSD",
                   "NZDUSD",
                   "EURGBP",
                   "EURJPY",
                   "EURCHF",
                   "EURCAD",
                   "EURAUD",
                   "EURNZD",
                   "GBPJPY",
                   "GBPCHF",
                   "GBPCAD",
                   "GBPAUD",
                   "GBPNZD",
                   "CHFJPY",
                   "CADJPY",
                   "AUDJPY",
                   "NZDJPY",
                   "CADCHF",
                   "AUDCHF",
                   "NZDCHF",
                   "AUDCAD",
                   "NZDCAD",
                   "AUDNZD",
                   "USDPLN",
                   "EURPLN",
                   "USDMXN",
                   "USDZAR",
                   "USDCNH",
                   "XAUUSD",
                   "XAGUSD",
                   "XAUEUR"
                };

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

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

                Давайте рассмотрим теперь пример. Например, имя инструмента "EURUSD" у вашего брокера пишется так: "eurusd_". Значит, вы все равно файл с настройками именуете с использованием "EURUSD", но дополнительно в настройках делаете следующее:

                //+------------------------------------------------------------------+
                //|                        symbol correction                         |
                //+------------------------------------------------------------------+
                input bool bToLowerE=true;//To Lower Symbol
                input string SymbolPrefixE="";//Symbol Prefix
                input string SymbolPostfixE="_";//Symbol Postfix

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

                Тем не менее, мы разобрались лишь с тем, как именовать файл, чтобы шаблон понял, что это именно его настройка "BotNick", и как сопоставить ее с правильным торговым инструментом, но нам осталось еще сопоставить настройку с конкретным периодом графика. Для этого я ввел следующее правило:

                • После имени файла ставится пробел и пишется эквивалент этого периода в минутах.
                • Допускаются периоды графиков в диапазоне M1... H4.

                Думаю, не лишним будет конкретно перечислить периоды, которые находятся в данном диапазоне. Очень важным для меня являлся тот факт, чтобы все эти периоды были представлены как в MetaTrader 4, так и в MetaTrader 5, именно это и являлось основной причиной выбора периодов. Другой очень важной причиной было то, что очень высокие периоды выше "H4" не используются, как правило, в автоматической торговле по барам. Во всяком случае, я таких примеров не видел, потому выбор пал на следующие периоды:

                • M1 - 1 минута
                • M5 - 5 минут
                • M15 - 15 минут
                • M30 - 30 минут
                • H1 - 60 минут
                • H4 - 240 минут

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

                • "INSTRUMENT" + " " + "PERIOD IN MINUTES" + " " + BotNick + ".txt"

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

                • "CLOSING" + " " + "INSTRUMENT" + " " + "PERIOD IN MINUTES" + " " + BotNick + ".txt"

                Как видно, эти два файла будут отличаться лишь припиской "CLOSING" и последующим отделяющим пробелом. Все сигнальные строки в имени, конечно, отделяются единичным пробелом, чтобы интерпретатор шаблона мог распознать и вычленить эти маркеры из имени файла. Таким образом, принадлежность настройки к какому-либо графику определяется лишь его именем. Давайте теперь посмотрим на несколько примеров настроек, которые построены по данному правилу:

                • EURUSD 15 dynamictemplate.txt
                • GBPUSD 240 dynamictemplate.txt
                • EURCHF 60 dynamictemplate.txt
                • CLOSING GBPUSD 240 dynamictemplate.txt

                Очевидно, для примера вполне достаточно. Отдельно прошу обратить внимание на последнее имя. Этот файл будет скопирован на основе "GBPUSD 240 dynamictemplate.txt" и помещен в папку именно того терминала, в котором советник производил копирование. Это сделано для того, чтобы предотвратить множественную запись в одни и те же файлы со стороны разных, но одинаковых советников внутри нескольких терминалов. Если отключить опцию чтения из общей папки терминала, то и обычные файлы будут писаться туда же. Такое может быть необходимо, если требуется настроить каждую конкретную копию советника в соответствующем терминале своим независимым набором настроек. Несколько файлов я оставлю рядом с шаблонами в качестве примера, чтобы было понятнее на конкретном примере, и чтобы вы смогли поэкспериментировать с ними, перекладывая их в разные папки. На этом рассмотрение общих моментов использования настроек завершено.


                Методы чтения файлов и их создания

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

                //+------------------------------------------------------------------+
                //|                 used for configuration settings                  |
                //+------------------------------------------------------------------+
                bool QuantityConfiguration()
                {
                    FilesGrab(); // Determine the names of valid files
                    
                    // Check if there are changes in the configuration settings (either add or delete)
                    if (bNewConfiguration())
                    {
                        return true;
                    }     
                    return false;
                }

                Данный метод как раз определяет, обновился ли набор файлов в нашей рабочей директории. Если да, то мы используем этот метод как сигнал к перезапуску всех графиков и советников, чтобы добавить новые или удалить лишние инструменты-периоды. Давайте теперь посмотрим на метод "FilesGrab".

                //+------------------------------------------------------------------+
                //|   reads all files and forms a list of instruments and periods    |
                //+------------------------------------------------------------------+
                void FilesGrab()
                   {
                   string file;
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; // SubfolderE is the path to the specific subfolder
                   // Returns the handle of the first found file with the specified characteristics, based on whether CommonReadE is True or False
                   long total_files = !bCommonReadE? FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file) :FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file,FILE_COMMON);
                   if(total_files > 0)
                      {
                         ArrayResize(SettingsFileNames,0); // Clear the array from previous values if there are files to be read
                         do
                         {
                            int second_space = StringFind(file, " ", StringFind(file, " ") + 1); // Searches for the index of the second space in the file's name
                            if(second_space > 0) 
                            {
                                string filename = StringSubstr(file, 0, second_space); // Extracts the string/characters from the filename up to the second space
                                ArrayResize(SettingsFileNames, ArraySize(SettingsFileNames) + 1); // Increases the size of the array by one
                                SettingsFileNames[ArraySize(SettingsFileNames) - 1] = filename; // Adds the new filename into the existing array
                            }
                         }
                         while(FileFindNext(total_files, file)); // Repeat for all the files        
                         FileFindClose(total_files); // Close the file handle to free resources
                      }
                   }  

                Этот метод как раз и производит предварительный сбор имен тех файлов, что относятся к нашему советнику, во что-то типа "EURUSD 60". Иначе говоря, оставляет от имени только ту часть, что в дальнейшем будет разобрана на пару инструмент-период. Чтение этих файлов, однако, происходит не здесь, а внутри каждого виртуального советника отдельно. Но для этого сначала нужно разобрать саму строку на символ и период. Этому предшествуют несколько моментов. Один из них таков.

                //+------------------------------------------------------------------+
                //|                        symbol validator                          |
                //+------------------------------------------------------------------+
                bool AdaptDynamicArrays()
                {
                    bool RR=QuantityConfiguration();
                    // If a new configuration of files is detected (new files, changed order, etc.)
                    if (RR)
                    {
                        // Read the settings (returns the count)
                        int Readed = ArraySize(SettingsFileNames); 
                        int Valid =0;
                
                        // Only valid symbol name needs to be populated (filenames are taken from already prepared array)
                        ArrayResize(S, Readed);
                     
                        for ( int j = 0; j < Readed; j++ )
                        {
                            for ( int i = 0; i < ArraySize(III); i++ )
                            {
                                // check the symbol to valid
                                if ( III[i] == BasicNameToSymbol(SettingsFileNames[j]) )
                                {
                                    S[Valid++]=SettingsFileNames[j];
                                    break; // stop the loop
                                }
                            }
                        } 
                        //resize S with the actual valid quantity
                        ArrayResize(S, Valid);
                        return true;
                    }
                    return false;
                }

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

                Немаловажным моментом также является резервирование настроек, которое происходит постоянно, по мере того, как производится чтение основных настроек. Если у нас открылась позиция на основе базовой настройки, то мы прекращаем периодическое резервирование настроек. Резервный файл с припиской "CLOSING" всегда сохраняется в директорию текущего терминала.

                //+------------------------------------------------------------------+
                //|         сopy settings from the main file to a CLOSING file       |
                //+------------------------------------------------------------------+
                void SaveCloseSettings()
                   {
                   string FileNameString=Charts[chartindex].BasicName;
                   bool bCopied;
                   string filenametemp;
                   string filename="";
                   long handlestart;   
                   
                   //Checking if SubfolderE doesn't exist, if yes, assign tempsubfolder to be an empty string
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                
                   //Find the first file in the subfolder according to bCommonReadE and assign the result to handlestart 
                   if (bCommonReadE) handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);
                   else handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);
                   
                   //Check if the start of our found file name matches FileNameString 
                   if ( StringSubstr(filenametemp,0,StringLen(FileNameString)) == FileNameString )
                      {
                      //if yes, complete the file's path 
                      filename=tempsubfolder+filenametemp;
                      }
                     //keep finding the next file while conditions are aligned  
                   while ( FileFindNext(handlestart,filenametemp) )
                      {
                      //if found file's name matches FileNameString then add found file's name to the path
                      if ( StringSubstr(filenametemp,0,StringLen(FileNameString)) == FileNameString )
                         {
                         filename=tempsubfolder+filenametemp;
                         break;
                         }
                      }   
                   //if handlestart is not INVALID_HANDLE then close the handle to release the resources after the search
                   if (handlestart != INVALID_HANDLE) FileFindClose(handlestart); 
                
                   //Perform file copy operation and notice if it was successful 
                   if ( bCommonReadE ) bCopied=FileCopy(filename,FILE_COMMON,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI);
                   else bCopied=FileCopy(filename,0,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI);
                   }

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


                Создание виртуальных графиков и советников

                Дальше мы уже работаем с ним и достаём все данные оттуда в следующем методе, параллельно создавая нужные виртуальные графики.

                //+------------------------------------------------------------------+
                //|                      creates chart objects                       |
                //+------------------------------------------------------------------+
                void CreateCharts()
                   {
                   bool bAlready;
                   int num=0;
                   string TempSymbols[];
                   string Symbols[];
                   ArrayResize(TempSymbols,ArraySize(S)); // Resize TempSymbols array to the size of S array
                   for (int i = 0; i < ArraySize(S); i++) // Populate TempSymbols array with empty strings
                      {
                      TempSymbols[i]="";
                      }
                   for (int i = 0; i < ArraySize(S); i++) // Count the required number of unique trading instruments
                      {
                      bAlready=false;
                      for (int j = 0; j < ArraySize(TempSymbols); j++)
                         {
                         if ( S[i] == TempSymbols[j] ) // If any symbol is already present in TempSymbols from S, then it's not unique
                            {
                            bAlready=true;
                            break;
                            }
                         }
                      if ( !bAlready ) // If the symbol is not found in TempSymbols i.e., it is unique, add it to TempSymbols
                         {
                         for (int j = 0; j < ArraySize(TempSymbols); j++)
                            {
                            if ( TempSymbols[j] == "" )
                               {
                               TempSymbols[j] = S[i];
                               break;
                               }
                            }
                         num++; // Increments num if a unique element is added          
                         }
                      }      
                   ArrayResize(Symbols,num); // Resize the Symbols array to the size of the num
                
                   for (int j = 0; j < ArraySize(Symbols); j++) // Now that the Symbols array has the appropriate size, populate it
                      {
                      Symbols[j]=TempSymbols[j];
                      } 
                   ArrayResize(Charts,num); // Resize Charts array to the size of num
                
                   int tempcnum=0;
                   tempcnum=1000; // Sets all charts to a default of 1000 bars
                   Chart::TCN=tempcnum; 
                   for (int j = 0; j < ArraySize(Charts); j++)
                      {
                      Charts[j] = new Chart();
                      Charts[j].lastcopied=0; // Initializes the array position where the last copy of the chart was stored
                      Charts[j].BasicName=Symbols[j]; 
                      ArrayResize(Charts[j].CloseI,tempcnum+2); // Resizes the CloseI array to store closing price of each bar
                      ArrayResize(Charts[j].OpenI,tempcnum+2); // Resizes the OpenI array for opening prices
                      ArrayResize(Charts[j].HighI,tempcnum+2); // HighI array for high price points in each bar
                      ArrayResize(Charts[j].LowI,tempcnum+2); // LowI array for low price points of each bar
                      ArrayResize(Charts[j].TimeI,tempcnum+2); // TimeI array is resized to store time of each bar
                      string vv = BasicNameToSymbol(Charts[j].BasicName); 
                      StringToLower(vv);
                      // Append prefix and postfix to the basic symbol name to get the specific symbol of the financial instrument 
                      Charts[j].CurrentSymbol = SymbolPrefixE +  (!bToLowerE ? BasicNameToSymbol(Charts[j].BasicName) : vv) + SymbolPostfixE;
                      Charts[j].Timeframe = BasicNameToTimeframe(Charts[j].BasicName); // Extracts the timeframe from the basic name string
                      }
                   ArrayResize(Bots,ArraySize(S)); // Resize Bots array to the size of S array
                   }

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

                //+------------------------------------------------------------------+
                //|              attaching all virtual robots to charts              |
                //+------------------------------------------------------------------+
                void CreateInstances()
                   {
                   // iterating over the S array
                   for (int i = 0; i < ArraySize(S); i++)
                      {
                      // iterating over the Charts array
                      for (int j = 0; j < ArraySize(Charts); j++)
                         {
                         // checking if the BasicName of current Chart matches with the current item in S array
                         if ( Charts[j].BasicName == S[i] )
                            {
                            // creating a new Bot instance with indices i, j and assigning it to respective position in Bots array
                            Bots[i] = new BotInstance(i,j);
                            break;
                            } 
                         }
                      }
                   }

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


                Динамическое чтение и перенастройка виртуальных советников

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

                //+------------------------------------------------------------------+
                //|             we will read the settings every 5 minutes +          |
                //+------------------------------------------------------------------+
                bool bReadTimer()
                   {
                   if (  TimeCurrent() - LastTime > 5*60 + int((double(MathRand())/32767.0) * 60) )
                      {
                      LastTime=TimeCurrent();
                      int orders=OrdersG();
                      bool bReaded=false;
                      if (orders == 0)  bReaded = ReadSettings(false,Charts[chartindex].BasicName);//reading a regular file
                      else bReaded = ReadSettings(true,Charts[chartindex].BasicName);//reading file to close position
                      if (orders == 0 && bReaded) SaveCloseSettings();//save settings for closing position
                      return bReaded;
                      }
                   return false;
                   }

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

                //+------------------------------------------------------------------+
                //|                        reading settings                          |
                //+------------------------------------------------------------------+
                bool BotInstance::ReadSettings(bool bClosingFile,string Path)
                   {
                   string FileNameString=Path;
                   int Handle0x;
                   string filenametemp;
                   string filename="";
                   long handlestart;
                            
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                        
                   if (!bClosingFile)//reading a regular file
                      {
                      if (!bCommonReadE)
                         {
                         handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);         
                         int SearchStart=0;
                         
                         if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                            {
                            filename=tempsubfolder+filenametemp;
                            }
                         if (filename != filenametemp || filename == "")
                            {
                            while ( FileFindNext(handlestart,filenametemp) )
                               {
                               if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                                  {
                                  filename=tempsubfolder+filenametemp;
                                  break;
                                  }
                               }         
                            }
                         if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                         
                         if (filename != "")
                            {
                            Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);
                            
                            if ( Handle0x != INVALID_HANDLE )//if the file exists
                               {
                               FileSeek(Handle0x,0,SEEK_SET);
                               ulong size = FileSize(Handle0x);
                               string str = "";
                               for(ulong i = 0; i < size; i++)
                                  {
                                     str += FileReadString(Handle0x);
                                  }
                               if (str != "" && str != PrevReaded)
                                  {
                                  FileSeek(Handle0x,0,SEEK_SET);
                                  //read the required parameters
                                  ReadFileStrings(Handle0x);
                                  //                     
                                  FileClose(Handle0x);
                                  LastRead = TimeCurrent();
                                  RestartParams();
                                  }
                               else
                                  {
                                  FileClose(Handle0x);
                                  }
                               return true;
                               }
                            else
                               {
                               return false;
                               }         
                            }         
                         }
                      else
                         {
                         handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);   
                         int SearchStart=0;
                         
                         if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                            {
                            filename=tempsubfolder+filenametemp;
                            }
                         if (filename != filenametemp || filename == "")
                            {
                            while ( FileFindNext(handlestart,filenametemp) )
                               {
                               if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                                  {
                                  filename=tempsubfolder+filenametemp;
                                  break;
                                  }
                               }         
                            }
                         if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                         
                         if (filename != "")
                            {
                            Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
                            
                            if ( Handle0x != INVALID_HANDLE )//if the file exists
                               {
                               FileSeek(Handle0x,0,SEEK_SET);
                               ulong size = FileSize(Handle0x);
                               string str = "";
                               for(ulong i = 0; i < size; i++)
                                  {
                                     str += FileReadString(Handle0x);
                                  }
                               if (str != "" && str != PrevReaded)
                                  {
                                  FileSeek(Handle0x,0,SEEK_SET);
                                  //read the required parameters
                                  ReadFileStrings(Handle0x);
                                  //      
                                  FileClose(Handle0x);
                                  LastRead = TimeCurrent();
                                  RestartParams();
                                  }
                               else
                                  {
                                  FileClose(Handle0x);
                                  }
                               return true;
                               }
                            else
                               {
                               return false;
                               }         
                            }         
                         }
                      }         
                   else//reading a file to close a position
                      {
                      handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);   
                      int SearchStart=8;//when the line starts with "CLOSING "
                      
                      if ( StringLen(filenametemp) >= (8 + StringLen(FileNameString)) && StringSubstr(filenametemp,0,8) == "CLOSING " 
                      && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                         {
                         filename=tempsubfolder+filenametemp;
                         }
                      if (filename != filenametemp || filename == "")
                         {
                         while ( FileFindNext(handlestart,filenametemp) )
                            {
                            if ( StringLen(filenametemp) >= (8 + StringLen(FileNameString)) && StringSubstr(filenametemp,0,8) == "CLOSING " 
                            && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                               {
                               filename=tempsubfolder+filenametemp;
                               break;
                               }
                            }         
                         }
                      if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                      
                      if (filename != "")
                         {
                         Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);
                         
                         if ( Handle0x != INVALID_HANDLE )//if the file exists
                            {
                            FileSeek(Handle0x,0,SEEK_SET);
                            ulong size = FileSize(Handle0x);
                            string str = "";
                            for(ulong i = 0; i < size; i++)
                               {
                                  str += FileReadString(Handle0x);
                               }
                            if (str != "" && str != PrevReaded)
                               {
                               PrevReaded=str;
                               FileSeek(Handle0x,0,SEEK_SET);
                               //read the required parameters
                               ReadFileStrings(Handle0x);
                               //
                               FileClose(Handle0x);
                               LastRead = TimeCurrent();
                               RestartParams();                  
                               }
                            else
                               {
                               FileClose(Handle0x);
                               }
                            return true;
                            }
                         else
                            {
                            return false;
                            }         
                         }         
                      }         
                   return false;   
                   }

                Сначала хочу подчеркнуть, что данный метод как раз предназначен для чтения двух видов файлов. В зависимости от переданного маркера "bClosingFile", читается или общая настройка, или "для закрытия". Каждое чтение файлов состоит из нескольких этапов:

                1. Сравнение содержимого предыдущего прочитанного файла и текущего;
                2. В случае, если содержимое отличается, мы считываем уже наши обновленные настройки;
                3. Перезапускаем наш виртуальный советник с новыми настройками, если требуется.

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

                //+------------------------------------------------------------------+
                //|               read settings from file line by line               |
                //+------------------------------------------------------------------+
                void BotInstance::ReadFileStrings(int handle)
                   {
                   //FileReadString(Handle,0);
                   
                   }

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

                //+------------------------------------------------------------------+
                //|                function to prepare new parameters                |
                //+------------------------------------------------------------------+
                void BotInstance::RestartParams() 
                {
                   //additional code
                
                   //
                   MagicF=SmartMagic(BasicNameToSymbol(Charts[chartindex].BasicName), Charts[chartindex].Timeframe);
                   CurrentSymbol=Charts[chartindex].CurrentSymbol;
                   m_trade.SetExpertMagicNumber(MagicF);
                }

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


                Автоматическая генерация магиков

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

                Я назначаю шаг в "10000", например, между ближайшими магиками. Для каждого инструмента я сначала фиксирую его предварительный магик, например, "10000" или "70000". Однако этого не достаточно, ведь у нас не только инструмент, но у него ещё есть период. Поэтому я добавляю к данному промежуточному магику ещё одно число. 

                Самое простое — это добавить также само, как в схеме с чтением файлов, минутный эквивалент данных периодов. Делается это так.

                //+------------------------------------------------------------------+
                //|              Smart generation of magical numbers                 |
                //|    (each instrument-period has its own fixed magic number)       |
                //+------------------------------------------------------------------+
                int BotInstance::SmartMagic(string InstrumentSymbol,ENUM_TIMEFRAMES InstrumentTimeframe)
                {
                   // initialization
                   int magicbuild=0;
                   
                   // loop through the array
                   for ( int i=0; i<ArraySize(III); i++ )
                   {
                      // check the symbol to assign a magic number
                      if ( III[i] == InstrumentSymbol )
                      {
                          magicbuild=MagicHelp+(i+1)*10000;
                          break; // stop the loop
                      }
                   }  
                   
                   // add identifier for time frame    
                   magicbuild+=InstrumentTimeframe;      
                   return magicbuild;
                }

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

                //+------------------------------------------------------------------+
                //|               unique shift for difference of EA                  |
                //+------------------------------------------------------------------+
                #define MagicHelp 0 //bot magic shift [0...9999]

                В целом, все довольно просто. Большой шаг между магиками обеспечивает достаточно много вариантов сдвига, чего более чем достаточно для создания нужного количества советников. Единственное условие использования данной схемы — не превышать число "9999". Кроме того, нужно контролировать, чтобы сдвиг не совпадал с нашими эквивалентами таймфреймов в минутах, так как в этом случае возможны совпадения по магикам у двух разных шаблонов. Чтобы не думать над такими вариантами, можно просто делать сдвиг чуть больше числа "240", например, "241", "241*2", "241*3", "241*N".

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


                Система нормализации объемов

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

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

                Для этого сразу важно ввести следующие определения и формулы. Для начала, я ввёл такие параметры для регулировки рисков:

                • DeltaBarPercent - процент от DepositDeltaEquity,
                • DepositDeltaEquity - депозит одного бота для вычисления его допустимой дельты по эквити для одного M1 бара при открытой позиции.

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

                Задача нашего кода - автоматически подобрать объемы входов, учитывая наши требования. Чтобы осуществить это, нам потребуются дополнительные математические величины и формулы, построенные на их основе. Вывод делать не буду, просто дам вам их для пояснения кода:

                • "Mb" - средний размер баров в пунктах на выбранном диапазоне истории размером в "bars" на рабочем графике советника,
                • "Mb1" - средний размер баров в пунктах на выбранном диапазоне истории размером в "bars" на рабочем графике советника, пересчитанный к M1,
                • "Kb" - коэффициент связи среднего размера баров текущего графика и его эквивалента "M1",
                • "T" - период выбранного графика, сведенный к минутному эквиваленту (также как у нас в файлах было),
                • "BasisI" - требуемый средний прирост или падение линии эквити в валюте депозита за средний размер свечи M1 графика выбранного инструмента,
                • "Basisb" - фактический средний прирост или падение линии эквити в валюте депозита за средний размер свечи M1 графика выбранного инструмента, для трейда размером в "1" лот,
                • "Lot" - подобранный лот (объем).

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

                Коэффициент масштабирования для перехода к M1

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

                //+------------------------------------------------------------------+
                //|       timeframe to average movement adjustment coefficient       |
                //+------------------------------------------------------------------+
                double PeriodK(ENUM_TIMEFRAMES tf)
                   {
                   double ktemp;
                   switch(tf)
                      {
                      case  PERIOD_H1:
                          ktemp = MathSqrt(1.0/60.0);
                          break;
                      case  PERIOD_H4:
                          ktemp = MathSqrt(1.0/240.0);
                          break;
                      case PERIOD_M1:
                          ktemp = 1.0;
                          break;
                      case PERIOD_M5:
                          ktemp = MathSqrt(1.0/5.0);
                          break;
                      case PERIOD_M15:
                          ktemp = MathSqrt(1.0/15.0);
                          break;
                      case PERIOD_M30:
                          ktemp = MathSqrt(1.0/30.0);
                          break;
                      default: ktemp = 0;
                      }
                   return ktemp;
                   }

                Теперь, конечно, нужно понять, какую же величину мы адаптируем к "M1". Вот она.

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

                Оба этих действия происходят в следующем методе.

                //+------------------------------------------------------------------+
                //|     average candle size in points for M1 for the current chart   |
                //+------------------------------------------------------------------+
                double CalculateAverageBarPoints(Chart &Ch)
                   {
                   double SummPointsSize=0.0;
                   double MaxPointSize=0.0;
                   for (int j = 0; j < ArraySize(Ch.HighI); j++)
                      {
                      if (Ch.HighI[j]-Ch.LowI[j] > MaxPointSize) MaxPointSize= Ch.HighI[j]-Ch.LowI[j];
                      }
                        
                   for (int j = 0; j < ArraySize(Ch.HighI); j++)
                      {
                      if (Ch.HighI[j]-Ch.LowI[j] > 0) SummPointsSize+=(Ch.HighI[j]-Ch.LowI[j]);
                      else SummPointsSize+=MaxPointSize;
                      }  
                   SummPointsSize=(SummPointsSize/ArraySize(Ch.HighI))/Ch.ChartPoint;
                   return PeriodK(Ch.Timeframe)*SummPointsSize;//return the average size of candles reduced to a minute using the PeriodK() adjustment function
                   }

                Теперь можно использовать полученную величину, сведенную к M1, для того, чтобы произвести расчет переменной "Basisb". Вычислять ее следует так.

                Если кто-то не знает, размер тика — это величина изменения эквити открытой позиции объемом в "1" лот, при движении цены на "1" пункт. Если умножить ее на средний размер минутной свечи, то мы получим, насколько изменится эквити позиции с единичным лотом, но учитывая, что размер движения стал равным среднему размеру минутного бара. Далее мы должны посчитать оставшуюся величину "BasisI", и делается это так.

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

                Все описанные операции производятся в следующем методе.

                //+------------------------------------------------------------------+
                //|    calculate the optimal balanced lot for the selected chart     |
                //+------------------------------------------------------------------+
                double OptimalLot(Chart &Ch)
                   {
                   double BasisDX0 =  (DeltaBarPercent/100.0) * DepositDeltaEquityE;
                   double DY0=CalculateAverageBarPoints(Ch)*SymbolInfoDouble(Ch.CurrentSymbol,SYMBOL_TRADE_TICK_VALUE);
                   return BasisDX0/DY0;
                   }

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


                Автолот

                Нормализованный лот можно увеличить пропорционально депозиту. Чтобы понять, как, введу следующие обозначения:

                • - Lot - нормализованный (уравновешенный) лот для конкретного виртуального советника;
                • - AutoLot - пересчитанный к депозиту "Lot" для режима авто лота (его нам и нужно получать при включенном авто лоте);
                • - DepositPerOneBot - часть текущего депозита (часть от Deposit), которой может распоряжаться лишь один из виртуальных советников;
                • - DepositDeltaEquity - депозит, относительно которого мы выполняли нормализацию (уравновешивали лоты);
                • - Deposit - наш текущий банк;
                • - BotQuantity - текущее количество виртуальных советников, торгующих внутри мультибота.

                После чего можно написать, чему равен наш "AutoLot":

                • AutoLot = Lot * (DepositPerOneBot / DepositDeltaEquity).

                Получается, что в случае с обычным нормализованным объемом мы игнорируем наш депозит и, как бы, принимаем, что на одного виртуального советника выделяется вот такой депозит - "DepositDeltaEquity". Но в случае с авто лотом данный депозит не является реальным, и мы должны пропорционально изменить нормализованные объемы, чтобы наши риски адаптировались к реальному депозиту. Однако, адаптировать нужно с тем учетом, что на одного виртуального советника приходится лишь часть депозита.

                • DepositPerOneBot = Deposit / BotQuantity.

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

                авто лот с нормализованными объемами

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


                Синхронизация с API

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

                //+------------------------------------------------------------------+
                //|            used to read the settings every 5 minutes +           |
                //+------------------------------------------------------------------+
                void DownloadTimer()
                {
                    // Check if the passed time from the last download time is more than 5 minutes
                    if (  TimeCurrent() - LastDownloadTime > 5*60 + int((double(MathRand())/32767.0) * 60) )
                    {
                        // Set the last download time to the current time
                        LastDownloadTime=TimeCurrent();
                        // Download files again
                        DownloadFiles();
                     }
                } 

                Теперь давайте посмотрим на основной метод "DownloadFiles".

                //+------------------------------------------------------------------+
                //|       used to download control files if they isn't present       |
                //+------------------------------------------------------------------+
                void DownloadFiles()
                {
                    string Files[];
                    // Initialize the response code by getting files from the signal directory
                    int res_code=GetFiles(SignalDirectoryE,Files); 
                
                    // Check if the list of files is successfully got
                    if (res_code == 200)
                    {
                        // Proceed if there is at least one file in the server
                        if (ArraySize(Files) > 0)
                        {
                            // Download each file individually
                            for (int i = 0; i < ArraySize(Files); i++)
                            {
                                string FileContent[];
                                // Get the content of the file
                                int resfile =  GetFileContent(SignalDirectoryE,Files[i],FileContent);
                
                                // Check if the file content is successfully got
                                if (resfile == 200)
                                {
                                    // Write the file content in our local file
                                    WriteData(FileContent,Files[i]);
                                }
                            }
                        }
                    }
                }

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

                //+------------------------------------------------------------------+
                //|              getting the list of files into an array             |
                //+------------------------------------------------------------------+
                int GetFiles(string directory,string &fileArray[])
                   {
                   //string for getting a list of files in the form of JSON via GET to API 
                   string urlList = ApiDomen+"/filelist/"+directory;//URL
                   char message[];//Body of the request
                   string headers = "Password_key: " + key;// We form the headers of the request
                   string resultheaders = "";//returning headers          
                   string cookie = "";//cookies
                   int timeout = 1500;//waiting for a response when requesting a file or json
                   char result[];
                   
                   // We send a GET request to the server to receive JSON with a list of files
                   int res_code =  WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
                   bool rez = extractFiles(CharArrayToString(result),fileArray);
                   if (rez) return res_code;
                   else return 400;
                   }

                Все, что вам нужно будет сделать здесь,  — это сформировать свою строку "URL" по образу и подобию. Самым важным тут являются вот эти части (строки):

                • filelist
                • Password_key

                Первая строка  — это одна из функций вашего "API", которую вы можете назвать по-разному или оставить предложенное мной название. Таких функций у вас будет несколько. Например, для обеспечения следующих операций:

                1. Загрузка файлов настроек на ваше API (из вашего отдельного приложения или программы).
                2. Очистка файлов настроек на вашем API (из вашего отдельного приложения или программы).
                3. Загрузка списков файлов из существующей директории (из вашего советника).
                4. Загрузка содержимого файла (из вашего советника).

                Могут быть и иные функции, которые вам нужны, но конкретно в советнике понадобятся лишь последние две. Вторая строка  — это один из заголовков "headers". Ее вам тоже нужно будет сформировать, исходя из того, что вы передаете в API. Как правило, достаточно лишь ключа доступа, для того чтобы туда не ломились все кому не лень. Но можно и еще добавить, если нужно передать какую-то дополнительную информацию. Разбор полученной строки от сервера следует производить здесь.

                //+------------------------------------------------------------------+
                //|                  get file list from JSON string                  |
                //+------------------------------------------------------------------+
                bool extractFiles(string json, string &Files[])
                   {
                
                   return false;
                   }

                Получаем "JSON" и разбираем его на имена. К сожалению, не существует универсального кода разбора. В каждом случае все индивидуально. Лично я писал свой код разбора и не сказал бы, что это слишком сложно, и не сказал бы, что слишком долго. Хорошо, конечно, если у вас есть какие-то библиотеки, которые делают это, но лично я предпочитаю максимум кода писать самостоятельно. Теперь давайте посмотрим на аналогичный метод, который получает содержимое файла.

                //+------------------------------------------------------------------+
                //|                    getting the file content                      |
                //+------------------------------------------------------------------+
                int GetFileContent(string directory,string filename,string &OutContent[])
                   {
                   //string for getting a file content in the form of JSON via GET to API 
                   string urlList = ApiDomen+"/file_content/"+directory+"/"+filename;//
                   char message[];// Body of the request
                   string headers = "Password_key: " + key;// We form the headers of the request
                   string resultheaders = "";//returning headers             
                   string cookie = "";//cookies
                   int timeout = 1500;//waiting for a response when requesting a file or json
                   char result[];
                   
                   // We send a GET request to the server to receive JSON with a file content
                   int res_code =  WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
                   bool rez = extractContent(CharArrayToString(result),OutContent);
                   if (rez) return res_code;
                   else return 400;
                   } 

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

                //+------------------------------------------------------------------+
                //|   read the contents of the file from JSON each line separately   |
                //+------------------------------------------------------------------+ 
                bool extractContent(string json, string &FileLines[])
                   {
                   
                   return false;
                   }

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

                //+-----------------------------------------------------------------------+
                //|    fill the file with its lines, which are all contained in data      |
                //|  with a new line separator, and save in the corresponding directory   |
                //+-----------------------------------------------------------------------+
                void WriteData(string &data[],string FileName)
                   {
                   int fileHandle;
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                   
                   if (!bCommonReadE)
                      {
                      fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI);
                      }
                   else
                      {
                      fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON);
                      } 
                      
                   if(fileHandle != INVALID_HANDLE) 
                       {
                       FileSeek(fileHandle,0,SEEK_SET);
                       for (int i=0; i < ArraySize(data) ; i++)
                          {
                          FileWriteString(fileHandle,data[i]+"\r\n");
                          }
                       FileClose(fileHandle);
                       }
                   }
                
                

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


                Общий метод с логикой для торговли

                Я касался данного вопроса в предыдущей статье, но хочу еще раз его осветить и напомнить, что данный метод срабатывает при открытии нового бара в каждом виртуальном советнике. Можете считать его обработчиком типа "OnTick", но в нашем случае это, конечно, будет уже "OnBar". Такого обработчика, кстати говоря, нет в MQL5. Его работа получается немного не той, что хотелось бы, но на самом деле это не оказывает существенного влияния на торговлю по барам, поэтому это — меньшая из наших проблем.

                //+------------------------------------------------------------------+
                //|      the main trading function of individual robot instance      |
                //+------------------------------------------------------------------+
                void BotInstance::Trade() 
                {
                   //data access
                   
                   //Charts[chartindex].CloseI[0]//current bar (zero bar is current like in mql4)
                   //Charts[chartindex].OpenI[0]
                   //Charts[chartindex].HighI[0]
                   //Charts[chartindex].LowI[0]
                   //Charts[chartindex]. ???
                   
                   //close & open
                   
                   //CloseBuyF();
                   //CloseSellF();
                   //BuyF();
                   //SellF();   
                
                   // Here we can include operations such as closing the buying position, closing selling position and opening new positions.
                   // Other information from the chart can be used for making our buying/selling decisions.
                   
                   // Here is a simple trading logic example
                   if ( Charts[chartindex].CloseI[1] > Charts[chartindex].OpenI[1] )
                   {
                      CloseBuyF();
                      SellF();
                   }
                   if ( Charts[chartindex].CloseI[1] < Charts[chartindex].OpenI[1] )
                   {
                      CloseSellF();
                      BuyF();
                   }      
                } 

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


                Графический интерфейс

                В шаблоне, так же как и в предыдущей версии, присутствует пример простого пользовательского интерфейса, цветовую схему и содержание которого, при желании, можно, конечно, поменять. Этот интерфейс идентичен для обоих шаблонов: как для MetaTrader 4, так и для MetaTrader 5, и выглядит он уже получше.

                графический интерфейс

                Там, где вы видите знаки вопроса, можно добавить какую-то дополнительную информацию, или удалить ненужные блоки. Сделать это очень легко. Для работы с интерфейсом есть два метода: "CreateSimpleInterface" и "UpdateStatus". Они очень простые. Показывать их в действии я не буду. Вы можете найти их по соответствующим названиям.

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


                Заключение

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

                Кросс-валютный оптимизатор позволит нам осуществлять одновременную оптимизацию всех виртуальных инструментов-периодов. При слиянии всех этих настроек мы будем получать более качественную и безопасную кривую прибыли с уменьшенными рисками. Автоматическую диверсификацию и усиление прибыли считаю абсолютным приоритетом дальнейших улучшений. По итогу хотелось бы получить некую базовую надстройку над любой стратегией, которая бы позволяла извлечь максимальную прибыль из нее, с сохранением максимальной функциональности и удобства для конечного пользователя. Что-то вроде экзоскелета для нашего торгового сигнала.

                Ссылки

                Прикрепленные файлы |
                DynamicTemplate.zip (80.28 KB)
                Разрабатываем мультивалютный советник (Часть 4): Отложенные виртуальные ордера и сохранение состояния Разрабатываем мультивалютный советник (Часть 4): Отложенные виртуальные ордера и сохранение состояния
                Приступив к разработке мультивалютного советника мы уже достигли некоторых результатов и успели провести несколько итераций улучшения кода. Однако наш советник не мог работать с отложенными ордерами и возобновлять работу после перезапуска терминала. Давайте добавим эти возможности.
                Базовый класс популяционных алгоритмов как основа эффективной оптимизации Базовый класс популяционных алгоритмов как основа эффективной оптимизации
                Уникальная исследовательская попытка объединения разнообразных популяционных алгоритмов в единый класс с целью упрощения применения методов оптимизации. Этот подход не только открывает возможности для разработки новых алгоритмов, включая гибридные варианты, но и создает универсальный базовый стенд для тестирования. Этот стенд становится ключевым инструментом для выбора оптимального алгоритма в зависимости от конкретной задачи.
                Интеграция ML-моделей с тестером стратегий (Заключение): Реализация регрессионной модели для прогнозирования цен Интеграция ML-моделей с тестером стратегий (Заключение): Реализация регрессионной модели для прогнозирования цен
                В данной статье описывается реализация регрессионной модели на основе дерева решений для прогнозирования цен финансовых активов. Мы уже провели подготовку данных, обучение и оценку модели, а также ее корректировку и оптимизацию. Однако важно отметить, что данная модель является лишь исследованием и не должна использоваться при реальной торговле.
                Добавляем пользовательскую LLM в торгового робота (Часть 2): Пример развертывания среды Добавляем пользовательскую LLM в торгового робота (Часть 2): Пример развертывания среды
                Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.