English
preview
Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5

Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5

MetaTrader 5Эксперты |
66 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Оглавление

  1. Введение
  2. Обзор архитектуры
  3. Основные структуры данных и утилиты
  4. Расчет размера позиции на основе вероятности в MQL5
  5. Динамический расчет размера позиции по прогнозной цене в MQL5
  6. Бюджетно-ограниченный расчет размера позиции в MQL5
  7. Резервный метод расчета размера позиции через смесь гауссиан в MQL5
  8. Интеграция конвейера: полный Expert Advisor
  9. Заключение
  10. Прикрепленные файлы


Введение

Часть 10 этой серии строго вывела четыре метода из базовых принципов и показала их реализации на Python в модуле afml.bet_sizing. Каждый метод решает конкретную задачу: метод на основе вероятности преобразует уверенность классификатора в размер позиции и учитывает одновременность и перекрытие меток; динамический метод сопоставляет непрерывное расхождение прогнозной цены с размером позиции через калиброванную функциональную форму; бюджетно-ограниченный метод контролирует экспозицию, когда оценка уверенности отсутствует; а резервный метод расчета размера позиции полностью строит кривую размера позиции по данным. Аналитические основы теперь установлены. Остается задача перевода: как запустить эти методы внутри MetaTrader 5, где каждое вычисление должно укладываться в событийный цикл, управляемый тиками, и где нет SciPy, NumPy и multiprocessing?

В статье дается практический ответ на этот вопрос. В ней представлены четыре include-файла MQL5 — по одному на каждый метод расчета размера позиции, — которые воспроизводят математическое поведение модуля Python. Также включен пятый файл с общими структурами данных и статистическими утилитами, необходимыми всем методам. Каждый файл достаточно самодостаточен, чтобы его можно было добавить в существующий советник с минимальными изменениями, но при этом файлы достаточно согласованны между собой, чтобы все четыре метода можно было объединить в одном советнике, выбирающем подходящий механизм расчета размера позиции во время выполнения. Реализации точны там, где точность достижима, и численно эквивалентны там, где строгая точность непрактична. Нормальная CDF вычисляется через минимаксную рациональную аппроксимацию с точностью до семи значащих цифр.

Параметры смеси EF3M оцениваются многостартовым аналитическим решением: для каждой из n_runs случайных пар начальных значений (μ₁, p₁) оставшиеся три параметра (μ₂, σ₁, σ₂) выводятся в закрытой форме из линейной системы 2×2, построенной по первым трем сырым моментам, и выбирается кандидат с максимальным значением логарифма правдоподобия. Для запросов числа одновременно активных позиций в бюджетном и резервном методах вложенный проход O(N²) заменяется на O(N log N) алгоритм сканирующей прямой по отсортированному массиву событий; шаг усреднения в вероятностном методе (AvgActiveSignals) остается O(N²), поскольку вычисление среднего сигнала по всем активным интервалам требует значений сигналов, а не только счетчиков.

После прочтения этой статьи у вас будет полная, запускаемая система расчета размера позиции в MQL5, подходящая для любого советника на основе классификатора из предыдущих статей серии. Система выдает размер позиции со знаком в [−1, 1] на каждом новом баре, диагностическую структуру, фиксирующую каждый фактор, сформировавший размер, и лимитную цену для исполнения, когда активен динамический метод. Часть 14 связывает этот слой расчета размера позиции с фреймворком CPCV-бэктестирования внутри MetaTrader 5 Strategy Tester.


Обзор архитектуры

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

Слой Файл Ответственность
Утилиты BetSizingUtils.mqh Normal CDF/ICDF, вычисление моментов, определения struct, счетчик на основе алгоритма заметающей прямой событий
EF3M EF3M.mqh Оценка параметров смеси гауссиан по первым трем сырым моментам
Фрагменты кода Ch10Snippets.mqh GetSignal, AvgActiveSignals, DiscreteSignal, сигмоидальная и степенная функции размера позиции, GetW, LimitPrice
Пользовательский API BetSizing.mqh BetSizeProbability, BetSizeDynamic, BetSizeBudget, BetSizeReserve
Expert Advisor BetSizingEA.mq5 Связывает выход классификатора с выбранным механизмом расчета размера позиции; отправляет ордера

Блок-схема ниже показывает поток данных на каждом новом баре. Советник находится сверху и получает два входа от пайплайна классификатора, построенного в предыдущих статьях: массив предсказанных вероятностей и массив предсказанных сторон. В зависимости от того, какой метод расчета размера позиции выбран через входной параметр, советник направляет эти входы через соответствующую пользовательскую функцию. Каждая пользовательская функция вызывает одну или несколько функций уровня фрагментов кода, а все они вызывают утилиты. Результат возвращается в советник в виде структуры BetSizeResult, из которой советник извлекает размер позиции со знаком и лимитную цену.

Архитектура механизма расчета размера позиции

Рисунок 1. Стек зависимостей из пяти файлов

  • Слой Expert Advisor: BetSizingEA.mq5 связывает выход классификатора с выбранным модулем расчета размера позиции и управляет ордерами.
  • Слой пользовательского API: BetSizing.mqh предоставляет четыре функции-оркестратора и вспомогательную функцию для инициализации бюджета.
  • Слой фрагментов кода / EF3M: Ch10Snippets.mqh и EF3M.mqh реализуют низкоуровневые математические примитивы, каждый из которых зависит только от слоя утилит.
  • Слой утилит: BetSizingUtils.mqh предоставляет статистические примитивы и общие структуры данных без входящих зависимостей.

Одно архитектурное решение заслуживает отдельного упоминания. Python-модуль хранит сигналы в pandas DataFrame с datetime-индексом, что делает запросы перекрытия меток тривиально простыми. В MQL5 такой структуры нет. Все данные с временной индексацией хранятся как параллельные массивы, отсортированные по индексу бара, а алгоритм сканирующей прямой в BetSizingUtils.mqh заменяет обнаружение перекрытий на основе DataFrame. Это не упрощение, а полноценная замена: она дает идентичные результаты и работает быстрее на больших массивах, потому что избегает повторных линейных проходов.


Основные структуры данных и утилиты

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

Аппроксимация нормальной CDF

Минимаксная рациональная аппроксимация Харта (1968) достигает точности в семь значащих цифр по всей действительной прямой и требует лишь небольшого числа операций с плавающей точкой — это критично в цикле, управляемом тиками, где CDF может вызываться тысячи раз в секунду:

//--- BetSizingUtils.mqh (excerpt)
double NormCDF(double x)
  {
// Hart (1968) minimax rational approximation, |error| < 7.5e-8
   double t = 1.0 / (1.0 + 0.2316419 * MathAbs(x));
   double poly = t * (0.319381530
                      + t * (-0.356563782
                             + t * (1.781477937
                                    + t * (-1.821255978
                                           + t *  1.330274429))));
   double cdf = 1.0 - (1.0 / MathSqrt(2.0 * M_PI))
                * MathExp(-0.5 * x * x) * poly;
   return((x >= 0.0) ? cdf : 1.0 - cdf);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double NormICDF(double p)
  {
// Beasley-Springer-Moro algorithm, accurate to 1e-7.
// Central region uses the AS 111 rational form (Beasley & Springer 1977);
// the 9-coefficient Chebyshev tail extension is from Moro (1995).
   static const double a[] = {2.50662823884, -18.61500062529,
                              41.39119773534, -25.44106049637
                             };
   static const double b[] = {-8.47351093090, 23.08336743743,
                              -21.06224101826, 3.13082909833
                             };
   static const double c[] = {0.3374754822726147, 0.9761690190917186,
                              0.1607979714918209, 0.0276438810333863,
                              0.0038405729373609, 0.0003951896511349,
                              0.0000321767881768, 0.0000002888167364,
                              0.0000003960315187
                             };
   double y = p - 0.5;
   if(MathAbs(y) < 0.42)
     {
      double r = y * y;
      return(y * (((a[3] * r + a[2]) * r + a[1]) * r + a[0]) /
             ((((b[3] * r + b[2]) * r + b[1]) * r + b[0]) * r + 1.0));
     }
   double r = (y > 0.0) ? 1.0 - p : p;
   r = MathLog(-MathLog(r));
   double q = c[0] + r * (c[1] + r * (c[2] + r * (c[3] + r * (c[4] +
                                      r * (c[5] + r * (c[6] + r * (c[7] + r * c[8])))))));
   return((y > 0.0) ? q : -q);
  }
//+------------------------------------------------------------------+

Структура BetSizeResult

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

struct BetSizeResult
  {
   double            bet_size;     // Размер позиции со знаком в [-1, 1]
   double            t_pos;        // Целевая целочисленная позиция (только динамический метод)
   double            l_p;          // Лимитная цена (только динамический метод)
   double            raw_signal;   // Сигнал до усреднения и дискретизации
   double            avg_signal;   // Сигнал после усреднения, до дискретизации
   int               active_long;  // Одновременно активные длинные позиции на этом баре
   int               active_short; // Одновременно активные короткие позиции на этом баре
   double            c_t;          // Дисбаланс длинных и коротких позиций (бюджетный/резервный методы)
   datetime          bar_time;     // Время открытия бара, к которому относится результат
  };

Счетчик одновременности на основе сканирующей прямой

Python-модуль определяет одновременность активных сигналов, проходя по DatetimeIndex и проверяя, какие интервалы сигналов [start, t1) покрывают каждое время запроса. Для больших массивов это O(N²). Замена на алгоритм сканирующей прямой работает за O(N log N): отсортировать все события начала и конца интервалов в один массив, один раз пройти по нему, увеличивая счетчик на каждом start и уменьшая на каждом end, и записать счетчики long/short в каждом момент времени запроса.

//--- Sweep-line active-count for parallel arrays of open/close times
void SweepLineActiveCounts(
   const datetime &open_times[],  // Временные метки открытия позиций
   const datetime &close_times[], // Временные метки закрытия позиций (t1)
   const int      &sides[],       // +1 / -1
   const datetime &query_times[], // Bar times to evaluate at
   int            &active_long[], // Output: active long count per query
   int            &active_short[] // Output: active short count per query
)
  {
// Build event list: (time, +1=open / -1=close, side)
   SLEvent events[];
   ArrayResize(events, 2 * n_bets);
   for(int i = 0; i < n_bets; i++)
     {
      events[2 * i].t         = open_times[i];
      events[2 * i].delta     = 1;
      events[2 * i].side      = sides[i];
      events[2 * i + 1].t     = close_times[i];
      events[2 * i + 1].delta = -1;
      events[2 * i + 1].side  = sides[i];
     }
// Sort by time (insertion sort, adequate for N < 20000)
   SortSLEvents(events);

   int long_cnt = 0, short_cnt = 0, ev_idx = 0;
   for(int q = 0; q < n_query; q++)
     {
      while(ev_idx < ArraySize(events) &&
            events[ev_idx].t <= query_times[q])
        {
         if(events[ev_idx].side == 1)
            long_cnt  += events[ev_idx].delta;
         else
            short_cnt += events[ev_idx].delta;
         ev_idx++;
        }
      active_long[q]  = MathMax(0, long_cnt);
      active_short[q] = MathMax(0, short_cnt);
     }
  }
//+------------------------------------------------------------------+

Рисунок 2 показывает алгоритм на примере шести позиций. Панель (a) рисует полуоткрытые интервалы [open_t, close_t) как горизонтальные полосы, закодированные цветом по стороне. Панель (b) показывает отсортированный список событий: каждое событие открытия — треугольник вверх, каждое закрытие — треугольник вниз. Желтая пунктирная линия отмечает запрос в t = 10; все события слева от него уже обработаны проходом заметающей прямой, поэтому текущие счетчики точно отражают активный набор на этом баре. Панель (c) показывает полученные ступенчатые функции для active_long и active_short.

Алгоритм заметающей прямой для подсчёта активных интервалов

Рисунок 2. Иллюстрация алгоритма sweep-line подсчета активных позиций из 3 панелей

  • Панель (a): шесть интервалов позиций, нарисованные как полуоткрытые полосы [open_t, close_t); синий = long, оранжевый = short. Желтая пунктирная линия отмечает момент времени запроса на баре 10.
  • Панель (b): отсортированный список событий, полученный из интервалов. События открытия (▲) предшествуют событиям закрытия (▼) на каждой временной метке. Курсор сканирующей прямой обрабатывает все события с time ≤ query_t перед записью счетчиков.
  • Панель (c): полученная active_long (синяя ступенчатая функция) и active_short (оранжевая ступенчатая функция) на каждом индексе бара. При запросе t = 10: active_long = 1, active_short = 2.


Расчет размера позиции на основе вероятности в MQL5

Это первый метод, который нужно реализовать, потому что он наиболее универсален и потому что требуемые им слои усреднения и дискретизации используются также бюджетным и резервным методами. Реализация MQL5 следует тому же трехэтапному конвейеру, что и исходная версия на Python: преобразование z-оценки через GetSignal, временное усреднение через AvgActiveSignals, и дискретизация через DiscreteSignal.

GetSignal в MQL5

//--- Ch10Snippets.mqh
// Snippet 10.1: transform predicted probability to signed bet size
// prob:        predicted probability for the positive class
// num_classes: number of outcome classes (2 for binary)
// pred:        +1 (long) or -1 (short); 0 returns unsigned magnitude
double GetSignal(double prob, int num_classes, int pred)
  {
   double base_rate = 1.0 / num_classes;
   double denom     = MathSqrt(prob * (1.0 - prob));
   if(denom < 1e-10)
      return((prob > base_rate) ? 1.0 : -1.0);
   double z      = (prob - base_rate) / denom;
   double signal = 2.0 * NormCDF(z) - 1.0;
   if(pred != 0)
      signal *= (pred > 0) ? 1.0 : -1.0;
   return(signal);
  }
//+------------------------------------------------------------------+

Защитная проверка denom обрабатывает крайние случаи p = 0 и p = 1, где стандартное отклонение Бернулли схлопывается до нуля и z-оценка переполнилась бы. На практике эти случаи возникают, когда классификатор присваивает предсказанию полную уверенность — редко, но возможно для градиентно-бустинговых деревьев, переобученных на обучающем наборе.

AvgActiveSignals в MQL5

Python-реализация проходит по отсортированному DatetimeIndex и использует булеву индексацию, чтобы определить, какие сигналы активны на каждой временной метке. Версия MQL5 использует то же соглашение о параллельных массивах и применяет вложенный проход:

// Snippet 10.2: compute the mean of all concurrently active signals
void AvgActiveSignals(
   const datetime &open_t[],  // Signal start times
   const datetime &close_t[], // Signal end times (t1)
   const double   &signals[], // Per-signal bet sizes from GetSignal
   const datetime &query_t[], // Временные метки баров для оценки
   double         &avg_out[]  // Output: averaged signal per bar
)
  {
   int n_sig   = ArraySize(open_t);
   int n_query = ArraySize(query_t);
   ArrayResize(avg_out, n_query);

   for(int q = 0; q < n_query; q++)
     {
      double sum   = 0.0;
      int    count = 0;
      for(int i = 0; i < n_sig; i++)
        {
         if(open_t[i] <= query_t[q] && query_t[q] < close_t[i])
           {
            sum += signals[i];
            count++;
           }
        }
      avg_out[q] = (count > 0) ? sum / count : 0.0;
     }
  }

// Snippet 10.3: discretize a continuous signal to multiples of step_size
double DiscreteSignal(double signal, double step_size)
  {
   if(step_size <= 0.0)
      return(signal);
   double s = MathRound(signal / step_size) * step_size;
   return(MathMax(-1.0, MathMin(1.0, s)));
  }
//+------------------------------------------------------------------+

Внутренний цикл проходит по всем n_sig сигналам для каждого из n_query времен запроса, давая сложность O(N²). Это корректно по замыслу: вычисление взвешенного среднего по всем интервалам, покрывающим точку запроса, требует чтения значение каждого сигнала, поэтому инфраструктуру sweep-line из BetSizingUtils.mqh нельзя переиспользовать здесь как есть. В реальной торговле массив временных меток запроса всегда содержит одну временную метку (текущий бар), поэтому внешний цикл выполняется один раз; только внутренний проход по всем n_sig историческим сигналам линеен по длине истории. Худший случай O(N²) возникает во время OnInit когда все исторические моменты времени баров передаются как запросы одновременно.

Оркестратор BetSizeProbability

//--- BetSizing.mqh (excerpt)
// User-level function matching bet_size_probability() in the Python module
BetSizeResult BetSizeProbability(
   const datetime &open_t[],       // Label open times
   const datetime &close_t[],      // Label close times (t1)
   const double   &prob[],         // Classifier probabilities
   const int      &pred[],         // Predicted sides (+1 / -1)
   int             num_classes,    // 2 for binary classification
   double          step_size,      // Discretization grid (0 = off)
   bool            average_active, // Apply concurrency correction
   datetime        query_time      // Current bar time
)
  {
   BetSizeResult result = {};
   result.bar_time = query_time;
   int n = ArraySize(prob);

// Stage 1: per-signal z-score transformation
   double raw_signals[];
   ArrayResize(raw_signals, n);
   for(int i = 0; i < n; i++)
      raw_signals[i] = GetSignal(prob[i], num_classes, pred[i]);

// Stage 2: concurrency correction (optional)
   if(average_active)
     {
      datetime query_arr[] = {query_time};
      double avg_arr[];
      AvgActiveSignals(open_t, close_t, raw_signals, query_arr, avg_arr);
      result.avg_signal = avg_arr[0];
     }
   else
      result.avg_signal = (n > 0) ? raw_signals[n - 1] : 0.0;

   result.raw_signal = (n > 0) ? raw_signals[n - 1] : 0.0;

// Stage 3: discretization
   result.bet_size = DiscreteSignal(result.avg_signal, step_size);
   return(result);
  }
//+------------------------------------------------------------------+

Использование в торговом советнике

//--- Inside OnBar() in BetSizingEA.mq5
BetSizeResult r = BetSizeProbability(
                     open_t, close_t, prob, pred,
                     2,     // binary classifier
                     0.05,  // 5% position increments
                     true,  // correct for overlapping triple-barrier labels
                     TimeCurrent()
                  );
// r.bet_size is in [-1, 1]; send order proportional to max_lots * r.bet_size
double lots = NormalizeDouble(max_lots * MathAbs(r.bet_size), 2);
int    side = (r.bet_size > 0.0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
//+------------------------------------------------------------------+

Флаг average_active=true следует считать обязательным значением по умолчанию для любой стратегии, построенной на triple-barrier метках, ровно по причине, описанной в Части 10: его пропуск равносилен утверждению, что ваши метки никогда не перекрываются, что на практике почти никогда не верно. Риск резкого роста числа одновременно активных сигналов не гипотетичен — это систематическая переэкспозиция, которая усиливается в периоды плотных сигналов и полностью предотвратима.


Динамический расчет размера позиции по прогнозной цене в MQL5

Динамический механизм расчета размера позиции применим всякий раз, когда модель выдает непрерывную прогнозную цену, а не вероятность класса. Его реализация в MQL5 охватывает обе функциональные формы — sigmoid и power, закрытую калибровку GetW и вычисление лимитной цены.

Функции Sigmoid и Power

//--- Ch10Snippets.mqh
// Snippet 10.4a: sigmoid bet size
// price_div: forecast_price - market_price
// w:         calibration parameter (larger = more conservative)
double SigmoidBetSize(double price_div, double w)
  {
   return(price_div / MathSqrt(w + price_div * price_div));
  }

// Snippet 10.4b: power bet size
// price_div must be pre-normalized to [-1, 1]
double PowerBetSize(double price_div, double w)
  {
   if(MathAbs(price_div) > 1.0)
     {
      Print("PowerBetSize: price_div must be in [-1,1]. Got: ",
            DoubleToString(price_div, 6));
      return(MathSign(price_div));
     }
   return(MathSign(price_div) * MathPow(MathAbs(price_div), w));
  }

// GetW: calibrate w from a (divergence, target_bet_size) pair
double GetW(double price_div, double m_bet_size,
            string func = "sigmoid")
  {
   if(func == "sigmoid")
     {
      // Invert: m = x/sqrt(w+x^2)  =>  w = x^2*(1-m^2)/m^2
      if(MathAbs(m_bet_size) >= 1.0)
        {
         Print("GetW: target bet size must be strictly inside (-1,1)");
         return(0.0);
        }
      double m2 = m_bet_size * m_bet_size;
      return(price_div * price_div * (1.0 - m2) / m2);
     }
   else
     {
      // Power form: m = |x|^w  =>  w = log(m)/log(|x|)
      if(MathAbs(price_div) <= 0.0 || MathAbs(price_div) >= 1.0 ||
         m_bet_size <= 0.0 || m_bet_size >= 1.0)
        {
         Print("GetW power: price_div and m_bet_size must be in (0,1)");
         return(1.0);
        }
      return(MathLog(MathAbs(m_bet_size)) /
             MathLog(MathAbs(price_div)));
     }
  }
//+------------------------------------------------------------------+

Вычисление лимитной цены

Лимитная цена — это средняя безубыточная цена исполнения по каждой дискретной единице изменения позиции от current_pos до t_pos. Она вычисляется путем обращения функции размера позиции: если f(x) = m — это формула расчета размера позиции, то расхождение, которое оправдывает единицу позиции k равно f⁻¹(k/max_pos), а соответствующая лимитная цена равна market_price + f⁻¹(k/max_pos).

// Compute limit price for moving from pos_curr to pos_target
double LimitPrice(double market_price, double pos_curr,
                  double pos_target,   double max_pos,
                  double w,            string func)
  {
   double sum = 0.0;
   double sgn = (pos_target > pos_curr) ? 1.0 : -1.0;
   int    steps = (int)MathRound(MathAbs(pos_target - pos_curr));
   if(steps == 0)
      return(market_price);
   for(int k = 1; k <= steps; k++)
     {
      double m     = sgn * (MathAbs(pos_curr) + k) / max_pos;
      double div_k;
      if(func == "sigmoid")
         div_k = sgn * MathSqrt(w) * m / MathSqrt(1.0 - m * m);
      else
         div_k = sgn * MathPow(MathAbs(m), 1.0 / w);
      sum += div_k;
     }
   return(market_price + sum / steps);
  }
//+------------------------------------------------------------------+

Оркестратор BetSizeDynamic

BetSizeResult BetSizeDynamic(
   double  current_pos,    // Current open position (signed integer)
   double  max_pos,        // Maximum allowed position size
   double  market_price,   // Current mid price
   double  forecast_price, // Model forecast price
   double  cal_divergence, // Calibration: target divergence (e.g. 10 pips)
   double  cal_bet_size,   // Calibration: target bet size at cal_divergence
   string  func            // "sigmoid" or "power"
)
  {
   BetSizeResult result = {};
   double w   = GetW(cal_divergence, cal_bet_size, func);
   double div = forecast_price - market_price;
   double bsz = (func == "sigmoid")
                ? SigmoidBetSize(div, w)
                : PowerBetSize(div / cal_divergence, w);
   result.bet_size   = MathMax(-1.0, MathMin(1.0, bsz));
   result.t_pos      = MathRound(result.bet_size * max_pos);
   result.l_p        = LimitPrice(market_price, current_pos,
                                  result.t_pos, max_pos, w, func);
   result.raw_signal = div;            // расхождение цены; полезно для диагностики
   result.avg_signal = result.bet_size;
   return(result);
  }
//+------------------------------------------------------------------+

Выбор между Sigmoid и Power в реальной торговле

Для стратегии возврата к среднему в реальной торговле, где можно оценить кривую расхождения EV по walk-forward бэктесту, откалибруйте w путем подгонки PowerBetSize(x, w) к эмпирической кривой с помощью простого перебора по сетке по w ∈ [0.1, 5.0] с критерием минимальной среднеквадратичной ошибки. Для неизвестной стратегии или стратегии, разработанной с нуля, используйте сигмоидальную функцию с консервативной целью калибровки cal_bet_size = 0.50 при расхождении в одно стандартное отклонение. Сигмоидальная функция будет недоразмеривать позицию при больших расхождениях относительно выпуклой кривой EV, но также будет недоразмеривать при малых расхождениях относительно вогнутой; это выбор по принципу максимальной энтропии, когда форма EV действительно неизвестна.


Бюджетно-ограниченный расчет размера позиции в MQL5

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

//--- BetSizing.mqh (excerpt)
BetSizeResult BetSizeBudget(
   const datetime &open_t[],  // Bet open times
   const datetime &close_t[], // Bet close times (t1)
   const int      &sides[],   // +1 / -1
   datetime        query_time // Current bar time
)
  {
   BetSizeResult result = {};
   result.bar_time = query_time;

   datetime query_arr[] = {query_time};
   int al[], as_arr[];
   SweepLineActiveCounts(open_t, close_t, sides, query_arr, al, as_arr);
   result.active_long  = al[0];
   result.active_short = as_arr[0];

// Running maxima: persisted as глобальными переменными уровня файла (g_budget_max_long/short)
// initialized in OnInit via SeedBudgetMaxima(). See BetSizing.mqh.
   if(al[0]     > g_budget_max_long)
      g_budget_max_long  = al[0];
   if(as_arr[0] > g_budget_max_short)
      g_budget_max_short = as_arr[0];

   double frac_long  = (double)al[0]     / g_budget_max_long;
   double frac_short = (double)as_arr[0] / g_budget_max_short;
   result.c_t        = frac_long - frac_short;
   result.bet_size   = MathMax(-1.0, MathMin(1.0, result.c_t));
   return(result);
  }
//+------------------------------------------------------------------+

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

//--- In OnInit(): начальное значение the running maxima from historical bets
void SeedBudgetMaxima(const datetime &open_t[], const datetime &close_t[],
                      const int &sides[])
  {
   int n = ArraySize(open_t);
   if(n == 0)
      return;
// Sample at each open time to find the true historical maxima
   int al[], as_arr[];
   SweepLineActiveCounts(open_t, close_t, sides, open_t, al, as_arr);
   int ml = 1, ms = 1;
   for(int i = 0; i < n; i++)
     {
      if(al[i] > ml)
         ml = al[i];
      if(as_arr[i] > ms)
         ms = as_arr[i];
     }
   g_budget_max_long  = ml;
   g_budget_max_short = ms;
  }
//+------------------------------------------------------------------+

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


Резервный метод расчета размера позиции через смесь гауссиан в MQL5

Резервный метод является самым вычислительно требовательным из четырех. Его MQL5-реализация должна выполнять оценку параметров EF3M, которая в Python-модуле использует multiprocessing. В MQL5 эквивалентом служит запуск нескольких аналитических решений из случайных начальных условий в OnInit — где задержка некритична — и кэширование полученных параметров для использования во время OnTick.

Оценка параметров EF3M в MQL5

Алгоритм EF3M подгоняет смесь двух гауссиан к эмпирическому распределению ряда дисбаланса c_t = active_long − active_short. Пятипараметрическую смесь (μ₁, μ₂, σ₁, σ₂, p₁) нельзя идентифицировать менее чем из пяти уравнений моментов. Реализация MQL5 использует вариант EF3M-3: зафиксировать два свободных параметра (μ₁, p₁) как случайные начальные значения; аналитически вывести оставшиеся три из первых трех сырых моментов через линейное решение 2×2; затем оценить логарифм правдоподобия и сохранить лучшего из n_runs кандидатов.

Ключевой вывод идет в два шага. Во-первых, первый сырой момент сразу дает μ₂:

   mu2 = (m1 - p1 * mu1) / (1 - p1)     // from E[X] = p1*mu1 + (1-p1)*mu2

Во-вторых, перестановка второго и третьего сырых моментов смеси в две формулы по неизвестным (σ₁², σ₂²) дает линейную систему 2×2:

   [ p1         (1-p1)    ] [ s1^2 ]   [ A ]
   [ p1*mu1   (1-p1)*mu2  ] [ s2^2 ] = [ B ]

   where A = m2 - p1*mu1^2 - (1-p1)*mu2^2          // from E[X^2]
         B = (m3 - p1*mu1^3 - (1-p1)*mu2^3) / 3    // from E[X^3] = mu^3 + 3*mu*sigma^2

Правило Крамера дает:

   det  = p1 * (1-p1) * (mu2 - mu1)
   s1^2 = (A*mu2 - B) / (p1 * (mu2-mu1))
   s2^2 = (B - A*mu1) / ((1-p1) * (mu2-mu1))

Система становится вырожденной только когда μ₁ = μ₂ (вырожденный случай одного компонента), что обнаруживается и отклоняется. Полная реализация:

//--- EF3M.mqh
bool DeriveComponentParams(double mu1, double p1,
                           const double &m[],
                           double &mu2, double &s1, double &s2)
  {
   if(p1 <= 0.0 || p1 >= 1.0)
      return(false);

   mu2 = (m[0] - p1 * mu1) / (1.0 - p1);   // from m1

   double gap = mu2 - mu1;
   if(MathAbs(gap) < 1e-12)
      return(false); // degenerate

// A = p1*s1^2 + (1-p1)*s2^2  (from m2)
   double A = m[1] - p1 * (mu1 * mu1) - (1.0 - p1) * (mu2 * mu2);

// B = p1*mu1*s1^2 + (1-p1)*mu2*s2^2  (from m3)
   double B = (m[2] - p1 * (mu1 * mu1 * mu1) - (1.0 - p1) * (mu2 * mu2 * mu2)) / 3.0;

// Cramer's rule: det = p1*(1-p1)*gap
   double var1 = (A * mu2 - B) / (p1 * gap);
   double var2 = (B - A * mu1) / ((1.0 - p1) * gap);

   if(var1 <= 0.0 || var2 <= 0.0)
      return(false);

   s1 = MathSqrt(var1);
   s2 = MathSqrt(var2);
   return(true);
  }

// Multi-start random search: n_runs analytic solves, keep best log-likelihood
M2NParams FitM2N(const double &data[], int n_runs = 100)
  {
   M2NParams best = {};
   best.log_likelihood = -1e18;

   int n = ArraySize(data);
   if(n < 30)
     {
      Print("FitM2N: too few observations");
      return(best);
     }

   double m[];
   RawMoments(data, m, 3);                       // E[X], E[X^2], E[X^3]

   double sigma_est = MathSqrt(MathMax(1e-12, m[1] - m[0] * m[0]));

   MathSrand((int)TimeCurrent());
   for(int r = 0; r < n_runs; r++)
     {
      double noise    = 2.0 * sigma_est *
                        ((double)(MathRand() - 16383) / 16383.0);
      double mu1_init = m[0] + noise;
      double p1_init  = 0.1 + 0.8 * ((double)MathRand() / 32767.0);

      double mu2, s1, s2;
      if(!DeriveComponentParams(mu1_init, p1_init, m, mu2, s1, s2))
         continue;

      M2NParams candidate;
      candidate.mu1 = mu1_init;
      candidate.mu2 = mu2;
      candidate.s1  = s1;
      candidate.s2  = s2;
      candidate.p1  = p1_init;
      candidate.log_likelihood = LogLikelihood(data, candidate);

      if(candidate.log_likelihood > best.log_likelihood)
         best = candidate;
     }

   return(best);
  }
//+------------------------------------------------------------------+

Рисунок 3 иллюстрирует этот процесс. Панель (a) показывает плоскость параметров (μ₁, p₁) для синтетического набора из 1000 точек с двумя компонентами (μ₁ = −3, σ₁ = 1.2, μ₂ = 2, σ₂ = 0.9, p₁ = 0.45). Темная область недопустима — эти начальные точки подразумевают отрицательные дисперсии компонентов и немедленно отклоняются. Синяя заливка в допустимой области кодирует логарифм правдоподобия; желтая звезда отмечает лучшего кандидата, найденного поиском с 300 начальными запусками. Панель (b) сравнивает CDF лучшей подогнанной смеси с эмпирической CDF данных и истинной порождающей CDF; подогнанная кривая неотличима от истинной в этом масштабе.

EF3M-3 Multi-Start Analytic Parameter Search

Рисунок 3. Двухпанельная иллюстрация многостартового аналитического поиска параметров EF3M-3

  • Панель (a): пространство поиска (μ₁, p₁). Темный фон = недопустимая область (подразумеваемые дисперсии ≤ 0). Синяя заливка = логарифм правдоподобия в допустимой области (темнее = выше). Желтая звезда = лучший подогнанный кандидат. Точки на диаграмме рассеяния — 300 случайных начальных точек, давших допустимые наборы параметров.
  • Панель (b): функция распределения лучшей подогнанной смеси (синий) против эмпирической функции распределения (серый) и истинной порождающей функции распределения (зеленый пунктир). Подогнанная кривая близко повторяет истинную функцию распределения, подтверждая аналитическое решение по трем моментам.

Функция распределения смеси и резервная формула расчета размера позиции

// Evaluate the mixture CDF at x
double MixtureCDF(double x, const M2NParams &p)
  {
   return(p.p1 * NormCDF((x - p.mu1) / p.s1)
          + (1.0 - p.p1) * NormCDF((x - p.mu2) / p.s2));
  }

// Convert c_t imbalance to bet size via the mixture CDF.
// Note: c_t is the сырой целочисленный дисбаланс (active_long - active_short),
// not the normalized fraction used by BetSizeBudget. The EF3M model
// must be fitted to the same raw c_t series.
double ReserveBetSize(double c_t, const M2NParams &p)
  {
   double F0 = MixtureCDF(0.0, p);
   double Fx = MixtureCDF(c_t, p);
   if(c_t >= 0.0)
      return((1.0 - F0 < 1e-10) ? 1.0 : (Fx - F0) / (1.0 - F0));
   else
      return((F0 < 1e-10) ? -1.0 : (Fx - F0) / F0);
  }
//+------------------------------------------------------------------+

Одно различие требует внимательного отношения. В BetSizeBudget, дисбаланс c_t сохраненный в BetSizeResult — это нормированная доля frac_long − frac_short ∈ [−1, 1]. В BetSizeReserve, это сырой целочисленный счетчик active_long − active_short. Модель EF3M подгоняется к сырому ряду c_t, поэтому вход CDF также должен быть сырыми счетчиками. Эти два поля имеют одно имя ради концептуальной преемственности, но работают в разных масштабах.

Паттерн инициализации

Оценка параметров EF3M не должна запускаться на каждом тике. Правильный паттерн — подогнать один раз в OnInit по истории прогрева и закэшировать результат:

//--- In BetSizingEA.mq5 OnInit()
M2NParams g_reserve_params;

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit(void)
  {
// ... load historical bets into g_open_t[], g_close_t[], g_sides[] ...

// Build the c_t series over the период прогрева
   int n = ArraySize(g_open_t);
   int al[], as_arr[];
   SweepLineActiveCounts(g_open_t, g_close_t, g_sides, g_open_t,
                         al, as_arr);
   double ct_series[];
   ArrayResize(ct_series, n);
   for(int i = 0; i < n; i++)
      ct_series[i] = (double)(al[i] - as_arr[i]);

// Fit the mixture — may take 1-3 seconds for 2000 bets
   g_reserve_params = FitM2N(ct_series, 100);
   PrintFormat("EF3M fit: mu1=%.4f mu2=%.4f s1=%.4f s2=%.4f p1=%.4f LL=%.2f",
               g_reserve_params.mu1, g_reserve_params.mu2,
               g_reserve_params.s1,  g_reserve_params.s2,
               g_reserve_params.p1,  g_reserve_params.log_likelihood);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Подгонку EF3M следует запускать повторно периодически: ежеквартально для стратегии на дневных барах или всякий раз, когда обнаружен значительный структурный сдвиг в распределении позиций (внезапное изменение среднего или дисперсии c_t более чем на два исторических стандартных отклонения — надежный триггер). Автоматическое обнаружение требует отслеживать скользящее окно c_t и сравнивать его первые два выборочных момента с подразумеваемыми моментами подогнанной смеси.


Интеграция конвейера: полный Expert Advisor

Четыре метода расчета размера позиции, модуль подгонки EF3M и служебные функции собираются в единый советник, который выбирает подходящий метод во время выполнения через входной параметр. Рисунок 4 показывает, что готовый советник производит на синтетической последовательности сигналов: каждая панель показывает выход bet_size одного метода по 80 барам при одной и той же истории из 40 направленных сигналов со случайными периодами удержания. Методы реагируют на одни и те же данные через принципиально разные подходы — вероятностный метод усредняет сырую уверенность, динамический метод реагирует на расхождение цены, бюджетный метод отслеживает доли занятости портфеля, а резервный метод учитывает эмпирическую форму распределения дисбаланса.

Four Sizing Methods — Bet Size on a Synthetic Signal Sequence

Рисунок 4. Четырехпанельная иллюстрация четырех методов расчета размера позиции на одной синтетической истории позиций

  • Панель (a): BetSizeProbability. Столбиковая диаграмма (левая ось) показывает число активных длинных и коротких позиций на бар. Синяя линия (правая ось) — дискретизированный, скорректированный по перекрытия размер позиции. Бары без активных сигналов дают нулевой размер позиции.
  • Панель (b): BetSizeDynamic (sigmoid). Зеленая линия отслеживает сигмоиду синтетического синусоидального расхождения цены. Серый пунктир показывает нормированное расхождение для ориентира.
  • Панель (c): BetSizeBudget. Желтая линия — нормированная доля дисбаланса длинных и коротких позиций. Она более гладкая, чем вероятностный метод, потому что не несет уверенности по отдельным сигналам.
  • Панель (d): BetSizeReserve. Фиолетовая линия отображает сырой целочисленный дисбаланс через функцию распределения подогнанной смеси. Ее форма напоминает бюджетный сигнал, но масштабирована эмпирическим распределением исторического дисбаланса.

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

//--- BetSizingEA.mq5
// Angle-bracket form: searches MQL5\Include\. All four header files must
// be placed in MQL5\Include\BetSizing; this EA belongs in MQL5\Experts\.
#include <BetSizing\BetSizing.mqh>   // pulls in Ch10Snippets, EF3M, and BetSizingUtils

enum ENUM_SIZING_METHOD
  {
   METHOD_PROBABILITY, // BetSizeProbability
   METHOD_DYNAMIC,     // BetSizeDynamic
   METHOD_BUDGET,      // BetSizeBudget
   METHOD_RESERVE      // BetSizeReserve
  };

input ENUM_SIZING_METHOD InpMethod      = METHOD_PROBABILITY;
input double             InpMaxLots     = 1.0;
input double             InpStepSize    = 0.05;
input bool               InpAvgActive   = true;
input double             InpCalDiv      = 10.0;   // pips, dynamic method
input double             InpCalBetSize  = 0.95;   // dynamic method
input string             InpDynFunc     = "sigmoid";
input int                InpEF3MRuns    = 100;

// Classifier output arrays — populated by your signal generation logic
datetime g_open_t[];
datetime g_close_t[];
double   g_prob[];
int      g_pred[];
int      g_sides[];

M2NParams g_reserve_params;
double    g_current_pos = 0.0;

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit(void)
  {
// Load historical signals from your classifier pipeline.
   LoadHistoricalSignals();

   if(InpMethod == METHOD_BUDGET)
      SeedBudgetMaxima(g_open_t, g_close_t, g_sides);

   if(InpMethod == METHOD_RESERVE)
     {
      int n = ArraySize(g_open_t);
      int al[], as_arr[];
      SweepLineActiveCounts(g_open_t, g_close_t, g_sides, g_open_t,
                            al, as_arr);
      double ct[];
      ArrayResize(ct, n);
      for(int i = 0; i < n; i++)
         ct[i] = (double)(al[i] - as_arr[i]);
      g_reserve_params = FitM2N(ct, InpEF3MRuns);
     }
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTick(void)
  {
   if(!IsNewBar())
      return;

   AppendNewSignal();

   BetSizeResult r = {};
   datetime now = TimeCurrent();

   switch(InpMethod)
     {
      case METHOD_PROBABILITY:
         r = BetSizeProbability(g_open_t, g_close_t, g_prob, g_pred,
                                2, InpStepSize, InpAvgActive, now);
         break;
      case METHOD_DYNAMIC:
         r = BetSizeDynamic(g_current_pos, InpMaxLots * 100,
                            SymbolInfoDouble(_Symbol, SYMBOL_BID),
                            GetForecastPrice(),
                            InpCalDiv * _Point, InpCalBetSize,
                            InpDynFunc);
         break;
      case METHOD_BUDGET:
         r = BetSizeBudget(g_open_t, g_close_t, g_sides, now);
         break;
      case METHOD_RESERVE:
         r = BetSizeReserve(g_open_t, g_close_t, g_sides,
                            now, g_reserve_params);
         break;
     }

   PrintDiagnostics(r);

   double target_lots = NormalizeDouble(InpMaxLots * MathAbs(r.bet_size), 2);
   if(target_lots < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN))
      target_lots = 0.0;
   AdjustPosition(r);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void PrintDiagnostics(const BetSizeResult &r)
  {
   PrintFormat("[%s] method=%d bet=%.4f raw=%.4f avg=%.4f aL=%d aS=%d c_t=%.2f lp=%.5f",
               TimeToString(r.bar_time, TIME_DATE | TIME_MINUTES),
               (int)InpMethod,
               r.bet_size, r.raw_signal, r.avg_signal,
               r.active_long, r.active_short,
               r.c_t, r.l_p);
  }
//+------------------------------------------------------------------+

Функция AdjustPosition (полная реализация в приложенном BetSizingEA.mq5) сравнивает target_lots и знак r.bet_size с текущей открытой позицией и отправляет рыночные ордера только тогда, когда требуемое изменение превышает минимальный размер лота. Это эквивалент шага дискретизации на уровне исполнения в Python-пайплайне: он предотвращает микро-корректировки, чья транзакционная стоимость превышает ожидаемый вклад в P&L.

Выбор правильного метода при развертывании

Сценарий Рекомендуемый метод Ключевая конфигурация
Классификатор с вероятностным выходом, перекрывающиеся метки METHOD_PROBABILITY InpAvgActive=true, InpStepSize=0.05
Регрессионная модель, создающая прогнозные цены METHOD_DYNAMIC Сигмоидальная функция для неизвестной формы EV; степенная функция для эмпирически охарактеризованной формы кривой EV
Правиловой направленный сигнал, без вероятности METHOD_BUDGET Инициализировать max_long/max_short из истории прогрева в OnInit
Направленный сигнал, бимодальная история позиций, большая выборка METHOD_RESERVE InpEF3MRuns≥100; повторно подгонять ежеквартально; минимум \~500 исторических позиций
Классификатор + контроль экспозиции портфеля METHOD_PROBABILITY × METHOD_BUDGET Умножить BetSizeProbability.bet_size на BetSizeBudget.bet_size


Заключение

Четыре реализации MQL5 в этой статье — прямой порт модуля Python afml.bet_sizing с сохранением математического поведения ценой переписывания каждого фрагмента статистической инфраструктуры, которую Python получает от NumPy и SciPy. Ключевые решения реализации, которые стоит повторить:

  • Нормальная CDF через рациональную аппроксимацию. Минимаксная рациональная аппроксимация Харта (1968) точна до семи значащих цифр и выполняется за фиксированное число операций с плавающей точкой. Избегайте табличных методов или итерационных методов для этой функции — она вызывается во внутреннем цикле и вероятностного, и резервного методов.
  • Заметающая прямая для подсчета перекрытия. Алгоритм sweep-line с сложностью O(N log N) заменяет вложенный цикл O(N²), который получился бы при наивном переводе Python-обнаружения перекрытий DatetimeIndex на MQL5. Для стратегий с тысячами исторических позиций разница между двухсекундной и двухминутной инициализацией — это именно этот алгоритм. Обратите внимание, что шаг усреднения в AvgActiveSignals остается O(N²) по замыслу — вычисление среднего по активным интервалам требует чтения значений сигналов, а не только счетчиков.
  • EF3M в OnInit, а не OnTick. Подгонка смеси запускает 100 аналитических решений, каждое из которых выводит параметры компонентов в закрытой форме из первых трех сырых моментов. Для 2000 точек данных основное время уходит на вычисление логарифма правдоподобия: планируйте одну-три секунды суммарно. Кэшируйте полученную структуру M2NParams в глобальной переменной и повторно оценивайте параметры только при обнаружении структурного сдвига в распределении позиций.
  • Диагностическая структура BetSizeResult. Каждый фактор, сформировавший итоговый размер позиции — сырой сигнал до усреднения, сигнал после усреднения, число активных длинных и коротких позиций, дисбаланс c_t, и лимитная цена — возвращается вместе с самим размером позиции. Используйте эту структуру, чтобы строить журнал аудита на уровне тиков, делающий поведение расчета размера позиции полностью прозрачным при мониторинге в реальном времени и послеторговом анализе.
  • Коррекция перекрытия обязательна для меток тройного барьера. Флаг InpAvgActive=true должен быть значением по умолчанию для любого советника, построенного на пайплайне разметки из Части 2 до Части 5 этой серии. Альтернатива — суммирование одновременных позиций — дает экспозицию, растущую пропорционально плотности сигналов, то есть систематическую переэкспозицию как раз в высоко-уверенных периодах, когда модель наиболее активна.
  • Требуются include-файлы в угловых скобках. Все четыре заголовочных файла должны быть помещены в MQL5\Include\BetSizing. Любой файл, который их использует — индикатор, советник или скрипт — должен ссылаться на них синтаксисом с угловыми скобками (#include <BetSizing\BetSizing.mqh>). Форма в кавычках ищет только в собственной директории включающего файла и не найдет файл, если тот отсутствует в каталоге включающего файла.

Часть 14 берет построенную здесь пятифайловую систему MQL5 и связывает ее с CPCV-бэктестинговым фреймворком внутри MetaTrader 5 Strategy Tester. Оценки вероятности, которые поступают в BetSizeProbability, сами подвержены систематическим смещениям; их характеристика и коррекция рассматриваются в Части 12. Множитель Келли, который масштабирует выход bet_size с учетом payoff-ratio awareness, описан в Части 11.


Прикрепленные файлы

  Файл Поместить в Зависит от Описание
1.BetSizingUtils.mqhMQL5\Include\BetSizingNormCDF, NormICDF, NormPDF, RawMoments, SweepLineActiveCounts, структура BetSizeResult, Clamp, MathSign.
2.EF3M.mqhMQL5\Include\BetSizingBetSizingUtils.mqhM2NParams struct, DeriveComponentParams (аналитическое трехмоментное решение), FitM2N (многостартовый поиск), MixtureCDF, ReserveBetSize.
3.Ch10Snippets.mqhMQL5\Include\BetSizingBetSizingUtils.mqhGetSignal, AvgActiveSignals, DiscreteSignal, SigmoidBetSize, PowerBetSize, GetW, LimitPrice.
4.BetSizing.mqhMQL5\Include\BetSizingCh10Snippets.mqh, EF3M.mqhBetSizeProbability, BetSizeDynamic, BetSizeBudget, BetSizeReserve, SeedBudgetMaxima. Пользовательский слой оркестрации.
5.BetSizingEA.mq5MQL5\Experts\BetSizing.mqhПолный Expert Advisor. Выбор метода во время выполнения, диагностическое логирование, корректировка позиции, подготовительная инициализация EF3M.
6.ch10_snippets.pyafml/bet_sizing/numpy, pandas, numba, scipy.statsФрагменты кода 10.1–10.4: get_signal, avg_active_signals, discrete_signal; сигмоидальный и степенной варианты bet_size, get_target_pos, inv_price, limit_price, get_w.
7.ef3m.pyafml/bet_sizing/numpy, pandas, numba, scipy.special, scipy.statsКласс M2N (алгоритм EF3M, многостартовая параллельная подгонка через mp_fit); вспомогательные функции raw_moment, centered_moment, most_likely_parameters, cdf_mixture.
8.bet_sizing.pyafml/bet_sizing/ch10_snippets.py, ef3m.py, numpy, pandas, numba, scipy.statsПользовательский слой оркестрации: bet_size_probability, bet_size_dynamic, bet_size_budget, bet_size_reserve, get_concurrent_sides, cdf_mixture, single_bet_size_mixed, confirm_and_cast_to_df.
9.__init__.pyafml/bet_sizing/bet_sizing.py, ef3m.pyТочка входа пакета. Реэкспортирует восемь публичных символов: bet_size_budget, bet_size_dynamic, bet_size_probability, bet_size_reserve, cdf_mixture, confirm_and_cast_to_df, get_concurrent_sides, single_bet_size_mixed.


Дополнительная литература

  • Lopez de Prado, M. (2018). Advances in Financial Machine Learning. John Wiley & Sons. Глава 10.
  • Lopez de Prado, M. and Foreman, M. (2014). Подход смеси двух гауссиан к математическому надзору за портфелем: алгоритм EF3M. Quantitative Finance, 14(5), 913–930.
  • Hart, J. F. (1968). Computer Approximations. Wiley. (Рациональная аппроксимация NormCDF.)
  • Beasley, J. D. and Springer, S. G. (1977). Algorithm AS 111: процентные точки нормального распределения. Applied Statistics, 26, 118–121. (Исходная рациональная аппроксимация центральной области NormICDF.)
  • Moro, B. (1995). The full Monte. Risk, 8(2), 57–58. (9-коэффициентное хвостовое расширение Чебышева, используемое в NormICDF.)

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

Прикрепленные файлы |
afml.zip (13.9 KB)
BetSizing.zip (18.59 KB)
Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5 Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5
Упростите графики MetaTrader 5 с помощью советника Multi Indicator Handler. Этот интерактивный инструмент объединяет индикаторы тренда, моментума и волатильности в единую панель, работающую в реальном времени. Мгновенно переключайтесь между профилями, чтобы сосредоточиться на нужном вам типе анализа. Одним кликом скрывайте и показывайте элементы панели и сохраняйте фокус на движении цены. Читайте дальше, чтобы шаг за шагом узнать, как самостоятельно создать и настроить этот инструмент на MQL5.
Инжиниринг признаков для машинного обучения (Часть 1): Дробное дифференцирование — стационарность без потери памяти Инжиниринг признаков для машинного обучения (Часть 1): Дробное дифференцирование — стационарность без потери памяти
Целочисленное дифференцирование заставляет выбирать между стационарностью и памятью: доходности (d=1) стационарны, но отбрасывают всю информацию об уровне цены; исходные цены (d=0) сохраняют память, но нарушают предположения ML о стационарности. Мы реализуем метод дробного дифференцирования фиксированной ширины (FFD) из главы 5 AFML: get_weights_ffd (итеративная рекурсия с отсечением по порогу), frac_diff_ffd (ограниченное скалярное произведение на каждом баре) и fracdiff_optimal (бинарный поиск минимального стационарного d*).
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения
В этой статье представлен инструмент "Multi-Timeframe Harmony Index" – продвинутый советник для MetaTrader 5, который рассчитывает взвешенное смещение рынка по нескольким таймфреймам, сглаживает значения с помощью EMA и выводит результат на аккуратной панели на графике. Он поддерживает настраиваемые алерты и автоматически наносит сигналы покупки и продажи на график, когда значение смещения пересекает значимые пороги. Подходит трейдерам, которые используют анализ нескольких таймфреймов, чтобы соотносить точки входа с общей структурой рынка.