English Deutsch 日本語
preview
Автоматизация торговых стратегий на MQL5 (Часть 39): Стратегия статистического возврата к среднему с доверительными интервалами и информационной панелью

Автоматизация торговых стратегий на MQL5 (Часть 39): Стратегия статистического возврата к среднему с доверительными интервалами и информационной панелью

MetaTrader 5Торговые системы |
61 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В нашей предыдущей статье (Часть 38) мы разработали на языке MetaQuotes Language 5 (MQL5) систему торговли по скрытым дивергенциям RSI. Она находила скрытые бычьи и медвежьи дивергенции по свинговым точкам, применяла строгие проверки валидности сигнала по диапазонам баров и допуску, фильтровала сигналы по настраиваемым углам наклона ценовых линий и линий RSI, открывала сделки с применением риск-менеджмента и отображала на графиках визуальные метки и значения углов. В Части 39 мы создадим систему статистического возврата к среднему с доверительными интервалами и информационной панелью.

Система анализирует ценовые данные за заданный период и рассчитывает статистические характеристики: среднее, дисперсию, асимметрию, эксцесс и статистику Харке-Бера. Она формирует сигналы на основе доверительных интервалов (далее — интервальные границы), адаптивных порогов и подтверждения по старшему таймфрейму. Для управления позициями используются расчёт объёма позиции на основе текущих средств (Equity), трейлинг-стоп, частичное закрытие и выход по времени, а текущие показатели выводятся на информационную панель графика. Рассмотрим следующие темы:

  1. Принцип работы стратегии статистического возврата к среднему
  2. Реализация на MQL5
  3. Бэктестирование
  4. Заключение

К концу статьи у вас будет готовая к дальнейшей настройке стратегия MQL5 для торговли по принципу статистического возврата к среднему. Приступим!


Принцип работы стратегии статистического возврата к среднему

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

Для сигнала на покупку цена должна опуститься ниже нижней границы доверительного интервала, а отрицательная асимметрия должна указывать на потенциальный восходящий импульс после состояния перепроданности. Для сигнала на продажу цена должна подняться выше верхней границы, а положительная асимметрия — указывать на перекупленность и вероятную последующую коррекцию вниз. Дополнительно статистика Харке-Бера подтверждает ненормальность распределения, а порог эксцесса позволяет исключать чрезмерно рынки с чрезмерно выраженными «тяжёлыми хвостами».

Точки входа дополнительно сопоставляются с данными старшего таймфрейма, чтобы учитывать трендовый контекст старшего таймфрейма. Динамическое управление риском включает расчёт объёма позиции на основе текущих средств (Equity), базовые расстояния Stop Loss и Take Profit, трейлинг-стоп для фиксации прибыли, частичное закрытие на заданном уровне и выход по истечении установленного времени. Сочетание статистических фильтров и риск-менеджмента помогает находить более надёжные возможности для возврата цены к среднему на волатильных рынках. Ниже можно увидеть схематическое представление статистических распределений, которые мы будем использовать.

СХЕМЫ СТАТИСТИЧЕСКОГО ВОЗВРАТА К СРЕДНЕМУ

Статистика Харке-Бера объединяет показатели асимметрии и эксцесса. Её расчёт мы покажем на этапе реализации, так что беспокоиться не о чем. Мы вычислим среднее, дисперсию, асимметрию, эксцесс и статистику Харке-Бера за заданный период; сформируем сигналы при выходе цены за расчётные границы доверительного интервала с адаптивными порогами асимметрии и фильтром ненормальности; при необходимости подтвердим сигнал по старшему таймфрейму; откроем сделки фиксированным объёмом или с расчётом риска; добавим базовые SL/TP, трейлинг-стоп, частичное закрытие и ограничение длительности позиции; затем выведем показатели на информационную панель в реальном времени. В итоге мы получим именно такую систему.

ОБЩАЯ СХЕМА СИСТЕМЫ


Реализация на MQL5

Чтобы создать этот советник на MQL5, откройте MetaEditor, перейдите в окно «Навигатор», найдите папку Experts и нажмите вкладку «New» и следуйте указаниям мастера. После создания файла объявим входные параметры и глобальные переменные, которые будут использоваться во всей программе.

//+------------------------------------------------------------------+
//|                                 StatisticalReversionStrategy.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>
#include <Math\Stat\Math.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

//+------------------------------------------------------------------+
//| Входные параметры |
//+------------------------------------------------------------------+
input group "=== Statistical Parameters ==="
input int InpPeriod = 50;                   // Период статистических расчётов
input double InpConfidenceLevel = 0.95;     // Уровень доверия для интервалов (0,90–0,99)
input double InpJBThreshold = 2.0;          // Порог статистики Харке-Бера (снижен для увеличения числа сделок)
input double InpKurtosisThreshold = 5.0;    // Максимальный эксцесс (смягчённый порог)
input ENUM_TIMEFRAMES InpHigherTF = 0;      // Старший таймфрейм для подтверждения (0 — отключить)

input group "=== Trading Parameters ==="
input double InpRiskPercent = 1.0;          // Риск на сделку (% от средств; 0 — фиксированный объём)
input double InpFixedLots = 0.01;           // Фиксированный объём при InpRiskPercent = 0
input int InpBaseStopLossPips = 50;         // Базовый Stop Loss в пипсах
input int InpBaseTakeProfitPips = 100;      // Базовый Take Profit в пипсах
input int InpMagicNumber = 123456;          // Магический номер сделок
input int InpMaxTradeHours = 48;            // Максимальная длительность сделки в часах (0 — отключить)

input group "=== Risk Management ==="
input bool InpUseTrailingStop = true;       // Включить трейлинг-стоп
input int InpTrailingStopPips = 30;         // Расстояние трейлинг-стопа в пипсах
input int InpTrailingStepPips = 10;         // Шаг трейлинг-стопа в пипсах
input bool InpUsePartialClose = true;       // Включить частичную фиксацию прибыли
input double InpPartialClosePercent = 0.5;  // Доля позиции для закрытия при достижении 50% TP

input group "=== Dashboard Parameters ==="
input bool InpShowDashboard = true;         // Показывать информационную панель
input int InpDashboardX = 30;               // Координата X панели
input int InpDashboardY = 30;               // Координата Y панели
input int InpFontSize = 10;                 // Размер шрифта панели

//+------------------------------------------------------------------+
//| Глобальные переменные |
//+------------------------------------------------------------------+
CTrade trade;                                      //--- Торговый объект
datetime g_lastBarTime = 0;                        //--- Время последнего обработанного бара
double g_pointMultiplier = 1.0;                    //--- Множитель пунктов с учётом разрядности котировки
CChartObjectRectLabel* g_dashboardBg = NULL;       //--- Объект фона панели
CChartObjectRectLabel* g_headerBg = NULL;          //--- Объект фона заголовка
CChartObjectLabel* g_titleLabel = NULL;            //--- Объект метки заголовка
CChartObjectLabel* g_staticLabels[];               //--- Массив постоянных меток
CChartObjectLabel* g_valueLabels[];                //--- Массив меток значений
string g_staticNames[] = {
   "Symbol:", "Timeframe:", "Price:", "Skewness:", "Jarque-Bera:", "Kurtosis:",
   "Mean:", "Lower CI:", "Upper CI:", "Position:", "Lot Size:", "Profit:", "Duration:", "Signal:",
   "Equity:", "Balance:", "Free Margin:"
};                                                 //--- Названия постоянных меток
int g_staticCount = ArraySize(g_staticNames);      //--- Количество постоянных меток

Сначала подключим необходимые библиотеки: #include <Trade\Trade.mqh> для торговых операций, #include <Math\Stat\Math.mqh> для статистических расчётов и #include <ChartObjects\ChartObjectsTxtControls.mqh> для работы с объектами информационной панели. Затем объявим входные параметры, сгруппированные по назначению. В группе «Statistical Parameters» («Статистические параметры») параметр InpPeriod со значением 50 задаёт период расчёта, InpConfidenceLevel = 0,95 — уровень доверия, InpJBThreshold = 2,0 — порог статистики Харке-Бера, InpKurtosisThreshold = 5,0 — максимально допустимый эксцесс, а InpHigherTF позволяет включить подтверждение по старшему таймфрейму.

В группе «Trading Parameters» («Торговые параметры») параметр InpRiskPercent = 1,0 задаёт риск на сделку как процент от текущих средств счёта (equity); при нулевом значении используется фиксированный объём InpFixedLots = 0,01. Параметры InpBaseStopLossPips и InpBaseTakeProfitPips задают базовые расстояния Stop Loss и Take Profit. InpMagicNumber идентифицирует сделки советника, а InpMaxTradeHours ограничивает максимальную длительность позиции. В группе «Risk Management» («Управление рисками») включаются трейлинг-стоп и частичное закрытие позиции.

В группе «Информационная панель Parameters» («Параметры информационной панели») параметр InpShowИнформационная панель включает информационную панель, InpИнформационная панельX и InpИнформационная панельY задают её положение, а InpFontSize — размер шрифта.

Далее объявим глобальные переменные. Объект CTrade trade используется для управления торговыми операциями; g_lastBarTime хранит время последнего обработанного бара; g_pointMultiplier учитывает количество знаков в котировке. Указатели g_информационная панельBg, g_headerBg и g_titleLabel относятся к элементам панели, а массивы g_staticLabels и g_valueLabels содержат текстовые метки. Массив g_staticNames хранит постоянные названия полей, а g_staticCount определяется функцией ArraySize. Теперь создадим информационную панель.

//+------------------------------------------------------------------+
//| Создание информационной панели |
//+------------------------------------------------------------------+
bool CreateDashboard() {
   if (!InpShowDashboard) return true;             //--- Возвращаем true, если информационная панель отключена

   Print("Creating dashboard...");                 //--- Записываем начало создания информационной панели в журнал

// Создаём основной прямоугольный фон панели
   if (g_dashboardBg == NULL) {                    //--- Проверяем, существует ли объект фона
      g_dashboardBg = new CChartObjectRectLabel(); //--- Создаём объект фона
      if (!g_dashboardBg.Create(0, "StatReversion_DashboardBg", 0, InpDashboardX, InpDashboardY + 30, 300, g_staticCount * (InpFontSize + 6) + 30)) { //--- Создаём фон информационной панели
         Print("Error creating dashboard background: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         return false;                             //--- Возвращаем false при ошибке
      }
      g_dashboardBg.Color(clrDodgerBlue);          //--- Задаём цвет границы
      g_dashboardBg.BackColor(clrNavy);            //--- Задаём цвет фона
      g_dashboardBg.BorderType(BORDER_FLAT);       //--- Задаём тип границы
      g_dashboardBg.Corner(CORNER_LEFT_UPPER);     //--- Задаём угол привязки
   }

// Создаём фон заголовка
   if (g_headerBg == NULL) {                       //--- Проверяем, существует ли фон заголовка
      g_headerBg = new CChartObjectRectLabel();    //--- Создаём объект фона заголовка
      if (!g_headerBg.Create(0, "StatReversion_HeaderBg", 0, InpDashboardX, InpDashboardY, 300, InpFontSize + 20)) { //--- Создаём фон заголовка
         Print("Error creating header background: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         return false;                             //--- Возвращаем false при ошибке
      }
      g_headerBg.Color(clrDodgerBlue);             //--- Задаём цвет границы
      g_headerBg.BackColor(clrDarkBlue);           //--- Задаём цвет фона
      g_headerBg.BorderType(BORDER_FLAT);          //--- Задаём тип границы
      g_headerBg.Corner(CORNER_LEFT_UPPER);        //--- Задаём угол привязки
   }

// Создаём выровненную по центру метку заголовка
   if (g_titleLabel == NULL) {                     //--- Проверяем, существует ли метка заголовка
      g_titleLabel = new CChartObjectLabel();      //--- Создаём объект метки заголовка
      if (!g_titleLabel.Create(0, "StatReversion_Title", 0, InpDashboardX + 75, InpDashboardY + 5)) { //--- Создаём метку заголовка
         Print("Error creating title label: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         return false;                             //--- Возвращаем false при ошибке
      }
      if (!g_titleLabel.Font("Arial Bold") || !g_titleLabel.FontSize(InpFontSize + 2) || !g_titleLabel.Description("Statistical Reversion")) { //--- Задаём свойства заголовка
         Print("Error setting title properties: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         return false;                             //--- Возвращаем false при ошибке
      }
      g_titleLabel.Color(clrWhite);                //--- Задаём цвет заголовка
   }

// Инициализируем статические метки с выравниванием по левому краю
   ArrayFree(g_staticLabels);                      //--- Освобождаем массив статических меток
   ArrayResize(g_staticLabels, g_staticCount);     //--- Изменяем размер массива статических меток
   int y_offset = InpDashboardY + 30 + 10;         //--- Задаём смещение меток по оси Y
   for (int i = 0; i < g_staticCount; i++) {       //--- Перебираем статические метки
      g_staticLabels[i] = new CChartObjectLabel(); //--- Создаём объект статической метки
      string label_name = "StatReversion_Static_" + IntegerToString(i); //--- Формируем имя метки
      if (!g_staticLabels[i].Create(0, label_name, 0, InpDashboardX + 10, y_offset)) { //--- Создаём статическую метку
         Print("Error creating static label: ", label_name, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         DeleteDashboard();                        //--- Удаляем информационную панель
         return false;                             //--- Возвращаем false при ошибке
      }
      if (!g_staticLabels[i].Font("Arial") || !g_staticLabels[i].FontSize(InpFontSize) || !g_staticLabels[i].Description(g_staticNames[i])) { //--- Задаём свойства статической метки
         Print("Error setting static label properties: ", label_name, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         DeleteDashboard();                        //--- Удаляем информационную панель
         return false;                             //--- Возвращаем false при ошибке
      }
      g_staticLabels[i].Color(clrLightGray);       //--- Задаём цвет статической метки
      y_offset += InpFontSize + 6;                 //--- Обновляем смещение по оси Y
   }

// Инициализируем метки значений, начиная от центра панели
   ArrayFree(g_valueLabels);                       //--- Освобождаем массив меток значений
   ArrayResize(g_valueLabels, g_staticCount);      //--- Изменяем размер массива меток значений
   y_offset = InpDashboardY + 30 + 10;             //--- Сбрасываем смещение значений по оси Y
   for (int i = 0; i < g_staticCount; i++) {       //--- Перебираем метки значений
      g_valueLabels[i] = new CChartObjectLabel();  //--- Создаём объект метки значения
      string label_name = "StatReversion_Value_" + IntegerToString(i); //--- Формируем имя метки
      if (!g_valueLabels[i].Create(0, label_name, 0, InpDashboardX + 150, y_offset)) { //--- Создаём метку значения
         Print("Error creating value label: ", label_name, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         DeleteDashboard();                        //--- Удаляем информационную панель
         return false;                             //--- Возвращаем false при ошибке
      }
      if (!g_valueLabels[i].Font("Arial") || !g_valueLabels[i].FontSize(InpFontSize) || !g_valueLabels[i].Description("")) { //--- Задаём свойства метки значения
         Print("Error setting value label properties: ", label_name, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         DeleteDashboard();                        //--- Удаляем информационную панель
         return false;                             //--- Возвращаем false при ошибке
      }
      g_valueLabels[i].Color(clrCyan);             //--- Задаём цвет метки значения
      y_offset += InpFontSize + 6;                 //--- Обновляем смещение по оси Y
   }

   ChartRedraw();                                  //--- Перерисовываем график
   Print("Dashboard created successfully");        //--- Выводим сообщение об успешном создании в журнал
   return true;                                    //--- Возвращаем true при успешном выполнении
}

Функция CreateИнформационная панель создаёт на графике панель с показателями стратегии, если InpShowИнформационная панель равно true. В противном случае она сразу возвращает true. Сначала через Print выводится сообщение о начале создания. Затем при отсутствии объекта g_информационная панельBg создаётся экземпляр CChartObjectRectLabel с заданными координатами, шириной 300 пикселей и высотой, рассчитанной по количеству строк. При ошибке функция выводит код GetLastError и возвращает false. После этого задаются цвет рамки, фон, тип границы и привязка к левому верхнему углу.

Аналогично создаётся фон заголовка g_headerBg. Затем создаётся метка g_titleLabel с названием «Statistical Reversion», шрифтом Arial Bold и белым цветом. Все ошибки создания или настройки фиксируются в журнале.

Массив g_staticLabels очищается и изменяется до размера g_staticCount. В цикле создаются подписи постоянных полей, размещённые в левой части панели. Их текст берётся из g_staticNames, а вертикальное смещение увеличивается после каждой строки. При ошибке вызывается DeleteИнформационная панель и функция возвращает false.

Затем аналогичным образом создаются метки значений g_valueLabels в правой части панели. После формирования всех объектов вызывается ChartRedraw, график обновляется, а функция возвращает true. Далее реализуем обновление и удаление панели.

//+------------------------------------------------------------------+
//| Обновление информационной панели |
//+------------------------------------------------------------------+
void UpdateDashboard(double mean, double lower_ci, double upper_ci, double skewness, double jb_stat, double kurtosis,
                     double skew_buy, double skew_sell, string position, double lot_size, double profit, string duration, string signal) {
   if (!InpShowDashboard || ArraySize(g_valueLabels) != g_staticCount) { //--- Проверяем, включена ли панель и корректен ли массив меток
      Print("Dashboard update skipped: Not initialized or invalid array size"); //--- Выводим сообщение о пропуске обработки в журнал
      return;                                      //--- Завершаем выполнение функции
   }

   double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Получаем баланс счёта
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Получаем текущие средства счёта (equity)
   double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Получаем свободную маржу
   double price = iClose(_Symbol, _Period, 0);     //--- Получаем цену закрытия текущего бара

   string values[] = {
      _Symbol, EnumToString(_Period), DoubleToString(price, _Digits),
      DoubleToString(skewness, 4), DoubleToString(jb_stat, 2), DoubleToString(kurtosis, 2),
      DoubleToString(mean, _Digits), DoubleToString(lower_ci, _Digits), DoubleToString(upper_ci, _Digits),
      position, DoubleToString(lot_size, 2), DoubleToString(profit, 2), duration, signal,
      DoubleToString(equity, 2), DoubleToString(balance, 2), DoubleToString(free_margin, 2)
   };                                              //--- Формируем массив строковых значений

   color value_colors[] = {
      clrWhite, clrWhite, (price > 0 ? clrCyan : clrGray), (skewness != 0 ? clrCyan : clrGray), (jb_stat != 0 ? clrCyan : clrGray), (kurtosis != 0 ? clrCyan : clrGray),
      (mean != 0 ? clrCyan : clrGray), (lower_ci != 0 ? clrCyan : clrGray), (upper_ci != 0 ? clrCyan : clrGray),
      clrWhite, clrWhite, (profit > 0 ? clrLimeGreen : profit < 0 ? clrRed : clrGray), clrWhite,
      (signal == "Buy" ? clrLimeGreen : signal == "Sell" ? clrRed : clrGray),
      (equity > balance ? clrLimeGreen : equity < balance ? clrRed : clrGray), clrWhite, clrWhite
   };                                              //--- Формируем массив цветов значений

   for (int i = 0; i < g_staticCount; i++) {       //--- Перебираем значения
      if (g_valueLabels[i] != NULL) {              //--- Проверяем, существует ли метка
         g_valueLabels[i].Description(values[i]);  //--- Обновляем отображаемое значение
         g_valueLabels[i].Color(value_colors[i]);  //--- Задаём цвет значения
      } else {                                     //--- Обрабатываем отсутствие метки
         Print("Warning: Value label ", i, " is NULL"); //--- Выводим предупреждение в журнал
      }
   }
   ChartRedraw();                                  //--- Перерисовываем график
   Print("Dashboard updated: Signal=", signal, ", Position=", position, ", Profit=", profit); //--- Выводим сообщение об обновлении панели в журнал
}

//+------------------------------------------------------------------+
//| Удаление информационной панели |
//+------------------------------------------------------------------+
void DeleteDashboard() {
   if (g_dashboardBg != NULL) {                    //--- Проверяем, существует ли объект фона
      g_dashboardBg.Delete();                      //--- Удаляем фон
      delete g_dashboardBg;                        //--- Освобождаем память объекта фона
      g_dashboardBg = NULL;                        //--- Обнуляем указатель на объект фона
      Print("Dashboard background deleted");       //--- Записываем удаление в журнал
   }
   if (g_headerBg != NULL) {                       //--- Проверяем, существует ли фон заголовка
      g_headerBg.Delete();                         //--- Удаляем фон заголовка
      delete g_headerBg;                           //--- Освобождаем память объекта фона заголовка
      g_headerBg = NULL;                           //--- Обнуляем указатель на фон заголовка
      Print("Header background deleted");          //--- Записываем удаление в журнал
   }
   if (g_titleLabel != NULL) {                     //--- Проверяем, существует ли метка заголовка
      g_titleLabel.Delete();                       //--- Удаляем метку заголовка
      delete g_titleLabel;                         //--- Освобождаем память метки заголовка
      g_titleLabel = NULL;                         //--- Обнуляем указатель на метку заголовка
      Print("Title label deleted");                //--- Записываем удаление в журнал
   }
   for (int i = 0; i < ArraySize(g_staticLabels); i++) { //--- Перебираем статические метки
      if (g_staticLabels[i] != NULL) {             //--- Проверяем, существует ли метка
         g_staticLabels[i].Delete();               //--- Удаляем статическую метку
         delete g_staticLabels[i];                 //--- Освобождаем память статической метки
         g_staticLabels[i] = NULL;                 //--- Обнуляем указатель на статическую метку
      }
   }
   for (int i = 0; i < ArraySize(g_valueLabels); i++) { //--- Перебираем метки значений
      if (g_valueLabels[i] != NULL) {              //--- Проверяем, существует ли метка
         g_valueLabels[i].Delete();                //--- Удаляем метку значения
         delete g_valueLabels[i];                  //--- Освобождаем память метки значения
         g_valueLabels[i] = NULL;                  //--- Обнуляем указатель на метку значения
      }
   }
   ArrayFree(g_staticLabels);                      //--- Освобождаем массив статических меток
   ArrayFree(g_valueLabels);                       //--- Освобождаем массив меток значений
   ChartRedraw();                                  //--- Перерисовываем график
   Print("Dashboard labels cleared");              //--- Записываем очистку панели в журнал
}

Функция UpdateИнформационная панель обновляет значения на панели. Она принимает среднее, границы доверительного интервала, асимметрию, статистику Харке-Бера, эксцесс, пороги сигналов, сведения о позиции, объём, прибыль, длительность и текущий сигнал. Если панель отключена или массив меток имеет неверный размер, функция завершает работу. Затем она получает баланс, средства счёта (Equity), свободную маржу и цену закрытия текущего бара.

Массив values содержит отформатированные значения символа, таймфрейма, цены, статистических показателей, состояния позиции и счёта. Массив value_colors задаёт цвет каждого значения: прибыль и сигналы выделяются зелёным или красным, отсутствующие данные — серым. В цикле текст и цвет передаются соответствующим меткам, после чего график перерисовывается.

Функция DeleteИнформационная панель освобождает все ресурсы панели. Для каждого фонового объекта и метки вызывается метод Delete, затем память освобождается оператором delete, а указатель устанавливается в NULL. Массивы статических меток и значений также последовательно очищаются.

//+------------------------------------------------------------------+
//| Функция инициализации советника |
//+------------------------------------------------------------------+
int OnInit() {
   trade.SetExpertMagicNumber(InpMagicNumber);     //--- Задаём магический номер
   trade.SetDeviationInPoints(10);                 //--- Задаём допустимое отклонение в пунктах
   trade.SetTypeFilling(ORDER_FILLING_FOK);        //--- Задаём тип исполнения ордера

// Настраиваем множитель пунктов с учётом разрядности котировки
   if (_Digits == 5 || _Digits == 3)               //--- Проверяем разрядность котировки
      g_pointMultiplier = 10.0;                    //--- Задаём множитель для пяти- и трёхзначных котировок
   else                                            //--- Используем стандартную разрядность
      g_pointMultiplier = 1.0;                     //--- Задаём множитель для остальных форматов котировок

// Проверяем входные параметры с помощью локальных переменных
   double confidenceLevel = InpConfidenceLevel;    //--- Копируем уровень доверия
   if (InpConfidenceLevel < 0.90 || InpConfidenceLevel > 0.99) { //--- Проверяем допустимый диапазон уровня доверия
      Print("Warning: InpConfidenceLevel out of range (0.90-0.99). Using 0.95."); //--- Выводим предупреждение в журнал
      confidenceLevel = 0.95;                      //--- Устанавливаем уровень доверия по умолчанию
   }
   double riskPercent = InpRiskPercent;            //--- Копируем процент риска
   if (InpRiskPercent < 0 || InpRiskPercent > 10) { //--- Проверяем допустимый диапазон риска
      Print("Warning: InpRiskPercent out of range (0-10). Using 1.0."); //--- Выводим предупреждение в журнал
      riskPercent = 1.0;                           //--- Устанавливаем процент риска по умолчанию
   }

// Инициализируем информационную панель; её ошибка не является критической
   if (InpShowDashboard && !CreateDashboard()) {   //--- Проверяем, не завершилось ли создание панели ошибкой
      Print("Failed to initialize dashboard, continuing without it"); //--- Выводим сообщение об ошибке в журнал
   }

   Print("Statistical Reversion Strategy Initialized. Period: ", InpPeriod, ", Confidence: ", confidenceLevel * 100, "% on ", _Symbol, "/", Period()); //--- Записываем результат инициализации в журнал
   return(INIT_SUCCEEDED);                         //--- Возвращаем успешное выполнение
}

//+------------------------------------------------------------------+
//| Функция деинициализации советника |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   DeleteDashboard();                              //--- Удаляем информационную панель
   Print("Statistical Reversion Strategy Deinitialized. Reason: ", reason); //--- Записываем деинициализацию в журнал
}

В обработчике OnInit объекту trade задаётся магический номер, допустимое отклонение 10 пунктов и тип исполнения ORDER_FILLING_FOK. Затем определяется множитель g_pointMultiplier: для инструментов с тремя или пятью знаками он равен 10, для остальных — 1. Входные значения уровня доверия и риска проверяются и при необходимости заменяются допустимыми значениями. При желании вы можете задать собственные диапазоны: приведённые здесь значения выбраны условно, просто как пример.

Если информационная панель включена, вызывается CreateИнформационная панель. Ошибка её создания не останавливает советник. В завершение в журнал выводятся параметры инициализации, после чего возвращается INIT_SUCCEEDED.

ПЕРВЫЙ ЗАПУСК

После компиляции получаем следующий результат.

//+------------------------------------------------------------------+
//| Аппроксимация обратной функции нормального распределения |
//+------------------------------------------------------------------+
double NormalInverse(double p) {
   double t = MathSqrt(-2.0 * MathLog(p < 0.5 ? p : 1.0 - p)); //--- Рассчитываем промежуточное значение t
   double sign = (p < 0.5) ? -1.0 : 1.0;           //--- Определяем знак
   return sign * (t - (2.515517 + 0.802853 * t + 0.010328 * t * t) /
                  (1.0 + 1.432788 * t + 0.189269 * t * t + 0.001308 * t * t * t)); //--- Возвращаем приближённое значение обратной функции распределения
}

//+------------------------------------------------------------------+
//| Получение состояния позиции |
//+------------------------------------------------------------------+
string GetPositionStatus() {
   if (HasPosition(POSITION_TYPE_BUY)) return "Buy"; //--- Возвращаем "Buy", если открыта длинная позиция
   if (HasPosition(POSITION_TYPE_SELL)) return "Sell"; //--- Возвращаем "Sell", если открыта короткая позиция
   return "None";                                    //--- Возвращаем «None», если позиция отсутствует
}

//+------------------------------------------------------------------+
//| Получение текущего объёма позиции |
//+------------------------------------------------------------------+
double GetCurrentLotSize() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) //--- Проверяем символ и магический номер
         return PositionGetDouble(POSITION_VOLUME); //--- Возвращаем объём позиции
   }
   return 0.0;                                      //--- Возвращаем 0, если позиция отсутствует
}

//+------------------------------------------------------------------+
//| Получение текущей прибыли |
//+------------------------------------------------------------------+
double GetCurrentProfit() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) //--- Проверяем символ и магический номер
         return PositionGetDouble(POSITION_PROFIT); //--- Возвращаем прибыль по позиции
   }
   return 0.0;                                      //--- Возвращаем 0, если позиция отсутствует
}

//+------------------------------------------------------------------+
//| Получение длительности позиции |
//+------------------------------------------------------------------+
string GetPositionDuration() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { //--- Проверяем символ и магический номер
         datetime open_time = (datetime)PositionGetInteger(POSITION_TIME); //--- Получаем время открытия
         datetime current_time = TimeCurrent(); //--- Получаем текущее время
         int hours = (int)((current_time - open_time) / 3600); //--- Рассчитываем продолжительность в часах
         return IntegerToString(hours) + "h"; //--- Возвращаем строку с продолжительностью
      }
   }
   return "0h";                                   //--- Возвращаем «0h», если позиция отсутствует
}

//+------------------------------------------------------------------+
//| Получение состояния сигнала |
//+------------------------------------------------------------------+
string GetSignalStatus(bool buy_signal, bool sell_signal) {
   if (buy_signal) return "Buy";                  //--- Возвращаем "Buy", если сформирован сигнал на покупку
   if (sell_signal) return "Sell";                //--- Возвращаем "Sell", если сформирован сигнал на продажу
   return "None";                                 //--- Возвращаем «None», если сигнал отсутствует
}

//+------------------------------------------------------------------+
//| Проверка открытой позиции заданного типа |
//+------------------------------------------------------------------+
bool HasPosition(ENUM_POSITION_TYPE pos_type) {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetInteger(POSITION_TYPE) == pos_type) //--- Проверяем параметры позиции
         return true;                                //--- Возвращаем true при совпадении условий
   }
   return false;                                     //--- Возвращаем false, если условия не совпали
}

//+------------------------------------------------------------------+
//| Проверка наличия открытой позиции |
//+------------------------------------------------------------------+
bool HasPosition() {
   return (HasPosition(POSITION_TYPE_BUY) || HasPosition(POSITION_TYPE_SELL)); //--- Проверяем наличие позиции на покупку или продажу
}

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

Функция NormalInverse приближённо вычисляет обратную функцию распределения стандартного нормального закона. Она принимает вероятность p, рассчитывает промежуточное значение t и использует рациональную аппроксимацию. Для вероятностей ниже 0,5 знак результата меняется. Полученное значение используется как z-квантиль при построении доверительного интервала.

Далее создаются функции для проверки позиций и сигналов. GetPositionStatus возвращает «Buy», «Sell» или «None» в зависимости от открытых позиций. HasPosition перебирает позиции и проверяет символ, магический номер и тип. Перегруженная функция HasPosition() без параметра проверяет наличие любой открытой позиции. GetCurrentLotSize и GetCurrentProfit возвращают объём и прибыль текущей позиции, а GetSignalStatus — текст текущего сигнала.

//+------------------------------------------------------------------+
//| Обработчик тиков советника |
//+------------------------------------------------------------------+
void OnTick() {
// Проверяем формирование нового бара, чтобы избежать лишних вычислений
   if (iTime(_Symbol, _Period, 0) == g_lastBarTime) { //--- Проверяем, сформировался ли новый бар
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), GetSignalStatus(false, false)); //--- Обновляем панель при отсутствии сигнала
      return;                                        //--- Завершаем выполнение функции
   }
   g_lastBarTime = iTime(_Symbol, _Period, 0);       //--- Обновляем время последнего обработанного бара

// Проверяем доступность рыночных данных
   if (!SymbolInfoDouble(_Symbol, SYMBOL_BID) || !SymbolInfoDouble(_Symbol, SYMBOL_ASK)) { //--- Проверяем доступность рыночных данных
      Print("Error: Market data unavailable for ", _Symbol); //--- Выводим сообщение об ошибке в журнал
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
      return;                                        //--- Завершаем выполнение функции
   }

// Копируем исторические цены закрытия
   double prices[];                                  //--- Объявляем массив цен
   ArraySetAsSeries(prices, true);                   //--- Преобразуем массив в временной ряд
   int copied = CopyClose(_Symbol, _Period, 1, InpPeriod, prices); //--- Копируем цены закрытия
   if (copied != InpPeriod) {                        //--- Проверяем успешность копирования данных
      Print("Error copying prices: ", copied, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
      return;                                        //--- Завершаем выполнение функции
   }

// Рассчитываем статистические моменты
   double mean, variance, skewness, kurtosis;        //--- Объявляем статистические переменные
   if (!MathMoments(prices, mean, variance, skewness, kurtosis, 0, InpPeriod)) { //--- Рассчитываем моменты
      Print("Error calculating moments: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
      UpdateDashboard(0, 0, 0, 0, 0, 0, 0, 0, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
      return;                                        //--- Завершаем выполнение функции
   }

// Критерий Харке-Бера
   double n = (double)InpPeriod;                     //--- Задаём размер выборки
   double jb_stat = n * (skewness * skewness / 6.0 + (kurtosis * kurtosis) / 24.0); //--- Рассчитываем статистику Харке-Бера

// Выводим в журнал статистические значения
   Print("Stats: Skewness=", DoubleToString(skewness, 4), ", JB=", DoubleToString(jb_stat, 2), ", Kurtosis=", DoubleToString(kurtosis, 2)); //--- Записываем статистические показатели в журнал

// Адаптивные пороги асимметрии
   double skew_buy_threshold = -0.3 - 0.05 * kurtosis; //--- Рассчитываем порог асимметрии для покупки
   double skew_sell_threshold = 0.3 + 0.05 * kurtosis; //--- Рассчитываем порог асимметрии для продажи

// Фильтр эксцесса
   if (kurtosis > InpKurtosisThreshold) {          //--- Проверяем порог эксцесса
      Print("Trade skipped: High kurtosis (", kurtosis, ") > ", InpKurtosisThreshold); //--- Выводим сообщение о пропуске обработки в журнал
      UpdateDashboard(mean, 0, 0, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
      return;                                      //--- Завершаем выполнение функции
   }

   double std_dev = MathSqrt(variance);            //--- Рассчитываем стандартное отклонение

// Адаптивные интервальные границы
   double confidenceLevel = InpConfidenceLevel;    //--- Копируем уровень доверия
   if (confidenceLevel < 0.90 || confidenceLevel > 0.99) //--- Проверяем уровень доверия
      confidenceLevel = 0.95;                      //--- Устанавливаем уровень доверия по умолчанию
   double z_score = NormalInverse(0.5 + confidenceLevel / 2.0); //--- Рассчитываем стандартную z-оценку
   double ci_mult = z_score / MathSqrt(n);         //--- Рассчитываем множитель интервальных границ
   double upper_ci = mean + ci_mult * std_dev;     //--- Рассчитываем верхнюю границу ДИ
   double lower_ci = mean - ci_mult * std_dev;     //--- Рассчитываем нижнюю границу ДИ

// Текущая цена закрытия
   double current_price = iClose(_Symbol, _Period, 0); //--- Получаем цену закрытия текущего бара

// Подтверждение на старшем таймфрейме, если оно включено
   bool htf_valid = true;                          //--- Инициализируем флаг подтверждения по старшему таймфрейму
   if (InpHigherTF != 0) {                         //--- Проверяем, включено ли подтверждение по старшему таймфрейму
      double htf_prices[];                         //--- Объявляем массив цен старшего таймфрейма
      ArraySetAsSeries(htf_prices, true);          //--- Преобразуем массив в временной ряд
      int htf_copied = CopyClose(_Symbol, InpHigherTF, 1, InpPeriod, htf_prices); //--- Копируем цены закрытия старшего таймфрейма
      if (htf_copied != InpPeriod) {               //--- Проверяем успешность копирования данных старшего таймфрейма
         Print("Error copying HTF prices: ", htf_copied, ", Error: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
         return;                                   //--- Завершаем выполнение функции
      }
      double htf_mean, htf_variance, htf_skewness, htf_kurtosis; //--- Объявляем статистические показатели старшего таймфрейма
      if (!MathMoments(htf_prices, htf_mean, htf_variance, htf_skewness, htf_kurtosis, 0, InpPeriod)) { //--- Рассчитываем статистические моменты старшего таймфрейма
         Print("Error calculating HTF moments: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
         UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold, GetPositionStatus(), GetCurrentLotSize(), GetCurrentProfit(), GetPositionDuration(), "None"); //--- Обновляем панель при отсутствии сигнала
         return;                                   //--- Завершаем выполнение функции
      }
      htf_valid = (current_price <= htf_mean && skewness <= 0) || (current_price >= htf_mean && skewness >= 0); //--- Проверяем корректность подтверждения старшего таймфрейма
      Print("HTF Check: Price=", DoubleToString(current_price, _Digits), ", HTF Mean=", DoubleToString(htf_mean, _Digits), ", Valid=", htf_valid); //--- Записываем результат проверки старшего таймфрейма в журнал
   }

// Формируем сигналы
   bool buy_signal = htf_valid && (current_price < lower_ci) && (skewness < skew_buy_threshold) && (jb_stat > InpJBThreshold); //--- Проверяем условия сигнала на покупку
   bool sell_signal = htf_valid && (current_price > upper_ci) && (skewness > skew_sell_threshold) && (jb_stat > InpJBThreshold); //--- Проверяем условия сигнала на продажу

// Резервный сигнал
   if (!buy_signal && !sell_signal) {              //--- Проверяем отсутствие основного сигнала
      buy_signal = htf_valid && (current_price < mean - 0.3 * std_dev); //--- Проверяем резервный сигнал на покупку
      sell_signal = htf_valid && (current_price > mean + 0.3 * std_dev); //--- Проверяем резервный сигнал на продажу
      Print("Fallback Signal: Buy=", buy_signal, ", Sell=", sell_signal); //--- Записываем резервные сигналы в журнал
   }

// Записываем состояние сигнала в журнал
   Print("Signal Check: Buy=", buy_signal, ", Sell=", sell_signal, ", Price=", DoubleToString(current_price, _Digits),
         ", LowerCI=", DoubleToString(lower_ci, _Digits), ", UpperCI=", DoubleToString(upper_ci, _Digits),
         ", Skew=", DoubleToString(skewness, 4), ", BuyThresh=", DoubleToString(skew_buy_threshold, 4),
         ", SellThresh=", DoubleToString(skew_sell_threshold, 4), ", JB=", DoubleToString(jb_stat, 2)); //--- Записываем параметры сигнала в журнал

}

Функция GetPositionDuration находит время открытия позиции, вычисляет прошедшее количество часов и возвращает строку вида «Xh». Если подходящей позиции нет, возвращается «0h».

В обработчике OnTick сначала проверяется появление нового бара. Если новый бар ещё не сформирован, советник обновляет только информационную панель, после чего функция завершается. Затем проверяется возможность получения цен Bid и Ask. Массив prices переводится в режим таймсерии и заполняется функцией CopyClose значениями закрытия за InpPeriod завершённых баров. При недостатке данных выводится сообщение об ошибке.

Для расчёта статистики объявляются переменные mean, variance, skewness и kurtosis, после чего вызывается MathMoments. Статистика Харке-Бера рассчитывается как n, умноженное на сумму квадрата асимметрии, делённого на 6, и квадрата эксцесса, делённого на 24. Адаптивный порог покупки равен −0,3 − 0,05 × kurtosis, а порог продажи — 0,3 + 0,05 × kurtosis. Если эксцесс превышает InpKurtosisThreshold, сигнал пропускается из-за повышенного хвостового риска.

Стандартное отклонение рассчитывается как квадратный корень из дисперсии. После проверки уровня доверия функция NormalInverse определяет z-квантиль, а границы доверительного интервала вычисляются относительно среднего с учётом стандартной ошибки. При включённом InpHigherTF аналогичные статистики рассчитываются по старшему таймфрейму; подтверждение считается действительным, если положение цены относительно среднего соответствует знаку асимметрии.

ПОДТВЕРЖДЕНИЕ СИГНАЛА

Сигнал на покупку формируется, когда подтверждение по старшему таймфрейму действительно, цена ниже нижней границы доверительного интервала, асимметрия меньше порога покупки, а статистика Харке-Бера превышает InpJBThreshold. Условия продажи зеркальны. Если основной сигнал отсутствует, используется резервное условие отклонения цены от среднего на 0,3 стандартного отклонения. После компиляции получаем следующий результат.

//+------------------------------------------------------------------+
//| Закрытие всех позиций заданного типа |
//+------------------------------------------------------------------+
void CloseAllPositions(ENUM_POSITION_TYPE pos_type) {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber && PositionGetInteger(POSITION_TYPE) == pos_type) { //--- Проверяем параметры позиции
         ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет
         double profit = PositionGetDouble(POSITION_PROFIT); //--- Получаем прибыль
         trade.PositionClose(ticket);                //--- Закрываем позицию
      }
   }
}

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

// Управление позициями: закрываем позиции противоположного направления
if (HasPosition(POSITION_TYPE_BUY) && sell_signal) //--- Проверяем наличие покупки при сигнале на продажу
   CloseAllPositions(POSITION_TYPE_BUY);        //--- Закрываем все позиции на покупку
if (HasPosition(POSITION_TYPE_SELL) && buy_signal) //--- Проверяем наличие продажи при сигнале на покупку
   CloseAllPositions(POSITION_TYPE_SELL);       //--- Закрываем все позиции на продажу

// Рассчитываем объём позиции
double lot_size = InpFixedLots;                 //--- Задаём объём позиции по умолчанию
double riskPercent = InpRiskPercent;            //--- Копируем процент риска
if (riskPercent > 0) {                          //--- Проверяем, включён ли расчёт объёма по риску
   double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Получаем текущие средства счёта (equity)
   double sl_points = InpBaseStopLossPips * g_pointMultiplier; //--- Рассчитываем расстояние Stop Loss в пунктах
   double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); //--- Получаем стоимость тика
   if (tick_value == 0) {                       //--- Проверяем корректность стоимости тика
      Print("Error: Invalid tick value for ", _Symbol); //--- Выводим сообщение об ошибке в журнал
      return;                                   //--- Завершаем выполнение функции
   }
   lot_size = NormalizeDouble((account_equity * riskPercent / 100.0) / (sl_points * tick_value), 2); //--- Рассчитываем объём позиции на основе риска
   lot_size = MathMax(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN), MathMin(lot_size, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))); //--- Ограничиваем объём допустимым диапазоном
   Print("Lot Size: Equity=", account_equity, ", SL Points=", sl_points, ", Tick Value=", tick_value, ", Lot=", lot_size); //--- Записываем расчёт объёма позиции в журнал
}

// Открываем новые позиции
if (!HasPosition() && buy_signal) {             //--- Проверяем отсутствие позиции и наличие сигнала на покупку
   double sl = current_price - InpBaseStopLossPips * _Point * g_pointMultiplier; //--- Рассчитываем уровень Stop Loss
   double tp = current_price + InpBaseTakeProfitPips * _Point * g_pointMultiplier; //--- Рассчитываем уровень Take Profit
   if (trade.Buy(lot_size, _Symbol, 0, sl, tp, "StatReversion Buy: Skew=" + DoubleToString(skewness, 4) + ", JB=" + DoubleToString(jb_stat, 2))) { //--- Отправляем ордер на покупку
      Print("Buy order opened. Mean: ", DoubleToString(mean, 5), ", Current: ", DoubleToString(current_price, 5)); //--- Выводим сообщение об открытии позиции на покупку в журнал
   } else {                                     //--- Обрабатываем ошибку открытия покупки
      Print("Buy order failed: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
   }
} else if (!HasPosition() && sell_signal) {     //--- Проверяем отсутствие позиции и наличие сигнала на продажу
   double sl = current_price + InpBaseStopLossPips * _Point * g_pointMultiplier; //--- Рассчитываем уровень Stop Loss
   double tp = current_price - InpBaseTakeProfitPips * _Point * g_pointMultiplier; //--- Рассчитываем уровень Take Profit
   if (trade.Sell(lot_size, _Symbol, 0, sl, tp, "StatReversion Sell: Skew=" + DoubleToString(skewness, 4) + ", JB=" + DoubleToString(jb_stat, 2))) { //--- Отправляем ордер на продажу
      Print("Sell order opened. Mean: ", DoubleToString(mean, 5), ", Current: ", DoubleToString(current_price, 5)); //--- Выводим сообщение об открытии позиции на продажу в журнал
   } else {                                     //--- Обрабатываем ошибку открытия продажи
      Print("Sell order failed: ", GetLastError()); //--- Выводим сообщение об ошибке в журнал
   }
}

// Обновляем информационную панель
UpdateDashboard(mean, lower_ci, upper_ci, skewness, jb_stat, kurtosis, skew_buy_threshold, skew_sell_threshold,
                GetPositionStatus(), lot_size, GetCurrentProfit(), GetPositionDuration(), GetSignalStatus(buy_signal, sell_signal)); //--- Обновляем информационную панель с учётом сигналов

Функция CloseAllPositions закрывает все позиции указанного типа ENUM_POSITION_TYPE. Перебор выполняется в обратном порядке, чтобы закрытие позиции не нарушало индексацию. Для каждой позиции проверяются символ, магический номер и тип, после чего по тикету вызывается trade.PositionClose. Значение прибыли также считывается и может использоваться для журналирования.

Если открыта покупка и появляется сигнал продажи, закрываются все длинные позиции; при обратной ситуации закрываются короткие. По умолчанию используется InpFixedLots. Если InpRiskPercent больше нуля, размер позиции рассчитывается по доле от текущих средств счёта (equity), расстоянию Stop Loss и стоимости тика. Результат нормализуется и ограничивается минимальным и максимальным объёмом инструмента.

ПОДТВЕРЖДЕНИЕ СДЕЛКИ

При отсутствии открытой позиции и наличии сигнала покупки рассчитываются Stop Loss ниже текущей цены и Take Profit выше неё, после чего вызывается trade.Buy. Для продажи расчёты выполняются зеркально и вызывается trade.Sell. В комментарий сделки добавляются значения асимметрии и статистики Харке-Бера. Затем UpdateИнформационная панель получает все рассчитанные показатели и сведения о позиции. После компиляции получаем следующий результат.

//+------------------------------------------------------------------+
//| Управление трейлинг-стопом |
//+------------------------------------------------------------------+
void ManageTrailingStop() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Проверяем символ и магический номер
         continue;                                   //--- Пропускаем позицию, если параметры не совпадают

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет
      double current_sl = PositionGetDouble(POSITION_SL); //--- Получаем текущий уровень Stop Loss
      ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- Получаем тип позиции
      double current_price = (pos_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Получаем текущую цену

      double trail_distance = InpTrailingStopPips * _Point * g_pointMultiplier; //--- Рассчитываем расстояние трейлинг-стопа
      double trail_step = InpTrailingStepPips * _Point * g_pointMultiplier; //--- Рассчитываем шаг трейлинг-стопа

      if (pos_type == POSITION_TYPE_BUY) {            //--- Проверяем длинную позицию
         double new_sl = current_price - trail_distance; //--- Рассчитываем новый уровень Stop Loss
         if (new_sl > current_sl + trail_step || current_sl == 0) { //--- Проверяем необходимость обновления
            trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); //--- Изменяем параметры позиции
         }
      } else if (pos_type == POSITION_TYPE_SELL) {    //--- Проверяем короткую позицию
         double new_sl = current_price + trail_distance; //--- Рассчитываем новый уровень Stop Loss
         if (new_sl < current_sl - trail_step || current_sl == 0) { //--- Проверяем необходимость обновления
            trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP)); //--- Изменяем параметры позиции
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Управление частичным закрытием |
//+------------------------------------------------------------------+
void ManagePartialClose() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Проверяем символ и магический номер
         continue;                                   //--- Пропускаем позицию, если параметры не совпадают

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет
      double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Получаем цену открытия
      double tp = PositionGetDouble(POSITION_TP); //--- Получаем уровень Take Profit
      double current_price = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Получаем текущую цену
      double volume = PositionGetDouble(POSITION_VOLUME); //--- Получаем объём позиции

      double half_tp_distance = MathAbs(tp - open_price) * 0.5; //--- Рассчитываем половину расстояния до Take Profit
      bool should_close = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && current_price >= open_price + half_tp_distance) ||
                          (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && current_price <= open_price - half_tp_distance); //--- Проверяем достижение половины Take Profit

      if (should_close) {                             //--- Проверяем необходимость частичного закрытия
         double close_volume = NormalizeDouble(volume * InpPartialClosePercent, 2); //--- Рассчитываем объём частичного закрытия
         if (close_volume >= SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) { //--- Проверяем минимально допустимый объём
            trade.PositionClosePartial(ticket, close_volume); //--- Частично закрываем позицию
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Управление выходом по времени |
//+------------------------------------------------------------------+
void ManageTimeBasedExit() {
   if (InpMaxTradeHours == 0) return;                //--- Завершаем функцию, если ограничение длительности отключено

   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Перебираем позиции
      if (PositionGetSymbol(i) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) //--- Проверяем символ и магический номер
         continue;                                   //--- Пропускаем позицию, если параметры не совпадают

      ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Получаем тикет
      datetime open_time = (datetime)PositionGetInteger(POSITION_TIME); //--- Получаем время открытия
      datetime current_time = TimeCurrent();         //--- Получаем текущее время
      if ((current_time - open_time) / 3600 >= InpMaxTradeHours) { //--- Проверяем превышение максимальной длительности
         double profit = PositionGetDouble(POSITION_PROFIT); //--- Получаем прибыль
         trade.PositionClose(ticket);                //--- Закрываем позицию
      }
   }
}

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

Функция ManageTrailingStop перебирает позиции советника и динамически изменяет Stop Loss при движении цены в благоприятную сторону. Расстояние трейлинг-стопа и шаг пересчитываются через _Point и g_pointMultiplier. Для покупок новый Stop Loss размещается ниже текущей цены и перемещается только вверх; для продаж — выше текущей цены и только вниз.

Функция ManagePartialClose частично фиксирует прибыль, когда цена проходит половину расстояния до Take Profit. Для подходящей позиции определяются цена открытия, TP, текущая цена и объём. Объём закрытия рассчитывается как доля InpPartialClosePercent и проверяется относительно минимального допустимого объёма, после чего вызывается trade.PositionClosePartial.

if (InpUseTrailingStop)                      //--- Проверяем, включён ли трейлинг-стоп
   ManageTrailingStop();                     //--- Управляем трейлинг-стопом
if (InpUsePartialClose)                      //--- Проверяем, включено ли частичное закрытие
   ManagePartialClose();                     //--- Управляем частичным закрытием
ManageTimeBasedExit();                       //--- Выполняем выход по времени

Функция ManageTimeBasedExit отключается, если InpMaxTradeHours равно нулю. В остальных случаях она сравнивает время открытия каждой позиции с TimeCurrent. Если установленный предел достигнут, позиция закрывается функцией trade.PositionClose. После этого функции управления вызываются из основной логики.

УПРАВЛЕНИЕ ПОЗИЦИЯМИ

После компиляции получаем следующий результат.


Бэктестирование

На изображении видно, что советник активно сопровождает прибыльные позиции и перемещает защитные уровни. Реализация завершена; остаётся протестировать программу в тестере стратегий.

После подробного бэктестирования получены следующие результаты.

График бэктестирования:

ГРАФИК

Отчёт бэктестирования:

ОТЧЁТ


Заключение

В заключение мы разработали на MQL5 систему статистического возврата к среднему. Она рассчитывает среднее, дисперсию, асимметрию, эксцесс и статистику Харке-Бера, формирует сигналы при выходе цены за границы доверительного интервала с адаптивными порогами и необязательным подтверждением на старшем таймфрейме, открывает сделки фиксированным объёмом или с расчётом риска и применяет трейлинг-стоп, частичное закрытие и выход по времени. Все основные показатели отображаются на информационной панели в реальном времени.

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

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

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

Прикрепленные файлы |
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 4): Преодоление ограничений многострочного ввода, сохранение чатов между сеансами и генерация торговых сигналов Создание торговых систем с искусственным интеллектом на MQL5 (Часть 4): Преодоление ограничений многострочного ввода, сохранение чатов между сеансами и генерация торговых сигналов
В этой статье мы улучшим программу с интеграцией ChatGPT на MQL5: устраним ограничения многострочного ввода благодаря улучшенной отрисовке текста, добавим боковую панель для навигации по истории сохраненных чатов с шифрованием AES256 и ZIP-сжатием, а также сформируем начальные торговые сигналы на основе данных графика.
Нейросети в трейдинге: Долговременная память торговых сценариев (VLADriver-RAG) Нейросети в трейдинге: Долговременная память торговых сценариев (VLADriver-RAG)
Описан перенос идей фреймворка VLADriver-RAG в торговые задачи: добавляется внешняя память завершённых эпизодов поверх нейросетевой политики. Формируется ячейка памяти с эмбеддингом рыночной сцены, нормированным действием и результатом, а близкие случаи группируются в центроиды сценария и действия. Реализованы ядра для косинусного скоринга и поиска ближайшей пары, позволяющие искать и уплотнять опыт.
Автоматизация торговых стратегий на MQL5 (Часть 40): Торговля по уровням коррекции Фибоначчи с пользовательскими уровнями Автоматизация торговых стратегий на MQL5 (Часть 40): Торговля по уровням коррекции Фибоначчи с пользовательскими уровнями
В статье разрабатывается советник MQL5 для торговли по уровням коррекции Фибоначчи. Пользовательские уровни, например 50% и 61,8%, рассчитываются по диапазону дневной свечи или массиву данных за выбранный период. Направление определяется по соотношению цен закрытия и открытия; позиции открываются при пересечении уровней с ограничением числа сделок, необязательным закрытием при новом расчёте, трейлинг-стопом в пунктах и буферами SL/TP в процентах от диапазона.
От начального до среднего уровня: Очереди, списки и деревья (III) От начального до среднего уровня: Очереди, списки и деревья (III)
В этой статье мы сделаем следующий шаг в понимании того, что такое связный список и как он работает. Хотя содержание данной статьи может показаться довольно сложным и запутанным для новичков, я постараюсь объяснить его максимально доступным языком. Это поможет вам понять, почему и когда следует использовать связные списки.