EA Forex baseado em rede neural N-BEATS Network
Os robôs de negociação modernos no Forex ainda usam indicadores de vinte anos atrás. Cruzamento de médias móveis, RSI acima de 70, MACD virando para baixo, e o robô decide comprar ou vender milhões de dólares. Parece absurdo, mas é exatamente assim que a maioria dos EAs opera.
Os mercados financeiros têm características críticas que as abordagens clássicas ignoram. A relação sinal-ruído é de aproximadamente 1:10: nove décimos de todos os movimentos de preço são flutuações aleatórias. As dependências de longo prazo são decisivas: decisões do Banco Central Europeu tomadas há um mês podem influenciar a cotação do euro hoje, mas os indicadores clássicos não levam isso em conta.
A não estacionariedade dos mercados cria dificuldades adicionais. Padrões que eram eficazes um mês atrás podem se tornar irrelevantes devido a mudanças na situação macroeconômica ou no comportamento dos grandes agentes do mercado. Qualquer sistema de negociação precisa ser capaz de se adaptar a essas mudanças.
Foi exatamente por isso que recorremos à arquitetura N-BEATS: uma abordagem revolucionária para previsão de séries temporais, criada especificamente para lidar com dependências temporais complexas.
N-BEATS: um avanço na arquitetura para a análise de séries temporais
Em 2019, pesquisadores da Element AI propuseram uma abordagem radicalmente nova para previsão. Em vez de modificar arquiteturas RNN ou CNN existentes, partiram de uma pergunta fundamental: qual arquitetura seria ideal especificamente para tarefas de forecasting?
O N-BEATS baseia-se no princípio da decomposição da série temporal. Imagine uma composição musical: em vez de tentar prever a próxima nota pelo som geral, analisamos separadamente melodia, ritmo e harmonia, e então construímos a previsão. De forma semelhante, o N-BEATS divide a série temporal em tendência, oscilações sazonais e padrões residuais.
A arquitetura utiliza aprendizado residual, no qual cada bloco não apenas faz uma previsão à frente, mas também explica o histórico. Se a explicação for imprecisa, o erro é passado para o bloco seguinte. Isso lembra uma investigação policial: cada detetive explica parte das pistas e passa as questões não resolvidas a um colega.
A vantagem crítica do N-BEATS é a interpretabilidade. Diferentemente da caixa-preta das redes neurais convencionais, a arquitetura consegue mostrar o peso da tendência na previsão e a contribuição das oscilações sazonais. Para o trader, isso significa entender não apenas o que vai acontecer, mas também por quê.
Os resultados dos testes em benchmarks padrão chocaram a comunidade científica. O N-BEATS superou todos os métodos existentes, incluindo redes LSTM e arquiteturas Transformer. Mas o mais impressionante é que o modelo conseguiu esse resultado usando apenas os dados históricos da própria série temporal, sem atributos externos.
Diferentemente das RNN, que processam os dados de forma sequencial, o N-BEATS consegue processar toda a série temporal em paralelo. Para o trading algorítmico, isso significa a possibilidade de gerar previsões em tempo real em mercados de dinâmica rápida.
No entanto, adaptar uma arquitetura acadêmica ao mundo caótico dos mercados cambiais exige modificações profundas. São necessárias previsões quantílicas em vez de estimativas pontuais, entradas multivariadas e um tratamento especializado do ruído de mercado.
Estrutura básica Matrix com gradientes
O MetaTrader 5 não oferece ferramentas prontas para aprendizado de máquina, por isso é preciso construir tudo do zero. Na base do nosso sistema está a estrutura Matrix, que armazena não apenas os dados, mas também os gradientes de cada elemento:
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); // Обнуление перед новой итерацией } };
Essa estrutura permite que cada camada da rede acumule automaticamente os gradientes e os propague para as camadas anteriores.
Para a ativação dos neurônios, é usada a função SiLU, que apresenta melhores resultados em dados financeiros do que a ReLU clássica. É importante implementar não apenas a função em si, mas também sua derivada para uma backpropagation correta.// 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); }
A limitação dos valores de entrada ao intervalo de -50 a 50 evita overflow no cálculo da exponencial, algo crítico para a estabilidade do treinamento em dados financeiros ruidosos.
A descida de gradiente simples funciona mal com séries temporais financeiras devido ao alto nível de ruído. Por isso, o sistema utiliza o otimizador Adam, que ajusta a taxa de aprendizado de forma adaptativa para cada parâmetro com base no histórico dos gradientes.
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(); // Очищаем градиенты после обновления } };
Propagação para frente e propagação reversa no bloco N-BEATS
Cada bloco N-BEATS executa uma sequência complexa de operações. É fundamental preservar os valores intermediários para a propagação reversa:
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 к входам // ... аналогичные вычисления для всех слоев } };
Função de perda quantílica
Para prever a incerteza, usamos perdas quantílicas em vez do MSE convencional:
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(); }
A qualidade dos dados de entrada é crítica para o sucesso de qualquer modelo de aprendizado de máquina, mas, para algoritmos de negociação, isso pode determinar a sobrevivência da conta de trading.
Atributos multivariados
Em vez de uma sequência simples de preços de fechamento, usamos um conjunto rico de atributos:
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; } } }
O clipping dos valores em intervalos razoáveis evita que valores atípicos extremos afetem o treinamento do modelo.
Normalização robusta contra ruídos
Os dados financeiros estão cheios de anomalias: picos de preço, falhas técnicas e volumes anômalos. Uma simples normalização min-max pode ser comprometida por um único valor atípico:
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)); } } }
O uso da mediana e do MAD em vez da média e do desvio-padrão torna a normalização resistente a valores atípicos.
Detecção e tratamento de anomalias
O sistema precisa detectar automaticamente condições anômalas de mercado:
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); }Ao detectar anomalias, o sistema de negociação pode reduzir temporariamente o tamanho das posições ou suspender a negociação.
As abordagens tradicionais de previsão fornecem uma única estimativa pontual: o preço será este ou aquele valor. Mas, no trading, é fundamental entender a incerteza da previsão. É exatamente por isso que nossa implementação do N-BEATS utiliza previsão quantílica.
Em vez de um único valor, o sistema fornece três estimativas: cenário pessimista (10º quantil), cenário central (mediana) e cenário otimista (90º quantil). A distância entre os quantis mostra o grau de confiança do modelo na previsão.
Para treinar previsões quantílicas, é usada uma função de perda especial, que penaliza subestimações e superestimações de forma diferente conforme o quantil-alvo.
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: combate ao desequilíbrio de classes
Os mercados financeiros são desequilibrados por natureza: períodos de negociação calma se alternam com movimentos raros, mas fortes. Uma função de perda convencional não lida bem com esse problema, por isso usamos 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); } };
A Focal Loss atribui automaticamente mais peso a eventos de mercado raros, mas importantes, exatamente aqueles em que é possível lucrar mais.
Stop-loss adaptativo com base na incerteza
A abordagem tradicional para stop-loss é primitiva: um número fixo de pontos a partir da entrada. Nosso sistema usa a incerteza prevista:
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]; }
Integração com a lógica de negociação do MetaTrader 5
Criar uma rede neural poderosa é apenas metade do trabalho. A principal tarefa é integrá-la à plataforma de negociação de modo que o sistema opere em tempo real, processe cada tick e tome decisões de negociação em milissegundos.
O MetaTrader 5 segue um modelo orientado a eventos. Cada tick, cada alteração de preço, aciona a função OnTick(). Mas executar cálculos pesados da rede neural a cada tick é ineficiente; é necessário um sistema inteligente de cache:
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); } }Sistema de sinais com histerese
Uma lógica simples do tipo "acima do limite, compramos; abaixo, vendemos" cria um problema de tremulação dos sinais. Quando a previsão oscila em torno do valor-limite, o sistema passa a abrir e fechar posições constantemente. A solução é a histerese:
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; }Treinamento contínuo em produção
Os mercados evoluem constantemente. Um modelo que apresentava bom desempenho um mês atrás pode se tornar inútil hoje. Por isso, é necessário um sistema de treinamento contínuo:
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;
O N-BEATS é uma arquitetura complexa, com muitos parâmetros. Em ambiente de produção, cada milissegundo de latência pode custar dinheiro, por isso a otimização de desempenho é crítica.
Gestão da memóriaCada matriz da rede pode conter milhares de parâmetros. Sem uma gestão adequada da memória, o sistema rapidamente esgota os recursos:
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;Profiling de caminhos críticos
É importante saber exatamente onde o tempo de execução está sendo gasto:
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"); }
O profiling ajuda a identificar gargalos e concentrar os esforços de otimização onde eles realmente importam.
Um robô de negociação com rede neural não é apenas um código que pode ser executado e esquecido. É um sistema complexo, que exige monitoramento, diagnóstico e ajustes constantes. Sem um sistema adequado de observabilidade, até mesmo a arquitetura mais avançada pode se degradar silenciosamente e começar a gerar perdas.
Monitoramento em tempo real da qualidade das previsõesA primeira linha de defesa é o acompanhamento constante da precisão das previsões em tempo real:
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;Detecção de drift conceitual
Os mercados financeiros evoluem constantemente. Padrões que eram eficazes um mês atrás podem se tornar irrelevantes. O sistema precisa detectar automaticamente essas mudanças:
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;Sistema de alertas e reação automática
Situações críticas exigem uma reação imediata, mesmo que o trader esteja dormindo ou indisponível:
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;
Após o desenvolvimento do sistema N-BEATS, chegou a hora de verificar sua eficiência em condições reais de negociação. O MetaTrader 5 oferece ferramentas profissionais de teste, e são elas que usamos.
Resultados dos testes do robô N-BEATS
Infelizmente, diferentemente dos modelos anteriores deste ciclo, Mamba e PatchTST, apesar da arquitetura promissora, não conseguimos gerar lucro no período de teste de janeiro de 2025 a agosto de 2025:

E nem o estudo de dezenas de artigos sobre o algoritmo nem a criação da segunda versão do EA mudaram muito esse cenário:

Apesar da complexidade técnica e da elegância da implementação, os testes em dados históricos no período de janeiro a agosto de 2025 mostraram a incapacidade do sistema de gerar lucro estável. Diferentemente das arquiteturas bem-sucedidas anteriores, Mamba e PatchTST, o N-BEATS não conseguiu operar de maneira eficiente em condições reais de negociação.
Conclusão
O estudo realizado mostrou que a adaptação direta da arquitetura N-BEATS à tarefa de previsão em séries temporais financeiras não entrega os resultados esperados. As principais causas foram o alto nível de ruído dos dados Forex, a falta de consideração da microestrutura de mercado e a incompatibilidade entre os custos computacionais e os ganhos efetivos de desempenho na negociação.
Ainda assim, o projeto tem valor prático significativo. Foi criada uma infraestrutura para experimentos com redes neurais em MQL5, a backpropagation foi implementada do zero e também foram desenvolvidas ferramentas de gestão de risco baseadas em incerteza. Além disso, foram incorporados sistemas de monitoramento e diagnóstico, incluindo detecção de drift conceitual e reação automática à degradação dos modelos, o que é relevante para uma ampla gama de soluções de negociação baseadas em aprendizado de máquina.
O código apresentado pode servir de base para pesquisas futuras e para o desenvolvimento de algoritmos especializados que representem com mais precisão as características dos mercados financeiros. Os resultados obtidos destacam a necessidade de adaptar arquiteturas e métodos de aprendizado de máquina à dinâmica específica do mercado e confirmam que o sucesso nessa área exige uma combinação de soluções de engenharia e uma compreensão profunda dos processos econômicos.
| Nome do arquivo | Uso do arquivo |
|---|---|
NBeats_TimeSeriesNet.mqh | Primeira versão do modelo do artigo; deve ser colocada na pasta /Include |
NBeats_TimeSeriesNetV2.mqh | Segunda versão do modelo do artigo; deve ser colocada na pasta /Include |
NBeats_EA.mq5 | Primeira versão do EA do artigo; deve ser colocada na pasta /Experts |
NBeats_EAV2.mq5 | Segunda versão do EA; deve ser colocada na pasta /Experts |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/19317
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Redes neurais em trading: modelo de difusão adaptativa em grafos (módulo de atenção)
Desenvolvimento do Kit de Ferramentas de Análise de Price Action (Parte 19): Analisador ZigZag
Está chegando o novo MetaTrader 5 e MQL5
Criando um Painel Administrativo de Negociação em MQL5 (Parte IX): Organização do Código (IV): Classe do Painel de Gerenciamento de Negociações
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso