preview
Форекс советник на нейросети N-BEATS Network

Форекс советник на нейросети N-BEATS Network

MetaTrader 5Торговые системы |
72 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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

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

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

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



N-BEATS: архитектурный прорыв в анализе временных рядов

Исследователи из Element AI в 2019 году предложили кардинально новый подход к прогнозированию. Вместо модификации существующих RNN или CNN архитектур, они задались фундаментальным вопросом: какая архитектура идеально подойдет именно для задач forecasting?

N-BEATS строится на принципе декомпозиции временного ряда. Представьте музыкальную композицию: вместо попытки предсказать следующую ноту по общему звучанию, мы анализируем отдельно мелодию, ритм и гармонию, а затем синтезируем прогноз. Аналогично N-BEATS разбивает временной ряд на тренд, сезонные колебания и остаточные паттерны.

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

Критическое преимущество N-BEATS — интерпретируемость. В отличие от черного ящика обычных нейросетей, архитектура может показать, какую роль в прогнозе играет тренд, а какую — сезонные колебания. Для трейдера это означает понимание не только что будет, но и почему.

Результаты тестирования на стандартных бенчмарках шокировали научное сообщество. N-BEATS превзошла все существующие методы, включая LSTM сети и Transformer архитектуры. Но самое впечатляющее — она добилась этого, используя только исторические данные самого временного ряда, без внешних признаков.

В отличие от RNN, которые обрабатывают данные последовательно, N-BEATS может обрабатывать весь временной ряд параллельно. Для алгоритмического трейдинга это означает возможность делать прогнозы в реальном времени на быстрых рынках.

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



Базовая структура Matrix с градиентами

MetaTrader 5 не предоставляет готовых инструментов для машинного обучения, поэтому приходится строить все с нуля. В основе нашей системы лежит структура Matrix, которая хранит не только данные, но и градиенты для каждого элемента:

struct Matrix {
    double data[];
    double gradients[];  // Ключевое дополнение для backprop
    int rows, cols;
    
    void Init(int r, int c) {
        rows = r; cols = c;
        ArrayResize(data, r * c);
        ArrayResize(gradients, r * c);
        ArrayInitialize(data, 0.0);
        ArrayInitialize(gradients, 0.0);
    }
    
    double Get(int r, int c) {
        if(r < 0 || r >= rows || c < 0 || c >= cols) return 0.0;
        return data[r * cols + c];
    }
    
    void AddGrad(int r, int c, double grad) {
        if(r < 0 || r >= rows || c < 0 || c >= cols) return;
        gradients[r * cols + c] += grad;  // Накопление градиентов
    }
    
    void ZeroGrad() {
        ArrayInitialize(gradients, 0.0);  // Обнуление перед новой итерацией
    }
};

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

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

// SiLU (Swish) активация - более гладкая, чем ReLU
double SiLU(double x) { 
    double sig = 1.0 / (1.0 + MathExp(-MathMin(MathMax(x, -50), 50))); 
    return x * sig;
}

double SiLUDerivative(double x) {
    double sig = 1.0 / (1.0 + MathExp(-MathMin(MathMax(x, -50), 50)));
    return sig * (1.0 + x * (1.0 - sig));
}

// Sigmoid для выходного слоя
double Sigmoid(double x) { 
    return 1.0 / (1.0 + MathExp(-MathMin(MathMax(x, -50), 50))); 
}

double SigmoidDerivative(double x) {
    double s = Sigmoid(x);
    return s * (1.0 - s);
}

Ограничение входных значений в диапазоне от -50 до 50 предотвращает overflow при вычислении экспоненты, что критично для стабильности обучения на зашумленных финансовых данных.

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

struct AdamOptimizer {
    Matrix m, v;  // Момент и скорость
    double beta1, beta2, eps;
    int step;
    
    void Init(int rows, int cols) {
        m.Init(rows, cols);
        v.Init(rows, cols);
        beta1 = 0.9; beta2 = 0.999; eps = 1e-8; step = 0;
    }
    
    void Update(Matrix &weights, double lr, double weight_decay = 0.01) {
        step++;
        for(int i = 0; i < weights.rows; i++) {
            for(int j = 0; j < weights.cols; j++) {
                double grad = weights.GetGrad(i, j) + weight_decay * weights.Get(i, j);
                
                // Обновление моментов
                double m_val = beta1 * m.Get(i, j) + (1 - beta1) * grad;
                double v_val = beta2 * v.Get(i, j) + (1 - beta2) * grad * grad;
                
                m.Set(i, j, m_val); v.Set(i, j, v_val);
                
                // Bias correction
                double m_hat = m_val / (1 - MathPow(beta1, step));
                double v_hat = v_val / (1 - MathPow(beta2, step));
                
                // Обновление весов
                double update = lr * m_hat / (MathSqrt(v_hat) + eps);
                weights.Set(i, j, weights.Get(i, j) - update);
            }
        }
        weights.ZeroGrad();  // Очищаем градиенты после обновления
    }
};



Forward и Backward проходы в N-BEATS блоке

Каждый блок N-BEATS выполняет сложную последовательность операций. Критически важно сохранять промежуточные значения для backward прохода:

struct NBeatsBlock {
    Matrix fc1_w, fc1_b, fc2_w, fc2_b, theta_projection;
    AdamOptimizer fc1_opt, fc2_opt, theta_opt;
    
    // Промежуточные значения для backprop
    Matrix fc1_input, fc1_output, fc2_input, fc2_output;
    
    void Forward(Matrix &input_patch, Matrix &backcast, Matrix &forecast) {
        // Сохраняем вход для backprop
        fc1_input.Copy(input_patch);
        
        // FC1 layer
        fc1_output.Init(1, HIDDEN_SIZE);
        for(int j = 0; j < HIDDEN_SIZE; j++) {
            double s = 0.0;
            for(int i = 0; i < input_patch.cols; i++) 
                s += input_patch.Get(0, i) * fc1_w.Get(i, j);
            s += fc1_b.Get(0, j);
            fc1_output.Set(0, j, s);  // Сохраняем ДО активации
        }
        
        // Активация с сохранением для backprop
        fc2_input.Init(1, HIDDEN_SIZE);
        for(int j = 0; j < HIDDEN_SIZE; j++) {
            fc2_input.Set(0, j, SiLU(fc1_output.Get(0, j)));
        }
        
        // FC2 layer аналогично...
        // Theta projection и генерация backcast/forecast
    }
    
    void Backward(Matrix &backcast_grad, Matrix &forecast_grad) {
        // Backprop начинается с выходных градиентов
        // Комбинируем градиенты backcast и forecast
        Matrix theta_grad;
        theta_grad.Init(1, theta_projection.cols);
        
        // Вычисляем градиенты theta projection
        for(int i = 0; i < theta_projection.cols; i++) {
            double grad = 0.0;
            if(i < backcast_size) {
                grad += backcast_grad.GetGrad(0, i);
            } else {
                grad += forecast_grad.GetGrad(0, i - backcast_size);
            }
            theta_grad.SetGrad(0, i, grad);
        }
        
        // Backprop через theta projection к FC2
        Matrix fc2_grad;
        fc2_grad.Init(1, HIDDEN_SIZE);
        for(int i = 0; i < HIDDEN_SIZE; i++) {
            double grad_sum = 0.0;
            for(int j = 0; j < theta_projection.cols; j++) {
                grad_sum += theta_grad.GetGrad(0, j) * theta_projection.Get(i, j);
                // Накапливаем градиенты для theta_projection весов
                theta_projection.AddGrad(i, j, theta_grad.GetGrad(0, j) * fc2_output.Get(0, i));
            }
            fc2_grad.SetGrad(0, i, grad_sum);
        }
        
        // Backprop через SiLU активацию
        Matrix fc2_input_grad;
        fc2_input_grad.Init(1, HIDDEN_SIZE);
        for(int i = 0; i < HIDDEN_SIZE; i++) {
            double grad = fc2_grad.GetGrad(0, i) * SiLUDerivative(fc1_output.Get(0, i));
            fc2_input_grad.SetGrad(0, i, grad);
        }
        
        // Backprop через FC1 layer к входам
        // ... аналогичные вычисления для всех слоев
    }
};



Квантильная функция потерь

Для прогнозирования неопределенности используем квантильные потери вместо обычного MSE:

void Train(double &time_series[], double target) {
    step_count++;
    
    // Forward pass для всех квантилей
    double predictions[QUANTILE_LEVELS];
    Predict(time_series, predictions);
    
    // Вычисляем квантильные потери
    for(int q = 0; q < QUANTILE_LEVELS; q++) {
        double pred = predictions[q];
        double error = target - pred;
        double quantile = quantiles[q];  // 0.1, 0.5, 0.9
        
        // Квантильная потеря
        double loss_weight = (error >= 0) ? quantile : (1.0 - quantile);
        double focal_weight = class_weights.GetFocalWeight(target, pred);
        
        double loss = focal_weight * loss_weight * MathAbs(error);
        
        // Градиент для квантильной потери
        double grad_wrt_pred = focal_weight * loss_weight * ((error >= 0) ? -1.0 : 1.0);
        double grad_wrt_logit = grad_wrt_pred * SigmoidDerivative(pred);
        
        // Backprop к выходным весам
        for(int h = 0; h < HIDDEN_SIZE; h++) {
            double grad = grad_wrt_logit * pooled_features[h];
            output_proj[q].SetGrad(h, 0, grad);
        }
    }
    
    // Запускаем backward pass через всю сеть
    BackpropagateToAllLayers();
    
    // Обновляем веса всех слоев
    UpdateAllWeights();
}

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



Мультивариантные признаки

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

void PrepareInputData(MqlRates &rates[], double &input_data[], int input_bars) {
    ArrayResize(input_data, input_bars * 2);  // 2 признака на бар
    
    for(int i = 0; i < input_bars; i++) {
        if(i < ArraySize(rates) - 1) {
            // Признак 1: нормализованное изменение цены
            double price_change = (rates[i].close - rates[i].open) / rates[i].open;
            input_data[i * 2] = MathMax(-1.0, MathMin(1.0, price_change * 100));
            
            // Признак 2: логарифмически нормализованный объем
            double vol_ratio = (rates[i].tick_volume > 0) ? 
                MathLog(1.0 + rates[i].tick_volume / 1000.0) : 0.0;
            input_data[i * 2 + 1] = MathMax(0.0, MathMin(1.0, vol_ratio / 10.0));
        } else {
            input_data[i * 2] = 0.0;
            input_data[i * 2 + 1] = 0.0;
        }
    }
}

Клипирование значений в разумных диапазонах предотвращает влияние экстремальных выбросов на обучение модели.



Робастная нормализация против шумов

Финансовые данные полны аномалий: спайки цен, технические сбои, аномальные объемы. Простая min-max нормализация может быть сломана одним выбросом:

void RobustNormalization(double &data[], int window_size = 200) {
    int data_size = ArraySize(data);
    
    for(int i = window_size; i < data_size; i++) {
        // Вычисляем робастные статистики для скользящего окна
        double window_data[];
        ArrayResize(window_data, window_size);
        
        for(int j = 0; j < window_size; j++) {
            window_data[j] = data[i - window_size + j];
        }
        
        // Медиана вместо среднего (устойчива к выбросам)
        ArraySort(window_data);
        double median = window_data[window_size / 2];
        
        // MAD (Median Absolute Deviation) вместо стандартного отклонения
        double deviations[];
        ArrayResize(deviations, window_size);
        for(int j = 0; j < window_size; j++) {
            deviations[j] = MathAbs(window_data[j] - median);
        }
        ArraySort(deviations);
        double mad = deviations[window_size / 2];
        
        // Робастная нормализация
        if(mad > 1e-8) {
            data[i] = MathMax(-3.0, MathMin(3.0, (data[i] - median) / mad));
        }
    }
}

Использование медианы и MAD вместо среднего и стандартного отклонения делает нормализацию устойчивой к выбросам.



Детекция и обработка аномалий

Система должна автоматически обнаруживать аномальные рыночные условия:

bool DetectMarketAnomaly(MqlRates &rates[], int lookback = 50) {
    if(ArraySize(rates) < lookback) return false;
    
    // Вычисляем волатильность за период
    double volatilities[];
    ArrayResize(volatilities, lookback);
    
    for(int i = 1; i < lookback; i++) {
        double change = MathAbs(rates[i].close - rates[i-1].close) / rates[i-1].close;
        volatilities[i] = change;
    }
    
    ArraySort(volatilities);
    double median_vol = volatilities[lookback / 2];
    double vol_95 = volatilities[(int)(lookback * 0.95)];
    
    // Текущая волатильность
    double current_vol = MathAbs(rates[0].close - rates[1].close) / rates[1].close;
    
    // Аномалия, если текущая волатильность превышает 95-й перцентиль в 2+ раза
    return (current_vol > vol_95 * 2.0);
}
При обнаружении аномалий торговая система может временно снизить размер позиций или приостановить торговлю.

Традиционные подходы к прогнозированию дают единственную точечную оценку — цена будет такой-то. Но для трейдинга критично понимать неопределенность прогноза. Именно поэтому наша реализация N-BEATS использует квантильное прогнозирование.

Вместо одного значения система выдает три оценки: пессимистичный сценарий (10-й квантиль), реалистичный прогноз (медиана) и оптимистичный сценарий (90-й квантиль). Разность между квантилями показывает уверенность модели в прогнозе.

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

void ProcessQuantilePredictions(double &predictions[]) {
    double q10 = predictions[0];  // Пессимистичный сценарий
    double q50 = predictions[1];  // Медианный прогноз
    double q90 = predictions[2];  // Оптимистичный сценарий
    
    // Вычисляем неопределенность модели
    double uncertainty = q90 - q10;
    double confidence = 1.0 - MathMin(1.0, uncertainty / 0.4);  // Нормализуем [0,1]
    
    // Динамическое изменение размера позиции
    double base_lot = CalculateBaseLotSize();
    double adjusted_lot = base_lot * confidence;
    
    if(q50 > 0.65 && uncertainty < 0.2) {
        // Высокая уверенность в росте, низкая неопределенность
        OpenPosition(ORDER_TYPE_BUY, adjusted_lot * 1.5, "High confidence");
    }
    else if(q50 > 0.6 && uncertainty < 0.3) {
        // Умеренная уверенность в росте
        OpenPosition(ORDER_TYPE_BUY, adjusted_lot, "Medium confidence");
    }
    else if(q10 > 0.55) {
        // Даже пессимистичный сценарий показывает рост
        OpenPosition(ORDER_TYPE_BUY, adjusted_lot * 0.7, "Conservative long");
    }
    
    Print("Uncertainty: ", DoubleToString(uncertainty, 3), 
          " | Confidence: ", DoubleToString(confidence, 3),
          " | Adjusted lot: ", DoubleToString(adjusted_lot, 2));
}



Focal Loss: борьба с дисбалансом классов

Финансовые рынки по природе несбалансированы — периоды спокойной торговли перемежаются редкими, но сильными движениями. Обычная функция потерь не справляется с этой проблемой, поэтому используем Focal Loss:

struct ClassWeights {
    double strong_bull, weak_bull, neutral, weak_bear, strong_bear;
    
    void UpdateWeights(int &counts[]) {
        int total = counts[0] + counts[1] + counts[2] + counts[3] + counts[4];
        if(total == 0) return;
        
        // Инвертированные веса — редкие классы получают больший вес
        strong_bear = (counts[0] > 0) ? (double)total / (5 * counts[0]) : 1.0;
        weak_bear = (counts[1] > 0) ? (double)total / (5 * counts[1]) : 1.0;
        neutral = (counts[2] > 0) ? (double)total / (5 * counts[2]) : 1.0;
        weak_bull = (counts[3] > 0) ? (double)total / (5 * counts[3]) : 1.0;
        strong_bull = (counts[4] > 0) ? (double)total / (5 * counts[4]) : 1.0;
    }
    
    double GetFocalWeight(double target, double pred) {
        double alpha = GetClassWeight(target);
        double pt = (target > 0.5) ? pred : (1.0 - pred);
        
        // Focal Loss: фокусируется на сложных примерах
        return alpha * MathPow(MathMax(1.0 - pt, 1e-8), FOCAL_GAMMA);
    }
};

Focal Loss автоматически увеличивает внимание к редким, но важным рыночным событиям — именно тем, на которых можно заработать больше всего.



Адаптивный стоп-лосс на основе неопределенности

Традиционный подход к стоп-лоссам примитивен: фиксированное количество пунктов от входа. Наша система использует предсказанную неопределенность:

double CalculateAdaptiveStopLoss(double entry_price, ENUM_ORDER_TYPE order_type, 
                                double uncertainty, double base_sl_points = 500) {
    // Увеличиваем стоп-лосс при высокой неопределенности
    double uncertainty_multiplier = 1.0 + uncertainty * 2.0;  // [1.0, 3.0]
    double adaptive_sl_points = base_sl_points * uncertainty_multiplier;
    
    // Учитываем текущую волатильность
    double atr = CalculateATR(14);  // Average True Range за 14 периодов
    double volatility_adjustment = MathMax(0.5, MathMin(2.0, atr / (Point() * 100)));
    
    adaptive_sl_points *= volatility_adjustment;
    
    double sl_price;
    if(order_type == ORDER_TYPE_BUY) {
        sl_price = entry_price - adaptive_sl_points * Point();
    } else {
        sl_price = entry_price + adaptive_sl_points * Point();
    }
    
    Print("Adaptive SL: base=", base_sl_points, 
          " uncertainty_mult=", DoubleToString(uncertainty_multiplier, 2),
          " volatility_adj=", DoubleToString(volatility_adjustment, 2),
          " final=", DoubleToString(adaptive_sl_points, 0));
    
    return sl_price;
}

double CalculateATR(int period) {
    double atr_values[];
    if(CopyBuffer(iATR(Symbol(), Period(), period), 0, 0, 1, atr_values) <= 0) {
        return Point() * 100;  // Fallback значение
    }
    return atr_values[0];
}


Интеграция с торговой логикой MetaTrader 5

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

MetaTrader 5 работает по событийной модели. Каждый тик, каждое изменение цены вызывает функцию OnTick(). Но запускать тяжёлые вычисления нейросети на каждом тике неэффективно — нужна умная система кэширования:

datetime g_last_bar_time = 0;
double   g_cached_prediction = 0.5;
bool     g_prediction_valid = false;

bool IsNewBar() {
    datetime current_bar_time = iTime(Symbol(), Period(), 0);
    if(current_bar_time != g_last_bar_time) {
        g_last_bar_time = current_bar_time;
        g_prediction_valid = false;  // Кэш устарел
        return true;
    }
    return false;
}

void OnTick() {
    if(!g_net_initialized) return;
    
    // Лёгкие операции выполняем на каждом тике
    if(UseTrailingStop) UpdateTrailingStops();
    
    // Тяжёлые вычисления только на новом баре
    if(IsNewBar()) {
        double prediction = GetAIPrediction();  // Тяжёлая операция
        g_cached_prediction = prediction;
        g_prediction_valid = true;
        
        ProcessTradingSignals(prediction);
    }
    
    // Экстренные выходы проверяем на каждом тике
    if(g_prediction_valid) {
        CheckEmergencyExits(g_cached_prediction);
    }
}
Система сигналов с гистерезисом

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

void ProcessTradingSignalsWithHysteresis(double prediction) {
    static double last_prediction = 0.5;
    static datetime last_signal_time = 0;
    
    // Минимальный интервал между сигналами
    if(TimeCurrent() - last_signal_time < PeriodSeconds(Period()) * 2) return;
    
    int current_positions = CountPositions();
    bool currently_long = HasLongPosition();
    bool currently_short = HasShortPosition();
    
    // Разные пороги для входа и выхода (гистерезис)
    double long_entry_threshold = 0.70;
    double long_exit_threshold = 0.55;
    double short_entry_threshold = 0.30;
    double short_exit_threshold = 0.45;
    
    // Логика с гистерезисом
    if(!currently_long && prediction > long_entry_threshold && 
       last_prediction <= long_entry_threshold) {
        
        if(currently_short) CloseAllPositions("Signal reversal");
        
        OpenPosition(ORDER_TYPE_BUY, prediction);
        last_signal_time = TimeCurrent();
        Print("🚀 LONG ENTRY: ", DoubleToString(prediction, 3));
    }
    else if(currently_long && prediction < long_exit_threshold && 
            last_prediction >= long_exit_threshold) {
        
        CloseAllPositions("Long exit signal");
        Print("📤 LONG EXIT: ", DoubleToString(prediction, 3));
    }
    
    // Аналогичная логика для коротких позиций...
    
    last_prediction = prediction;
}
Непрерывное обучение в production

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

struct ContinuousLearning {
    datetime last_retrain_time;
    double performance_buffer[];
    int performance_window;
    double performance_threshold;
    
    void Init() {
        last_retrain_time = TimeCurrent();
        performance_window = 100;  // Отслеживаем последние 100 сделок
        performance_threshold = 0.45;  // Порог деградации
        ArrayResize(performance_buffer, performance_window);
    }
    
    void AddTradeResult(double prediction, double actual_outcome) {
        // Сдвигаем буфер
        for(int i = 0; i < performance_window - 1; i++) {
            performance_buffer[i] = performance_buffer[i + 1];
        }
        
        // Добавляем новый результат (1.0 = точный прогноз, 0.0 = полностью неверный)
        double accuracy = 1.0 - MathAbs(prediction - actual_outcome);
        performance_buffer[performance_window - 1] = accuracy;
        
        // Проверяем, нужно ли переобучение
        if(ShouldRetrain()) {
            ScheduleRetraining();
        }
    }
    
    bool ShouldRetrain() {
        // Условие 1: Прошло достаточно времени
        bool time_condition = (TimeCurrent() - last_retrain_time) > 24 * 3600;
        
        // Условие 2: Снизилась производительность
        double avg_performance = 0.0;
        for(int i = 0; i < performance_window; i++) {
            avg_performance += performance_buffer[i];
        }
        avg_performance /= performance_window;
        bool performance_condition = avg_performance < performance_threshold;
        
        return time_condition || performance_condition;
    }
    
    void ScheduleRetraining() {
        Print("🔄 Scheduling model retraining...");
        Print("Average performance: ", DoubleToString(GetAveragePerformance(), 3));
        
        // Переобучение в отдельном потоке (симулируем)
        RetrainModel();
        last_retrain_time = TimeCurrent();
    }
};

ContinuousLearning g_learning_system;

N-BEATS — сложная архитектура с множеством параметров. В production среде каждая миллисекунда задержки может стоить денег, поэтому критична оптимизация производительности.

Управление памятью

Каждая матрица в сети может содержать тысячи параметров. Без правильного управления памятью система быстро исчерпает ресурсы:

struct MemoryManager {
    Matrix temp_matrices[];
    int allocated_count;
    int max_temp_matrices;
    
    void Init() {
        max_temp_matrices = 100;
        ArrayResize(temp_matrices, max_temp_matrices);
        allocated_count = 0;
    }
    
    Matrix* AllocateTempMatrix(int rows, int cols) {
        if(allocated_count >= max_temp_matrices) {
            Print("⚠️ Temporary matrix pool exhausted!");
            return NULL;
        }
        
        temp_matrices[allocated_count].Init(rows, cols);
        return &temp_matrices[allocated_count++];
    }
    
    void ResetTempPool() {
        // Быстрое "освобождение" без реальной деаллокации
        allocated_count = 0;
    }
    
    void PrintMemoryUsage() {
        int total_elements = 0;
        for(int i = 0; i < allocated_count; i++) {
            total_elements += temp_matrices[i].rows * temp_matrices[i].cols;
        }
        
        double memory_mb = total_elements * sizeof(double) / (1024.0 * 1024.0);
        Print("Memory usage: ", DoubleToString(memory_mb, 2), " MB");
        Print("Active matrices: ", allocated_count);
    }
};

MemoryManager g_memory_manager;
Профилирование критических путей

Важно знать, где именно тратится время выполнения:

struct Profiler {
    string operation_names[];
    ulong operation_times[];
    int operation_counts[];
    int num_operations;
    
    void Init() {
        num_operations = 0;
        ArrayResize(operation_names, 50);
        ArrayResize(operation_times, 50);
        ArrayResize(operation_counts, 50);
    }
    
    void StartTimer(string operation) {
        // MQL5 не имеет высокоточных таймеров, используем GetMicrosecondCount()
        ulong start_time = GetMicrosecondCount();
        
        // Ищем существующую операцию или создаём новую
        int index = FindOrCreateOperation(operation);
        operation_times[index] = start_time;
    }
    
    void EndTimer(string operation) {
        ulong end_time = GetMicrosecondCount();
        int index = FindOperation(operation);
        
        if(index >= 0) {
            ulong elapsed = end_time - operation_times[index];
            operation_times[index] = elapsed;  // Переиспользуем для хранения времени
            operation_counts[index]++;
        }
    }
    
    void PrintReport() {
        Print("=== PERFORMANCE REPORT ===");
        for(int i = 0; i < num_operations; i++) {
            double avg_time_ms = operation_times[i] / (double)operation_counts[i] / 1000.0;
            Print(operation_names[i], ": ", DoubleToString(avg_time_ms, 3), " ms avg, ", 
                  operation_counts[i], " calls");
        }
    }
};

Profiler g_profiler;

// Использование:
void OptimizedPredict(double &time_series[]) {
    g_profiler.StartTimer("PatchEmbedding");
    // ... код patch embedding
    g_profiler.EndTimer("PatchEmbedding");
    
    g_profiler.StartTimer("StackForward");
    // ... прямой проход через стеки
    g_profiler.EndTimer("StackForward");
}

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

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

Real-time мониторинг качества прогнозов

Первая линия обороны — постоянное отслеживание точности прогнозов в реальном времени:

struct PredictionMonitor {
    double accuracy_buffer[];
    double confidence_buffer[];
    double actual_outcomes[];
    int buffer_size, current_index;
    datetime last_evaluation;
    
    void Init(int size = 1000) {
        buffer_size = size;
        current_index = 0;
        ArrayResize(accuracy_buffer, buffer_size);
        ArrayResize(confidence_buffer, buffer_size);
        ArrayResize(actual_outcomes, buffer_size);
        ArrayInitialize(accuracy_buffer, 0.5);
        last_evaluation = TimeCurrent();
    }
    
    void RecordPrediction(double prediction, double confidence) {
        confidence_buffer[current_index] = confidence;
        // Сохраняем прогноз для будущей оценки
        // actual_outcomes будет заполнено позже, когда станет известен результат
    }
    
    void RecordOutcome(double actual_movement) {
        if(current_index >= buffer_size) return;
        
        actual_outcomes[current_index] = actual_movement;
        
        // Вычисляем точность прогноза
        double predicted = confidence_buffer[current_index];
        double accuracy = 1.0 - MathAbs(predicted - actual_movement);
        accuracy_buffer[current_index] = accuracy;
        
        current_index = (current_index + 1) % buffer_size;
        
        // Периодически выводим статистику
        if(TimeCurrent() - last_evaluation > 3600) {  // Каждый час
            PrintAccuracyReport();
            last_evaluation = TimeCurrent();
        }
    }
    
    void PrintAccuracyReport() {
        double total_accuracy = 0.0, total_confidence = 0.0;
        int valid_samples = 0;
        
        for(int i = 0; i < buffer_size; i++) {
            if(accuracy_buffer[i] > 0) {
                total_accuracy += accuracy_buffer[i];
                total_confidence += confidence_buffer[i];
                valid_samples++;
            }
        }
        
        if(valid_samples > 10) {
            double avg_accuracy = total_accuracy / valid_samples;
            double avg_confidence = total_confidence / valid_samples;
            
            Print("=== PREDICTION QUALITY REPORT ===");
            Print("Average accuracy: ", DoubleToString(avg_accuracy * 100, 2), "%");
            Print("Average confidence: ", DoubleToString(avg_confidence * 100, 2), "%");
            Print("Valid samples: ", valid_samples);
            
            // Предупреждение о деградации
            if(avg_accuracy < 0.5) {
                Print("⚠️ WARNING: Model accuracy below random baseline!");
            }
            if(avg_accuracy < avg_confidence - 0.1) {
                Print("⚠️ WARNING: Model overconfident - accuracy vs confidence gap!");
            }
        }
    }
    
    double GetRecentAccuracy(int lookback = 100) {
        double sum = 0.0;
        int count = 0;
        
        for(int i = 0; i < MathMin(lookback, buffer_size); i++) {
            int idx = (current_index - i - 1 + buffer_size) % buffer_size;
            if(accuracy_buffer[idx] > 0) {
                sum += accuracy_buffer[idx];
                count++;
            }
        }
        
        return (count > 0) ? sum / count : 0.5;
    }
};

PredictionMonitor g_monitor;
Детекция концептуального дрейфа

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

struct ConceptDriftDetector {
    double recent_performance[];
    double historical_baseline;
    int window_size;
    bool drift_detected;
    datetime last_drift_check;
    
    void Init(int window = 200) {
        window_size = window;
        ArrayResize(recent_performance, window_size);
        historical_baseline = 0.55;  // Базовая точность модели
        drift_detected = false;
        last_drift_check = TimeCurrent();
    }
    
    void AddPerformancePoint(double accuracy) {
        // Сдвигаем окно
        for(int i = 0; i < window_size - 1; i++) {
            recent_performance[i] = recent_performance[i + 1];
        }
        recent_performance[window_size - 1] = accuracy;
        
        // Проверяем дрейф каждые 4 часа
        if(TimeCurrent() - last_drift_check > 4 * 3600) {
            CheckForDrift();
            last_drift_check = TimeCurrent();
        }
    }
    
    void CheckForDrift() {
        // Вычисляем среднюю производительность в окне
        double window_mean = 0.0;
        int valid_count = 0;
        
        for(int i = 0; i < window_size; i++) {
            if(recent_performance[i] > 0) {
                window_mean += recent_performance[i];
                valid_count++;
            }
        }
        
        if(valid_count < 50) return;  // Недостаточно данных
        
        window_mean /= valid_count;
        
        // Статистический тест на значимое отклонение
        double variance = 0.0;
        for(int i = 0; i < window_size; i++) {
            if(recent_performance[i] > 0) {
                double diff = recent_performance[i] - window_mean;
                variance += diff * diff;
            }
        }
        variance /= (valid_count - 1);
        double std_error = MathSqrt(variance / valid_count);
        
        // Z-тест для определения значимости отклонения
        double z_score = (window_mean - historical_baseline) / std_error;
        bool significant_drift = MathAbs(z_score) > 2.58;  // 99% confidence level
        
        if(significant_drift && !drift_detected) {
            drift_detected = true;
            Print("🚨 CONCEPT DRIFT DETECTED!");
            Print("Historical baseline: ", DoubleToString(historical_baseline, 3));
            Print("Recent performance: ", DoubleToString(window_mean, 3));
            Print("Z-score: ", DoubleToString(z_score, 2));
            Print("Recommendation: Schedule model retraining");
            
            // Автоматически запускаем переобучение
            TriggerEmergencyRetraining();
        }
        else if(!significant_drift && drift_detected) {
            drift_detected = false;
            Print("✅ Concept drift resolved - model performance stabilized");
        }
    }
    
    void UpdateBaseline(double new_baseline) {
        historical_baseline = new_baseline;
        Print("📊 Performance baseline updated to ", DoubleToString(new_baseline, 3));
    }
};

ConceptDriftDetector g_drift_detector;
Система алертов и автоматического реагирования

Критические ситуации требуют немедленного реагирования, даже если трейдер спит или недоступен:

struct AlertSystem {
    enum ALERT_LEVEL {
        INFO = 0,
        WARNING = 1,
        CRITICAL = 2,
        EMERGENCY = 3
    };
    
    struct Alert {
        ALERT_LEVEL level;
        string message;
        datetime timestamp;
        bool acknowledged;
    };
    
    Alert active_alerts[];
    int max_alerts;
    datetime last_notification;
    
    void Init() {
        max_alerts = 100;
        ArrayResize(active_alerts, max_alerts);
        last_notification = 0;
    }
    
    void RaiseAlert(ALERT_LEVEL level, string message) {
        // Ищем свободный слот для алерта
        int slot = -1;
        for(int i = 0; i < max_alerts; i++) {
            if(active_alerts[i].timestamp == 0) {
                slot = i;
                break;
            }
        }
        
        if(slot == -1) {
            // Заменяем самый старый алерт
            datetime oldest = TimeCurrent();
            for(int i = 0; i < max_alerts; i++) {
                if(active_alerts[i].timestamp < oldest) {
                    oldest = active_alerts[i].timestamp;
                    slot = i;
                }
            }
        }
        
        // Создаем алерт
        active_alerts[slot].level = level;
        active_alerts[slot].message = message;
        active_alerts[slot].timestamp = TimeCurrent();
        active_alerts[slot].acknowledged = false;
        
        // Выводим в лог с соответствующими префиксами
        string prefix = GetAlertPrefix(level);
        Print(prefix, " ", message);
        
        // Критические алерты требуют немедленного действия
        if(level >= CRITICAL) {
            HandleCriticalAlert(message);
        }
        
        // Ограничиваем частоту уведомлений
        if(TimeCurrent() - last_notification > 300) {  // Не чаще раза в 5 минут
            SendNotification(level, message);
            last_notification = TimeCurrent();
        }
    }
    
    string GetAlertPrefix(ALERT_LEVEL level) {
        switch(level) {
            case INFO: return "ℹ️ INFO:";
            case WARNING: return "⚠️ WARNING:";
            case CRITICAL: return "🚨 CRITICAL:";
            case EMERGENCY: return "🆘 EMERGENCY:";
            default: return "❓ UNKNOWN:";
        }
    }
    
    void HandleCriticalAlert(string message) {
        // Автоматические действия при критических алертах
        if(StringFind(message, "accuracy") >= 0) {
            // Проблемы с точностью - снижаем размер позиций
            ReducePositionSizes(0.5);
        }
        else if(StringFind(message, "memory") >= 0) {
            // Проблемы с памятью - принудительная очистка
            g_memory_manager.ResetTempPool();
        }
        else if(StringFind(message, "drift") >= 0) {
            // Концептуальный дрейф - останавливаем торговлю до переобучения
            DisableTrading("Concept drift detected");
        }
    }
    
    void SendNotification(ALERT_LEVEL level, string message) {
        // В реальной системе здесь может быть:
        // - Отправка email через веб-запросы
        // - Push уведомления через Telegram Bot API
        // - SMS через внешние сервисы
        
        string notification = StringFormat("[%s] %s: %s", 
            TimeToString(TimeCurrent()), 
            EnumToString(level), 
            message);
        
        // Для демонстрации просто выводим в терминал
        Print("📱 NOTIFICATION SENT: ", notification);
    }
};

AlertSystem g_alerts;

После разработки N-BEATS системы настало время проверить её эффективность в реальных торговых условиях. MetaTrader 5 предоставляет профессиональные инструменты тестирования, которые мы и используем.


Результаты тестирования N-BEATS робота

К сожалению, в отличие от предыдущих рабочих моделей цикла - Mamba и PatchTST, несмотря на многообещающую архитектуру, нам не удалось извлечь прибыль на тестовом участке с января 2025 по август 2025:

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

Несмотря на техническую сложность и элегантность реализации, тестирование на исторических данных за период январь-август 2025 года показало неспособность системы генерировать стабильную прибыль. В отличие от предыдущих успешных архитектур Mamba и PatchTST, N-BEATS не смогла эффективно работать в реальных торговых условиях.


Заключение

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

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

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

Название файлаИспользование файла

NBeats_TimeSeriesNet.mqh

Первая версия модели из статьи - нужно поместить в папку /Include

NBeats_TimeSeriesNetV2.mqh

Вторая версия модели из статьи - нужно поместить в папку /Include

NBeats_EA.mq5

Первая версия советника из статьи - нужно поместить в папку /Experts

NBeats_EAV2.mq5

Вторая версия советника из статьи - нужно поместить в папку /Experts
Прикрепленные файлы |
NBeats_EA.mq5 (29.26 KB)
NBeats_EAV2.mq5 (53.43 KB)
Возвратные стратегии дневной торговли RSI2 Ларри Коннорса Возвратные стратегии дневной торговли RSI2 Ларри Коннорса
Ларри Коннорс — известный трейдер и автор книг, наиболее известный своими работами в области количественной (алгоритмизированной) торговли и таких стратегий, как 2-периодный индекс относительной силы RSI (RSI2), помогающих определять краткосрочные состояния перекупленности и перепроданности рынка. В этой статье объясним сначала актуальность нашего исследования, затем воссоздадим три самые известные стратегии Коннорса на языке MQL5 и применим их к внутридневной торговле на индексе CFD S&P 500.
Нейросети в трейдинге: Модель адаптивной графовой диффузии (модуль внимания) Нейросети в трейдинге: Модель адаптивной графовой диффузии (модуль внимания)
В этой статье мы подробно рассмотрим практическую реализацию ключевых компонентов фреймворка SAGDFN. Покажем, как организованы разреженное внимание и выбор значимых соседей для прогнозирования временных рядов. Представленные подходы демонстрируют баланс между точностью прогнозов и эффективностью вычислений.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Возможности Мастера MQL5, которые вам нужно знать (Часть 51): Обучение с подкреплением с помощью SAC Возможности Мастера MQL5, которые вам нужно знать (Часть 51): Обучение с подкреплением с помощью SAC
Soft Actor Critic (мягкий актер-критик) — это алгоритм обучения с подкреплением, использующий три нейронные сети — сеть актеров и две сети критиков. Такие модели машинного обучения объединены в партнерство "главный-подчиненный", где критики моделируются для повышения точности прогнозов сети актеров. Как обычно, рассмотрим, как эти идеи можно протестировать в качестве пользовательского сигнала советника, собранного с помощью Мастера.