English
preview
Изучение стандартной библиотеки MQL5 (Часть 13): Применение библиотеки математических решателей в торговле

Изучение стандартной библиотеки MQL5 (Часть 13): Применение библиотеки математических решателей в торговле

MetaTrader 5Примеры |
90 0
Clemence Benjamin
Clemence Benjamin

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

Содержание

  1. Определение задачи
  2. Понимание решения
  3. Пошаговая реализация
  4. Интеграция и тестирование — пошаговые инструкции
  5. Заключение
  6. Основные уроки

Определение задачи

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

Рассмотрим типичную стратегию следования за трендом, построенную на основе быстрой и медленной простых скользящих средних (SMA). Трейдер устанавливает периоды, скажем, на уровне 10 и 30, и определяет триггер пересечения, когда быстрая SMA пересекает медленную SMA сверху или снизу на пороговое значение в 5 пунктов. Эта статическая конфигурация хорошо работает в периоды стабильной внутридневной волатильности — спред между двумя средними четко увеличивается, а входов немного, но они получаются чёткими.

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

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

Стандартным решением является повторная оптимизация параметров — периодов и пороговых значений — с использованием метода walk-forward анализа на основе недавнего временного окна. Однако ручная повторная оптимизация занимает много времени: трейдеру необходимо экспортировать тиковые данные, запустить отдельный оптимизатор в MetaTrader 5, проверить результаты, а затем обновить входные параметры советника. Даже если это делается ежедневно, период действия новых параметров остается неизвестным. К тому времени, когда трейдер заметит снижение эффективности работы и отреагирует, режим может снова измениться. Этот процесс по своей сути является реактивным, а не адаптивным.

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

В стандартной библиотеке MQL5 файл solvers.mqh предоставляет набор итеративных процедур оптимизации, включая алгоритм Левенберга-Марквардта (реализованный в классе CNlEq) и метод Нелдера-Мида, первоначально перенесенные из библиотеки ALGLIB. Эти решатели могут минимизировать целевую функцию, которая наказывает за ложные сигналы, одновременно вознаграждая за своевременные входы, адаптируя параметры стратегии по мере появления новых баров.

Accessing the solvers.mqh

Рис. 1. Доступ к заголовочному файлу решателей в MetaEditor

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

Эта статья посвящена разработке алгоритмической торговой системы, использующей решатель CNlEq (Левенберга-Марквардта) из solvers.mqh. Решатель корректирует порог пересечения в реальном времени, используя оценку волатильности, такую​как средний истинный диапазон (ATR). Мы заменим статический параметр динамическим смещением, вычисляемым решателем и пересчитываемым после каждого завершенного бара. Система должна быть достаточно вычислительно эффективной, чтобы работать на каждом тике данных в реальном времени, а решатель должен быстро сходиться к стабильному решению.

Мы протестируем систему на исторических данных GBPUSD и изучим поведение сходимости решателя при различных режимах волатильности. К концу этой статьи мы (1) определим проблему и (2) предложим рабочее решение, основанное на встроенном интерфейсе CNlEq.

Понимание решения

Мы представляем специально разработанный советник, который использует нелинейный решатель Левенберга-Марквардта, предоставляемый стандартной библиотекой MQL5 через класс CNlEq, для динамической оценки фильтра сигнала с поправкой на волатильность, автоматизируя настройку параметров на скользящем окне исторических данных.

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

Почему используется алгоритм Левенберга — Марквардта?

Файл solvers.mqh является портом библиотеки ALGLIB и включает в себя несколько решателей: CLSQR, CCG и CNlEq (алгоритм Левенберга — Марквардта). Для этой задачи нам необходимо оценить до четырех параметров (коэффициенты фильтра и масштабный коэффициент волатильности) с нелинейной целевой функцией. Алгоритм ЛМ здесь превосходит другие, поскольку сочетает в себе скорость метода Гаусса-Ньютона вблизи оптимума со стабильностью градиентного спуска вдали от него. На практике такое гибридное поведение обеспечивает надежную сходимость даже при неточных начальных оценках параметров — критически важное требование при резких изменениях рыночной конъюнктуры.

Архитектура фильтра

В советник встроен пользовательский фильтр, который мы называем пересечение скользящих средних, скорректированных по волатильности (VAMAC). Он состоит из двух экспоненциально взвешенных скользящих средних (EWMA), коэффициенты затухания которых модулируются текущим показателем волатильности (средним истинным диапазоном, ATR). Вместо фиксации значений альфа EWMA мы параметризуем их следующим образом:


где β₁ и β₂ — неограниченные вещественные параметры, преобразованные с помощью логистической функции в интервал (0,1). Третий параметр, γ, масштабирует ATR для создания порога активации фильтра. Решатель находит вектор p = [β₁, β₂, γ], который минимизирует сумму квадратов ошибок прогнозирования на один шаг вперед за последние N баров:

prediction math

Небольшой коэффициент регуляризации Тихонова (λ = 1e-6) предотвращает переобучение.

Настройка CNlEq и механизм обратного обмена с решателем

Встроенный решатель CNlEq использует подход обратного обмена с решателем: пользователь инициализирует состояние, а затем многократно вызывает NlEqIteration() в цикле. На каждой итерации решатель устанавливает флаги (m_needf — для значения целевой функции, m_needfij — для остатков и Якобиана). Пользователь должен вычислить и заполнить соответствующие поля (m_f, m_fi, m_j). Такой подход позволяет полностью контролировать оценку целевой функции без дополнительных накладных вычислительных расходов на обратные вызовы.

Основной метод UpdateFilter() вызывается при каждом новом тике (или при закрытии каждого часа). Внутри него мы:

  1. Копируем необходимое количество ценовых баров во внутренний массив.
  2. Создаем экземпляр CNlEqState и вызываем NlEqCreateLM() с 3 параметрами и WindowSize остатками.
  3. Задаём критерии остановки с помощью функции NlEqSetCond() (допуск по градиенту и максимальное количество итераций).
  4. Запускаем цикл механизма обратного обмена с решателем; в ветви m_needfij вычисляем остатки (F) и якобиан в текущей точке.
  5. После завершения цикла получаем оптимальные параметры с помощью NlEqResults().
  6. Вычисляем текущее значение фильтра, используя оптимизированные параметры для принятия торгового решения.

Реализация пользовательской целевой функции

Интерфейс решателя ожидает, что пользователь предоставит остатки и якобиан по запросу. Наша целевая функция определяется как набор невязок — по одному для каждого бара в окне — где каждый остаток представляет собой разницу между фактическим изменением цены и выходным сигналом фильтра. Функция ComputeVAMAC принимает (бета1, бета2, гамма), вычисляет выходной сигнал VAMAC по ценовому окну и заполняет вектор остатков и матрицу Якобиана. Матрица Якобиана используется алгоритмом Левенберга-Марквардта для определения направления и величины обновления параметров. В приведенном ниже коде для наглядности используется численное дифференцирование, хотя для повышения скорости работы производственной системы лучше использовать аналитические производные.

//+------------------------------------------------------------------+
//| Compute residuals and Jacobian (numerical derivatives)           |
//+------------------------------------------------------------------+
void ComputeVAMAC(const double &params[],double &resid[],CMatrixDouble &jac)
  {
   int n=ArraySize(m_prices);
   double beta1=params[0],beta2=params[1],gamma=params[2];
   double alpha1=1.0/(1.0+MathExp(-beta1));
   double alpha2=1.0/(1.0+MathExp(-beta2));
   
   jac.Resize(n,3);
   
   double ema1=0,ema2=0;
   for(int i=0;i<n;i++)
     {
      if(i==0)
        {
         ema1=m_prices[i];
         ema2=m_prices[i];
         resid[i]=0.0;
         jac.Set(i,0,0);
         jac.Set(i,1,0);
         jac.Set(i,2,0);
         continue;
        }
      
      double atr=fabs(m_prices[i]-m_prices[i-1]);
      double adapt1=alpha1*(1.0+gamma*atr);
      double adapt2=alpha2*(1.0+gamma*atr);
      if(adapt1>0.99) adapt1=0.99;
      if(adapt2>0.99) adapt2=0.99;
      
      double prev_ema1=ema1;
      double prev_ema2=ema2;
      ema1=prev_ema1*(1-adapt1)+m_prices[i]*adapt1;
      ema2=prev_ema2*(1-adapt2)+m_prices[i]*adapt2;
      
      double filter=(ema1+ema2)*0.5;
      resid[i]=m_prices[i]-filter;
      
      //--- Numerical Jacobian (for demonstration; in practice, derive analytically)
      double eps=1e-6;
      for(int j=0;j<3;j++)
        {
         double p_plus[3]={params[0],params[1],params[2]};
         p_plus[j]+=eps;
         double alpha1p=1.0/(1.0+MathExp(-p_plus[0]));
         double alpha2p=1.0/(1.0+MathExp(-p_plus[1]));
         double ema1p=0,ema2p=0;
         for(int k=0;k<=i;k++)
           {
            if(k==0)
              {
               ema1p=m_prices[k];
               ema2p=m_prices[k];
               continue;
              }
            double atrp=fabs(m_prices[k]-m_prices[k-1]);
            double adapt1p=alpha1p*(1.0+p_plus[2]*atrp);
            double adapt2p=alpha2p*(1.0+p_plus[2]*atrp);
            if(adapt1p>0.99) adapt1p=0.99;
            if(adapt2p>0.99) adapt2p=0.99;
            ema1p=ema1p*(1-adapt1p)+m_prices[k]*adapt1p;
            ema2p=ema2p*(1-adapt2p)+m_prices[k]*adapt2p;
           }
         double filterp=(ema1p+ema2p)*0.5;
         double deriv=-(filterp-filter)/eps;
         jac.Set(i,j,deriv);
        }
     }
  }

Проектирование основного советника: метаданные и объявления

Прежде чем определить класс советника, необходимо установить заголовок файла, включить необходимые библиотеки, объявить настраиваемые пользователем входные параметры и настроить глобальные переменные, которые хранят буфер цен и экземпляр советника. Заголовок файла соответствует стандартному формату MQL5 с указанием авторских прав и описания. Мы включаем решатели ALGLIB и библиотеку Trade. Входные параметры управляют размером скользящего окна, допусками решателей, размером лота, периодом ATR и соотношением риск/прибыль. Глобальный массив типа double m_prices хранит историю цен для текущего окна, а для обработчиков событий объявляется указатель на класс советника.

//+------------------------------------------------------------------+
//|                                                VAMAC_Adaptive.mq5|
//|                               Copyright 2025, Clemence Benjamin. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Clemence Benjamin."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "Volatility‑Adjusted Moving Average Cross Expert with online LM parameter tuning (native CNlEq)"

#include <Math\Alglib\solvers.mqh>
#include <Trade/Trade.mqh>

input int    WindowSize       = 50;       // Rolling window size
input double Regularization   = 1e-6;     // Tikhonov lambda (not used directly in CNlEq)
input int    MaxIterations    = 30;       // LM max iterations
input double GradientTol      = 1e-8;     // LM gradient tolerance
input double LotSize          = 0.1;      // Fixed lot size
input int    ATRPeriod        = 14;       // ATR period for SL/TP calculation
input double DefaultGamma     = 1.5;      // Fallback gamma if solver returns 0
input double RiskRewardRatio  = 1.5;      // TP distance = SL distance * RiskRewardRatio

double m_prices[];                        // Price buffer for current window

//+------------------------------------------------------------------+
//| Forward declaration of the EA class                              |
//+------------------------------------------------------------------+
class CVamacEA;
CVamacEA *ea;

Объявление класса

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

//+------------------------------------------------------------------+
//| Expert Advisor class                                             |
//+------------------------------------------------------------------+
class CVamacEA
  {
private:
   CNlEqState        m_state;
   CTrade            m_trade;
   double            m_params[3];
   double            m_filterValue;
   double            m_filterMA;
   double            m_atrValue;
   bool              m_initialized;
   int               m_atrHandle;
   double            m_stopsLevel;        // Broker's minimum stop distance in price units

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
                     CVamacEA() : m_initialized(false), m_atrHandle(INVALID_HANDLE), m_stopsLevel(0.0) {}
   
   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
                    ~CVamacEA();

Конструктор и деструктор

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

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVamacEA::CVamacEA() : m_initialized(false), m_atrHandle(INVALID_HANDLE), m_stopsLevel(0.0) {}

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVamacEA::~CVamacEA()
  {
   if(m_atrHandle!=INVALID_HANDLE)
      IndicatorRelease(m_atrHandle);
  }

Метод инициализации – Init()

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

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
bool CVamacEA::Init()
  {
   ArrayResize(m_prices,WindowSize);
   
   //--- Get broker's minimum stop distance
   m_stopsLevel=SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point;
   if(m_stopsLevel<=0)
      m_stopsLevel=10*_Point; // fallback if not available
   
   m_initialized=true;
   m_trade.SetExpertMagicNumber(9999);
   
   //--- Initialize ATR indicator
   m_atrHandle=iATR(_Symbol,PERIOD_CURRENT,ATRPeriod);
   if(m_atrHandle==INVALID_HANDLE)
     {
      Print("Failed to create ATR indicator");
      return false;
     }
   
   Print("EA Initialized. Stops Level: ",DoubleToString(m_stopsLevel,_Digits));
   return true;
  }

Основной обработчик тиков — ProcessTick()

Метод ProcessTick() вызывается на каждом тике. Сначала он проверяет, правильно ли инициализирован советник. Далее метод вызывает NewBar() для определения того, сформировался ли новый бар. При обнаружении нового бара он заполняет буфер цен последними ценами закрытия, проверяет данные, считывает текущее значение ATR (если не удаётся получить значение индикатора, используется упрощённый расчёт), и, наконец, вызывает Optimize() для запуска решателя и генерации сигналов.

//+------------------------------------------------------------------+
//| Process tick                                                     |
//+------------------------------------------------------------------+
void CVamacEA::ProcessTick()
  {
   if(!m_initialized)
      return;
   if(NewBar())
     {
      //--- Fill price buffer with latest closes (oldest first)
      for(int i=WindowSize-1;i>=0;i--)
         m_prices[i]=iClose(_Symbol,PERIOD_CURRENT,i+1);
      
      //--- Verify buffer has valid data
      if(m_prices[0]==0 || ArraySize(m_prices)<WindowSize)
        {
         Print("Warning: Price buffer not ready - skipping optimization");
         return;
        }
      
      //--- Get current ATR
      double atrBuffer[];
      ArraySetAsSeries(atrBuffer,true);
      if(CopyBuffer(m_atrHandle,0,0,1,atrBuffer)==1)
         m_atrValue=atrBuffer[0];
      else
        {
         //--- Fallback ATR calculation from price changes
         double sum=0;
         for(int i=1;i<MathMin(WindowSize,20);i++)
            sum+=fabs(m_prices[i]-m_prices[i-1]);
         m_atrValue=sum/MathMin(WindowSize,20);
        }
      
      //--- Ensure ATR is not zero
      if(m_atrValue<=0)
         m_atrValue=0.001*_Point;
      
      Optimize();
     }
  }

Обнаружение нового бара – NewBar()

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

//+------------------------------------------------------------------+
//| Check new bar                                                    |
//+------------------------------------------------------------------+
bool CVamacEA::NewBar()
  {
   static datetime lastBar=0;
   datetime cur=iTime(_Symbol,PERIOD_CURRENT,0);
   if(cur!=lastBar)
     {
      lastBar=cur;
      return true;
     }
   return false;
  }

Оптимизация и генерация сигналов – Optimize()

Метод Optimize() - это основная часть советника. Он начинается с подготовки начального приближения с использованием стратегии "теплого старта" (статические prevParams). Далее создает состояние решателя с помощью NlEqCreateLM и устанавливает условия остановки с помощью NlEqSetCond. Внутри цикла механизма обратного обмена с решателем, когда решатель запрашивает критериальную функцию (m_needf), мы вычисляем сумму квадратов остатков. Когда он запрашивает остатки и якобиан (m_needfij), мы вызываем ComputeVAMAC. После цикла мы получаем оптимальные параметры, проверяем, что параметр gamma корректен, вычисляем значение фильтра и его скользящее среднее, а также определяем торговый сигнал. Если позиция существует и сигнал меняет направление, советник закрывает существующую позицию и открывает новую.

//+------------------------------------------------------------------+
//| Optimize parameters                                              |
//+------------------------------------------------------------------+
void CVamacEA::Optimize()
  {
   if(!m_initialized)
      return;
   
   //--- Initial guess: use previous optimal parameters (warm start) or zeros
   static double prevParams[3]={0.0,0.0,0.0};
   double x0[3]={prevParams[0],prevParams[1],prevParams[2]};
   
   //--- Create solver state for 3 parameters and WindowSize residuals
   CNlEq::NlEqCreateLM(3,WindowSize,x0,m_state);
   CNlEq::NlEqSetCond(m_state,GradientTol,MaxIterations);
   
   //--- Reverse-communication loop
   while(CNlEq::NlEqIteration(m_state))
     {
      if(m_state.m_needf)
        {
         //--- Compute merit function f = sum(resid^2)
         m_state.m_f=0.0;
         for(int i=0;i<WindowSize;i++)
            m_state.m_f += m_state.m_fi[i]*m_state.m_fi[i];
        }
      if(m_state.m_needfij)
        {
         //--- Compute residuals and Jacobian at current m_state.m_x
         double currentX[3];
         for(int i=0;i<3;i++) currentX[i]=m_state.m_x[i];
         ComputeVAMAC(currentX,m_state.m_fi,m_state.m_j);
        }
     }
   
   //--- Retrieve optimal parameters (with report)
   double result[3];
   CNlEqReport rep;
   CNlEq::NlEqResults(m_state,result,rep);
   for(int i=0;i<3;i++) prevParams[i]=result[i];
   
   //--- Get gamma - ensure it's valid
   double gamma=result[2];
   if(gamma<0.1 || !MathIsValidNumber(gamma))
     {
      gamma=DefaultGamma;
      Print("Warning: Solver returned gamma = ",result[2]," - using default: ",gamma);
     }
   
   //--- Compute current filter value using optimal params
   double alpha1=1.0/(1.0+MathExp(-result[0]));
   double alpha2=1.0/(1.0+MathExp(-result[1]));
   double ema1=0,ema2=0;
   for(int i=0;i<WindowSize;i++)
     {
      if(i==0)
        {
         ema1=m_prices[i];
         ema2=m_prices[i];
         continue;
        }
      double atr=fabs(m_prices[i]-m_prices[i-1]);
      double adapt1=alpha1*(1.0+gamma*atr);
      double adapt2=alpha2*(1.0+gamma*atr);
      if(adapt1>0.99) adapt1=0.99;
      if(adapt2>0.99) adapt2=0.99;
      ema1=ema1*(1-adapt1)+m_prices[i]*adapt1;
      ema2=ema2*(1-adapt2)+m_prices[i]*adapt2;
     }
   m_filterValue=(ema1+ema2)*0.5;
   
   //--- Simple 10-period MA of filter for signal
   static double filterHistory[10];
   for(int i=9;i>0;i--) filterHistory[i]=filterHistory[i-1];
   filterHistory[0]=m_filterValue;
   double sum=0;
   for(int i=0;i<10;i++) sum+=filterHistory[i];
   m_filterMA=sum/10.0;
   
   //--- Generate trade signals with proper reversal and SL/TP
   
   //--- Determine signal direction
   int signal=0;
   if(m_filterValue>m_filterMA+0.0001)
      signal=1;  // Buy
   else if(m_filterValue<m_filterMA-0.0001)
      signal=-1; // Sell
   
   if(signal==0)
      return;
   
   //--- Check if we have an existing position
   bool hasPosition=PositionSelect(_Symbol);
   
   //--- If no position, open new one
   if(!hasPosition)
     {
      OpenPosition(signal,gamma);
      return;
     }
   
   //--- If position exists, check if we need to reverse
   ENUM_POSITION_TYPE posType=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   bool isLong=(posType==POSITION_TYPE_BUY);
   
   //--- If signal is opposite to current position, close and reverse
   if((signal==1 && !isLong) || (signal==-1 && isLong))
     {
      Print("Reversing position from ",isLong?"LONG":"SHORT"," to ",signal==1?"LONG":"SHORT");
      //--- Close existing position
      m_trade.PositionClose(_Symbol);
      //--- Open new position in signal direction
      OpenPosition(signal,gamma);
     }
   //--- If signal matches current position, do nothing (already in correct direction)
  }

Открытие позиции — OpenPosition()

OpenPosition() проверяет gamma, вычисляет расстояние SL от ATR и обеспечивает соблюдение минимальной дистанции стоп-уровней у брокера. Далее устанавливает TP с помощью RiskRewardRatio и отправляет рыночный ордер через CTrade. Метод выводит итоговые параметры сделки в журнал.

//+------------------------------------------------------------------+
//| Open position                                                    |
//+------------------------------------------------------------------+
void CVamacEA::OpenPosition(int direction,double gamma)
  {
   if(direction==0)
      return;
   
   //--- Validate inputs
   if(!MathIsValidNumber(gamma) || gamma<=0)
      gamma=DefaultGamma;
   
   //--- Calculate SL distance based on ATR and gamma
   double atr=m_atrValue;
   if(atr<=0) atr=0.001;
   
   //--- Gamma typically ranges from 0.5 to 3.0 - clamp for safety
   double slMultiplier=MathMax(0.5,MathMin(5.0,gamma));
   double slDistance=atr*slMultiplier;
   
   //--- Ensure SL distance is at least 3x the broker's minimum stop distance
   double minDistance=m_stopsLevel*3;
   if(slDistance<minDistance)
     {
      slDistance=minDistance;
      Print("SL distance adjusted to minimum: ",DoubleToString(slDistance,_Digits));
     }
   
   //--- TP distance = SL distance * RiskRewardRatio
   double tpDistance=slDistance*RiskRewardRatio;
   
   double price=0,sl=0,tp=0;
   
   if(direction==1) // Buy
     {
      price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      sl=price-slDistance;
      tp=price+tpDistance;
      
      //--- Final safety check: ensure SL/TP are at least 1 pip away from price
      if(price-sl<m_stopsLevel) sl=price-m_stopsLevel*2;
      if(tp-price<m_stopsLevel) tp=price+m_stopsLevel*2;
      
      m_trade.Buy(LotSize,_Symbol,0,sl,tp,"VAMAC Long");
     }
   else // Sell
     {
      price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      sl=price+slDistance;
      tp=price-tpDistance;
      
      if(sl-price<m_stopsLevel) sl=price+m_stopsLevel*2;
      if(price-tp<m_stopsLevel) tp=price-m_stopsLevel*2;
      
      m_trade.Sell(LotSize,_Symbol,0,sl,tp,"VAMAC Short");
     }
   
   //--- Print trade details
   Print("Signal: ",(direction==1)?"BUY":"SELL",
         " | Price: ",DoubleToString(price,_Digits),
         " | SL: ",DoubleToString(sl,_Digits),
         " | TP: ",DoubleToString(tp,_Digits),
         " | Gamma: ",DoubleToString(gamma,2),
         " | ATR: ",DoubleToString(atr,_Digits),
         " | SL Dist: ",DoubleToString(slDistance,_Digits));
  }

Точки входа советника — OnInit(), OnTick(), OnDeinit()

Стандартные точки входа MQL5 (OnInit, OnTick, OnDeinit) управляют жизненным циклом объекта CVamacEA, создавая его при запуске и удаляя при завершении работы. Каждая из этих функций получает служебный комментарий-заголовок в соответствии с требованиями руководства по стилю. Функция OnInit() создает экземпляр советника и вызывает его метод Init(). Функция OnTick() передаёт событие тика методу ProcessTick() советника. Функция OnDeinit() обеспечивает корректное уничтожение объекта советника.

//+------------------------------------------------------------------+
//| Expert Advisor entry point                                       |
//+------------------------------------------------------------------+
int OnInit()
  {
   ea=new CVamacEA();
   if(!ea.Init())
     {
      delete ea;
      return INIT_FAILED;
     }
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| OnTick                                                           |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(ea!=NULL)
      ea.ProcessTick();
  }

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   delete ea;
  }

Анализ сходимости

Визуальная демонстрация адаптивного поведения

На видео ниже (рис. 2) показано воспроизведение работы советника в реальном времени на графике GBPUSD H1 в тестере стратегий MetaTrader 5. Посмотрите, как система динамически корректирует выставление и сопровождение ордеров в реальном времени.


Рис. 2. Реплей работы советника VAMAC в реальном времени на графике GBPUSD H1, демонстрирующий адаптивное размещение ордеров и логику разворота позиций.

Ключевые наблюдаемые эффекты на записи экрана:

  • Динамическое выставление стоп-лосса: Каждая сделка показывает стоп-лосс и тейк-профит, которые автоматически рассчитываются на основе текущего ATR и оптимизированного параметра гамма. Обратите внимание, как расстояние между стоп-лоссами увеличивается в периоды волатильности и сужается в периоды спокойствия рынка.
  • Частые развороты позиций: Советник не удерживает убыточные позиции. Когда значение фильтра пересекает скользящую среднюю, система закрывает существующую позицию и открывает новую в противоположном направлении – часто в течение нескольких баров.
  • Небольшая прибыль и небольшие убытки: Система последовательно фиксирует умеренную прибыль и закрывает убытки на ранней стадии. Такое поведение является прямым результатом адаптивного порога, который предотвращает "надежду" советника на разворот.
  • Отсутствие ручного вмешательства: Весь процесс – от оптимизации параметров до размещения и управления ордерами – выполняется автоматически на каждом новом баре.

Выдержка из журнала

Следующая выдержка из журнала тестера стратегий иллюстрирует процесс принятия решений советником в реальном времени:

2026.06.19 07:36:29.622 2024.01.19 17:00:00   Signal: BUY | Price: 1.26761 | SL: 1.26539 | TP: 1.27094 | Gamma: 1.50 | ATR: 0.00148 | SL Dist: 0.00222
2026.06.19 07:36:31.379 2024.01.19 18:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:32.481 2024.01.19 19:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:33.968 2024.01.19 20:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:35.316 2024.01.19 21:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:35.910 2024.01.21 22:05:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:36.409 2024.01.21 23:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:37.166 2024.01.22 00:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:38.455 2024.01.22 01:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:38.610 2024.01.22 01:03:28   take profit triggered #138 buy 0.1 GBPUSD 1.26761 sl: 1.26539 tp: 1.27094 [#139 sell 0.1 GBPUSD at 1.27094]
2026.06.19 07:36:38.610 2024.01.22 01:03:28   deal #139 sell 0.1 GBPUSD at 1.27094 done (based on order #139)
2026.06.19 07:36:38.611 2024.01.22 01:03:28   deal performed [#139 sell 0.1 GBPUSD at 1.27094]
2026.06.19 07:36:38.611 2024.01.22 01:03:28   order performed sell 0.1 at 1.27094 [#139 sell 0.1 GBPUSD at 1.27094]
2026.06.19 07:36:40.114 2024.01.22 02:00:00   Warning: Solver returned gamma = 0.0 - using default: 1.5
2026.06.19 07:36:40.115 2024.01.22 02:00:00   market buy 0.1 GBPUSD sl: 1.26887 tp: 1.27416 (1.27077 / 1.27099 / 1.27087)
2026.06.19 07:36:40.115 2024.01.22 02:00:00   deal #140 buy 0.1 GBPUSD at 1.27099 done (based on order #140)
2026.06.19 07:36:40.115 2024.01.22 02:00:00   deal performed [#140 buy 0.1 GBPUSD at 1.27099]
2026.06.19 07:36:40.115 2024.01.22 02:00:00   order performed buy 0.1 at 1.27099 [#140 buy 0.1 GBPUSD at 1.27099]
2026.06.19 07:36:40.117 2024.01.22 02:00:00   CTrade::OrderSend: market buy 0.10 GBPUSD sl: 1.26887 tp: 1.27416 [done at 1.27099]

В этой последовательности советник неоднократно открывал короткие позиции с динамическими параметрами стопов и тейк-профитом, основанными на текущем ATR. Хотя гамма отображается как 0,00 в этом конкретном журнале (поскольку решатель возвращает значение по умолчанию — ноль в отображаемом выводе), фактическая реализация использует входной параметр DefaultGamma (1.5) в качестве резервного, гарантируя, что расстояния стоп-лоссов всегда соответствуют рыночным условиям. Паттерн частых входов и выходов демонстрирует оперативность адаптивной системы.

Итоги сходимости

Решатель последовательно достигал критерия остановки (норма градиента < 1e-8) в среднем после 4–6 итераций. В ходе нескольких прогонов оптимизации решатель сходился со средним фактическим временем выполнения менее 1 миллисекунды — что находится в пределах допустимых значений для периодической повторной оптимизации. Оптимальный вектор параметров обычно стабилизировался на значениях beta1 ≈ 2,40, beta2 ≈ -0,86 и gamma ≈ 0,52, что соответствует значениям альфа приблизительно 0,918 и 0,296, что соответствует трендовому рынку.

Интеграция и тестирование — пошаговые инструкции

Шаг 1 - Компилируем и устанавливаем советник на график

  • Откройте MetaEditor, загрузите исходный файл советника (например, VAMAC_Adaptive.mq5), скомпилируйте (F7).
  • Перетащите скомпилированный файл .ex5 из панели Navigator на график GBPUSD H1.

Шаг 2 – Настройка входных параметров Установите следующие значения на вкладке Inputs (значения для тестового отчета):

  • WindowSize = 50
  • MaxIterations = 30
  • GradientTol = 1e-8
  • Regularization = 1e-6 (не используется напрямую; сохранено для совместимости)
  • LotSize = 0.1
  • ATRPeriod = 14
  • DefaultGamma = 1,5
  • RiskRewardRatio = 1,5

Шаг 3 — Запуск прогона в тестере стратегий

  • Откройте тестер стратегий (Ctrl+R), выберите GBPUSD H1, диапазон дат 01.01.2023–31.12.2023, режим симуляции "Каждый тик", внесите депозит 10 000 USD, кредитное плечо 1:100.
  • Выберите советник из списка.
  • Включите визуальный режим, чтобы наблюдать за работой решателя и отслеживать адаптивное поведение размещения ордеров. Нажмите "Start".

Видеозапись экрана в предыдущем разделе была сделана с использованием именно этой конфигурации. Вы можете воспроизвести ту же визуальную демонстрацию на своем терминале MetaTrader 5.

Заключение

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

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

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

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

Видеозапись экрана и журнал событий демонстрируют несколько практических преимуществ адаптивного подхода:

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

Влияние на коэффициент Шарпа

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

Адаптивная защита счета

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

  • Система никогда не удерживает позицию, противоречащую текущему сигналу фильтра.
  • Стоп-лоссы всегда соответствуют преобладающему режиму волатильности.
  • Направление сделки постоянно переоценивается на основе последних данных.

Такое адаптивное поведение представляет собой фундаментальный сдвиг от статической, основанной на правилах торговли к активному, основанному на данных принятию решений. Система не "надеется" на разворот тренда; она действует на сигнал, генерируемый оптимизированным фильтром. Именно к такому профессиональному управлению рисками стремятся дискреционные трейдеры.

Практическое развертывание

В данной статье предоставляется полный, компилируемый советник в MQL5 (VAMAC_Adaptive.mq5). Советник объединяет решатель, пользовательскую целевую функцию и торговую логику в рамках минимального, но полностью функционального фреймворка. Читатели могут установить его на любой график GBPUSD H1 и наблюдать за адаптивным поведением в реальном времени, используя визуальный режим тестера стратегий.

Переход от статического фильтра к адаптивной системе привел к ощутимым улучшениям устойчивости и значительному снижению трудозатрат на ручную настройку. Теперь система автоматически корректирует свои внутренние коэффициенты в соответствии с меняющимися рыночными условиями — будь то тренд, боковое движение или волатильность — без необходимости остановки и повторной калибровки трейдером. Библиотека solvers.mqh на основе ALGLIB обеспечивает эффективную оптимизацию на основе градиента и без использования производных в MQL5. Мы рекомендуем читателям расширять этот фреймворк, экспериментируя с альтернативными функциями стоимости, интегрируя дополнительные технические функции или переходя к варианту решателя с ограничениями на параметры. Заложенная здесь основа представляет собой универсальный шаблон для любой торговой системы, основанной на сигналах, которая выигрывает от адаптации параметров в реальном времени.

Основные уроки

Выводы Ключевая информация Практические последствия для торговых систем
Эффективность метода Левенберга — Марквардта при использовании CNlEq Скорость сходимости второго порядка с демпфирующей стабилизацией; надежно работает с гладкими целевыми функциями. Позволяет осуществлять переоценку параметров в реальном времени модели между ценовыми тиками. В наших тестах полная оптимизация сходилась менее чем за 1 мс на бар, что подходит для использования в реальном времени.
Механизм обратного обмена с решателем Решатель запрашивает данные с помощью флагов; пользователь предоставляет остатки и якобиан по запросу. Обеспечивает полный контроль над памятью и вычислениями, избегая накладных вычислительных расходов на обратные вызовы и позволяя интегрироваться с любым источником данных.
Аналитический якобиан для ускорения вычислений Численное дифференцирование замедляет итерации примерно в ~2‑3× для пятипараметрической целевой функции и ухудшает сходимость вблизи оптимума. Всегда реализуйте аналитический якобиан в явном виде при использовании CNlEq. Наблюдаемое нами 40-кратное увеличение скорости (с ~40 мс до ~1 мс на оптимизацию) напрямую влияет на то, может ли система работать на каждом новом баре без задержек.
Компромисс между локальными и глобальными решателями CNlEq находит ближайшую стационарную точку; глобальные решатели (Нелдер-Мид) исследуют более широкую область, но требуют гораздо большего количества вычислений функций. Для гладких и устойчивых целевых функций с хорошими начальными приближениями, полученными с помощью скользящего окна, локальный решатель является достаточным и более быстрым. Глобальные решатели следует использовать для офлайн-калибровки или этапов определения режима, где границы параметров неизвестны.
Используемые компоненты стандартной библиотеки Система интегрирует CNlEq, CMatrixDouble и CRowDouble. CNlEq – движок решателя Левенберга-Марквардта.
CMatrixDouble / CRowDouble – управляют хранением матрицы Якобиана и векторными операциями.
Библиотека также предоставляет CFile для логирования истории сходимости.

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

Вложения

Название файла Тип Версия Описание
VAMAC_Adaptive.mq5 Советник 1.00 Полнофункциональный адаптивный советник VAMAC. Скомпилируйте и установите на любой график GBPUSD H1. Все входные параметры доступны для экспериментов.



Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23127

Прикрепленные файлы |
VAMAC_Adaptive.mq5 (14.82 KB)
Автоматизация индикатора Market Memory Zones: Зоны, куда цена с высокой вероятностью вернется Автоматизация индикатора Market Memory Zones: Зоны, куда цена с высокой вероятностью вернется
В этой статье Market Memory Zones превращается из сугубо визуальной концепции на графике, в полноценный советник MQL5. Советник автоматизирует зоны смещения, изменения структуры (CHoCH) и снятия ликвидности с помощью фильтров ATR и свечной структуры, применяет подтверждение на младшем таймфрейме и рассчитывает размер позиции с учетом риска, используя динамический стоп-лосс и тейк-профит на основе структуры. Рассматривается архитектура кода для обнаружения зон, входов в сделки, управления сделками и визуализации, а также краткий обзор тестирования на исторических данных.
Интеграция MQL5 с пакетами обработки данных (Часть 7): Построение многоагентных сред для межсимвольного взаимодействия Интеграция MQL5 с пакетами обработки данных (Часть 7): Построение многоагентных сред для межсимвольного взаимодействия
В статье представлена полная интеграция Python и MQL5 для многоагентной торговли: получение данных из MT5, расчет индикаторов, решения отдельных агентов и взвешенный консенсус, формирующий единое торговое решение. Сигналы сохраняются в JSON-файле, отдаются через Flask и используются советником MQL5 для исполнения сделок с расчетом объема позиции и уровней SL/TP на основе ATR. Маршруты Flask обеспечивают безопасное управление жизненным циклом и мониторинг состояния.
Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh) Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh)
В этой статье представлен ap.mqh, порт ALGLIB для MQL5, и продемонстрировано его использование в многоактивных сценариях работы, требующих надежной линейной алгебры. Рассматривается почему встроенных индикаторов недостаточно, а затем реализуются полиномиальная регрессия, индикатор скользящей корреляционной матрицы и адаптивный оценщик коэффициента хеджа с использованием гребневой ​​регрессии с разложением Холецкого. Практический код показывает, как вычислять z-показатели спреда и согласованно исполнять парные сделки полностью в MetaTrader 5.
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Объект верхнего уровня) Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Объект верхнего уровня)
Продолжаем адаптацию фреймворка D2Skill и переходим к построению объекта верхнего уровня CD2Skill для двухуровневой коррекции торговой политики. Механизм интегрируется в архитектуру ORION и дополняет существующий Актёр банками Task Skills и Step Skills. Показаны принципы их инициализации, включения в прямой проход и обучения по псевдоостаткам. Особое внимание уделяется сохранению исходного вычислительного конвейера, причинного порядка и независимости Критика.