Конструктор советников MQL5 (Часть 1): Простой статический шаблон
Содержание
- Введение
- Зачем нужен простой шаблон
- Архитектура простого шаблона
- Входные параметры
- Инициализация, деинициализация и главный цикл
- Торговая логика: сигнал и открытие/закрытие
- Авто-лот и контроль рисков
- Докупки, мартингейл и пересиживание убытков
- Интерфейс и тестирование
- Дополнительный обзор
- Заключение
- Приложенные файлы
- Ссылки
Введение
После написания значительного объема кода на языке MQL5 приходит понимание, что многие алгоритмы, подходы и локальные решения очень похожи, если не сказать идентичны. Так зачем все время писать одно и то же только разными способами, если все это можно стандартизировать в нечто общее и таким образом экономить время на написании новых торговых стратегий? Вопрос, на самом деле, риторический, но не для всех.
Задав себе этот вопрос и проведя работу по обобщению основных алгоритмов, которые мы используем в наших решениях, можно прийти к шаблонному подходу и экономить максимум времени на написании торговых роботов. Это можно осуществить путем создания линейки шаблонов, которые применимы в достаточно широком спектре решений. Комбинируя такие шаблоны, можно добиваться не только ускорения и стандартизации своих решений, но и выработки привычки создавать все новые и новые подобные шаблоны, покрывая большее пространство решений.
Я пришёл к такой парадигме после того, как многократно повторял одни и те же действия в разных советниках: загрузка истории, проверка нового бара, открытие и закрытие позиций, учёт комиссии и свопа. Очевидно, что имеет смысл один раз оформить это в шаблон, а потом лишь подставлять в него разную логику входа и выхода. Простой статический шаблон — это как экзоскелет для вашего торгового сигнала: вы отвечаете за "когда входить" и "когда выходить", а шаблон берёт на себя всё остальное. Конечно, подобные шаблоны не являются панацеей от всех проблем, но всё же способны решить значительную часть типовых задач трейдеров начального и среднего уровня. На базе этого же подхода строятся более сложные шаблоны — мультипериодные, диверсифицированные и другие по мере развития идеи; о них пойдёт речь в следующих статьях серии. Сначала — прочный фундамент.
Зачем нужен простой шаблон
Начинающие часто пишут советника «в лоб»: в OnTick проверяют условия, открывают позицию, потом где-то ещё проверяют закрытие. Получается монолит, в котором перемешаны стратегия, управление рисками и технические детали платформы. Сложно тестировать, сложно менять логику, легко допустить ошибку в расчёте лота или в учёте свопа.
Шаблонный подход решает это иначе. Есть каркас: один раз написанный, протестированный и пригодный для повторного использования. В каркасе чётко выделено место под стратегию — как правило, один или несколько методов класса робота. Всё остальное — загрузка данных, обновление виртуального графика, проверка спреда, расчёт лота, открытие и закрытие позиций, интерфейс — реализовано в шаблоне и не требует вашего вмешательства при смене стратегии.
Что вы получите с этим шаблоном:
- готового робота на один инструмент и период,
- автоматический расчёт лота от депозита или фиксированный лот,
- режим усреднения или мартингейл,
- режим "пересиживания убытков" (удержание убыточных позиций заданное время),
- настраиваемые стоп-лосс и тейк-профит,
- расширяемую обвязку для написания собственных стратегий.
Таким образом, использование этого каркаса позволяет трейдеру сфокусироваться исключительно на торговых правилах, не отвлекаясь на техническую реализацию контроля рисков и исполнения ордеров. Вы подставляете в метод расчёта сигнала свою логику (в примере — контртренд по цепочке баров) и при необходимости настраиваете параметры.
Архитектура простого шаблона
Шаблон построен вокруг трёх основных сущностей:
- виртуальный график — хранит исторические данные (OHLC и время) текущего символа и таймфрейма, обновляется при каждом тике;
- экземпляр робота — хранит торговые параметры, состояние (есть ли открытые позиции, какой сигнал) и объекты для работы с позициями и сделками;
- глобальные функции инициализации и цикла — связывают всё воедино.
Такая модульная структура упрощает отладку и позволяет легко расширять функционал советника в будущем. Вся торговая логика — расчёт сигнала, открытие и закрытие — инкапсулирована в классе робота. При старте эксперта создаётся один объект графика и один объект робота; при остановке — они удаляются. В реальном времени основной цикл запускается по таймеру (раз в секунду); в тестере — по тику. Это сделано для стабильной работы на реале, где тики могут приходить редко.
Один цикл Simulated():
- обновление виртуального графика (новый бар, копирование котировок),
- расчёт и исполнение торговой логики робота,
- обновление графического интерфейса.
Четкое разделение ответственности между этими этапами гарантирует, что торговый сигнал всегда будет опираться на актуальную информацию о состоянии счета и рынка. Ниже на схеме показан поток от инициализации до одного «тика» симуляции. Прямоугольники — этапы; стрелки — последовательность. После OnInit() создаётся таймер (или в тестере он не создаётся); при первом срабатывании таймера или тика вызывается Init(), который создаёт график, робота и интерфейс. При каждом последующем срабатывании вызывается Simulated(): обновление графика, расчёт и сделки робота, обновление статуса на графике.

Рис. 1. Поток инициализации и основной цикл
теперьосмотрим, как это реализовано в коде. Инициализация эксперта — функция "OnInit()". В ней определяется, работаем ли мы в тестере; в реале создаётся таймер с интервалом 1 секунда (до пяти попыток). В тестере таймер не создаётся, и логика будет вызываться из "OnTick()". Так мы добиваемся стабильной работы и на истории, и на реале.
//+------------------------------------------------------------------+ //| Expert initialization function | //| Sets up timer for real-time or marks tester mode | //+------------------------------------------------------------------+ int OnInit() { // Determine if we are in the strategy tester bTester=MQLInfoInteger(MQL_TESTER); // Try to create a 1-second timer (up to 5 attempts) for (int i = 0; i < 5; i++) { if (!bTester) { bTimerCreated = EventSetTimer(1); // Timer fires every 1 second } else bTimerCreated=false; // No timer needed in tester // If timer creation failed, kill old timer and retry if (!bTimerCreated) { EventKillTimer(); Sleep(100); } else break; // Timer created successfully } return(INIT_SUCCEEDED); }
Флаг "bTester" позже используется в коде, чтобы понимать, в каком режиме мы работаем. В реальном времени основной цикл вызывается по таймеру — это гарантирует стабильную работу даже при редких тиках. В тестере таймер не создаётся: вся логика запускается из "OnTick()".
Посмотрим, как устроены остальные обработчики событий MQL5: деинициализация, таймер и тик. Обратите внимание на общий паттерн: при первом вызове (флаги "bFirstTimer / bFirstTick") запускается "Init()", при последующих — "Simulated()". Это обеспечивает единую точку входа независимо от того, работаем мы в тестере или в реальном времени.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { bTimerCreated=false; // Reset timer flag bFirstTimer = false; // Reset first-timer flag bFirstTick = false; // Reset first-tick flag DeInit(); // Delete chart, bot and interface EventKillTimer(); // Stop the timer } //+------------------------------------------------------------------+ //| Timer event handler — fires every 1 second in real-time | //+------------------------------------------------------------------+ void OnTimer() { if (bTimerCreated) { if (!bFirstTimer) { Init(); // First call: create chart, bot, interface bFirstTimer=true; } else Simulated(); // Subsequent calls: main trading loop } } //+------------------------------------------------------------------+ //| Tick event handler — used in tester instead of timer | //+------------------------------------------------------------------+ void OnTick() { if (!bTimerCreated) // Only runs if timer was not created (tester) { if (!bFirstTick) { Init(); // First tick: create chart, bot, interface bFirstTick=true; } else Simulated(); // Subsequent ticks: main trading loop } }
Структура очевидна: OnTimer и OnTick — зеркальные обработчики. Первый вызов — Init(), все последующие — Simulated(). Деинициализация корректно освобождает все ресурсы. Таким образом, вся логика живёт в Simulated(), а Init() выполняется ровно один раз после старта. Сама Init() выполняет последовательность, а DeInit() удаляет интерфейс и освобождает память (delete ChartO, delete BotO).
Метод Simulated():
Simulated() в одну итерацию вызывает сразу несколько последовательных операций. Суть проста. Сначала обновляем состояние графика к которому цепляется виртуальный робот, и только после этого можно приступать к обработке процессов внутри виртуального робота, убедившись что у нас есть самые свежие данные с графика.- ChartTick() — обновление виртуального графика
- BotTick() — расчёт сигнала и исполнение сделок робота
- UpdateStatus() — обновление подписей на графике
Этот компактный набор функций обеспечивает выполнение всех необходимых действий робота внутри каждого расчетного периода. Здесь мы можем увидеть как это происходит в методе Simulated():
//+------------------------------------------------------------------+ //| Main simulation loop — called on every timer/tick event | //| Coordinates: chart data -> bot logic -> interface update | //+------------------------------------------------------------------+ void Simulated() { ChartO.ChartTick(); // Update virtual chart with latest OHLC data BotTick(); // Execute bot trading logic (signals, open/close) UpdateStatus(); // Refresh interface labels on the chart }
Хочу дополнительно уточнить, почему метод ChartTick() является частью экземпляра ChartO, а BotTick() является глобальным методом. Все дело в том, что BotTick() управляет гораздо более сложным, хотя и сходным процессом по аналогии с ChartO. В этом методе нам нужно не просто делать одно и то же действие каждый тик, а еще и оптимизировать этот процесс так, чтобы не делать лишних вычислений. При сложных стратегиях это будет иметь критическое значение для скорости работы робота. Данный метод мы рассмотрим позже.
Входные параметры
Все настройки шаблона вынесены во входные параметры, сгруппированные по смыслу. Так и вам проще ориентироваться, и при оптимизации в тестере можно перебирать нужную группу. Ниже — компактная таблица категорий и ключевых параметров.
| Категория | Параметр | Описание |
|---|---|---|
| Volumes | bInitLotControl | Включить автоматический расчёт лота от депозита |
| LotE | Фиксированный лот (если авто-лот выключен) | |
| DepositE | Депозит для расчёта авто-лота | |
| Waiting Out Losses | bInitSitE | Режим «линеаризации убытков» — удержание позиции заданное время |
| MinutesHoldE | Время удержания в минутах | |
| Martingale | bInitMartinE | Включить мартингейл |
| MaxMartinLotMultiplierStepsE | Максимум шагов множителя лота | |
| Repurchase | bInitRepurchaseE | Включить докупки |
| MinPercenPriceStepE | Первый шаг цены для докупки (%) | |
| NextStepMultiplierE | Множитель шага докупки | |
| Other | MagicE | Магический номер |
| SLE, TPE | Стоп-лосс и тейк-профит в "_Point" (0 — выключены) | |
| ComissionPerLotE | Комиссия за лот | |
| SpreadEE | Максимальный спред в пунктах | |
| CommentI | Комментарий к ордерам | |
| TBE, TSE | Разрешение покупок и продаж | |
| Strategy Variables | BarsChainForOpenE, BarsChainForCloseE | Количество баров подряд для открытия и закрытия (пример стратегии) |
В коде параметры объявлены через input group и input/sinput. Группы задают блоки в окне свойств эксперта; sinput — параметр, который отключает возможность оптимизации в оптимизаторе тестера стратегий (например, magic или комментарий). Ниже — полный блок входных параметров, таким, каким его вы увидите в коде:
//+------------------------------------------------------------------+ //| Robot input parameters | //+------------------------------------------------------------------+ #define EANICK "Simple Template" // robot name in GUI #define GLOBALVARPREFIX "HelpVar" // prefix for global variables #define EABARS 990 // max bars for internal virtual chart //+----------------- group "Volumes" -----------------------+ input group "Volumes"; sinput bool bInitLotControl=false; // automatic lot calculation sinput double LotE=0.01; // fixed lot size sinput double DepositE=1000.0; // deposit for lot calculation //+----------------- group "Waiting Out Losses Mode" -----------------------+ input group "Waiting Out Losses Mode"; input bool bInitSitE=false; // linearize losses (hold position for set time) input int MinutesHoldE=144000; // hold time in minutes //+----------------- group "Martingale" -----------------------+ input group "Martingale"; input bool bInitMartinE=false; // enable martingale input int MaxMartinLotMultiplierStepsE=5; // max martingale steps //+----------------- group "Repurchase" -----------------------+ input group "Repurchase"; input bool bInitRepurchaseE=false; // enable repurchase (averaging) input double MinPercenPriceStepE=0.25; // first price step for repurchase (%) input double NextStepMultiplierE=1.5; // repurchase step multiplier //+----------------- group "Other" -----------------------+ input group "Other"; sinput int MagicE = 15464; // magic number input int SLE=0; // stop loss in points input int TPE=0; // take profit in points input double ComissionPerLotE=0.0; // commission per lot input const int SpreadEE=5000; // max spread in points sinput string CommentI="Simple Template"; // order comment //+----------------- group "Fighting The Obsolescence Of Settings" -----------------------+ input group "Fighting The Obsolescence Of Settings"; sinput uint DaysToFuture=365; // days into future for opening positions //+----------------- group "Direction Variables" -----------------------+ input group "Direction Variables"; input bool TBE=true; // allow BUY input bool TSE=true; // allow SELL //+----------------- group "Strategy Variables" -----------------------+ input group "Strategy Variables"; input int BarsChainForOpenE = 5; // bars in a row for open input int BarsChainForCloseE = 5; // bars in a row for close
Параметр DaysToFuture ограничивает, на сколько дней вперёд от текущей даты разрешено открывать новые позиции — защита от «устаревания» настроек при долгой работе советника. При этом старым позициям разрешено закрываться. По умолчанию этот параметр выставлен в "365" дней, и отсчитывается от переменной среды __DATETIME__, которая автоматически подставляется с компилированный исходник и содержит дату на момент компиляции (взято для простоты, а вы можете это изменить).
Инициализация, деинициализация и главный цикл
Раздел инициализации и цикла мы уже частично затронули выше, но это не совсем та инициализация, о которой пойдёт речь здесь. Сами по себе обработчики, в особенности OnInit(), очень чувствительны к ёмким вычислениям и, честно говоря, не предназначались для этого. В них можно производить некую предварительную обработку, очистку или что-то подобное. Именно поэтому лучше реализовать полноценные методы Init() и DeInit(), которые будут нивелировать недостатки базовых обработчиков.
Последовательность Init():
Важно не путать этот метод с OnInit(). Последний является обязательным обработчиком, который присутствует в любом роботе или индикаторе. Не стоит там производить ёмкие вычисления, которые занимают более 2 секунд, потому что это может привести к ошибке инициализации. Чтобы избежать подобной ошибки, был создан похожий метод, который выполняется один раз при первом тике или таймере и не подвержен подобным ограничениям.
- Удалить старый интерфейс DeleteSimpleInterface
- создать объект графика CreateChart
- создать объект робота CreateInstance
- построить интерфейс CreateSimpleInterface
- обновить статус UpdateStatus
Здесь мы видим некий порядок, который гарантирует корректный перезапуск робота даже в случае некорректного завершения работы в предыдущем сеансе. Соблюдение этого порядка критично для корректного связывания объектов между собой и предотвращения ошибок доступа к памяти. DeInit() намного проще: он лишь освобождает динамическую память, удаляя Chart и BotInstance, а также очищает график от графических объектов нашего робота.
//+------------------------------------------------------------------+ //| System initialization — creates all objects | //| Called once on first timer/tick event | //+------------------------------------------------------------------+ void Init() { DeleteSimpleInterface(); // Remove old interface objects if any CreateChart(); // Create virtual chart with OHLC arrays CreateInstance(); // Create single bot instance CreateSimpleInterface(); // Build info panel on the chart UpdateStatus(); // Populate panel with initial values } //+------------------------------------------------------------------+ //| System deinitialization — frees all resources | //+------------------------------------------------------------------+ void DeInit() { DeleteSimpleInterface(); // Remove all interface objects from chart delete ChartO; // Free chart object memory delete BotO; // Free bot instance memory }
Инициализация заведомо облегчена, так как мне показалось что лучше разделить логику подготовки внутреннего состояния объектов от логики динамического сопровождения этого же состояния, поэтому мы лишь создаём новые объекты и заполняем их наиболее необходимые поля, назначаем размеры массивов и так далее. Это необходимый минимум, своего рода предварительные шаги. В таком простом шаблоне это может быть не столь критично, но когда мы столкнёмся с более сложными шаблонами в следующих статьях, это станет важным. Все недостающие данные, такие как бары и тики, будут подгружены позже, после завершения инициализации и начала работы BotTick() но уже в рамках отдельного процесса.
В данной связи не лишним будет упомянуть, функцию дальнейшего динамического сопровождения — BotTick(). Она вызывается из функции Simulated() на каждом тике и решает, нужно ли запускать торговую логику, или вычислить недостающие значения либо оба варианта. У объектов класса Chart, есть похожая функция
Порядок проверок в BotTick():
Фактически данный метод предлагает решение для двух ситуаций. Первая ситуация возникает, если мы зафиксировали рождение нового бара не более чем за 20 секунд до его фактического открытия. Эта ситуация, как правило, встречается наиболее часто. Так происходит, если робот достаточно долго работает на графике, но возможны и внештатные ситуации. Например, мы запустили робота только что, но сейчас находимся в середине бара. Второй сценарий как раз призван обработать такие случаи:
- готовность данных графика и торговая сессия,
- определение нового бара (START — в первые 20 секунд, LATE — после),
- запуск логики немедленно START или с задержкой 20 секунд LATE.
Эта защитная логика позволяет роботу корректно обрабатывать условия «тишины» и высокой волатильности на рынке. Это критически важно: на реале тики приходят нерегулярно, и без такого механизма робот мог бы пропустить момент открытия нового бара.
Механизм START/LATE решает типичную проблему реальной торговли: тики могут приходить с задержкой. Например, на низколиквидных инструментах или в периоды низкой активности тик может прийти через минуту после открытия нового бара. Без LATE-режима робот выполнил бы логику один раз при обнаружении нового бара и больше не возвращался бы к нему. С LATE-режимом каждые 20 секунд идёт повторная попытка выполнить логику, пока не появится следующий бар. Это гарантирует, что торговые сигналы не будут пропущены даже при редких тиках.
//+--------------------------------------------------------------+ //| Virtual robot tick processing | //| Main trading logic function, called on every tick | //| Detects new bars and triggers trading algorithms | //+--------------------------------------------------------------+ void BotTick() { // Check data readiness and trading session if ( ChartO.lastcopied >= Chart::TCN+1 && ChartO.ChartPoint > 0.0 && BotO.bInTradeSession() ) { BotO.bNewBarV=BotO.bNewBar(); // Determine if a new bar has appeared } else BotO.bNewBarV=false; // If data is not ready, there is no new bar datetime tc=TimeCurrent(); // Current server time to check conditions for trading logic execution // Check conditions for trading logic execution // Either a new bar (START mode), or 20 seconds have passed (LATE mode) if ( (BotO.NewBarType == "START" && BotO.bNewBarV) || (BotO.NewBarType == "LATE" && tc - BotO.PreviousLateTick >= 20)) { BotO.PreviousLateTick=tc; // Update last tick time // Recalculate trading parameters only if time has changed if (BotO.PreviousTimeCalc != ChartO.TimeI[1]) { BotO.CalculateForTrade(); // Calculate trading signals and parameters BotO.PreviousTimeCalc=ChartO.TimeI[1]; // Store calculation time } BotO.bOpened=BotO.Opened(); // check for open positions if (!BotO.bOpened) BotO.InstanceTick(true); // first processing for closing positions BotO.InstanceTick(false); // second processing for opening positions } }
Вызов CalculateForTrade() — это единственное место, куда вы вставляете свою стратегию. Всё остальное в BotTick() — это инфраструктура шаблона: проверка сессии, определение нового бара, управление порядком открытия/закрытия. У класса Chart также есть подобный метод сопровождения динамического состояния, называемый ChartTick(), и читателю предлагается ознакомиться с ним самостоятельно.
Торговая логика: сигнал и открытие/закрытие
Вся ваша торговая стратегия в данном шаблоне сводится к управлению двумя ключевыми переменными, которые определяют действия робота на каждом новом баре. Первая отвечает за открытие новых позиций, а вторая — за их закрытие. Любое торговое действие в текущий момент можно разделить на закрытие позиций, открытие новых, а также простое ожидание, если у нас нет ни одного четкого сигнала. Такой подход я и реализовал. При правильном подходе к трейдингу больше и не надо. Все остальное — это надстройка над основной логикой, такая как, например, мартингейл или что-то поинтереснее.
- TradeDirection — сигнал на открытие: 1 (покупка), 2 (продажа), 0 (нет сигнала)
- TradeDirectionClose — сигнал на закрытие: 1 (закрыть продажи), 2 (закрыть покупки), 0 (не закрывать)
Эти значения вычисляются в методе CalculateForTrade(), а функции Trade() и TradeClose() автоматически переводят их в реальные торговые операции. В качестве примера в шаблоне реализована простейшая контртрендовая логика, написанная с учетом нашей парадигмы работы с шаблоном. Данный пример мы рассмотрим позже.
На схеме ниже показано, как от BotTick() мы приходим к расчёту сигнала и к открытию или закрытию позиций. После проверки нового бара вызывается CalculateForTrade(); результат попадает в TradeDirection и TradeDirectionClose; при наличии открытых позиций сначала выполняется закрытие, иначе — открытие новых.

Рис. 2. От BotTick к закрытию или открытию
Данная логика является примером и, конечно же, не обязательна, но лично мне показалось, что такая структура достаточно удобна. Тем не менее вы вольны всё переписать под себя. теперьосмотрим на полную реализацию "CalculateForTrade()".
Логика CalculateForTrade():
Данная логика основана на простой череде повторяющихся бычьих или медвежьих баров. Если, например, мы имеем несколько бычьих баров, то мы продаем (short), а для медвежьих баров все зеркально (long). При этом количество одинаковых баров подряд можно выбирать разным, эти настройки вынесены в inputs. Думаю, такого примера будет более чем достаточно для статьи.
- Определить направление последнего закрытого бара "1" (бычий или медвежий);
- в цикле проверить, что предыдущие бары идут в том же направлении;
- если набралось BarsChainForOpen баров подряд — сигнал на открытие в противоположную сторону;
- аналогично для BarsChainForClose — сигнал на закрытие;
- логика полностью симметрична для бычьей и медвежьей цепочки.
Давайте теперь посмотрим, как пример нашей стратегии будет выглядеть в коде:
//+------------------------------------------------------------------+ //| Calculate trade signals — YOUR STRATEGY GOES HERE | //| Sets TradeDirection (open) and TradeDirectionClose (close) | //| Example: counter-trend strategy based on consecutive bars | //+------------------------------------------------------------------+ void CalculateForTrade() { // Determine direction of the last closed bar [1] int firstTradeDirection = 0; if ( ChartO.OpenI[1] > ChartO.CloseI[1] ) firstTradeDirection = 1; // Bearish bar if ( ChartO.CloseI[1] > ChartO.OpenI[1] ) firstTradeDirection = 2; // Bullish bar bool Aborted=false; // Flag: chain broken by opposite bar if (firstTradeDirection == 1) // --- Bearish chain detected --- { // --- start of important block --- // Check that all previous bars are also bearish (for open signal) for ( int i=2; i<=BarsChainForOpen; i++ ) { if (ChartO.OpenI[i] <= ChartO.CloseI[i]) { Aborted=true; break; } } if ( !Aborted ) TradeDirection = 1; // Open LONG (counter-trend to bearish chain) else TradeDirection = 0; Aborted=false; // Check chain for close signal for ( int i=2; i<=BarsChainForClose; i++ ) { if (ChartO.OpenI[i] <= ChartO.CloseI[i]) { Aborted=true; break; } } if ( !Aborted ) TradeDirectionClose = 1; // Close SHORT positions else TradeDirectionClose = 0; // --- end of important block --- } else if (firstTradeDirection == 2) // --- Bullish chain detected --- { // --- start of important block --- for ( int i=2; i<=BarsChainForOpen; i++ ) { if (ChartO.CloseI[i] <= ChartO.OpenI[i]) { Aborted=true; break; } } if ( !Aborted ) TradeDirection = 2; // Open SHORT (counter-trend to bullish chain) else TradeDirection = 0; Aborted=false; for ( int i=2; i<=BarsChainForClose; i++ ) { if (ChartO.CloseI[i] <= ChartO.OpenI[i]) { Aborted=true; break; } } if ( !Aborted ) TradeDirectionClose = 2; // Close LONG positions else TradeDirectionClose = 0; // --- end of important block --- } else { TradeDirection = 0; // No signal — doji or insufficient data TradeDirectionClose = 0; } }
Это место вашей стратегии. Вы можете заменить тело CalculateForTrade() на любую логику: индикаторы, уровни, паттерны, машинное обучение — интерфейс с шаблоном остаётся тем же (TradeDirection и TradeDirectionClose). Методы Trade() и TradeClose() просто переводят эти сигналы в вызовы торговых функций.
Почему именно такой интерфейс? Дело в том, что шаблон должен быть универсальным — работать с любой стратегией, от простых паттернов до сложных алгоритмов. Если бы мы жёстко зашили логику входа в шаблон, пришлось бы переписывать весь код для каждой новой стратегии. Вместо этого мы выделили два числа: направление открытия и направление закрытия. Всё остальное — расчёт лота, проверка спреда, отправка ордеров, управление рисками — остаётся неизменным.
На практике это означает, что вы можете взять любую готовую стратегию с форума или из книги, переписать её логику в CalculateForTrade() — и сразу получить полноценного робота с авто-лотом, докупками и интерфейсом. Не нужно разбираться в тонкостях работы с позициями, не нужно писать код для расчёта лота от депозита, не нужно думать о том, как правильно закрывать позиции при докупках. Всё это уже есть в шаблоне. Методы Trade() и TradeClose() — это простые переводчики сигналов в действия.
Соответствие сигналов и действий:
Как уже упоминалось ранее, вся ваша стратегия в шаблоне сводится к заполнению двух целочисленных переменных: TradeDirection (1 — покупка, 2 — продажа, 0 — нет сигнала на открытие) и TradeDirectionClose (1 — закрыть продажи, 2 — закрыть покупки, 0 — не закрывать). В коде это выглядит следующим образом:
- TradeDirectionClose = 1 → CloseSellF() (закрыть все SELL)
- TradeDirectionClose = 2 → CloseBuyF() (закрыть все BUY)
- TradeDirection = 1 → BuyF()
- TradeDirection = 2 → SellF()
Такое распределение ролей позволяет четко разделить аналитическую часть советника от исполнительной обвязки. Также обратите внимание на проверку DaysToFuture в Trade() — это защита от торговли по устаревшим настройкам после долгой работы советника:
//+------------------------------------------------------------------+ //| Execute close logic based on TradeDirectionClose signal | //+------------------------------------------------------------------+ void TradeClose() { if (TradeDirectionClose==1) CloseSellF(); // Signal 1 = close SELL positions else if (TradeDirectionClose==2) CloseBuyF(); // Signal 2 = close BUY positions } //+------------------------------------------------------------------+ //| Execute open logic based on TradeDirection signal | //| DaysToFuture limits how long after optimization we can trade | //+------------------------------------------------------------------+ void Trade() { if (TradeDirection==1 && Days() <= DaysToFuture) BuyF(); // Signal 1 = open BUY else if (TradeDirection==2 && Days() <= DaysToFuture) SellF(); // Signal 2 = open SELL }
Условие Days() <= DaysToFuture в Trade() ограничивает открытие позиций по времени. Это защита от "устаревания" настроек: если после оптимизации прошло больше DaysToFuture дней, новые позиции не открываются. На практике это предотвращает торговлю по параметрам, которые давно потеряли актуальность.
Зачем это нужно? Представьте ситуацию: вы оптимизировали стратегию на исторических данных за прошлый год, получили отличные параметры и запустили советника на реальном счёте. Прошло полгода, рынок изменился, стратегия перестала работать, но советник продолжает открывать позиции по старым параметрам. DaysToFuture решает эту проблему: через заданное количество дней торговля автоматически останавливается, и вы вынуждены пересмотреть настройки или переоптимизировать стратегию.
Функция Days() считает количество дней с момента компиляции советника. Это важно понимать: если вы перекомпилируете советника, отсчёт начинается заново. Поэтому DaysToFuture имеет смысл устанавливать с запасом — например, "365" дней, чтобы не пришлось перекомпилировать каждый месяц. Но если вы хотите ограничить торговлю более коротким периодом (например, три месяца), установите "90" дней.
Авто-лот и контроль рисков
Шаблон поддерживает фиксированный лот и режим автоматического расчёта лота от депозита. В последнем случае используется не текущий баланс, а максимальный баланс за анализируемый период — так мы привязываем размер позиции к «вершине» счёта и ограничиваем просадку. Максимальный баланс пересчитывается в функции UpdateMaxBalance() перед каждым открытием позиции.

Рис. 3. Обход истории сделок и обновление максимального баланса
Посмотрим на полную реализацию UpdateMaxBalance().
Алгоритм UpdateMaxBalance():
Иногда вы можете столкнуться с ситуациями, например когда вы сняли какую-то сумму с вашего баланса счета, или произошла достаточно болезненная фиксация убытков позиций, которые висели очень долго. В этом случае ваш баланс изменится, а значит и автоматически рассчитываемый лот для новых позиций будет уменьшен. Чтобы не бояться этого, мы должны восстановить старую точку баланса до того, как он понизился, и все манипуляции с расчетами рисков делать относительно данной точки. Такой подход, конечно, повысит ваши риски на достаточно короткий промежуток времени, но зато позволит вам спокойно снимать некоторую разумную прибыль с вашего торгового счета, не корректируя риски. Через какой-то момент текущий баланс восстановится.
- Загрузить историю сделок за HistoryDaysLoadI дней;
- пройти от новых к старым;
- рассматривать только закрывающие сделки (DEAL_ENTRY_OUT) нашего магика;
- для каждой сделки вычитать прибыль из текущего баланса, восстанавливая баланс на тот момент;
- если восстановленный баланс выше LastMaxBalance — обновить;
- защита от аномалий: если превышает текущий более чем на 20% — ограничить 120% от текущего.
В целом данный механизм не обязателен, но у него есть некоторые преимущества, которые проявляются при расчёте автоматического лота. Очень немногие знают об эффекте обратного мартингейла. Данный эффект понижает профит-фактор всех без исключения стратегий. Это происходит потому, что при росте прибыли растёт и лот (условие, обратное мартингейлу), а когда мы терпим убытки, мы понижаем лот, что также является логикой, обратной мартингейлу.
В итоге мы таким образом понижаем просадку, но вместе с тем понижаем и профит-фактор. Этот эффект не будет заметен в переобученных торговых системах, но в тех системах, где реалистичные риски и торговые показатели, вы легко его увидите. Тем не менее данный эффект можно компенсировать, если брать последний максимальный баланс, который у нас был. Восстановим его с помощью торговой истории:
//+------------------------------------------------------------------+ //| Update maximum balance from deal history | //| Walks closed deals from newest to oldest, reconstructing balance | //| at each point to find the historical maximum | //+------------------------------------------------------------------+ void UpdateMaxBalance() { double TempSummDelta=0.0; // Cumulative profit delta for balance reconstruction datetime LastTimeBorderTemp=LastTimeBorder; // Temp border for already-processed deals // Load deal history for the configured period HistorySelect(TimeCurrent()-HistoryDaysLoadI*86400,TimeCurrent()); // Walk deals from newest to oldest for ( int i=HistoryDealsTotal()-1; i>=0; i-- ) { ulong ticket=HistoryDealGetTicket(i); // Only consider closing deals of our magic number if ( HistoryDealGetInteger(ticket,DEAL_ENTRY) == DEAL_ENTRY_OUT && bOurMagic(HistoryDealGetInteger(ticket,DEAL_MAGIC)) ) { datetime DealTime = datetime(HistoryDealGetInteger(ticket,DEAL_TIME)); if (DealTime >= LastTimeChecked && DealTime >= LastTimeBorder ) { // Subtract deal profit to reconstruct balance at that moment TempSummDelta-=HistoryDealGetDouble(ticket,DEAL_PROFIT); double AB = AccountInfoDouble(ACCOUNT_BALANCE); double ConstructedBalance = TempSummDelta + AB; // --- start of important block --- // Update maximum if reconstructed balance is higher if (ConstructedBalance > LastMaxBalance) LastMaxBalance=ConstructedBalance; // --- end of important block --- // Safety: cap at 120% of current balance to prevent anomalies if (AB > 0.0 && ConstructedBalance > AB && (ConstructedBalance - AB) / AB > 0.2 ) { LastMaxBalance = AB * 1.2; break; } if (DealTime > LastTimeBorderTemp) LastTimeBorderTemp=DealTime; } else break; // Reached already-processed deals } } // If no max balance found yet, use current balance as starting point if (LastMaxBalance == 0) LastMaxBalance=AccountInfoDouble(ACCOUNT_BALANCE); LastTimeBorder=LastTimeBorderTemp; }
Этот механизм даёт важное преимущество: размер позиции привязан к пику счёта, а не к текущему балансу. В просадке лот не уменьшается (он рассчитывается от максимума), что позволяет компенсировать убытки при восстановлении. Защита от аномалий (20%) предотвращает ситуацию, когда из-за ошибки в истории лот взлетает до нереальных значений.
Почему именно максимальный баланс, а не текущий? Представьте ситуацию: у вас был баланс $10,000, вы открыли позицию с лотом, рассчитанным от этого баланса. Позиция ушла в убыток, баланс упал до $8,000. Если бы лот рассчитывался от текущего баланса, следующая позиция была бы на 20% меньше — и восстановление баланса заняло бы больше времени. С максимальным балансом лот остаётся прежним, и при возврате цены баланс восстанавливается быстрее.
Защита от аномалий (ограничение на 120% от текущего баланса) нужна на случай ошибок в истории сделок или некорректных данных от брокера. Без этой защиты теоретически возможна ситуация, когда восстановленный баланс оказывается в разы больше текущего — и лот взлетает до нереальных значений. Проверка на 20% отклонение предотвращает такие случаи.
Цепочка до открытия позиции:
Сначала поступает общий сигнал на открытие или закрытие позиции по нашей стратегии. Это сигнал, который говорит только о направлении движения цены с повышенной вероятностью по мнению нашего алгоритма. После этого мы должны рассчитать объём входа исходя из наших рисков, а затем проверить, достаточно ли у нас средств на счёте для совершения операции, а также соблюсти ограничения по спреду.
- Проверка спреда,
- решение о фиксированном или авто-лоте (при авто — UpdateMaxBalance() и расчёт),
- опционально мартингейл,
- корректировка лота (нормализация),
- проверка средств,
- отправка ордера.
На рисунке ниже можно увидеть аналогию данного процесса в виде простенькой цепи:

Рис. 4. Цепочка от запроса на открытие до Buy/Sell
Также привожу участок кода, который выполняет данные подготовительные манипуляции. Это вырезка из соответствующего метода, который впоследствии отправляет уже готовые торговые запросы на открытие новых ордеров на сервер. Запросы выполняются только после успешного прохождения всей предшествующей цепочки:
//+------------------------------------------------------------------+ //| Lot calculation inside CheckForOpen (after spread check) | //+------------------------------------------------------------------+ if ( bLotControl0 == false ) { LotTemp=Lot0; // Use fixed lot size from input } else if ( bLotControl0 == true ) { UpdateMaxBalance(); // Refresh max balance from deal history LotTemp=Lot0*(LastMaxBalance/DepositE); // Lot proportional to max balance / deposit } if (bInitMartinE) { LotTemp=CalcMartinLot(LotTemp); // Apply martingale lot amplification } LotAntiError=GetLotAniError(LotTemp); // Normalize lot to symbol step // Send order only if lot is valid and margin is sufficient if ( OrdType == OP_SELL && LotAntiError > 0.0 && CheckMoneyForTrade(ChartO.CurrentSymbol,LotAntiError,ORDER_TYPE_SELL) ) { bool rez = m_trade.Sell(LotAntiError, ...); }
Это лишь пример того, как применять такие объединённые цепочки для предварительной подготовки к торговле. Их реализацию не обязательно повторять за мной. Главное — понимать логику, цель и порядок этих действий. Сначала проверяется спред или иные издержки. Если спред превышен, то не имеет смысла идти дальше по цепочке. Если спред в норме, нужно рассчитать предварительный объём входа.
Здесь стоит обратить внимание на то, что при различных вариантах расчёта может получиться, что данный лот больше максимального или меньше минимального. У каждого инструмента это окно ограничено. Кроме того, может потребоваться простейшая нормализация к шагу лота для данного инструмента.
Шаг лота мы в любом случае нормализуем, а вот если лот не попадает в допустимое окно, вам стоит принять решение: открывать позицию большим или меньшим объёмом из допустимого диапазона. Я всегда открываю — что дают, то и берём.
Докупки, мартингейл и пересиживание убытков
В шаблоне опционально реализованы три механизма: "докупка", "мартингейл" и режим "пересиживания убытков" (линеаризация — моё вольное название алгоритма удержания убыточных позиций заданное время без закрытия по стопу). Начнём, пожалуй, с алгоритма докупки. Реализация данной логики доверена предикату AdditionalOpenPrice(double CurrentPrice, bool bDirection). Предикат сигнализирует, можно ли открыть дополнительную позицию в том же направлении.
Логика AddtionalOpenPrice:
Данный предикат сигнализирует о том, что цена откатилась достаточно далеко от предыдущей докупки/допродажи в соответствии с настройками шага цены. Шаг может быть как фиксированным (в процентах от цены последней докупки), так и увеличивающимся или уменьшающимся (см. входные переменные шаблона). Проверка осуществляется только, если поступил сигнал на открытие от нашей основной логики, которая сигнализирует о возможности открытия позиции в определённом направлении.
- Для покупок — ищется крайняя (минимальная) цена открытия среди BUY-позиций; для продаж — максимальная среди SELL.
- Дополнительное открытие разрешено, если цена отошла от крайней на шаг (формула ниже).
- В неттинговом режиме счёта при уже открытых позициях дополнительное открытие отключено.
Подобный механизм позволяет более гибко настроить алгоритм докупки с учётом особенностей того или иного торгового инструмента или текущей рыночной ситуации, либо производить более тщательный поиск таких особенностей с помощью оптимизатора в тестере стратегий. Рассмотрим пример такой реализации:
//+------------------------------------------------------------------+ //| Check if additional position can be opened (repurchase system) | //| Finds extreme open price among current positions, then checks | //| if price moved enough for another entry | //+------------------------------------------------------------------+ bool AddtionalOpenPrice(double CurrentPrice, bool bDirection) { double BorderPriceBUY = -1.0; // Lowest open price among BUY positions (-1 = none) double BorderPriceSELL = -1.0; // Highest open price among SELL positions (-1 = none) double PriceSymbolASK = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double PriceSymbolBID = SymbolInfoDouble(_Symbol,SYMBOL_BID); int PositionsBuy=0; // Counter of open BUY positions int PositionsSell=0; // Counter of open SELL positions // Scan all positions to find extreme open price for ( int i=0; i<PositionsTotal(); i++ ) { ulong ticket=PositionGetTicket(i); if ( PositionSelectByTicket(ticket) && PositionGetInteger(POSITION_MAGIC) == MagicE && _Symbol == PositionGetString(POSITION_SYMBOL) ) { double PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN); if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { if (BorderPriceBUY == -1.0 || PriceOpen < BorderPriceBUY) BorderPriceBUY = PriceOpen; PositionsBuy++; } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { if (BorderPriceSELL == -1.0 || PriceOpen > BorderPriceSELL) BorderPriceSELL = PriceOpen; PositionsSell++; } } } // Netting mode: no additional opens if any positions exist if (AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_NETTING && (PositionsBuy > 0 || PositionsSell > 0)) return false; // Check if price moved enough from extreme: step = MinPercenPriceStepE * NextStepMultiplier^(N-1) if ( bDirection && (BorderPriceBUY == -1.0 || (BorderPriceBUY > PriceSymbolASK && PositionsBuy > 0 && BorderPriceBUY-PriceSymbolASK >= MathPow(NextStepMultiplierE,double(PositionsBuy-1))*BorderPriceBUY*MinPercenPriceStepE/100.0 )) ) return true; return false; }
Основную логику данного процесса можно, в том числе, изобразить графически для лучшего понимания:

Рис. 5. Проверка шага цены от крайней позиции для разрешения докупки
Формула шага докупки:
- MathPow(NextStepMultiplierE, N-1) * BorderPrice * MinPercenPriceStepE / 100
Математически выверенный шаг между докупками предотвращает лавинообразное накопление маржинальных требований при сильных однонаправленных движениях цены. Если подставить вместо N единицу, то шаг становится фиксированным, и, напротив, если число от 0 до 1, то это постепенное уменьшение шага. Возможна и обратная ситуация, когда N от 1 до бесконечности. В этом случае мы, наоборот, расширяем шаг с каждой новой докупкой.
При арифметической прогрессии:
- первый шаг — 0.25%;
- второй — 0.5%;
- третий — 0.75%.
Хотя этот путь кажется интуитивно понятным, он несет скрытые риски при попадании в зону затяжного флэта. Можно, конечно, настроить и как арифметическую прогрессию, это не запрещено. Наш же вариант более гибок. Например:
С геометрической прогрессией (множитель 1.5):
- 0.25%;
- 0.375%;
- 0.5625%;
- 0.84375%.
Использование экспоненциального расширения шага является более консервативной стратегией, значительно повышающей "живучесть" советника. Докупки происходят реже, что снижает риск перегрузки позициями. Закрытие докупок в плюс реализовано в CloseToProfit().
Алгоритм CloseToProfit():
- первый проход: посчитать суммарную прибыль всех позиций направления (включая свопы и комиссии);
- если сумма положительна — второй проход: собрать тикеты и закрыть все позиции одновременно.
Это важный момент: при докупках мы не закрываем позиции по отдельности, а ждём момента, когда суммарная прибыль всех позиций направления станет положительной. Почему так? Потому что отдельные позиции могут быть в убытке, но средняя цена входа может быть выгодной, и при движении цены в нужную сторону все позиции вместе дают прибыль. Закрытие по отдельности привело бы к тому, что мы закрывали бы прибыльные позиции, оставляя убыточные открытыми — это неэффективно.
//+------------------------------------------------------------------+ //| Close all positions of one direction when total profit is > 0 | //| Accounts for swaps and commissions in profit calculation | //+------------------------------------------------------------------+ void CloseToProfit(bool bDirection) { ulong Tickets[]; // Array of tickets to close int TicketsTotal=0; double SummProfit=0.0; // Total profit including swaps and commissions // First pass: calculate total profit across all positions of this direction for ( int i=0; i<PositionsTotal(); i++ ) { ulong ticket=PositionGetTicket(i); if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicE ) { if ( (bDirection && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) || (!bDirection && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) ) { TicketsTotal++; // Net profit = trade profit + swap - commission SummProfit += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP) - PositionGetDouble(POSITION_VOLUME)*ComissionPerLotE; } } } // Second pass: if total profit is positive, collect and close all tickets if (SummProfit > 0.0) { ArrayResize(Tickets,TicketsTotal); // ... collect tickets, then close them all ... for ( int i=0; i<ArraySize(Tickets); i++ ) { if (Tickets[i] != 0) BotO.m_trade.PositionClose(Tickets[i]); } } }
В нашем шаблоне используются и другие методы группировки позиций благодаря разнообразию встроенных эффектов и возможностей. Режимы можно комбинировать по своему усмотрению. В одной статье невозможно раскрыть всё разнообразие таких вариаций в силу разумных ограничений на объём текста, поэтому читателю предлагается разобраться в этом самостоятельно.
Алгоритм CalcMartinLot():
Данный алгоритм — не что иное, как всем известный "мартингейл". Однако в нашем случае используется "безопасный мартингейл", адаптированный с учётом универсальности шаблона. Здесь нет излишне агрессивного набора объёмов; напротив, применяется достаточно сбалансированный алгоритм, который подразумевает, что первичный сигнал является прибыльным, то есть имеет положительное матожидание. В противном случае, как бы мы ни использовали данный алгоритм, исход всегда будет один. Для реализации такого алгоритма я решил:
- Найти момент последнего максимума накопленной прибыли;
- посчитать средний убыток на лот по последним убыточным сделкам (не более MaxMartinLotMultiplierStepsE);
- нарастить лот так, чтобы компенсировать накопленный убыток;
- вернуть первоначальный объем + дополнительный объём.
Здесь нужно понимать, что у нас есть некоторый объём, которым мы бы хотели открыть новую позицию, если убыточных позиций в серии ещё не было. Если всё так, то мы открываем первую позицию базовым объёмом, а если нет, то просто прибавляем к нему объём всех предыдущих убыточных позиций. Просто, надёжно и без излишеств. Также агрессивный расчёт объёма сделок требует обязательного наличия "предохранителя" в виде максимального количества шагов наращивания. Это даёт усиление лота после серии убытков. Используйте с осторожностью и соблюдением лимитов. Подсчет лотов вынесен в единый метод. Все данные операции производятся здесь:
//+--------------------------------------------------------------+ //| Martingale system lot size calculation | //| Function analyzes deal history and calculates increased | //| lot size to compensate for previous losses | //| Parameters: BasicLot - base lot size | //| Returns: adjusted lot size (double) | //+--------------------------------------------------------------+ double CalcMartinLot(double BasicLot) { //martingale lot calculation //MaxMartinLotMultiplierStepsE bool bFirst=false; // flag for the first deal in the analyzed period bool bSecond=false; // flag for finding max balance in the analyzed period double TempSummProfit=0.0; // cumulative profit sum for maximum balance calculation double LastMaxProfit=0.0; // maximum found profit over the analyzed period datetime FirstDate=0; // date of the first deal in the analyzed period HistorySelect(TimeCurrent()-HistoryDaysLoadI*86400,TimeCurrent()); for ( int i=HistoryDealsTotal()-1; i>=0; i-- )//searching for the last maximum { ulong ticket=HistoryDealGetTicket(i); if ( HistoryDealGetInteger(ticket,DEAL_MAGIC) == MagicE && HistoryDealGetString(ticket,DEAL_SYMBOL) == ChartO.CurrentSymbol && HistoryDealGetInteger(ticket,DEAL_ENTRY) == DEAL_ENTRY_OUT ) { if ( HistoryDealGetInteger(ticket,DEAL_TIME) >= OptimizationBorderTime) { // current deal's profit considering commission and swap (inverted for loss calculation) double TempProfit=-(HistoryDealGetDouble(ticket,DEAL_PROFIT) + HistoryDealGetDouble(ticket,DEAL_COMMISSION) + HistoryDealGetDouble(ticket,DEAL_SWAP)); TempSummProfit+=TempProfit;// if (!bFirst) { FirstDate=datetime(HistoryDealGetInteger(ticket,DEAL_TIME)+1); bFirst=true; } if (TempSummProfit > LastMaxProfit) { LastMaxProfit = TempSummProfit; LastMaxBalanceTime=datetime(HistoryDealGetInteger(ticket,DEAL_TIME)); bSecond=true; } } else break; } } if (!bSecond) LastMaxBalanceTime=FirstDate; if (LastMaxBalanceTime > OptimizationBorderTime) OptimizationBorderTime=LastMaxBalanceTime; double TempSummLots=0.0; // sum lot of all losing positions to calculate average loss per lot int summs=0; // counter for found losing positions (limited by MaxMartinLotMultiplierStepsE) TempSummProfit=0.0; // cumulative loss sum to calculate average loss per lot double MiddleProfit=0.0; // average loss per one lot to calculate martingale multiplier // Now it remains to calculate the sum of lots from negative positions for ( int i=HistoryDealsTotal()-1; i>=0; i-- )//calculate average loss size over all trades per 1 lot { ulong ticket=HistoryDealGetTicket(i); if ( HistoryDealGetInteger(ticket,DEAL_MAGIC) == MagicE && HistoryDealGetString(ticket,DEAL_SYMBOL) == ChartO.CurrentSymbol && HistoryDealGetInteger(ticket,DEAL_ENTRY) == DEAL_ENTRY_OUT ) { if (HistoryDealGetInteger(ticket,DEAL_TIME) < LastMaxBalanceTime) break;//interruption if we've gone into an already calculated part of the history if (summs >= MaxMartinLotMultiplierStepsE) break;//interruption if we exceed the allowed number of losses // current deal profit with commission and swap double TempProfit=HistoryDealGetDouble(ticket,DEAL_PROFIT) + HistoryDealGetDouble(ticket,DEAL_COMMISSION) + HistoryDealGetDouble(ticket,DEAL_SWAP); if (TempProfit < 0 ) { double TempVolume=HistoryDealGetDouble(ticket,DEAL_VOLUME);//determine current deal volume TempSummProfit+=MathAbs(TempProfit/TempVolume); summs++; } } } // finish calculating average loss per 1 lot, if there was more than 1 position if (summs>1) { MiddleProfit = TempSummProfit/summs; } // summs=0;//reset counter // now using this average loss size we will add weights to each loss lot for ( int i=HistoryDealsTotal()-1; i>=0; i-- )//calculate sum of negative positions lots (considering max quantity limit) { ulong ticket=HistoryDealGetTicket(i); if ( HistoryDealGetInteger(ticket,DEAL_MAGIC) == MagicE && HistoryDealGetString(ticket,DEAL_SYMBOL) == ChartO.CurrentSymbol && HistoryDealGetInteger(ticket,DEAL_ENTRY) == DEAL_ENTRY_OUT ) { if (HistoryDealGetInteger(ticket,DEAL_TIME) < LastMaxBalanceTime) break;//interruption if we've gone into an already calculated part of history if (summs >= MaxMartinLotMultiplierStepsE) break;//interruption if we exceed allowed number of losses // current deal profit with commission and swap double TempProfit=HistoryDealGetDouble(ticket,DEAL_PROFIT) + HistoryDealGetDouble(ticket,DEAL_COMMISSION) + HistoryDealGetDouble(ticket,DEAL_SWAP); if (TempProfit < 0 ) { double TempVolume=HistoryDealGetDouble(ticket,DEAL_VOLUME); // current deal volume in lots if (MiddleProfit > 0.0) { double Multiplier = MathAbs(TempProfit/TempVolume)/MiddleProfit; // weight multiplier for current losing deal relative to average loss per lot TempSummLots += TempVolume * Multiplier; summs++; } else { TempSummLots += TempVolume; summs++; } } } } // decision making return BasicLot + TempSummLots; }
Важно также уточнить, что наращивание лота идёт от предыдущего виртуального пика баланса. Это не глобальный пик, а именно последний пик, что позволяет мартингейлу вовремя перезагружать риски, чтобы не приводить к перегрузке депозита. Таким образом, этот пик периодически сбрасывается, чтобы защитить ваш депозит. Убыточная стратегия всё равно сольёт ваш депозит, и у меня нет желания давать вам ложную надежду. Лучше, если слив будет плавным: может, кто-то из вас сможет вовремя опомниться, это тоже не исключено. Чудеса случаются.
Условие закрытия (Linearization-режим):
Параметры bInitSitE и MinutesHoldE включают режим, в котором позиция не закрывается по обычному сигналу, если закрытие не приводит к прибыли. Вместо этого мы даём роботу подержать позицию какое-то время в надежде на будущую прибыль. Если позиция выходит в плюс до истечения времени ожидания, то спокойно закрываем её, а если нет, то ждём до самого конца и сигнала на закрытие после истечения времени. Закрытие делаем по сигналу от нашей стратегии, а не просто в лоб, чтобы иметь возможность даже на этом этапе хоть немного, но минимизировать наши издержки.- Положительная прибыль — закрыть,
- держим позицию уже "MinutesHoldE" минут — закрыть.
Режим временного удержания позиции дает стратегии "право на ошибку", позволяя переждать кратковременный шум против входа. Кроме того данный режим наиболее эффективно комбинировать при торговле исключительно в направлении положительного свопа. Многие вообще не принимают в расчет эти издержки, не говоря уже о том, чтобы думать в направлении того, как сделать так, чтобы время работало на нас, а не вредило показателям нашей стратегии. Для определения точки выхода из режима ожидания сделан простенький метод:
//+--------------------------------------------------------------+ //| Position holding time check function | //| Returns: true if holding time has expired | //+--------------------------------------------------------------+ bool bTimeIsOut(int TimeStart) { if ( double(int(TimeCurrent()) - TimeStart)/60.0 > double(MinutesHoldE) ) return true; else return false; }
Передаем в него "datetime" открытия позиции и считаем, сколько это в минутах. Если время превышает наше окно, то выходим из ожидания. Вы можете найти в поиске, где применяется данный метод. Он лишь часть большого логического выражения. Там лишь два места: это методы-обработчики закрытия long/short позиций, которые были рассмотрены ранее.
Интерфейс и тестирование
Для примера на графике создаётся простой интерфейс с метками ключевых параметров. Интерфейс состоит из простейших графических элементов: текстовой метки, прямоугольника или кнопки. Чем проще, тем надёжнее, да и другим пользователям понятнее. Графика — это отдельный аспект, который при желании можно рассмотреть подробнее. Я считаю, что в процессе обучения работе с графикой мудрить не надо, а, напротив, сосредоточиться на наиболее важных частях логики.
Элементы интерфейса:
- инструменты
- периоды
- магики
- лоты
- текущая нереализованная прибыль
- баланс
- дополнительная и вспомогательная информация
Подобная визуализация статуса работы делает процесс мониторинга торгового счета быстрым и наглядным. Создание и удаление объектов (RectLabelCreate, LabelCreate и аналоги) — в CreateSimpleInterface() и DeleteSimpleInterface(); UpdateStatus() обновляет подписи при каждом тике. Это позволяет визуально контролировать состояние советника без открытия вкладки "Эксперты".

Рис. 6. Скриншот панели советника на графике
Режим тестера:
В тестере стратегий шаблон работает по тикам, а в остальных случаях — по таймеру. Именно поэтому я дополнительно должен уточнить, что такой шаблон стоит тестировать в тестере стратегий в режиме OHLC M1 (в MetaTrader 4 — это Control points). Это обеспечит как идеальное качество тестирования, так и максимальную его скорость. Одной из причин выбора такой схемы для данного шаблона и всех последующих было желание, чтобы роботы, построенные на его основе, тестировались в тестере стратегий и оптимизировались максимально быстро.- Таймер не создаётся;
- Init() вызывается при первом тике;
- Simulated() — при каждом последующем.
Соблюдение этих правил тестирования гарантирует, что полученные на бэктестах результаты будут максимально близки к реальным условиям торговли. Можно, конечно, тестировать в режимах "Каждый тик" или "Реальные тики", чтобы логика нового бара и учёт торговых сессий отрабатывали корректнее. Вы можете попробовать и убедиться, что результаты будут иметь едва заметные различия, а в некоторых случаях их не будет вообще. Всё сделано для скорости работы в тестере или оптимизаторе. Всё для того, чтобы вы не тратили время, тестируя на тиках или с задержками, и не занимались ерундой. Давайте теперь посмотрим на некоторые примеры тестирования шаблона в различных режимах работы, которые рассматривались ранее, используя режим OHLC M1.

Рис. 7. Результаты в тестере стратегий
На рисунке вы увидите четыре режима тестирования:
- голая стратегия (наш простейший пример "контртренда", который описывали выше),
- наша стратегия, усиленная "мартингейлом",
- наша стратегия, усиленная "пересиживанием",
- наша стратегия, усиленная "докупкой".
Хочу вас предупредить, что участок для тестирования выбран исключительно для демонстрации минимального функционала, как и сама стратегия простейшего контртренда, — просто, чтобы продемонстрировать основные возможности нашего шаблона, в частности вышеперечисленных дополнительных режимов торговли. Если честно, я бы сам не назвал это полноценной стратегией. Она выполнена процентов на 5 от силы, но у меня нет цели в данной статье дать вам рыбку, а скорее удочку, чтобы вы учились её ловить.
Дополнительный обзор
В основных разделах мы рассмотрели архитектуру, торговую логику и ключевые механизмы. теперьопнём глубже — разберём внутреннее устройство классов, полную логику открытия и закрытия позиций и вспомогательные функции. Этот раздел для тех, кто хочет не просто использовать шаблон, а понимать каждую строку и модифицировать под себя.
Класс Chart: виртуальный график
Класс Chart — обёртка над историческими данными торгового символа-периода. Если задуматься, как же работает основная часть классических роботов, то, как правило, они работают на том графике, на котором висит робот. Но при этом мы должны понимать, что у данного рабочего графика есть ещё и период. То есть мы всегда используем пару "инструмент-период". Те, кто работал ранее с MQL4, знают, что там было очень удобно получать данные текущего графика с помощью предопределённых массивов. Этот механизм был дополнен и изменён в MQL5, заставляя нас самостоятельно запрашивать данные графиков и контролировать этот процесс.
На первый взгляд всё стало неудобнее. Но кто мешает создать промежуточный класс или метод, который будет контролировать этот процесс, при этом предоставляя нам доступ к тем же самым массивам с данными графиков? Вопрос риторический. Да, придётся немного пошевелить мозгами, и хотя это полезно для всех, результат будет несоизмеримо ценнее. Такие виртуальные объекты графиков можно вести в нескольких экземплярах внутри одного робота, торгуя сразу на нескольких торговых инструментах или периодах одного и того же инструмента. Это превращает график в MetaTrader из центра принятия решений в простую точку запуска торгового кода. Какие инструменты торговать и как, решает сам код, а не график, на котором он висит. Именно это и должно было быть изначально основой при построении архитектуры любого достойного робота.
Данные класса Chart:
- принадлежность к инструменту и периоду
- массивы OHLC и времени
- параметры символа (ChartPoint, ChartAsk, ChartBid)
- границы торговых сессий для каждого дня недели (MONDAY_MinuteEquivalentFrom/To и т.д.)
- вспомогательные методы
Инкапсуляция массивов, важных методов и других маркеров виртуального графика способствует большему порядку в коде и выработке правильного подхода к проектированию более сложных алгоритмов, позволяя в первую очередь сосредоточиться на общей логике, а не на деталях, что обычно занимает большую часть времени. Перейдём к рассмотрению тела Chart:
//+------------------------------------------------------------------+ //| Chart class — manages virtual chart data and sessions | //+------------------------------------------------------------------+ class Chart { public: // Historical data arrays (indexed as time series: [0]=current, [1]=previous) datetime TimeI[]; // Bar timestamps double CloseI[]; // Close prices double OpenI[]; // Open prices double HighI[]; // High prices double LowI[]; // Low prices double ChartPoint; // Symbol point size (e.g. 0.00001 for EURUSD) double ChartAsk; // Current Ask price double ChartBid; // Current Bid price datetime tTimeI[]; // Helper array for new bar detection static int TCN; // Total bars count (shared across instances) string CurrentSymbol; // Symbol name (e.g. "EURUSD") ENUM_TIMEFRAMES Timeframe; // Chart timeframe (e.g. PERIOD_H1) int copied; // Bars copied in last CopyTime call int lastcopied; // Bars copied in last full OHLC update datetime LastCloseTime; // Timestamp of last known bar MqlTick LastTick; // Latest tick data // Trading session boundaries (minutes from midnight) for each weekday int MONDAY_MinuteEquivalentFrom; // Monday session start int MONDAY_MinuteEquivalentTo; // Monday session end // ... TUESDAY through SUNDAY (same pattern) ... };
Границы сессий (MONDAY_MinuteEquivalentFrom/To и т.д.) заполняются через SymbolInfoSessionTrade при инициализации. Это позволяет роботу торговать строго внутри расписания символа. Функция CorrectSeries() обрабатывает edge-case когда начало и конец сессии равны 0:00 (круглосуточная торговля) — в этом случае конец устанавливается на 23:59.
Обновление данных происходит в ChartTick() — вызывается на каждом тике из Simulated().
Шаги ChartTick():
- получить последний тик (SymbolInfoTick);
- проверить появление нового бара (CopyTime, сравнение с LastCloseTime);
- если бар сменился — перезагрузить все массивы OHLC (CopyClose, CopyOpen, CopyHigh, CopyLow, CopyTime);
- переключить массивы в режим временного ряда (ArraySetAsSeries);
- обновить текущие котировки (ChartBid, ChartAsk, ChartPoint).
Регулярное обновление внутренних структур гарантирует мгновенный доступ алгоритма к самым свежим значениям цен на каждом тике. Данные копируются в обычном порядке, а затем массивы переключаются в режим временного ряда, чтобы индекс "0" указывал на текущий бар.
//+------------------------------------------------------------------+ //| ChartTick — update chart data on every tick | //| Detects new bars and reloads OHLC history | //+------------------------------------------------------------------+ void ChartTick() { // Get the latest tick for current symbol SymbolInfoTick(CurrentSymbol,LastTick); // Check for new bar by comparing bar timestamps ArraySetAsSeries(tTimeI,false); copied=CopyTime(CurrentSymbol,Timeframe,0,2,tTimeI); ArraySetAsSeries(tTimeI,true); // If a new bar appeared, reload all OHLC data if ( copied == 2 && tTimeI[1] > LastCloseTime ) { // Set arrays to normal order for CopyXXX functions ArraySetAsSeries(CloseI,false); ArraySetAsSeries(OpenI,false); ArraySetAsSeries(HighI,false); ArraySetAsSeries(LowI,false); ArraySetAsSeries(TimeI,false); // Copy full OHLC history lastcopied=CopyClose(CurrentSymbol,Timeframe,0,Chart::TCN+2,CloseI); lastcopied=CopyOpen(CurrentSymbol,Timeframe,0,Chart::TCN+2,OpenI); lastcopied=CopyHigh(CurrentSymbol,Timeframe,0,Chart::TCN+2,HighI); lastcopied=CopyLow(CurrentSymbol,Timeframe,0,Chart::TCN+2,LowI); lastcopied=CopyTime(CurrentSymbol,Timeframe,0,Chart::TCN+2,TimeI); // Switch back to time series mode: index [0] = current bar ArraySetAsSeries(CloseI,true); ArraySetAsSeries(OpenI,true); ArraySetAsSeries(HighI,true); ArraySetAsSeries(LowI,true); ArraySetAsSeries(TimeI,true); LastCloseTime=tTimeI[1]; // Remember time of last known bar } // Update current quotes ChartBid=LastTick.bid; ChartAsk=LastTick.ask; ChartPoint=SymbolInfoDouble(CurrentSymbol,SYMBOL_POINT); }
Почему ArraySetAsSeries переключается дважды? Функции CopyClose/CopyOpen заполняют массив в хронологическом порядке (от старого к новому), а для удобства работы в стратегии индекс "0" должен указывать на текущий бар (как в индикаторах). Переключение делает это возможным без перекопирования данных — это стандартный паттерн в MQL5. Тиковые объёмы я не включал в класс Chart, так как считаю, что эта информация бесполезна. Тем не менее вы можете легко добавить недостающие массивы по аналогии. В том числе можно хранить историю отдельных тиков.

Рис. 8. Класс Chart
Класс BotInstance: ядро робота
После понимания назначения предыдущего класса гораздо проще понять аналог в виде данного класса. Фактически мы ушли от парадигмы написания кода под текущий график к парадигме работы на том графике, который выбран у нас в коде. В нашем случае мы уже внутри делаем заглушку: выбирается тот график и его период, используя данные того графика, на который прикреплен робот. Но чисто механически мы используем уже правильную архитектуру, которую можно будет расширить в следующих статьях.
Благодаря этой архитектуре теперьснова всего торгового кода сосредоточена здесь. Здесь представлены методы: от достаточно простых, помогающих в вычислениях, округлениях и прочих действиях (например, проверка торговых сессий и контроль торговли в соответствии с ними), до основных торговых методов, включая ключевой метод "CalculateForTrade()", в котором мы прописываем правила нашей стратегии.
Компоненты BotInstance:
- параметры — магик, спред, разрешённые направления (bInitBuy, bInitSell)
- объекты торговли — CPositionInfo и CTrade
- сигналы — TradeDirection, TradeDirectionClose
- переменные состояния — bOpened, bNewBarV, NewBarType
- вспомогательные методы и иные переменные
Интеграция всех управляющих параметров в рамках одного класса существенно упрощает масштабирование системы до мультивалютных конфигураций. Конструктор инициализирует значения по умолчанию и вызывает RestartParams(), который копирует входные параметры в поля класса.
//+------------------------------------------------------------------+ //| BotInstance — trading robot core class | //| Contains all trading logic, parameters and state | //+------------------------------------------------------------------+ class BotInstance { public: bool bInitBuy; // Allow BUY positions bool bInitSell; // Allow SELL positions int BarsChainForOpen; // Bars in a row for open signal int BarsChainForClose; // Bars in a row for close signal int SpreadE; // Max allowed spread for opening int MagicF; // Robot magic number int TradeDirection; // Open signal: 0=none, 1=buy, 2=sell int TradeDirectionClose; // Close signal: 0=none, 1=close sells, 2=close buys bool bOpened; // Position opened on current candle? CPositionInfo m_position; // Standard MQL5 position info object CTrade m_trade; // Standard MQL5 trade execution object bool bNewBarV; // New bar detected flag string NewBarType; // "START" (first 20s) or "LATE" (after 20s) datetime PreviousLateTick; // Last LATE mode tick timestamp //+------------------------------------------------------------------+ //| Constructor — sets defaults and copies input parameters | //+------------------------------------------------------------------+ BotInstance() { bOpened = false; SpreadE=SpreadEE; // Copy max spread from input MagicF=MagicE; // Copy magic number from input RestartParams(); // Initialize all trading parameters } //+------------------------------------------------------------------+ //| RestartParams — copy input parameters to class fields | //+------------------------------------------------------------------+ void RestartParams() { bInitBuy = TBE; // Allow buys from input bInitSell = TSE; // Allow sells from input BarsChainForOpen = BarsChainForOpenE; // Strategy parameter BarsChainForClose = BarsChainForCloseE; // Strategy parameter CurrentSymbol=ChartO.CurrentSymbol; this.m_trade.SetExpertMagicNumber(MagicF); // Set magic for all orders } };
Зачем RestartParams(), если можно скопировать в конструкторе? Потому что следует предусмотреть механизм перезапуска каждого виртуального робота с новыми параметрами без пересоздания объекта робота. В простом шаблоне это не критично, но архитектура заложена с запасом, для следующих статей цикла, описывающих более сложные и более функциональные шаблоны.
теперьассмотрим bNewBar() — функцию определения нового бара. Она возвращает true при смене бара и устанавливает NewBarType: START если тик пришёл в первые 20 секунд бара, LATE если позже. Это критически важно для работы на реале, где тики приходят нерегулярно.
//+------------------------------------------------------------------+ //| Detect new bar and classify as START or LATE | //| START: tick arrived within first 20 seconds of new bar | //| LATE: tick arrived after 20 seconds — logic runs every 20s | //+------------------------------------------------------------------+ bool bNewBar() { // START mode: new bar AND tick within first 20 seconds if (ChartO.LastTick.time > 0 && ChartO.TimeI[1] > 0 && ChartO.TimeI[1] != Time0 && ChartO.ChartPoint != 0.0 && ChartO.LastTick.time - ChartO.TimeI[0] <= 20 && ChartO.LastTick.time - ChartO.TimeI[0] >= 0 ) { NewBarType = "START"; // Execute logic immediately Time0=ChartO.TimeI[1]; return true; } // LATE mode: new bar but after first 20 seconds else if ( ChartO.LastTick.time > 0 && ChartO.TimeI[1] > 0 && ChartO.TimeI[1] != Time0 && ChartO.ChartPoint != 0.0 ) { NewBarType = "LATE"; // Execute logic every 20 seconds Time0=ChartO.TimeI[1]; return true; } else return false; }

Рис. 9. Определение нового бара
Зачем два режима? На реале тики могут прийти через минуту после открытия бара (на малоликвидных инструментах). Без LATE-режима робот выполнил бы логику один раз при обнаружении нового бара и больше не возвращался бы. С LATE — каждые 20 секунд идёт повторная попытка, пока не появится следующий бар.
Логика открытия long/short: BuyF и SellF
BuyF() — это "фасад" открытия покупки. В нём предварительно проверяется многое, например, наличие уже открытых позиций нашего виртуального робота, а также принимается решение о дальнейших проверках во вложенных методах, чтобы окончательно определить необходимость отправки торгового приказа на сервер. Такие методы, как для открытия, так и для закрытия позиций, всегда идут парами для каждого направления. В нашем случае для рассмотрения в статье хватит по одному экземпляру. Другой всегда можно посмотреть в исходниках.
Дальнейшие проверки в BuyF():
- валидность цен;
- режим нового бара (START/LATE);
- антидублирование через GlobalVariable;
- режим счёта (хеджинг/неттинг);
- разрешение покупок (bInitBuy);
- докупки (AddtionalOpenPrice если есть позиции).
Почему так много проверок перед открытием? Дело в том, что на реальном счёте могут возникать различные ситуации: тики могут приходить с задержкой, цены могут быть некорректными, на счёте может быть неттинг-режим (когда нельзя открывать противоположные позиции), докупки могут быть запрещены настройками. Все эти проверки защищают от ошибок и гарантируют, что ордер отправится только когда это действительно безопасно и уместно.
Особое внимание стоит обратить на антидублирование через GlobalVariable. Это защита от ситуации, когда один и тот же тик обрабатывается несколько раз (например, из-за задержек сети или особенностей работы терминала). GlobalVariable хранит время последней операции для данного магика, и если с момента последней операции прошло недостаточно времени — новая операция блокируется. Это предотвращает открытие нескольких одинаковых позиций подряд.
//+------------------------------------------------------------------+ //| BuyF — open BUY position with all pre-checks | //| Validates prices, timing, account mode, repurchase conditions | //+------------------------------------------------------------------+ void BuyF() { double SLTemp0=MathAbs(SLE); // Absolute stop-loss in points double TPTemp0=MathAbs(TPE); // Absolute take-profit in points // Time since last operation (anti-duplicate protection via GlobalVariable) double DtA=double(TimeCurrent())-GlobalVariableGet(GLOBALVARPREFIX+IntegerToString(MagicF)); // Main guard: valid prices, LATE price filter, anti-duplicate, account mode if ( ChartO.LastTick.bid > 0.0 && ChartO.OpenI[0] > 0.0 && ((NewBarType == "LATE" && ChartO.OpenI[0] >= ChartO.LastTick.bid) || NewBarType == "START") && (DtA > 0 || DtA < 0) && ((AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) || (AccountInfoInteger(ACCOUNT_MARGIN_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_NETTING && OrdersS()==0)) ) { // Check: buys allowed AND (no positions OR repurchase conditions met) if (bInitBuy && ( (!bInitRepurchaseE && OrdersG() == 0) || (bInitRepurchaseE && OrdersG(true) && AddtionalOpenPrice(ChartO.LastTick.ask,true))) ) { CheckForOpen(MagicF,OP_BUY,ChartO.ChartBid,ChartO.ChartAsk,ChartO.CloseI[0], int(SLTemp0),int(TPTemp0),LotE,ChartO.CurrentSymbol,0,CommentI, bInitLotControl,bInitSpreadControl,SpreadE); } } }
Обратите внимание на условие для LATE-режима: ChartO.OpenI[0] >= ChartO.LastTick.bid. Это ценовой фильтр — в LATE-режиме покупка разрешена только если текущая цена не выше цены открытия текущего бара. Это снижает риск входа по невыгодной цене после значительного движения.
Зачем нужен этот фильтр? Представьте ситуацию: новый бар открылся на цене 1.1000, но тик пришёл только через минуту, когда цена уже выросла до 1.1020. Без фильтра робот открыл бы позицию по цене 1.1020, что на 20 пунктов хуже цены открытия бара. С фильтром покупка блокируется, и робот ждёт более выгодного момента. Для продаж фильтр работает аналогично, но в обратную сторону: продажа разрешена только если текущая цена не ниже цены открытия бара.

Рис. 10. Класс BotInstance
Логика закрытия long/short: CloseBuyF и CloseSellF
Это с виду компактные, но предельно насыщенные логикой и ветвлениями методы, потому что здесь пересекаются сразу несколько механизмов, которые я поместил в единую точку обработки. Некоторые режимы закрытия взаимоисключающиеся, именно поэтому всё достаточно запутано. Например, длительное удержание позиций противоречит режиму докупки. В данном случае важно понимать приоритет одного режима над другим, чтобы обеспечить их корректную работу. В один момент может быть активирован лишь один из этих режимов. Совместное использование также возможно, но смысл этого лично для меня не очевиден.
Механизмы CloseSellF/CloseBuyF:
- обычное закрытие — по стопу, тейк-профиту или сигналу;
- режим ожидания убытков (sit) — проверка MinutesHoldE перед закрытием;
- докупки (repurchase) — при включённых докупках вызывается CloseToProfit.
Такая гибкость механизмов закрытия позволяет адаптировать робота как под трендовые, так и под контртрендовые сценарии. Без докупок — закрываются все SELL с проверкой "sit-условия"; с докупками — CloseToProfit. Почему именно эти механизмы наиболее сложные? Дело в том, что закрытие позиций — это место, где сходятся все механизмы управления рисками. Обычное закрытие по сигналу, закрытие при докупках (только когда суммарная прибыль положительна), закрытие в режиме ожидания убытков (только после истечения времени или при прибыли) — всё это нужно учесть в одной функции. Если бы мы разделили логику на несколько функций, пришлось бы дублировать проверки и усложнять код. Вместо этого мы используем условные ветвления, которые активируют нужный механизм в зависимости от настроек:
//+--------------------------------------------------------------+ //| Function for closing sell positions (SELL) | //| Closes short positions according to strategy conditions | //| Considers spread settings and holding time | //+--------------------------------------------------------------+ void CloseSellF() { //--- Local variables declaration ulong ticket; // stores ticket ID of the current position being iterated bool ord; // flag indicating successful selection of position by ticket ulong Tickets[]; // dynamic array to store tickets of positions eligible for closing int TicketsTotal = 0; // counter for total number of positions matching closing criteria //---+ GLOBAL MARKET CONDITIONS CHECK +------------------------- //--- Verify market data validity and strategy entry/exit permissions if(ChartO.LastTick.bid > 0.0 && ChartO.OpenI[0] > 0.0 && ((NewBarType == "LATE" && ChartO.LastTick.bid >= ChartO.OpenI[0]) || NewBarType == "START") && (!bInitSpreadControl || (bInitSpreadControl && MarketInfo(ChartO.CurrentSymbol, MODE_SPREAD) <= SpreadCloseE))) { //---+ LOGIC BRANCH CHECK +----------------------------------- //--- Proceed only if repurchase logic is disabled OR situational logic is enabled //--- This prevents conflicting exit strategies during repurchase phases if(!bInitRepurchaseE || bInitSitE) { //---+ PASS 1: COUNT ELIGIBLE POSITIONS +----------------- //--- First iteration is required to determine exact array size for ArrayResize() //--- This avoids memory reallocation overhead during the second pass for(int i = 0; i < PositionsTotal(); i++) { ticket = PositionGetTicket(i); // get ticket of the i-th open position ord = PositionSelectByTicket(ticket); // select position to access its properties //--- Filter conditions for SELL positions: if(ord && PositionGetInteger(POSITION_MAGIC) == MagicF && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && PositionGetString(POSITION_SYMBOL) == ChartO.CurrentSymbol && (!bInitSitE || (bInitSitE && ((PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP) - PositionGetDouble(POSITION_VOLUME) * ComissionPerLotE) > 0.0 || bTimeIsOut(int(PositionGetInteger(POSITION_TIME))))))) { //--- Validate ticket non-zero before counting if(ticket != 0) TicketsTotal++; } } //---+ PASS 2: COLLECT TICKETS INTO ARRAY +--------------- //--- Allocate exact memory size based on count from Pass 1 if(TicketsTotal > 0) { ArrayResize(Tickets, TicketsTotal); // resize array to fit exact number of positions TicketsTotal = 0; // reset counter to use as array index //--- Second iteration: fill the array with valid tickets for(int i = 0; i < PositionsTotal(); i++) { ticket = PositionGetTicket(i); ord = PositionSelectByTicket(ticket); //--- Apply identical filtering logic as in Pass 1 to ensure consistency if(ord && PositionGetInteger(POSITION_MAGIC) == MagicF && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && PositionGetString(POSITION_SYMBOL) == ChartO.CurrentSymbol && (!bInitSitE || (bInitSitE && ((PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP) - PositionGetDouble(POSITION_VOLUME) * ComissionPerLotE) > 0.0 || bTimeIsOut(int(PositionGetInteger(POSITION_TIME))))))) { if(ticket != 0) { Tickets[TicketsTotal] = ticket; // store ticket in array TicketsTotal++; // increment array index } } } //---+ PASS 3: EXECUTE CLOSE ORDERS +------------------- //--- Iterate through collected tickets and send close requests for(int i = 0; i < TicketsTotal; i++) { //--- Send market order to close position by ticket //--- m_trade is an instance of CTrade class m_trade.PositionClose(Tickets[i]); } } } else { //---+ FALLBACK LOGIC +----------------------------------- //--- If situational logic conditions are not met (else branch of logic check) //--- Call auxiliary function to close positions based on profit only //--- Parameter 'false' indicates specific mode for CloseToProfit function CloseToProfit(false); } } //--- Function ends silently if global market conditions are not met }
Было бы не лишним обратить внимание на sit-условие: позиция закрывается либо при положительной прибыли (с учётом свопа и комиссии), либо при истечении MinutesHoldE минут (bTimeIsOut). Если sit выключен, условие всегда "true". Если включены докупки bInitRepurchase без sit — вызывается CloseToProfit, который закроет все BUY/SELL только, когда суммарная прибыль станет положительной. Данный механизм включается или выключается переменной bInitSitE и работает независимо от любых других алгоритмов.
Режим ожидания убытков (sit) — это механизм, который позволяет удерживать убыточную позицию заданное время в надежде на разворот цены. Зачем это нужно? Иногда цена временно уходит против позиции, но затем возвращается и даёт прибыль. Без sit-режима позиция закрылась бы по сигналу сразу, не дожидаясь возможного разворота цены, даже если через час цена вернулась бы в плюс. С sit-режимом позиция удерживается минимум MinutesHoldE минут и закрывается только, если прибыль стала положительной или время истекло. При этом закрытие происходит не сразу по истечении отведённого времени, а только при поступлении сигнала на закрытие от нашей стратегии, которая реализована в CalculateForTrade(). Это важно, потому что сигнал на закрытие даёт нам дополнительное матожидание.
Заключение
Моей целью было не столько предоставить голый код или осветить какие-то торговые подходы, сколько дать возможность новичкам получить профессиональную обвязку для их будущих стратегий, чтобы они могли её изучить и протестировать. Это готовый к экспериментам и изучению код. Хоть он и не является эталоном для опытных пользователей, не следует забывать о новичках.
В итоге получился готовый рабочий код, в который достаточно внедрить свою логику в методе CalculateForTrade() и при необходимости подстроить параметры. Такой шаблон — базовый кирпичик: на его основе в следующих статьях серии будут рассмотрены мультипериодный шаблон (один инструмент, несколько периодов) и статическая диверсификация (несколько инструментов в одном советнике). Понимание простого шаблона упростит переход на новые уровни.
По сути, это уже готовый робот, в котором реализовано большинство механизмов, что вы так или иначе попробуете при построении своих торговых систем. Остается лишь реализовать вашу стратегию, а кому-то, может быть, даже такой простой вариант придется по душе. Надеюсь, с моим статическим шаблоном вы сможете ускорить процесс обучения или создания готовых продуктов. В любом случае это расширит ваш арсенал для разработки собственных торговых алгоритмов.
Приложенные файлы
| Файл | Описание |
|---|---|
| Simple Static Template MT5.mq5 | Исходный код шаблона для MetaTrader 5 |
| Simple Static Template MT4.mq4 | Аналог шаблона для тех, кто все еще неравнодушен к MetaTrader 4 |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 67): Использование паттернов TRIX и процентного диапазона Уильямса
Архитектура системы машинного обучения в MetaTrader 5 (Часть 1): Утечка данных и исправление меток времени
Моделирование рынка (Часть 19): Первые шаги на SQL (II)
Знакомство с языком MQL5 (Часть 33): Освоение API и функции WebRequest в языке MQL5 (VII)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования