Проектирование конечного автомата торговой стратегии на MQL5: Замена вложенных конструкций if-else явными состояниями
Введение
Разработка типичного советника (EA) начинается с функции OnTick(), в которой легко разобраться. Одно условие проверяет, открыта ли позиция, другое оценивает сигнал входа в рынок, третье проверяет приемлемость спреда, а четвёртое — активность торговой сессии. Четыре условия — понятные, наглядные и простые в отладке.
После шести месяцев добавления новых возможностей в той же функции насчитывается уже 43 вложенные условные конструкции на 230 строках кода. Чтобы добавить трейлинг-стоп, разработчику, недавно присоединившемуся к проекту, сначала нужно определить точный набор условий, при которых позиция открыта, остаётся прибыльной и ещё не достигла первого уровня тейк-профита. Для этого необходимо прочитать всю функцию, проанализировать пути выполнения и выбрать точку вставки, которая не нарушит существующее поведение. Вероятность регрессии достаточно высока.
Корень проблемы в том, что у советника (EA) есть неявные состояния — ожидание, поиск сигнала входа в рынок, сопровождение позиции, управление выходом из позиции, но они закодированы разбросанными по коду комбинациями булевых флагов, а не представлены как отдельные явные состояния. При использовании неявных состояний код их обработки смешан с кодом определения границ между состояниями. В результате в каждой точке проверки функция должна отвечать на два вопроса: каково текущее состояние и какое действие следует выполнить. Это две разные обязанности, и именно их совмещение делает код хрупким.

Рисунок 1 — Монолитная функция OnTick() по сравнению с конечным автоматом (FSM). При монолитном подходе (слева) на каждом тике проверяется каждая условная ветвь, включая не относящиеся к текущей ситуации пути выполнения, например логику трейлинг-стопа при отсутствии открытой позиции. Архитектура FSM (справа) делегирует выполнение одному активному состоянию посредством виртуальной диспетчеризации, при этом выполняется только код, относящийся к текущей фазе стратегии.
Помимо удобства сопровождения, следует учитывать и затраты на выполнение. Каждый дополнительный уровень вложенных условий увеличивает число ветвей, которые процессору приходится проверять, даже если большинство из них не имеют отношения к текущей рыночной ситуации. Конечный автомат (FSM) заменяет это дерево ветвлений единственным вызовом метода активного состояния, выполняя только логику, связанную с текущей фазой стратегии. В результате получается более четкая архитектура и более предсказуемый путь выполнения.
Архитектура конечного автомата
Реализация FSM состоит из пяти структурных уровней. Абстрактный интерфейс IState определяет контракт, которому должно соответствовать каждое конкретное состояние. Класс контекста CStrategyContext владеет указателем на текущее состояние и выступает посредником при всех переходах между состояниями. Каждый класс конкретного состояния (CIdleState, CEntryState, CInTradeState, CExitState) реализует интерфейс IState и содержит только логику, относящуюся к соответствующей фазе работы. Отдельный файл реализации StrategyContextImpl.mqh устраняет циклическую зависимость подключаемых файлов между CStrategyContext и конкретными состояниями. В нём определяются конструктор, деструктор и методы доступа к конкретным состояниям — после того как станут доступны определения всех соответствующих типов. Советник (EA) создает экземпляр контекста один раз и вызывает метод Update() на каждом тике.
Основной механизм паттерна "Состояние" — замена указателя на текущее состояние в контексте. CStrategyContext содержит один член типа IState* с именем m_current_state. Когда требуется переход, текущее состояние вызывает метод context.SetState(), передавая ему указатель на целевое состояние. При следующем вызове Update() контекст вызывает метод Evaluate() нового состояния. Контексту не нужно знать, какие состояния существуют и какова логика переходов между ними. Он знает лишь то, что хранит одно текущее состояние и что метод Update() делегирует ему выполнение.

Рисунок 2 — Структура классов FSM. Интерфейс IState определяет контракт для всех конкретных состояний. CStrategyContext хранит указатель на текущее состояние и предоставляет метод SetState() для переходов между состояниями. Четыре конкретных состояния реализуют логику стратегии: поиск торгового сигнала в режиме ожидания, размещение ордера, активное сопровождение позиции и выход из позиции.
Цепочка подключения файлов и устранение циклической зависимости
В MQL5 все типы должны быть полностью определены до создания их экземпляров. Это создаёт структурное противоречие в архитектуре FSM: классу CStrategyContext нужны предварительные объявления классов конкретных состояний, чтобы объявить члены-указатели CIdleState*, CEntryState*, CInTradeState* и CExitState*, тогда как классам конкретных состояний в States.mqh требуется полное объявление CStrategyContext, чтобы вызывать такие методы, как ctx.GetSymbol() и ctx.SetState(), в телах своих методов.
Решение состоит в разделении кода на три файла. Файл StrategyContext.mqh содержит объявление класса CStrategyContext с предварительными объявлениями классов конкретных состояний и тела только тех методов доступа, которые работают со скалярными данными и не требуют знания конкретных типов состояний. В начале файла States.mqh подключается StrategyContext.mqh, чтобы полное объявление CStrategyContext было доступно в теле каждого метода состояния. Файл StrategyContextImpl.mqh подключает файлы StrategyContext.mqh и States.mqh в указанном порядке. Затем в нём определяются конструктор, деструктор и четыре метода доступа GetXxxState(), для которых требуются полностью определённые конкретные типы состояний. Советник (EA) подключает только StrategyContextImpl.mqh, который автоматически подключает всю цепочку файлов.
Структура из трёх файлов, устраняющая циклическую зависимость подключаемых файлов, видна в самих директивах #include. Файл StrategyContextImpl.mqh явно задаёт порядок компиляции:
#include "StrategyContext.mqh" #include "States.mqh"
Файл StrategyContext.mqh начинается со следующего:
#include "IState.mqh" //--- Forward declarations: full definitions are in States.mqh class CIdleState; class CEntryState; class CInTradeState; class CExitState;
Предварительные объявления позволяют классу CStrategyContext объявлять члены-указатели на конкретные типы состояний, не требуя полных определений этих типов. Далее файл States.mqh начинается со следующего:
#include "StrategyContext.mqh"
Поскольку файл StrategyContext.mqh защищён директивой #ifndef STRATEGYCONTEXT_MQH, повторное подключение не имеет эффекта: компилятор пропускает тело файла и продолжает работу с уже доступным объявлением. Поэтому в телах методов классов конкретных состояний в States.mqh можно вызывать ctx.GetSymbol(), ctx.SetState() и все остальные методы контекста, поскольку полное объявление CStrategyContext уже доступно. Только конструктор и методы доступа GetXxxState() требуют, чтобы и CStrategyContext, и конкретные типы состояний были полностью определены. Поэтому они вынесены в файл StrategyContextImpl.mqh, в котором сначала подключаются оба необходимых файла.
Порядок компиляции, обеспечиваемый данной структурой, следующий:
StrategyContextImpl.mqh └── StrategyContext.mqh └── IState.mqh (enum + abstract class, no dependencies) CStrategyContext (class declaration + scalar bodies only) └── States.mqh └── StrategyContext.mqh (skipped — #ifndef guard already resolved) CIdleState, CEntryState, CInTradeState, CExitState (full definitions) Constructor, destructor, GetXxxState() bodies (all types fully defined here)
Перечисление состояний
enum ENUM_STRATEGY_STATE { STATE_IDLE = 0, // No active position; awaiting valid entry signal STATE_ENTRY = 1, // Entry signal confirmed; order submission in progress STATE_IN_TRADE = 2, // Position open; monitoring for exit conditions STATE_EXIT = 3 // Exit condition triggered; closing position };
Матрица переходов между состояниями
| Текущее состояние | Инициирующее событие | Целевое состояние | Защитное условие |
|---|---|---|---|
| STATE_IDLE | Сигнал входа в рынок подтвержден | STATE_ENTRY | Спред в допустимых пределах; открытых позиций с этим магическим номером нет |
| STATE_ENTRY | Исполнение ордера подтверждено | STATE_IN_TRADE | Значение PositionsTotal() увеличилось; найдена позиция с нужным магическим номером |
| STATE_ENTRY | Ордер отклонен или истекло время ожидания | STATE_IDLE | Код возврата не соответствует TRADE_RETCODE_DONE; либо прошло 10 тиков, но ордер не был исполнен |
| STATE_IN_TRADE | Внешнее срабатывание стоп-лосса | STATE_IDLE | Значение PositionsTotal() снизилось до нуля; позиций с нужным магическим номером нет |
| STATE_IN_TRADE | Сработал сигнал выхода из позиции | STATE_EXIT | На текущем баре подтверждён разворот по обратному пересечению скользящих средних |
| STATE_EXIT | Ордер на закрытие подтверждён | STATE_IDLE | Функция PositionsTotal() снова вернула ноль |
| STATE_EXIT | Ордер на закрытие отклонён | STATE_IN_TRADE | Достигнуто максимальное количество попыток; позиция по-прежнему открыта |
Интерфейс IState
IState определяет три метода жизненного цикла. Метод OnEnter() вызывается ровно один раз при переходе контекста в состояние. Метод Evaluate() вызывается на каждом тике, пока состояние активно. Метод OnExit() вызывается ровно один раз непосредственно перед выходом контекста из данного состояния. Этот контракт из трёх методов разделяет логику инициализации, обработки каждого тика и очистки на отдельные, непересекающиеся фазы.
Это разделение имеет практическое значение для отладки. Если уровень стоп-лосса позиции задан неверно, поиск причины начинается в методе CEntryState::OnEnter(), где формируется ордер, а не в дереве условных операторов неопределённой длины. Если трейлинг-стоп обновляется некорректно, поиск причины начинается в методе CInTradeState::Evaluate(). Название состояния в журнале сразу сужает область поиска до одного класса и одного метода.
Таблица методов интерфейса IState
| Метод | Тип возвращаемого значения | Доступ | Этап выполнения |
|---|---|---|---|
| OnEnter(CStrategyContext *ctx) | void | public virtual | Вызывается один раз при активации состояния; инициализирует локальные ресурсы состояния |
| Evaluate(CStrategyContext *ctx) | void | public virtual | Вызывается на каждом тике, пока состояние активно; проверяет условия и инициирует переходы между состояниями |
| OnExit(CStrategyContext *ctx) | void | public virtual | Вызывается один раз перед деактивацией; освобождает локальные ресурсы состояния |
| GetStateId() | ENUM_STRATEGY_STATE | public virtual | Возвращает идентификатор перечисления, соответствующий реализуемому состоянию |
| GetStateName() | string | public virtual | Возвращает понятное человеку имя для записи в журнал |
Именно благодаря предварительному объявлению CStrategyContext в начале файла IState.mqh сигнатуры методов могут принимать параметр типа CStrategyContext*, при этом компилятор на данном этапе не требует полного определения класса. Суффикс = 0 в объявлении каждого метода делает его чисто виртуальным: компилятор отклонит любой конкретный подкласс, в котором не переопределены все пять методов. Виртуальный деструктор в последней строке гарантирует, что при удалении конкретного состояния через указатель IState* будет вызвана правильная цепочка деструкторов, а не возникнет неопределённое поведение.
//+------------------------------------------------------------------+ //| IState.mqh | //| Abstract state interface for the strategy FSM. | //| Every concrete state must implement all five virtual methods. | //+------------------------------------------------------------------+ #ifndef ISTATE_MQH #define ISTATE_MQH //--- Forward declaration: CStrategyContext is defined in StrategyContext.mqh class CStrategyContext; //+------------------------------------------------------------------+ //| Enumeration of execution states for the strategy FSM | //+------------------------------------------------------------------+ enum ENUM_STRATEGY_STATE { STATE_IDLE = 0, // No active position; awaiting valid entry signal STATE_ENTRY = 1, // Entry signal confirmed; order submission in progress STATE_IN_TRADE = 2, // Position open; monitoring for exit conditions STATE_EXIT = 3 // Exit condition triggered; closing position }; //+------------------------------------------------------------------+ //| Class IState | //| Purpose: Interface defining actions within an execution state | //+------------------------------------------------------------------+ class IState { public: //--- Lifecycle and state execution logic hooks virtual void OnEnter(CStrategyContext *ctx) = 0; virtual void Evaluate(CStrategyContext *ctx) = 0; virtual void OnExit(CStrategyContext *ctx) = 0; //--- Metadata and state monitoring properties virtual ENUM_STRATEGY_STATE GetStateId(void) const = 0; virtual string GetStateName(void) const = 0; //--- Virtual destructor ensuring clean polymorphic teardown virtual ~IState(void) {} }; #endif // ISTATE_MQH //+------------------------------------------------------------------+
Проектирование CStrategyContext
CStrategyContext управляет конечным автоматом и выступает посредником при переходах между состояниями. Он содержит указатели на все четыре объекта конкретных состояний и управляет их жизненным циклом — от создания до уничтожения. Кроме того, он хранит контекст рыночных данных, необходимых состояниям для принятия решений: текущий торговый инструмент, дескрипторы индикаторов скользящих средних (MA) и тикет последнего отправленного ордера. Централизация этих данных в контексте избавляет состояния от необходимости самостоятельно запрашивать данные у терминала, что упрощает поток данных и делает все зависимости от контекста явными благодаря параметру-указателю ctx.
Метод SetState() обеспечивает прохождение полного жизненного цикла перехода. Он вызывает метод OnExit() у покидаемого состояния, обновляет m_current_state, фиксирует тик входа в состояние, вызывает метод OnEnter() у нового состояния и записывает переход в журнал. Выполнение этой последовательности гарантированно происходит в правильном порядке независимо от того, из какого состояния инициируется переход, поскольку все переходы осуществляются через один и тот же метод. Защита от повторного входа в текущее состояние в начале метода SetState() предотвращает выполнение жизненного цикла перехода, если запрашиваемое следующее состояние — тот же объект, что и текущее состояние.
Таблица свойств CStrategyContext
| Свойство | Тип | Доступ | Назначение |
|---|---|---|---|
| m_current_state | IState* | private | Указатель на активное состояние; на него направляется каждый вызов Update() |
| m_state_idle | CIdleState* | private | Заранее созданный экземпляр состояния ожидания |
| m_state_entry | CEntryState* | private | Заранее созданный экземпляр состояния входа в рынок |
| m_state_in_trade | CInTradeState* | private | Заранее созданный экземпляр состояния сопровождения позиции |
| m_state_exit | CExitState* | private | Заранее созданный экземпляр состояния выхода из позиции |
| m_symbol | string | private | Торговый инструмент, привязанный при создании объекта |
| m_magic | ulong | private | Магический номер советника (EA) для идентификации ордеров |
| m_ma_fast_handle | int | private | Дескриптор индикатора быстрой скользящей средней (MA) |
| m_ma_slow_handle | int | private | Дескриптор индикатора медленной скользящей средней (MA) |
| m_last_ticket | ulong | private | Тикет последнего отправленного ордера |
| m_tick_count | long | private | Накопительный счетчик тиков для соблюдения периода ожидания |
| m_state_entry_tick | long | private | Значение счетчика тиков на момент входа в текущее состояние |
| m_pending_direction | int | private | Направление планируемой сделки: 1 = длинная позиция, -1 = короткая позиция, 0 = нет |
Таблица методов CStrategyContext
| Метод | Тип возвращаемого значения | Доступ | Назначение |
|---|---|---|---|
| CStrategyContext(string, ulong, int, int) | — | public | Реализован в StrategyContextImpl.mqh; создает четыре экземпляра классов состояний и задает CIdleState в качестве начального состояния |
| ~CStrategyContext() | — | public | Реализован в StrategyContextImpl.mqh; удаляет все четыре экземпляра классов состояний |
| Update() | void | public | Увеличивает счетчик тиков; вызывает метод Evaluate() текущего состояния |
| SetState(IState*) | void | public | Вызывает OnExit, меняет указатель, сохраняет значение счетчика тиков при входе в состояние, вызывает OnEnter и записывает сообщение в журнал |
| GetCurrentStateId() | ENUM_STRATEGY_STATE | public | Возвращает идентификатор текущего состояния из перечисления |
| GetCurrentStateName() | string | public | Возвращает строку с названием текущего состояния |
| GetSymbol() | string | public | Возвращает привязанный торговый инструмент |
| GetMagic() | ulong | public | Возвращает магический номер советника (EA) |
| GetFastMAHandle() | int | public | Возвращает дескриптор индикатора быстрой скользящей средней (MA) |
| GetSlowMAHandle() | int | public | Возвращает дескриптор индикатора медленной скользящей средней (MA) |
| GetLastTicket() | ulong | public | Возвращает тикет последнего отправленного ордера |
| SetLastTicket(ulong) | void | public | Обновляет тикет последнего отправленного ордера |
| GetTickCount() | long | public | Возвращает накопленное значение счетчика тиков |
| GetStateEntryTick() | long | public | Возвращает значение счетчика тиков на момент входа в текущее состояние |
| GetPendingDirection() | int | public | Возвращает флаг направления планируемой сделки |
| SetPendingDirection(int) | void | public | Устанавливает флаг направления планируемой сделки |
| GetIdleState() | CIdleState* | public | Реализован в файле StrategyContextImpl.mqh; возвращает указатель на состояние ожидания |
| GetEntryState() | CEntryState* | public | Реализован в файле StrategyContextImpl.mqh; возвращает указатель на состояние входа в рынок |
| GetInTradeState() | CInTradeState* | public | Реализован в файле StrategyContextImpl.mqh; возвращает указатель на состояние сопровождения позиции |
| GetExitState() | CExitState* | public | Реализован в файле StrategyContextImpl.mqh; возвращает указатель на состояние выхода из позиции |
Метод SetState() — единая точка, через которую проходят все переходы между состояниями автомата:
//+------------------------------------------------------------------+ //| Safe sequential processing of transitions between distinct states| //+------------------------------------------------------------------+ void CStrategyContext::SetState(IState *next_state) { if(next_state == NULL) return; if(next_state == m_current_state) return; string from_name = m_current_state.GetStateName(); string to_name = next_state.GetStateName(); m_current_state.OnExit(&this); m_current_state = next_state; m_state_entry_tick = m_tick_count; m_current_state.OnEnter(&this); PrintFormat("[CStrategyContext] Transition: %s -> %s | Tick: %s", from_name, to_name, IntegerToString(m_tick_count)); }
Проверка на null и защита от повторного входа в текущее состояние в начале метода не позволяют некорректному вызову повредить указатель на состояние или вызвать OnExit и OnEnter, когда фактический переход не требуется. Строки from_name и to_name сохраняются до смены указателя, поскольку после выполнения m_current_state = next_state исходное состояние становится недоступным через контекст. Сохранение значения m_state_entry_tick в момент смены указателя даёт каждому состоянию надёжную точку отсчёта для расчёта тайм-аутов, например ограничения ожидания исполнения ордера десятью тиками в CEntryState. Вызов PrintFormat в конце записывает в журнал трассировку, которая напрямую соответствует структуре классов, описанной в заключении.
Метод Update(), который запускает этот процесс при каждом тике, намеренно сделан предельно простым:
//+------------------------------------------------------------------+ //| Drive current state evaluation sequence logic | //+------------------------------------------------------------------+ void CStrategyContext::Update(void) { m_tick_count++; if(CheckPointer(m_current_state) == POINTER_DYNAMIC) { m_current_state.Evaluate(&this); } }
Увеличение m_tick_count перед виртуальной диспетчеризацией гарантирует, что при чтении значений GetTickCount() или GetStateEntryTick() методом Evaluate() расчёт числа прошедших тиков всегда остаётся согласованным в пределах одного вызова обработчика тика.
Реализации классов конкретных состояний
CIdleState
CIdleState отслеживает рыночные условия на каждом тике и переходит в состояние CEntryState при обнаружении корректного сигнала входа в рынок. В качестве сигнала входа в рынок здесь используется пересечение скользящих средних. Когда быстрая скользящая средняя (MA) пересекает медленную снизу вверх, формируется сигнал на открытие длинной позиции, а когда пересекает её сверху вниз — сигнал на открытие короткой позиции. Перед разрешением перехода состояние проверяет два защитных условия: текущий спред не должен превышать заданный максимум в 200 пунктов, а по торговому инструменту не должно быть открытой позиции с магическим номером советника (EA).
Метод OnEnter() сбрасывает локальные переменные состояния, используемые для отслеживания MA. Метод OnExit() записывает в журнал обнаруженное направление сигнала, полученное из контекста, чтобы CEntryState мог прочитать его через ctx.GetPendingDirection() при вызове своего метода OnEnter() на том же тике.
В методе CIdleState::Evaluate() сосредоточены логика обнаружения пересечения и проверки защитных условий:
//+------------------------------------------------------------------+ //| Core evaluation loop tracking technical indicator crossovers | //+------------------------------------------------------------------+ void CIdleState::Evaluate(CStrategyContext *ctx) { string symbol = ctx.GetSymbol(); //--- Retrieve two bars of each MA to detect crossover double fast_buf[2], slow_buf[2]; if(CopyBuffer(ctx.GetFastMAHandle(), 0, 0, 2, fast_buf) < 2 || CopyBuffer(ctx.GetSlowMAHandle(), 0, 0, 2, slow_buf) < 2) { return; } double fast_curr = fast_buf[0]; double slow_curr = slow_buf[0]; double fast_prev = fast_buf[1]; double slow_prev = slow_buf[1]; //--- Guard: ensure no existing position with this magic number exists bool position_exists = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) == symbol && PositionGetInteger(POSITION_MAGIC) == (long)ctx.GetMagic()) { position_exists = true; break; } } if(position_exists) { return; } //--- Guard: spread within tolerance (200 points maximum) long spread = SymbolInfoInteger(symbol, SYMBOL_SPREAD); if(spread > 200) { return; } //--- Detect Moving Average technical crossover signals bool long_signal = (fast_prev <= slow_prev && fast_curr > slow_curr); bool short_signal = (fast_prev >= slow_prev && fast_curr < slow_curr); if(long_signal) { ctx.SetPendingDirection(1); ctx.SetState(ctx.GetEntryState()); } else if(short_signal) { ctx.SetPendingDirection(-1); ctx.SetState(ctx.GetEntryState()); } }
Функция CopyBuffer вызывается с параметром count, равным двум, чтобы в одном массиве были доступны как значение текущего бара ([0]), так и значение предыдущего бара ([1]). Условия пересечения сравнивают взаимное положение средних на предыдущем и текущем барах: выражение fast_prev <= slow_prev && fast_curr > slow_curr истинно только тогда, когда на предыдущем баре линии ещё не пересеклись, а на текущем уже пересеклись, что позволяет избежать повторных сигналов на последующих тиках в рамках одного и того же пересечения. Проверки наличия открытой позиции и допустимого спреда выполняются после чтения буферов, но до проверки сигнала, поэтому если прочитать буфер не удалось, функция сразу завершает работу, не выполняя лишних проверок условий. Вызов ctx.SetPendingDirection() перед ctx.SetState() гарантирует, что к моменту выполнения метода CEntryState::OnEnter() в рамках того же вызова SetState() значение направления уже доступно через ctx.GetPendingDirection().
CEntryState
Метод CEntryState::OnEnter() формирует и отправляет ордер сразу при активации состояния. Он не дожидается следующего тика. Такое проектное решение обусловлено тем, что сигнал уже подтверждён в конце метода CIdleState::Evaluate() на текущем тике, а задержка отправки на один тик создаёт ненужный риск проскальзывания. Ордер отправляется функцией OrderSend() с использованием структур MqlTradeRequest и MqlTradeResult. Для предварительной проверки используется отдельная структура MqlTradeCheckResult, передаваемая в OrderCheck() до отправки ордера, что соответствует требованию архитектурной изоляции торговых запросов.
Метод Evaluate() отслеживает результат отправки ордера на последующих тиках. Если значение PositionsTotal() увеличилось и найдена позиция с нужным магическим номером, выполняется переход в состояние CInTradeState. Если за десять тиков подтверждение исполнения не получено, состояние считает ордер неисполненным и инициирует возврат в CIdleState.
Формирование и отправка ордера полностью выполняются внутри метода OnEnter(), а не при первом вызове Evaluate() на тике:
//+------------------------------------------------------------------+ //| Submits order execution details upon state entry | //+------------------------------------------------------------------+ void CEntryState::OnEnter(CStrategyContext *ctx) { m_order_submitted = false; m_retry_count = 0; string symbol = ctx.GetSymbol(); int direction = ctx.GetPendingDirection(); double ask = SymbolInfoDouble(symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(symbol, SYMBOL_BID); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double price = (direction == 1) ? ask : bid; double sl = (direction == 1) ? price - 500 * point : price + 500 * point; double tp = (direction == 1) ? price + 1000 * point : price - 1000 * point; MqlTradeRequest request = {}; MqlTradeResult result = {}; MqlTradeCheckResult check = {}; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = 0.01; request.type = (direction == 1) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; request.price = price; request.sl = sl; request.tp = tp; request.deviation = 10; request.magic = ctx.GetMagic(); request.comment = "FSM Entry [" + IntegerToString(direction) + "]"; request.type_filling = ORDER_FILLING_IOC; if(!OrderCheck(request, check)) { PrintFormat("[CEntryState] OrderCheck failed. Retcode: %s. Reverting to idle.", IntegerToString(check.retcode)); ctx.SetPendingDirection(0); ctx.SetState(ctx.GetIdleState()); return; } if(OrderSend(request, result)) { ctx.SetLastTicket(result.deal); m_order_submitted = true; PrintFormat("[CEntryState] Order submitted. Deal: %s | Price: %s", IntegerToString((int)result.deal), DoubleToString(result.price, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS))); } else { PrintFormat("[CEntryState] OrderSend failed. Retcode: %s. Reverting to idle.", IntegerToString(result.retcode)); ctx.SetPendingDirection(0); ctx.SetState(ctx.GetIdleState()); } }
Структура MqlTradeCheckResult передается в функцию OrderCheck() перед вызовом OrderSend(). Эта предварительная проверка выявляет нарушения ограничений по марже, объему и цене без дополнительного обращения к брокеру; если проверка не пройдена, переход обратно в CIdleState выполняется немедленно, прямо из OnEnter(). Флаг m_order_submitted устанавливается в значение true только после успешного завершения OrderSend(), чтобы метод Evaluate() на последующих тиках не начинал отсчет времени ожидания подтверждения исполнения для запроса, который так и не дошел до брокера. Тикет, сохраненный с помощью ctx.SetLastTicket(result.deal), доступен другим состояниям через ctx.GetLastTicket() для поиска в истории ордеров при необходимости.
Подтверждение исполнения и резервный сценарий при истечении времени ожидания обрабатываются методом Evaluate():
//+------------------------------------------------------------------+ //| Monitors transaction confirmations or tracks expiration timeouts | //+------------------------------------------------------------------+ void CEntryState::Evaluate(CStrategyContext *ctx) { if(!m_order_submitted) { return; } m_retry_count++; string symbol = ctx.GetSymbol(); //--- Check for confirmed fill: position with correct magic exists for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) == symbol && PositionGetInteger(POSITION_MAGIC) == (long)ctx.GetMagic()) { Print("[CEntryState] Fill confirmed. Transitioning to CInTradeState."); ctx.SetState(ctx.GetInTradeState()); return; } } //--- Timeout after 10 ticks without fill confirmation if(m_retry_count >= 10) { Print("[CEntryState] Fill timeout. Reverting to CIdleState."); ctx.SetPendingDirection(0); ctx.SetState(ctx.GetIdleState()); } }
Досрочный возврат при значении m_order_submitted, равном false, предотвращает инкремент счетчика повторных попыток в том случае, если сама функция OnEnter() инициировала переход обратно в состояние CIdleState. В этом случае метод Evaluate() больше не будет вызван в рамках текущей активации состояния, однако защитное условие предусмотрено для гарантии корректности.
CInTradeState
Функция CInTradeState::Evaluate() выполняет три проверки при каждом тике. Сначала проверяется, существует ли еще позиция: для этого перебираются позиции, количество которых возвращает PositionsTotal(), в поисках позиции с нужным магическим номером. Если позиция была закрыта извне по стоп-лоссу или вручную, выполняется переход непосредственно в CIdleState, минуя CExitState, поскольку закрывать нечего. Во-вторых, метод проверяет условия сигнала выхода из позиции, используя ту же логику пересечения скользящих средних, что и CIdleState, но для противоположного направления. В-третьих, метод обновляет трейлинг-стоп, если превышен порог прибыли в 300 пунктов, корректируя уровень SL с помощью запроса TRADE_ACTION_SLTP, отправляемого через OrderSend().
Метод CInTradeState::Evaluate() проверяет наличие позиции, выявляет сигнал выхода из позиции и корректирует трейлинг-стоп за один проход по списку позиций:
//+------------------------------------------------------------------+ //| Analyzes market adjustments to apply active trailing mitigations | //+------------------------------------------------------------------+ void CInTradeState::Evaluate(CStrategyContext *ctx) { string symbol = ctx.GetSymbol(); ulong magic = ctx.GetMagic(); bool found = false; //--- Locate the managed position for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) != symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != (long)magic) continue; found = true; double open_price = PositionGetDouble(POSITION_PRICE_OPEN); double current_sl = PositionGetDouble(POSITION_SL); double current_bid = SymbolInfoDouble(symbol, SYMBOL_BID); double current_ask = SymbolInfoDouble(symbol, SYMBOL_ASK); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); long pos_type = PositionGetInteger(POSITION_TYPE); ulong pos_ticket = (ulong)PositionGetInteger(POSITION_TICKET); //--- Check MA crossover for exit signal double fast_buf[2], slow_buf[2]; if(CopyBuffer(ctx.GetFastMAHandle(), 0, 0, 2, fast_buf) >= 2 && CopyBuffer(ctx.GetSlowMAHandle(), 0, 0, 2, slow_buf) >= 2) { bool exit_long = (pos_type == POSITION_TYPE_BUY && fast_buf[0] < slow_buf[0] && fast_buf[1] >= slow_buf[1]); bool exit_short = (pos_type == POSITION_TYPE_SELL && fast_buf[0] > slow_buf[0] && fast_buf[1] <= slow_buf[1]); if(exit_long || exit_short) { Print("[CInTradeState] Exit signal detected. Transitioning to CExitState."); ctx.SetState(ctx.GetExitState()); return; } } //--- Trailing stop update for long position if(pos_type == POSITION_TYPE_BUY) { double profit_points = (current_bid - open_price) / point; if(profit_points >= m_trailing_activation) { double new_sl = current_bid - m_trailing_distance * point; if(new_sl > current_sl + point) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_SLTP; req.symbol = symbol; req.sl = new_sl; req.tp = PositionGetDouble(POSITION_TP); req.position = pos_ticket; if(OrderSend(req, res)) { PrintFormat("[CInTradeState] Trailing SL updated to: %s", DoubleToString(new_sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS))); } } } } //--- Trailing stop update for short position if(pos_type == POSITION_TYPE_SELL) { double profit_points = (open_price - current_ask) / point; if(profit_points >= m_trailing_activation) { double new_sl = current_ask + m_trailing_distance * point; if(new_sl < current_sl - point || current_sl == 0.0) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_SLTP; req.symbol = symbol; req.sl = new_sl; req.tp = PositionGetDouble(POSITION_TP); req.position = pos_ticket; if(OrderSend(req, res)) { PrintFormat("[CInTradeState] Trailing SL updated to: %s", DoubleToString(new_sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS))); } } } } break; } //--- Position closed externally: transition to idle without exit state if(!found) { Print("[CInTradeState] Position closed externally. Transitioning to CIdleState."); ctx.SetState(ctx.GetIdleState()); } }
Цикл выполняет перебор в обратном порядке, от PositionsTotal() - 1 до нуля, поскольку при переборе с начала списка удаление позиции в ходе обхода привело бы к сдвигу индексов. Флаг found устанавливается внутри цикла и проверяется после его завершения, поэтому если позиция была закрыта извне (например, по стоп-лоссу или вручную), происходит прямой переход в CIdleState, минуя CExitState, поскольку уже нет открытой позиции, для которой можно отправить ордер на закрытие. Проверка сигнала выхода из позиции выполняется до обновления трейлинг-стопа. Таким образом, сигнал на разворот сразу приводит к переходу в другое состояние без лишнего обращения к брокеру для корректировки уровня стоп-лосса, который вскоре утратит актуальность. При обновлении трейлинг-стопа для длинной позиции в качестве защитного условия изменения уровня используется выражение new_sl > current_sl + point: перед отправкой запроса TRADE_ACTION_SLTP новый уровень стоп-лосса должен быть как минимум на один пункт выше текущего. Это предотвращает отправку лишних торговых запросов на тиках, когда цена изменилась недостаточно для корректировки уровня.
CExitState
Функция CExitState::OnEnter() немедленно отправляет рыночный ордер на закрытие открытой позиции. Для закрытия длинной позиции используется TRADE_ACTION_DEAL с ORDER_TYPE_SELL, а для закрытия короткой — с ORDER_TYPE_BUY; объем запроса в точности соответствует объему открытой позиции. Метод Evaluate() продолжает проверять состояние позиции на последующих тиках, пока PositionsTotal() не подтвердит ее закрытие, после чего выполняется переход в CIdleState. Если ордер на закрытие не исполняется, попытка повторяется на следующем тике, пока не будет достигнут лимит m_max_attempts, равный пяти попыткам. После пяти неудачных попыток автомат возвращается в CInTradeState, чтобы не оставлять открытую позицию без контроля.
В классе CExitState отправка ордера на закрытие вынесена в приватный вспомогательный метод, поэтому и OnEnter(), и ветвь повторной попытки в Evaluate() используют одну и ту же логику без дублирования:
//+------------------------------------------------------------------+ //| Actions performed when entering the Exit state | //+------------------------------------------------------------------+ void CExitState::OnEnter(CStrategyContext *ctx) { m_close_attempts = 0; Print("[CExitState] Entered. Submitting close order."); SubmitClose(ctx); } //+------------------------------------------------------------------+ //| Dispatches raw order liquidations into the trade execution queue | //+------------------------------------------------------------------+ void CExitState::SubmitClose(CStrategyContext *ctx) { string symbol = ctx.GetSymbol(); ulong magic = ctx.GetMagic(); for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) != symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != (long)magic) continue; long pos_type = PositionGetInteger(POSITION_TYPE); double volume = PositionGetDouble(POSITION_VOLUME); ulong pos_ticket = (ulong)PositionGetInteger(POSITION_TICKET); double price = (pos_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_BID) : SymbolInfoDouble(symbol, SYMBOL_ASK); MqlTradeRequest request = {}; MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = volume; request.type = (pos_type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; request.price = price; request.deviation = 10; request.magic = magic; request.position = pos_ticket; request.comment = "FSM Exit"; request.type_filling = ORDER_FILLING_IOC; m_close_attempts++; if(OrderSend(request, result)) { PrintFormat("[CExitState] Close order submitted. Deal: %s", IntegerToString((int)result.deal)); } else { PrintFormat("[CExitState] Close order failed. Retcode: %s | Attempt: %s", IntegerToString(result.retcode), IntegerToString(m_close_attempts)); } return; } } //+------------------------------------------------------------------+ //| Verifies closing status and executes retries if required | //+------------------------------------------------------------------+ void CExitState::Evaluate(CStrategyContext *ctx) { string symbol = ctx.GetSymbol(); ulong magic = ctx.GetMagic(); //--- Check whether position has been closed bool still_open = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(PositionGetSymbol(i) == symbol && PositionGetInteger(POSITION_MAGIC) == (long)magic) { still_open = true; break; } } if(!still_open) { Print("[CExitState] Position confirmed closed. Transitioning to CIdleState."); ctx.SetPendingDirection(0); ctx.SetState(ctx.GetIdleState()); return; } //--- Retry close if position still open and under attempt limit if(m_close_attempts < m_max_attempts) { Print("[CExitState] Position still open. Retrying close."); SubmitClose(ctx); } else { //--- Max retries exhausted: return to monitoring to avoid orphaned position Print("[CExitState] Max close attempts reached. Returning to CInTradeState."); ctx.SetState(ctx.GetInTradeState()); } }
Метод SubmitClose() увеличивает значение m_close_attempts перед вызовом OrderSend(), а не после него, поэтому неудачная попытка отправки всё равно учитывается в лимите попыток. При отправке ордера на закрытие полю request.position присваивается тикет открытой позиции. Это необходимо для закрытия конкретной позиции по тикету, чтобы не полагаться на сопоставление по торговому инструменту и объему на стороне брокера. Политика исполнения ORDER_FILLING_IOC совпадает с настройкой ордера на вход в рынок, обеспечивая единообразное исполнение ордеров как при входе в рынок, так и при выходе из позиции. Когда метод Evaluate() обнаруживает, что still_open равно false, он перед переходом вызывает ctx.SetPendingDirection(0), сбрасывая флаг направления, чтобы при следующей активации CIdleState не сохранялось устаревшее значение направления из предыдущего торгового цикла. Возврат в CInTradeState после пяти неудачных попыток закрытия — намеренная мера безопасности. Вместо возврата в состояние ожидания с открытой позицией без контроля автомат возобновляет ее мониторинг, чтобы последующий сигнал выхода из позиции мог инициировать новую попытку закрытия при следующем пересечении скользящих средних.

Рисунок 3 — Диаграмма переходов между состояниями. Конечный автомат (FSM) циклически проходит через четыре состояния. Основные переходы (зеленые линии) соответствуют обычной последовательности торговых операций: ожидание → вход в рынок → сопровождение позиции → выход из позиции → ожидание. Пути восстановления (красные пунктирные линии) предусмотрены на случай отклонения ордера, истечения времени ожидания или неудачных попыток закрытия позиции. Закрытие позиции извне (оранжевая пунктирная линия) приводит к возврату непосредственно в состояние ожидания, минуя состояние выхода из позиции.
Анализ накладных расходов и структурные ограничения
Затраты на виртуальную диспетчеризацию: при каждом вызове Update() выполняется один виртуальный вызов метода через указатель m_current_state. В MQL5 виртуальная диспетчеризация выполняется через таблицу виртуальных методов (vtable): считывается адрес таблицы для объекта, на который ссылается указатель, затем в ней находится запись нужного метода и управление передается этому методу. Это две операции чтения из памяти и один косвенный переход, которые при прогретом кэше обычно занимают от трех до пяти тактов процессора. Затраты остаются фиксированными независимо от количества состояний в автомате и не зависят от числа возможных переходов, в отличие от последовательной цепочки условий "if-else", затраты на вычисление которой линейно зависят от числа условий.
Объём памяти, занимаемый объектами состояний: память для всех четырёх объектов конкретных состояний выделяется в куче однократно в конструкторе CStrategyContext, реализованном в файле StrategyContextImpl.mqh, а сами объекты существуют на протяжении всего времени работы советника (EA). В самом конечном автомате (FSM) память в куче не выделяется на каждом тике. Общий объём памяти, занимаемый четырьмя экземплярами состояний, невелик и заранее определён.
Атомарность перехода: метод SetState() синхронно выполняет методы OnExit() и OnEnter() в рамках одного тика. Это означает, что если состояние CInTradeState инициирует переход в состояние CExitState, отправка ордера на выход из позиции в методе CExitState::OnEnter() происходит в рамках того же вызова OnTick(), в котором было обнаружено условие выхода из позиции, а не на следующем тике. Такое поведение корректно, когда выход из позиции требует своевременной реакции, однако при реализации методов OnEnter() необходимо учитывать, что они выполняются непосредственно в обработчике текущего тика.
Защита от повторного входа в текущее состояние: перед выполнением последовательности действий перехода метод SetState() проверяет, является ли запрашиваемое следующее состояние тем же объектом, что и текущее. Это предотвращает вызов методов OnExit() и OnEnter() при переходе, который не меняет текущее состояние.
Отсутствие состояний истории: данная реализация не поддерживает сохранение предыдущих состояний. Конечный автомат (FSM) всегда входит в состояние заново, вызывая OnEnter(). Для советников (EA), которым требуются составные состояния или возможность возвращаться к ранее сохранённым состояниями, этот паттерн необходимо расширить, добавив стек состояний.

Рисунок 4 — Переход между состояниями за один тик. Когда CInTradeState обнаруживает сигнал выхода из позиции в методе Evaluate(), он вызывает ctx.SetState() для перехода в состояние CExitState. Сначала выполняется метод OnExit() покидаемого состояния, затем заменяется указатель, записывается тик входа в новое состояние и выполняется его метод OnEnter(). Весь переход, включая вызов OrderSend(), завершается синхронно в рамках одного вызова OnTick().
Заключение
Вложенное дерево условий — это не просто эстетическая проблема. Это архитектурная проблема. Когда логика определения состояния и логика его поведения находятся в теле одной функции, любое изменение одной из них рискует нарушить работу другой. Ошибки, внесённые таким образом, обычно проявляются как сбои в пограничных случаях при определённых рыночных условиях; такие сбои трудно воспроизвести и практически невозможно локализовать одним лишь чтением кода.
Паттерн конечного автомата решает эту проблему, делая неявное явным. Каждая фаза работы советника (EA) представлена именованным классом с определённой точкой входа, методом оценки, вызываемым на каждом тике, и определённой точкой выхода. Триггеры перехода описаны в таблице, которую можно изучать независимо от конкретной реализации. Ошибка при отправке ордера на вход в рынок локализована в классе CEntryState. Сбой в обновлении трейлинг-стопа локализован в CInTradeState. В журнале регистрируется каждый переход между состояниями с отметкой времени соответствующего тика, что обеспечивает полную трассировку выполнения, напрямую соотносимую со структурой классов.
Разделение на три файла — StrategyContext.mqh, States.mqh и StrategyContextImpl.mqh — устраняет циклическую зависимость подключаемых файлов, возникающую, когда телам методов состояний требуется полное определение CStrategyContext, а классу контекста необходимо создавать объекты состояний. Это структурное ограничение однопроходного компилятора MQL5, а не проектное решение, однако полученная архитектура проста и понятна: StrategyContextImpl.mqh служит единственной точкой входа для любого файла, которому требуется конечный автомат (FSM) целиком, и директива #include в советнике (EA) явно отражает это.
Дополнительные затраты сводятся к одной виртуальной диспетчеризации на каждый тик и выделению фиксированного объёма памяти для четырёх объектов состояний при инициализации. Оба вида затрат ограничены, предсказуемы и не зависят от количества состояний или переходов в автомате.
По мере развития стратегий подход на основе конечного автомата (FSM) позволяет локализовать сложность, благодаря чему расширять, тестировать и отлаживать стратегию значительно проще, чем при монолитной реализации OnTick().Программы, использованные в статье:
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | IState.mqh | Подключаемый файл | Абстрактный интерфейс состояния, в котором OnEnter(), Evaluate(), OnExit(), GetStateId() и GetStateName() объявлены как чисто виртуальные методы; содержит ENUM_STRATEGY_STATE и предварительное объявление CStrategyContext |
| 2 | StrategyContext.mqh | Подключаемый файл | Объявление класса CStrategyContext с предварительными объявлениями всех четырёх классов конкретных состояний; содержит Update(), SetState(), методы запроса состояния и тела всех методов доступа к скалярным данным; не подключает States.mqh |
| 3 | StrategyContextImpl.mqh | Подключаемый файл | Подключает сначала StrategyContext.mqh, затем States.mqh в порядке, определяемом зависимостями; содержит конструктор и деструктор CStrategyContext, а также реализации четырёх методов доступа GetXxxState(), для которых требуются полные определения классов конкретных состояний; советник (EA) подключает только этот файл |
| 4 | States.mqh | Подключаемый файл | Подключает StrategyContext.mqh, чтобы в телах всех методов состояний можно было вызывать ctx.GetSymbol(), ctx.SetState() и другие методы контекста; содержит полные реализации классов CIdleState, CEntryState, CInTradeState и CExitState |
| 5 | StateMachineEA.mq5 | Демонстрационный советник (EA) | Демонстрационный советник (EA), у которого StrategyContextImpl.mqh — единственный подключаемый файл; связывает CStrategyContext с событиями поступления тиков в реальном времени, отображает текущее состояние на графике и выводит сообщения о переходах между состояниями в журнал |
| 6 | Strategy_State_Machine.zip | Архив в формате ZIP | ZIP-архив, содержащий все прилагаемые файлы и их пути относительно корневой папки терминала. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22950
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Отложенная загрузка хендлов индикаторов в MQL5: Менеджер ресурсов для мультитаймфреймовых советников
Фильтрация Вьеториса — Рипса в MQL5: Построение симплексов и граничной матрицы
Архитектура машинного обучения в MetaTrader 5 (Часть 18): Исправленный последовательный бутстрэп — клонирование, стирание типа и инструментарий сравнения
Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования