English
preview
Проектирование конечного автомата торговой стратегии на MQL5: Замена вложенных конструкций if-else явными состояниями

Проектирование конечного автомата торговой стратегии на MQL5: Замена вложенных конструкций if-else явными состояниями

MetaTrader 5 — Торговые системы |
48 0
Ushana Kevin Iorkumbul
Ushana Kevin Iorkumbul

Введение

Разработка типичного советника (EA) начинается с функции OnTick(), в которой легко разобраться. Одно условие проверяет, открыта ли позиция, другое оценивает сигнал входа в рынок, третье проверяет приемлемость спреда, а четвёртое — активность торговой сессии. Четыре условия — понятные, наглядные и простые в отладке.

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

Корень проблемы в том, что у советника (EA) есть неявные состояния — ожидание, поиск сигнала входа в рынок, сопровождение позиции, управление выходом из позиции, но они закодированы разбросанными по коду комбинациями булевых флагов, а не представлены как отдельные явные состояния. При использовании неявных состояний код их обработки смешан с кодом определения границ между состояниями. В результате в каждой точке проверки функция должна отвечать на два вопроса: каково текущее состояние и какое действие следует выполнить. Это две разные обязанности, и именно их совмещение делает код хрупким.

Монолитная функция OnTick() в сравнении с конечным автоматом (FSM)

Рисунок 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() делегирует ему выполнение.

Структура классов FSM

Рисунок 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

Прикрепленные файлы |
IState.mqh (1.97 KB)
States.mqh (20.76 KB)
StateMachineEA.mq5 (5.73 KB)
Отложенная загрузка хендлов индикаторов в MQL5: Менеджер ресурсов для мультитаймфреймовых советников Отложенная загрузка хендлов индикаторов в MQL5: Менеджер ресурсов для мультитаймфреймовых советников
Мультитаймфреймовые торговые советники, которые инициализируют все хэндлы индикаторов в OnInit(), несут фиксированные затраты на запуск, даже если большинство хэндлов никогда не используется. CIndicatorCache использует отложенную загрузку с поиском по составному ключу, методы получения/освобождения AcquireHandle() и ReleaseHandle() с подсчетом ссылок и детерминированный метод FlushAll() для полной очистки кэша. Хэндлы создаются при первом запросе и повторно используются на последующих тиках, что сокращает задержку при запуске, позволяет избежать повторного выделения памяти в куче и предотвращает утечки ресурсов терминала благодаря централизованному владению ресурсами.
Фильтрация Вьеториса — Рипса в MQL5: Построение симплексов и граничной матрицы Фильтрация Вьеториса — Рипса в MQL5: Построение симплексов и граничной матрицы
Мы преобразуем облако точек, построенное с помощью вложения ценового ряда, в фильтрацию Вьеториса — Рипса и соответствующую ей граничную матрицу. В статье вершины, ребра и треугольники перечисляются с указанием значений фильтрации и сортируются в порядке добавления в фильтрацию; также создаются таблицы поиска вершин и ребер за время O(1). Вы получаете классы CTDARips и CTDABoundary на MQL5, а также разреженную граничную матрицу над Z/2, пригодную для последующей редукции при вычислении персистентности.
Архитектура машинного обучения в MetaTrader 5 (Часть 18): Исправленный последовательный бутстрэп — клонирование, стирание типа и инструментарий сравнения Архитектура машинного обучения в MetaTrader 5 (Часть 18): Исправленный последовательный бутстрэп — клонирование, стирание типа и инструментарий сравнения
В статье выявляются два дефекта, которые сводят на нет эффект последовательного бутстрэпа при перекрестной проверке: стирание типа SequentiallyBootstrappedBaggingClassifier и несоответствие размерностей на уровне фолда из-за клонирования полного набора информационных интервалов наблюдений. Предлагаемое исправление сохраняет принадлежность классификатора к исходному классу, добавляет функцию _find_seq_bagging() для повторной передачи в CalibratorCV.fit данных t1, ограниченных текущим фолдом, и сбрасывает состояние при каждом разбиении. Новый модуль bootstrap_comparison выводит OOF-метрики и на наблюдениях вне бутстрэп-выборки (OOB), а также данные об использовании памяти, позволяя проверить правильность применения последовательного отбора и количественно оценить его влияние.
Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5 Индикатор скользящего коэффициента Шарпа с доверительными границами в MQL5
В данной статье представлен пользовательский индикатор для MetaTrader 5, который вычисляет скользящий коэффициент Шарпа в годовом выражении и отображает полосы статистической значимости с настраиваемой z-оценкой на основе асимптотической стандартной ошибки по формуле Ло. В индикаторе используется кольцевой буфер доходностей с инкрементальным расчетом дисперсии, что обеспечивает обновление за время O(1). Мы объясняем, как неопределенность масштабируется пропорционально n^(-1/2), почему интервалы расширяются при высоких значениях коэффициента Шарпа и как настроить приведение к годовому выражению для каждого инструмента, чтобы корректно использовать индикатор.