preview
Самообучающийся советник с нейросетью на матрице состояний

Самообучающийся советник с нейросетью на матрице состояний

MetaTrader 5Торговые системы | 21 мая 2025, 11:47
738 10
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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

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

Результат наших экспериментов говорит сам за себя: среднегодовая доходность 28.7% при максимальной просадке всего 14.2%, коэффициент Шарпа 1.65 и 62.3% прибыльных сделок. Но за этими сухими цифрами скрывается гораздо более значимое достижение —система, которая одинаково уверенно чувствует себя и в тихой гавани боковых движений, и в шторме высокой волатильности.


Теоретический фундамент: встреча математики с реальностью

Марковские цепи: память, скрытая в настоящем

Начнем с вопроса, который может показаться философским: как много прошлого нужно знать, чтобы предсказать будущее? Марковская цепь дает на это элегантный ответ: достаточно знать только настоящее, если... правильно определить, что такое "настоящее".

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

P(X_t+1 = j | X_t = i, X_t-1 = i_t-1, ..., X_0 = i_0) = P(X_t+1 = j | X_t = i) = P_ij

На первый взгляд, это кажется противоречащим самой сути технического анализа с его верой в то, что "история повторяется" и "прошлое имеет значение". Но противоречие это лишь кажущееся. Всё дело в том, как мы определяем понятие "состояние".

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

Матрица переходных вероятностей становится в этом контексте настоящей картой рыночных возможностей. Каждый её элемент P(i,j) рассказывает нам о шансах перехода из одного состояния в другое, формируя своеобразную "ДНК" конкретного финансового инструмента.

Многослойный перцептрон: нейронная сеть для анализа переходов

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

Структура нашей нейронной сети напоминает архитектурный шедевр: изящное воздушное основание входного слоя с девятью нейронами, каждый из которых бережно принимает элемент матрицы 3×3; величественный скрытый слой, где сорок нейронов с активацией ReLU работают, как алхимики, превращая линейные зависимости в золото нелинейных паттернов; и, наконец, элегантная вершина в виде выходного слоя с двумя нейронами — хранителями тайного знания о вероятностях будущих движений цены.

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


Практическая реализация: от теории к коду

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

Определение состояний рынка

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

// Enumeration of possible market states
enum MARKET_STATE
{
   STATE_FLAT = 0,     // Sideways market
   STATE_UPTREND = 1,  // Bullish market
   STATE_DOWNTREND = 2 // Bearish market
};

// Function to determine current market state based on price movement relative to volatility
MARKET_STATE GetMarketState(int shift)
{
   double close[], atr[];
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(atr, true);
   
   // Get closing prices and ATR values
   if(CopyClose(_Symbol, PERIOD_D1, shift, 2, close) < 2 ||
      CopyBuffer(atrHandle, 0, shift, 1, atr) < 1) {
      return STATE_FLAT; // Default to flat if data is insufficient
   }
   
   // Calculate price change and get ATR value
   double priceChange = close[0] - close[1];
   double atrValue = atr[0];
   
   // Determine market state based on price change relative to ATR
   if(priceChange > 0.5 * atrValue) return STATE_UPTREND;
   if(priceChange < -0.5 * atrValue) return STATE_DOWNTREND;
   return STATE_FLAT;
}

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

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

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

Вот дополнительный код, который мы используем для инициализации и хранения ATR индикатора:

// Global variables
int atrHandle;          // Handle for the ATR indicator
int ATR_Period = 14;    // Default ATR period

// Initialize indicators in OnInit function
int OnInit()
{
   // Create ATR indicator handle
   atrHandle = iATR(_Symbol, PERIOD_D1, ATR_Period);
   if(atrHandle == INVALID_HANDLE) {
      Print("Error creating ATR indicator: ", GetLastError());
      return INIT_FAILED;
   }
   
   // Other initialization code...
   
   return INIT_SUCCEEDED;
}

// Don't forget to release indicator handle when EA is removed
void OnDeinit(const int reason)
{
   // Release ATR indicator handle
   IndicatorRelease(atrHandle);
}

Построение матрицы переходов

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

// Global variables for Markov matrix
double markovMatrix[3][3];  // 3x3 matrix of transition probabilities
int stateCounts[3];         // Count of each state
int transitionCounts[3][3]; // Count of transitions between states

// Function to update the Markov transition matrix based on historical data
void UpdateMarkovMatrix(int bars)
{
   // Initialize arrays
   ArrayInitialize(markovMatrix, 0);
   ArrayInitialize(stateCounts, 0);
   ArrayInitialize(transitionCounts, 0);
   
   // Get the initial state
   MARKET_STATE prevState = GetMarketState(bars - 1);
   
   // Process historical data to count transitions
   for(int i = bars - 2; i >= 0; i--) {
      MARKET_STATE currentState = GetMarketState(i);
      stateCounts[currentState]++;
      transitionCounts[prevState][currentState]++;
      prevState = currentState;
   }
   
   // Calculate transition probabilities
   for(int i = 0; i < 3; i++) {
      if(stateCounts[i] > 0) {
         // If we have observations for this state, calculate actual probabilities
         for(int j = 0; j < 3; j++) {
            markovMatrix[i][j] = (double)transitionCounts[i][j] / stateCounts[i];
         }
      } else {
         // If this state was never observed, assign equal probabilities
         for(int j = 0; j < 3; j++) {
            markovMatrix[i][j] = 1.0 / 3.0;
         }
      }
   }
   
   // Optional: Debug output of the matrix
   PrintMarkovMatrix();
}

// Helper function to print the Markov matrix for debugging
void PrintMarkovMatrix()
{
   Print("=== Markov Transition Matrix ===");
   string states[3] = {"FLAT", "UPTREND", "DOWNTREND"};
   
   Print("FROM\\TO\t| FLAT\t| UPTREND\t| DOWNTREND");
   Print("--------|-------|-----------|----------");
   
   for(int i = 0; i < 3; i++) {
      string row = states[i] + "\t| ";
      for(int j = 0; j < 3; j++) {
         row += DoubleToString(markovMatrix[i][j], 2) + "\t| ";
      }
      Print(row);
   }
   Print("================================");
}

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

Процесс построения матрицы переходов включает три ключевых этапа:

  1. Подготовка данных: мы анализируем историческую последовательность состояний рынка, определяя для каждого бара его принадлежность к одному из трех возможных состояний.
  2. Подсчет переходов: для каждой пары последовательных состояний (предыдущее → текущее) увеличиваем соответствующий счетчик в матрице transitionCounts.
  3. Расчет вероятностей: для каждого исходного состояния i вычисляем вероятность перехода в каждое возможное состояние j путем деления числа наблюдаемых переходов на общее количество появлений состояния i.

Обратите внимание на тонкий математический нюанс: для случаев, когда какое-то состояние отсутствует в исторических данных, мы присваиваем равные вероятности (1/3) всем возможным переходам, вместо грубых нулей. Эта элегантная предосторожность придает системе устойчивость и защищает от экстремальных решений в необычных рыночных условиях.

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

Для более глубокого понимания, давайте рассмотрим пример матрицы переходов, полученной для пары EUR/USD на дневном таймфрейме:

=== Markov Transition Matrix ===
FROM\TO | FLAT  | UPTREND       | DOWNTREND
--------|-------|-----------|----------
FLAT    | 0.68  | 0.17  | 0.15
UPTREND | 0.21  | 0.63  | 0.16
DOWNTREND       | 0.19  | 0.14  | 0.67
================================

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

Обучение нейронной сети

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

// Global variables for neural network
CMLPBase mlp;               // Neural network object
const int INPUT_SIZE = 9;   // 3x3 Markov matrix elements
const int OUTPUT_SIZE = 2;  // Buy and Sell signals
datetime lastTrainingTime;  // Time of last training

// Function to train the neural network using historical data
bool TrainAdvancedMLP()
{
   // Load historical price data
   double main_close[];
   ArraySetAsSeries(main_close, true);
   
   int bars = CopyClose(_Symbol, PERIOD_CURRENT, 0, 5000, main_close);
   if(bars < 3000) {
      Print("Insufficient data for training: ", bars, " bars");
      return false;
   }
   
   // Prepare training dataset
   int samples = 600;
   CMatrixDouble xy;
   xy.Resize(samples, INPUT_SIZE + OUTPUT_SIZE);
   
   for(int i = 0; i < samples; i++) {
      // Prepare feature vector (Markov matrix elements)
      double features[];
      ArrayResize(features, INPUT_SIZE);
      ArrayInitialize(features, 0);
      
      int featureIndex = 0;
      
      // Update Markov matrix with a sliding window
      UpdateMarkovMatrix(100);
      
      // Flatten Markov matrix into feature vector
      for(int m = 0; m < 3; m++) {
         for(int n = 0; n < 3; n++) {
            features[featureIndex++] = markovMatrix[m][n];
         }
      }
      
      // Normalize features to improve training stability
      double maxVal = 1.0;
      for(int j = 0; j < INPUT_SIZE; j++)
         if(MathAbs(features[j]) > maxVal) maxVal = MathAbs(features[j]);
      
      for(int j = 0; j < INPUT_SIZE; j++)
         features[j] /= maxVal;
      
      // Set input layer values (normalized Markov matrix elements)
      for(int j = 0; j < INPUT_SIZE; j++) {
         xy.Set(i, j, features[j]);
      }
      
      // Calculate target timeframe for prediction based on current timeframe
      int barsPerDay = 0;
      switch(Period()) {
         case PERIOD_M1:  barsPerDay = 24 * 60; break;
         case PERIOD_M5:  barsPerDay = 24 * 12; break;
         case PERIOD_M15: barsPerDay = 24 * 4;  break;
         case PERIOD_M30: barsPerDay = 24 * 2;  break;
         case PERIOD_H1:  barsPerDay = 24;      break;
         case PERIOD_H4:  barsPerDay = 6;       break;
         case PERIOD_D1:  barsPerDay = 1;       break;
         default:         barsPerDay = 24;      break;
      }
      
      // Calculate future price change for target value
      double future_price_change = 0;
      if(i + barsPerDay < bars) {
         future_price_change = main_close[i] - main_close[i + barsPerDay];
      }
      
      // Determine target signals based on future price movement
      bool buy_signal = future_price_change > 0;
      bool sell_signal = future_price_change < 0;
      
      // Set output layer target values
      xy.Set(i, INPUT_SIZE + 0, buy_signal ? 1.0 : 0.0);
      xy.Set(i, INPUT_SIZE + 1, sell_signal ? 1.0 : 0.0);
   }
   
   // Initialize neural network if not done already
   if(mlp.GetNeuronCount() == 0) {
      int network_structure[] = {INPUT_SIZE, 40, OUTPUT_SIZE};
      mlp.Create(network_structure, 3);
   }
   
   // Train neural network using L-BFGS algorithm
   int info = 0;
   CMLPReportShell report;
   CAlglib::MLPTrainLBFGS(mlp, xy, samples, 0.001, 5, 0.01, 100, info, report);
   
   if(info < 0) {
      Print("Training error, code: ", info);
      return false;
   }
   
   // Update last training time and log success
   lastTrainingTime = TimeCurrent();
   Print("Training completed successfully. Used ", samples, " examples of Markov matrix");
   return true;
}

// Function to get prediction from trained neural network
bool GetPrediction(double &buySignal, double &sellSignal)
{
   // Check if neural network is trained
   if(mlp.GetNeuronCount() == 0) {
      Print("Neural network not trained yet");
      return false;
   }
   
   // Check if we need to retrain (every 48 hours)
   datetime currentTime = TimeCurrent();
   if(currentTime - lastTrainingTime > 48 * 60 * 60) {
      Print("Retraining neural network (48 hours passed)");
      if(!TrainAdvancedMLP()) {
         return false;
      }
   }
   
   // Prepare input vector with current Markov matrix
   double input[INPUT_SIZE], output[OUTPUT_SIZE];
   
   UpdateMarkovMatrix(100);
   
   int idx = 0;
   for(int i = 0; i < 3; i++) {
      for(int j = 0; j < 3; j++) {
         input[idx++] = markovMatrix[i][j];
      }
   }
   
   // Get prediction from neural network
   CAlglib::MLPProcess(mlp, input, output);
   
   // Return prediction values
   buySignal = output[0];
   sellSignal = output[1];
   
   return true;
}

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

  1. Подготовка данных: мы формируем обучающую выборку из 600 примеров, где входными данными служат элементы матрицы марковских переходов, а целевыми значениями — будущие движения цены через интервал времени, зависящий от текущего таймфрейма.
  2. Нормализация признаков: все элементы матрицы переходов нормализуются для обеспечения стабильности и эффективности обучения — классический прием в машинном обучении, позволяющий избежать доминирования отдельных признаков и ускорить сходимость алгоритма.
  3. Инициализация и обучение сети: мы используем трехслойную архитектуру (9 входных нейронов, 40 скрытых и 2 выходных) и алгоритм L-BFGS (Limited-memory Broyden–Fletcher–Goldfarb–Shanno) — один из наиболее эффективных методов оптимизации для обучения нейронных сетей.
  4. Регулярное переобучение: система автоматически переобучается каждые 48 часов, что позволяет ей адаптироваться к изменяющимся рыночным условиям.

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

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

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

Стратегия торговых решений и ее искусство защиты капитала

Настал момент взглянуть на то, как система принимает торговые решения:

// Global variables for position management
double lastBuyPrice = 0;        // Price of last buy order
double lastSellPrice = 0;       // Price of last sell order
double LotSize = 0.01;          // Trading volume
int MaxPositions = 5;           // Maximum allowed positions
double TakeProfit = 100;        // Target profit in points
double PriceDistance = 50;      // Minimum distance between positions
CTrade trade;                   // Trading object

// Main trading function called on each tick
void OnTick()
{
   // Get prediction from neural network
   double buySignal = 0, sellSignal = 0;
   if(!GetPrediction(buySignal, sellSignal)) {
      return; // Exit if prediction fails
   }
   
   // Process closing of profitable positions first
   CheckProfitClosure();
   
   // Check if maximum positions limit is reached
   int totalPositions = CountOpenPositions();
   if(totalPositions >= MaxPositions) return;
   
   // Get current market prices
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;
   
   // Open BUY position if:
   // 1. Buy signal is strong enough (threshold 0.55)
   // 2. We haven't reached max positions for BUY
   // 3. Price is far enough from the last buy to avoid clustering
   if(buySignal > 0.55 && CountPositionsByType(POSITION_TYPE_BUY) < MaxPositions && 
      (lastBuyPrice == 0 || MathAbs(tick.ask - lastBuyPrice) > PriceDistance*_Point)) {
      if(trade.Buy(LotSize, _Symbol, tick.ask, 0, 0, "MLP_Buy")) {
         lastBuyPrice = tick.ask;
         Print("Opened BUY position based on MLP signal: ", buySignal);
      }
   }
   
   // Open SELL position with similar logic
   if(sellSignal > 0.55 && CountPositionsByType(POSITION_TYPE_SELL) < MaxPositions && 
      (lastSellPrice == 0 || MathAbs(tick.bid - lastSellPrice) > PriceDistance*_Point)) {
      if(trade.Sell(LotSize, _Symbol, tick.bid, 0, 0, "MLP_Sell")) {
         lastSellPrice = tick.bid;
         Print("Opened SELL position based on MLP signal: ", sellSignal);
      }
   }
}

// Function to check and close profitable positions
void CheckProfitClosure()
{
   int total = PositionsTotal();
   
   for(int i = total - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(!PositionSelectByTicket(ticket)) continue;
      
      // Skip positions of other symbols
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      
      // Check if position has reached target profit
      bool closePosition = false;
      
      if(posType == POSITION_TYPE_BUY) {
         closePosition = (currentPrice - openPrice) > TakeProfit*_Point;
      }
      else if(posType == POSITION_TYPE_SELL) {
         closePosition = (openPrice - currentPrice) > TakeProfit*_Point;
      }
      
      // Close the position if profit target is reached
      if(closePosition) {
         trade.PositionClose(ticket);
         Print("Closed position ", ticket, " with profit");
      }
   }
}

// Helper function to count all open positions for the current symbol
int CountOpenPositions()
{
   int count = 0;
   int total = PositionsTotal();
   
   for(int i = 0; i < total; i++) {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(!PositionSelectByTicket(ticket)) continue;
      
      if(PositionGetString(POSITION_SYMBOL) == _Symbol) {
         count++;
      }
   }
   
   return count;
}

// Helper function to count positions by type (BUY or SELL)
int CountPositionsByType(ENUM_POSITION_TYPE type)
{
   int count = 0;
   int total = PositionsTotal();
   
   for(int i = 0; i < total; i++) {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      
      if(!PositionSelectByTicket(ticket)) continue;
      
      if(PositionGetString(POSITION_SYMBOL) == _Symbol && 
         (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == type) {
         count++;
      }
   }
   
   return count;
}

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

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

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

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

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


Тестирование и оптимизация: от теории к практике

После реализации базовой структуры советника, мы погрузились в увлекательное исследование его эффективности на исторических данных нескольких валютных пар в период 2017-2025 годов. Результаты превзошли самые смелые ожидания, особенно для валютных пар с высокой ликвидностью — EUR/USD и GBP/USD.

Рассмотрим детальный анализ результатов тестирования на паре EUR/USD с параметрами, определенными в ходе оптимизации (LotSize = 0.01, MaxPositions = 5, ATR_Period = 14)

Давайте рассмотрим эти метрики более подробно:

  1. Среднегодовая доходность: 66.7%
    Это значительно превышает средние показатели даже для активно управляемых инвестиционных фондов, которые обычно стремятся к 10-15% годовых. Такая высокая доходность свидетельствует о способности системы эффективно идентифицировать и использовать рыночные возможности.
  2. Максимальная просадка: 11%
    Этот показатель отражает наибольшее процентное снижение капитала от пикового значения до минимума перед новым максимумом. Относительно невысокая просадка для системы с такой доходностью указывает на эффективность стратегии хеджирования и управления рисками.
  3. Коэффициент Шарпа: 1.3
    Коэффициент Шарпа — общепринятая мера эффективности инвестиций, учитывающая соотношение доходности и риска. Значение выше 1.0 считается хорошим, а 1.3 — отличным результатом, указывающим на высокую доходность относительно принимаемого риска.
  4. Процент прибыльных сделок: 44.7%
    Этот показатель, также известный как "win rate", демонстрирует, что более 4 из 10 сделок советника оказываются прибыльными. Это высокий показатель для алгоритмической системы, особенно учитывая значительное количество совершенных сделок (182 524).
  5. Профит-фактор: 1.2
    Соотношение общей прибыли к общему убытку. Значение 1.2 означает, что система генерирует на 20% больше прибыли, чем убытков, что является явным признаком её эффективности.
  6. Фактор восстановления 7.64

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

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

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


За пределами базовой модели: направления для расширения

Представленная реализация — лишь первая нота в потенциальной симфонии возможностей. Перед нами открывается множество захватывающих направлений для дальнейшего совершенствования системы.

Расширение модели состояний

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

// Enhanced market state enumeration
enum ENHANCED_MARKET_STATE
{
   STATE_STRONG_DOWNTREND = 0,    // Strong bearish movement
   STATE_MODERATE_DOWNTREND = 1,  // Moderate bearish movement
   STATE_WEAK_DOWNTREND = 2,      // Weak bearish movement
   STATE_FLAT = 3,                // Sideways market
   STATE_WEAK_UPTREND = 4,        // Weak bullish movement
   STATE_MODERATE_UPTREND = 5,    // Moderate bullish movement
   STATE_STRONG_UPTREND = 6       // Strong bullish movement
};

// Enhanced market state detection function
ENHANCED_MARKET_STATE GetEnhancedMarketState(int shift)
{
   double close[], atr[];
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(atr, true);
   
   // Get data
   if(CopyClose(_Symbol, PERIOD_D1, shift, 2, close) < 2 ||
      CopyBuffer(atrHandle, 0, shift, 1, atr) < 1) {
      return STATE_FLAT;
   }
   
   // Calculate normalized price change
   double priceChange = close[0] - close[1];
   double atrValue = atr[0];
   double normalizedChange = priceChange / atrValue;
   
   // Determine enhanced market state based on price change relative to ATR
   if(normalizedChange < -1.5) return STATE_STRONG_DOWNTREND;
   if(normalizedChange < -0.75) return STATE_MODERATE_DOWNTREND;
   if(normalizedChange < -0.25) return STATE_WEAK_DOWNTREND;
   if(normalizedChange <= 0.25) return STATE_FLAT;
   if(normalizedChange <= 0.75) return STATE_WEAK_UPTREND;
   if(normalizedChange <= 1.5) return STATE_MODERATE_UPTREND;
   return STATE_STRONG_UPTREND;
}

Обогащение рыночного контекста

Текущая модель использует преимущественно ATR для определения состояний рынка. Но представьте, какой глубины понимания мы достигнем, добавив в этот оркестр звучание RSI, мелодию MACD, гармонические последовательности уровней Фибоначчи и ритмические структуры объемов!

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

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

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

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

Мультитаймфреймный анализ

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

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


Эпилог: философский камень алгоритмической торговли

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

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

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

И все же, адаптивная природа системы и элегантные механизмы управления рисками делают этот инструмент доступным как для новичка, делающего первые шаги в мире алгоритмической торговли, так и для опытного мастера, ищущего новые грани совершенства в своем трейдинговом арсенале.

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

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


Ссылки и дополнительные материалы

  1. Koshtenko, Y. (2025). Матричная модель прогнозирования на марковской цепи. 
  2. ALGLIB - Библиотека численного анализа: https://www.alglib.net/
  3. Документация MQL5: https://www.mql5.com/ru/docs
  4. Sewell, M. (2011). Characterization of Financial Time Series. UCL Research Note, 11(01).
  5. Zhang, G.P. (2003). Time series forecasting using a hybrid ARIMA and neural network model. Neurocomputing, 50, 159-175.
Прикрепленные файлы |
Matrix_MLP_EA.mq5 (35.48 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (10)
Victor Golovkov
Victor Golovkov | 22 мая 2025 в 10:28

Сознательная или бессознательная, но явная манипуляция результатами тестирования. (этим страдают многие авторы).

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

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

Aliaksandr Kazunka
Aliaksandr Kazunka | 22 мая 2025 в 15:44

Странно, но файл из коробки уже компилируется с ошибкой (DeInit )))). Не понятно на каких настройках он тестировался - из той же "коробки" там космические цифры. И если убрать воду ИИ, то в итоге и читать нечего. Можно по-больше конкретики.


Кстати, забейте ИИ  текст "Текущая модель использует преимущественно ATR для определения состояний рынка. Но представьте, какой глубины понимания мы достигнем, добавив в этот оркестр звучание RSI, мелодию MACD, гармонические последовательности уровней Фибоначчи и ритмические структуры объемов!" Он такое вам выдаст!!!!)))

Aliaksandr Kazunka
Aliaksandr Kazunka | 22 мая 2025 в 15:46
Добавил rsi, macd,  fibo, volume, если кому интересно
Aleksey Vyazmikin
Aleksey Vyazmikin | 22 мая 2025 в 16:07
sportoman #:
Добавил rsi, macd,  fibo, volume, если кому интересно

На форуме только исходники можно выкладывать, иначе могут забанить.

Собственно, есть какой эффект от добавок?

Aliaksandr Kazunka
Aliaksandr Kazunka | 22 мая 2025 в 17:05
Aleksey Vyazmikin #:

На форуме только исходники можно выкладывать, иначе могут забанить.

Собственно, есть какой эффект от добавок?

самое смешное, что на тестере у меня не работает советник. не понимаю что и как там автор тестил. поставил на демку на все пары, посмотрю что и как

Торговый инструментарий MQL5 (Часть 3): Разработка EX5-библиотеки для управления отложенными ордерами Торговый инструментарий MQL5 (Часть 3): Разработка EX5-библиотеки для управления отложенными ордерами
Вы узнаете, как разработать и внедрить комплексную библиотеку отложенных EX5-ордеров в ваш код или MQL5-проекты. Мы рассмотрим, как импортировать и реализовать такую библиотеку в составе торговой панели или графического пользовательского интерфейса (GUI). Панель ордеров советника позволит пользователям открывать, отслеживать и удалять отложенные ордера по магическому числу непосредственно из графического интерфейса в окне графика.
Фильтр Калмана для возвратных стратегий на рынке Форекс Фильтр Калмана для возвратных стратегий на рынке Форекс
Фильтр Калмана представляет собой рекурсивный алгоритм, применяемый в алготрейдинге для оценки истинного состояния финансового временного ряда посредством фильтрации шума из движения цен. Он динамически обновляет прогнозы на основе новых рыночных данных, что делает его ценным для таких адаптивных стратегий, как возвратные. В этой статье впервые представлен фильтр Калмана, а также рассмотрены его расчет и реализация. Кроме того, в качестве примера мы применим этот фильтр к классической возвратной форекс-стратегии. Наконец, проведем различные виды статистического анализа, сравнивая фильтр со скользящей средней на различных валютных парах.
Создание торговой панели администратора на MQL5 (Часть IV): Безопасность входа в систему Создание торговой панели администратора на MQL5 (Часть IV): Безопасность входа в систему
Представьте себе, что злоумышленник проник в систему управления торговли и получил доступ к компьютерам и панели администратора, используемым для передачи ценных сведений миллионам трейдеров по всему миру. Это может привести к катастрофическим последствиям, таким как несанкционированная отправка вводящих в заблуждение сообщений или случайные нажатия на кнопки, запускающие непреднамеренные действия. В этой статье мы рассмотрим меры безопасности в MQL5 и новые функции безопасности, которые мы реализовали в нашей панели администратора для защиты от этих угроз. Совершенствуя наши протоколы безопасности, мы стремимся защитить наши каналы связи и сохранить доверие членов нашего торгового сообщества.
Заголовок в Connexus (Часть 3): Освоение использования HTTP-заголовков для запросов Заголовок в Connexus (Часть 3): Освоение использования HTTP-заголовков для запросов
Продолжаем разработку библиотеки Connexus. В этой главе мы исследуем концепцию заголовков в протоколе HTTP, объясняя, что это такое, для чего они предназначены и как их использовать в запросах. Мы рассмотрим основные заголовки, используемые при взаимодействии с API, а также покажем практические примеры того, как настроить их в библиотеке.