Изучение стандартной библиотеки MQL5 (Часть 13): Применение библиотеки математических решателей в торговле
Финансовые рынки демонстрируют нестационарность: кластеры волатильности, развороты тренда и сдвиги ликвидности создают характерные режимы. Торговая система, настроенная с фиксированными параметрами, может показывать отличные результаты в одном режиме, но рухнуть в следующем. Чтобы проиллюстрировать необходимость адаптации, рассмотрим типичную стратегию пересечения скользящих средних со статическими периодами и пороговыми значениями. Такие фиксированные настройки не позволяют адаптироваться к изменяющейся волатильности, что приводит к ложным сигналам или упущенным возможностям.
Содержание
- Определение задачи
- Понимание решения
- Пошаговая реализация
- Реализация пользовательской целевой функции
- Проектирование основного советника: метаданные и объявления
- Объявление класса
- Конструктор и деструктор
- Метод инициализации – Init()
- Основной обработчик тиков — ProcessTick()
- Обнаружение нового бара – NewBar()
- Оптимизация и генерация сигналов – Optimize()
- Открытие позиции — OpenPosition()
- Точки входа советника — OnInit(), OnTick(), OnDeinit()
- Анализ сходимости
- Интеграция и тестирование — пошаговые инструкции
- Заключение
- Основные уроки
Определение задачи
Трейдер, использующий пересечение скользящих средних с фиксированными параметрами, обнаруживает, что статические пороговые значения генерируют чрезмерное количество ложных сигналов во время изменений режима волатильности, а ручная повторная оптимизация занимает много времени и плохо работает в реальном времени.
Рассмотрим типичную стратегию следования за трендом, построенную на основе быстрой и медленной простых скользящих средних (SMA). Трейдер устанавливает периоды, скажем, на уровне 10 и 30, и определяет триггер пересечения, когда быстрая SMA пересекает медленную SMA сверху или снизу на пороговое значение в 5 пунктов. Эта статическая конфигурация хорошо работает в периоды стабильной внутридневной волатильности — спред между двумя средними четко увеличивается, а входов немного, но они получаются чёткими.
Рынки, однако, редко остаются стационарными. Когда волатильность возрастает — например, во время выхода новостей или смены режима от низкой волатильности к высокой — шум в ценовом движении расширяет средний спред, что приводит к многократному превышению установленного порогового значения. В результате возникает каскад ложных сигналов, каждый из которых вызывает вход в позицию, который затем быстро разворачивается. Трейдер, наблюдая за просадкой счета, замечает, что та же стратегия, которая показала себя надежно в бэктесте, теперь не может улавливать устойчивые тренды.
И наоборот, когда волатильность снижается (например, при приближении к праздничной сессии), пороговое значение становится слишком широким, и система пропускает все значимые пересечения, оставляя трейдера без позиции во время реального тренда.
Стандартным решением является повторная оптимизация параметров — периодов и пороговых значений — с использованием метода walk-forward анализа на основе недавнего временного окна. Однако ручная повторная оптимизация занимает много времени: трейдеру необходимо экспортировать тиковые данные, запустить отдельный оптимизатор в MetaTrader 5, проверить результаты, а затем обновить входные параметры советника. Даже если это делается ежедневно, период действия новых параметров остается неизвестным. К тому времени, когда трейдер заметит снижение эффективности работы и отреагирует, режим может снова измениться. Этот процесс по своей сути является реактивным, а не адаптивным.
Нам нужен механизм, который постоянно корректирует пороговое значение (или, шире, границу срабатывания сигнала) в ответ на текущую волатильность рынка — без необходимости ручного вмешательства. Это как раз задача многопараметрической оптимизации с ограничениями, которую может решать численный решатель.
В стандартной библиотеке MQL5 файл solvers.mqh предоставляет набор итеративных процедур оптимизации, включая алгоритм Левенберга-Марквардта (реализованный в классе CNlEq) и метод Нелдера-Мида, первоначально перенесенные из библиотеки ALGLIB. Эти решатели могут минимизировать целевую функцию, которая наказывает за ложные сигналы, одновременно вознаграждая за своевременные входы, адаптируя параметры стратегии по мере появления новых баров.

Рис. 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 баров:

Небольшой коэффициент регуляризации Тихонова (λ = 1e-6) предотвращает переобучение.
Настройка CNlEq и механизм обратного обмена с решателем
Встроенный решатель CNlEq использует подход обратного обмена с решателем: пользователь инициализирует состояние, а затем многократно вызывает NlEqIteration() в цикле. На каждой итерации решатель устанавливает флаги (m_needf — для значения целевой функции, m_needfij — для остатков и Якобиана). Пользователь должен вычислить и заполнить соответствующие поля (m_f, m_fi, m_j). Такой подход позволяет полностью контролировать оценку целевой функции без дополнительных накладных вычислительных расходов на обратные вызовы.
Основной метод UpdateFilter() вызывается при каждом новом тике (или при закрытии каждого часа). Внутри него мы:
- Копируем необходимое количество ценовых баров во внутренний массив.
- Создаем экземпляр CNlEqState и вызываем NlEqCreateLM() с 3 параметрами и WindowSize остатками.
- Задаём критерии остановки с помощью функции NlEqSetCond() (допуск по градиенту и максимальное количество итераций).
- Запускаем цикл механизма обратного обмена с решателем; в ветви m_needfij вычисляем остатки (F) и якобиан в текущей точке.
- После завершения цикла получаем оптимальные параметры с помощью NlEqResults().
- Вычисляем текущее значение фильтра, используя оптимизированные параметры для принятия торгового решения.
Реализация пользовательской целевой функции
Интерфейс решателя ожидает, что пользователь предоставит остатки и якобиан по запросу. Наша целевая функция определяется как набор невязок — по одному для каждого бара в окне — где каждый остаток представляет собой разницу между фактическим изменением цены и выходным сигналом фильтра. Функция ComputeVAMAC принимает (бета1, бета2, гамма), вычисляет выходной сигнал VAMAC по ценовому окну и заполняет вектор остатков и матрицу Якобиана. Матрица Якобиана используется алгоритмом Левенберга-Марквардта для определения направления и величины обновления параметров. В приведенном ниже коде для наглядности используется численное дифференцирование, хотя для повышения скорости работы производственной системы лучше использовать аналитические производные.
//+------------------------------------------------------------------+ //| Compute residuals and Jacobian (numerical derivatives) | //+------------------------------------------------------------------+ void ComputeVAMAC(const double ¶ms[],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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация индикатора Market Memory Zones: Зоны, куда цена с высокой вероятностью вернется
Интеграция MQL5 с пакетами обработки данных (Часть 7): Построение многоагентных сред для межсимвольного взаимодействия
Изучение стандартной библиотеки MQL5 (Часть 14): Создание советника для динамического хеджирования с использованием ALGLIB (ap.mqh)
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Объект верхнего уровня)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования