基于N-BEATS网络的外汇EA
现代外汇交易机器人仍在沿用二十年前的指标体系。当移动平均线发生交叉、RSI(相对强弱指数)高于70、MACD(指数平滑异同移动平均线)出现下跌时,机器人就据此作出买入或卖出数百万美元的决定。听起来十分荒谬,但绝大多数智能交易系统(EA)都是这样运行的。
金融市场存在很多经典方法忽略的关键特性。信噪比大约为1:10 —— 九成的价格波动都属于随机扰动。长期时序依赖至关重要:欧洲央行一个月前的决议,时至今日依然可能影响欧元汇率,但传统指标无法捕捉这类信息。
市场的非平稳性进一步增加了难度。上个月有效的行情形态,可能因为宏观环境变化或者主力资金行为改变而失效。任何交易系统都必须具备适应这类变化的能力。
这正是我们选用N-BEATS架构的原因 —— 它是一套面向时序预测的革新方案,专门用来处理复杂的时序依赖关系。
N-BEATS:时序分析领域的架构突破
早在2019年,Element AI的研究人员就提出一种全新的预测思路。他们没有去改造现有的RNN或CNN架构,而是提出了一个根本性问题:什么样的架构才最适合预测任务?
N-BEATS的核心思想是时间序列分解。可以类比一首乐曲:不是依靠整体声音去预测下一个音符,而是分别解析旋律、节奏与和声,再综合生成预测结果。与之类似,N-BEATS将时间序列拆解为趋势项、季节性波动项以及残差模式。
该架构采用残差学习:每个模块不仅输出对未来的预测,同时对历史序列进行拟合还原(backcast)。如果还原结果存在偏差,误差就会传递到下一个模块。就像侦探办案:每位探员解析一部分线索,把尚未解开的疑点交给下一位同事。
N-BEATS最突出的优势是可解释性。传统神经网络如同黑盒,而这套架构可以清晰区分趋势、季节性波动各自在预测结果中的贡献。对于交易者来说,这意味着不仅能预判行情走向,还能知晓背后成因。
它在标准基准数据集上的测试结果令学界震惊。N-BEATS性能优于当时所有现有方法,包括LSTM网络与Transformer架构。最令人惊叹的是,它无需任何外部特征,仅凭时序自身的历史数据就达到了这一效果。
与RNN不同,N-BEATS可以并行处理输入序列,而不是只能严格串行运算。对算法交易而言,这代表能够在行情快速变动的市场中实时生成预测。
但想要把这套学术架构适配到杂乱多变的外汇市场,需要大量改造:要采用分位数预测(而非单点估计)、多变量输入,并且专门处理市场噪声。
带梯度的矩阵基础结构
MetaTrader 5没有提供现成的机器学习工具,因此所有组件都需要从零开发。本系统基于矩阵结构搭建,矩阵除了存储数值数据,还为每个元素保存对应的梯度:
struct Matrix { double data[]; double gradients[]; // Key complement for 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; // Accumulation of gradients } void ZeroGrad() { ArrayInitialize(gradients, 0.0); // Zeroing before a new iteration } };
该结构允许网络的每一层自动累积梯度,并将梯度回传至前面各层。
模型选用SiLU作为神经元激活函数,在金融数据集上,其效果优于传统的ReLU。为保证反向传播正常运行,除激活函数本身外,还必须实现它的导数。// SiLU (Swish) activation is smoother than 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 for the output layer 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的区间内,可以避免指数运算时发生溢出,这对于在带噪声的金融数据上稳定训练至关重要。
简单梯度下降在金融时序数据上效果不佳,根源在于数据噪声极高。因此,这里实现了Adam优化器,它能够根据梯度历史,为每个参数自适应调整学习率。
struct AdamOptimizer { Matrix m, v; // Momentum and speed 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); // Update moments 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)); // Update weights double update = lr * m_hat / (MathSqrt(v_hat) + eps); weights.Set(i, j, weights.Get(i, j) - update); } } weights.ZeroGrad(); // Clear gradients after updating } };
N-BEATS模块的前向传播与反向传播
每个N-BEATS模块都会执行一系列的复杂运算。为反向传播保存中间变量至关重要:
struct NBeatsBlock { Matrix fc1_w, fc1_b, fc2_w, fc2_b, theta_projection; AdamOptimizer fc1_opt, fc2_opt, theta_opt; // Intermediate values for backprop Matrix fc1_input, fc1_output, fc2_input, fc2_output; void Forward(Matrix &input_patch, Matrix &backcast, Matrix &forecast) { // Save the input for 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); // Save BEFORE activation } // Activation with saving for 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))); } // Similar for FC2 layer... // Theta projection and backcast/forecast generation } void Backward(Matrix &backcast_grad, Matrix &forecast_grad) { // Backprop starts with output gradients // Combine backcast and forecast gradients Matrix theta_grad; theta_grad.Init(1, theta_projection.cols); // Calculate theta projection gradients 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 via 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); // Accumulate gradients for the theta_projection weights theta_projection.AddGrad(i, j, theta_grad.GetGrad(0, j) * fc2_output.Get(0, i)); } fc2_grad.SetGrad(0, i, grad_sum); } // Backprop via SiLU activation 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 through FC1 layer to inputs // ... similar calculations for all layers } };
分位数损失函数
为了预测不确定性,我们采用分位数损失,而非常规的均方误差(MSE):
void Train(double &time_series[], double target) { step_count++; // Forward pass for all quantiles double predictions[QUANTILE_LEVELS]; Predict(time_series, predictions); // Calculate quantile losses 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 // Quantile loss 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); // Gradient for quantile loss double grad_wrt_pred = focal_weight * loss_weight * ((error >= 0) ? -1.0 : 1.0); double grad_wrt_logit = grad_wrt_pred * SigmoidDerivative(pred); // Backprop to output weights for(int h = 0; h < HIDDEN_SIZE; h++) { double grad = grad_wrt_logit * pooled_features[h]; output_proj[q].SetGrad(h, 0, grad); } } // Run a backward pass through the entire network BackpropagateToAllLayers(); // Update the weights of all layers UpdateAllWeights(); }
输入数据质量对任意机器学习模型的成败都至关重要,对交易算法而言,数据质量甚至可能决定账户能否保全,还是遭遇爆仓。
多变量特征
我们不再只使用简单的收盘价序列,而是采用丰富的特征集:
void PrepareInputData(MqlRates &rates[], double &input_data[], int input_bars) { ArrayResize(input_data, input_bars * 2); // 2 features per bar for(int i = 0; i < input_bars; i++) { if(i < ArraySize(rates) - 1) { // Feature 1: Normalized price change 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)); // Feature 2: Logarithmically normalized volume 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; } } }
将数值裁剪至合理区间,可避免极端异常值干扰模型训练。
抗噪声鲁棒归一化
金融数据充斥着异常情况:价格脉冲、技术故障、异常成交量。简单的最小-最大归一化仅凭一个异常值就会失效:
void RobustNormalization(double &data[], int window_size = 200) { int data_size = ArraySize(data); for(int i = window_size; i < data_size; i++) { // Calculate robust statistics for a sliding window double window_data[]; ArrayResize(window_data, window_size); for(int j = 0; j < window_size; j++) { window_data[j] = data[i - window_size + j]; } // Median instead of mean (robust to outliers) ArraySort(window_data); double median = window_data[window_size / 2]; // MAD (Median Absolute Deviation) instead of standard 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]; // Robust normalization 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; // Calculate volatility for the period 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)]; // Current volatility double current_vol = MathAbs(rates[0].close - rates[1].close) / rates[1].close; // Anomaly if current volatility exceeds 95th percentile 2+ times return (current_vol > vol_95 * 2.0); }一旦检测到异常,交易系统可以临时降低仓位或者暂停交易。
传统预测方法只输出单点估计值 —— 价格将会是某个确定数值。但对于交易而言,掌握预测结果的不确定性至关重要。这也是我们实现的N-BEATS模型采用分位数预测的原因。
系统不再输出单一数值,而是给出三组估计:悲观情景(0.1分位数)、基准预测(中位数)、乐观情景(0.9分位数)。各分位数之间的差值,代表模型对本次预测的置信程度。
训练分位数预测需要专用损失函数,它会根据目标分位数,对低估和高估施加差异化惩罚。
void ProcessQuantilePredictions(double &predictions[]) { double q10 = predictions[0]; // Pessimistic scenario double q50 = predictions[1]; // Median forecast double q90 = predictions[2]; // Optimistic scenario // Calculate the model uncertainty double uncertainty = q90 - q10; double confidence = 1.0 - MathMin(1.0, uncertainty / 0.4); // Normalize [0,1] // Dynamically adjust position size double base_lot = CalculateBaseLotSize(); double adjusted_lot = base_lot * confidence; if(q50 > 0.65 && uncertainty < 0.2) { // High growth confidence, low uncertainty OpenPosition(ORDER_TYPE_BUY, adjusted_lot * 1.5, "High confidence"); } else if(q50 > 0.6 && uncertainty < 0.3) { // Moderate confidence in growth OpenPosition(ORDER_TYPE_BUY, adjusted_lot, "Medium confidence"); } else if(q10 > 0.55) { // Even the pessimistic scenario shows growth 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)); }
焦点损失:解决类别不平衡问题
金融市场天然存在类别不均衡的特点:大部分时间是平稳行情,其间穿插着稀少但幅度较大的行情波动。标准损失函数无法处理该问题,因此我们采用焦点损失:
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; // Inverted weights - rare classes get more weight 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: focuses on difficult samples return alpha * MathPow(MathMax(1.0 - pt, 1e-8), FOCAL_GAMMA); } };
焦点损失能够自动加大对稀少且重要市场事件的权重,即最具盈利潜力的事件。
基于不确定性的自适应止损
传统止损方式比较粗糙:距离入场价固定点数设置止损。我们的系统则基于预测不确定性来设计止损:
double CalculateAdaptiveStopLoss(double entry_price, ENUM_ORDER_TYPE order_type, double uncertainty, double base_sl_points = 500) { // Increase stop loss when uncertainty is high double uncertainty_multiplier = 1.0 + uncertainty * 2.0; // [1.0, 3.0] double adaptive_sl_points = base_sl_points * uncertainty_multiplier; // Take into account the current volatility double atr = CalculateATR(14); // Average True Range for 14 periods 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 value } return atr_values[0]; }
与MetaTrader 5交易逻辑集成
搭建性能优异的神经网络仅仅完成了一半工作。核心目标是将模型接入交易平台,实现实时运行,处理每个Tick,并在毫秒级完成交易决策。
MetaTrader 5采用事件驱动模型。每个Tick、每次价格变动都会触发OnTick()函数。但在每个Tick上都执行繁重的神经网络计算效率很低,因此需要一套智能缓存机制:
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; // Cache is out of date return true; } return false; } void OnTick() { if(!g_net_initialized) return; // Perform light operations on every tick if(UseTrailingStop) UpdateTrailingStops(); // Heavy calculations only on the new bar if(IsNewBar()) { double prediction = GetAIPrediction(); // Computationally expensive operation g_cached_prediction = prediction; g_prediction_valid = true; ProcessTradingSignals(prediction); } // Check emergency exits on every tick if(g_prediction_valid) { CheckEmergencyExits(g_cached_prediction); } }滞环信号系统
“高于阈值则买入、低于阈值则卖出”这种简单逻辑会产生信号抖动问题。当预测值在阈值附近来回震荡时,系统会频繁开仓和平仓。对应的解决方案就是引入滞环:
void ProcessTradingSignalsWithHysteresis(double prediction) { static double last_prediction = 0.5; static datetime last_signal_time = 0; // Minimum interval between signals if(TimeCurrent() - last_signal_time < PeriodSeconds(Period()) * 2) return; int current_positions = CountPositions(); bool currently_long = HasLongPosition(); bool currently_short = HasShortPosition(); // Different thresholds for entry and exit (hysteresis) double long_entry_threshold = 0.70; double long_exit_threshold = 0.55; double short_entry_threshold = 0.30; double short_exit_threshold = 0.45; // Logic with hysteresis 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)); } // Similar logic for short positions... last_prediction = prediction; }实盘环境下的持续训练
市场一直在演变。一个月前表现良好的模型,到今天可能已经失效。因此,需要一套持续的学习机制:
struct ContinuousLearning { datetime last_retrain_time; double performance_buffer[]; int performance_window; double performance_threshold; void Init() { last_retrain_time = TimeCurrent(); performance_window = 100; // Track the last 100 trades performance_threshold = 0.45; // Degradation threshold ArrayResize(performance_buffer, performance_window); } void AddTradeResult(double prediction, double actual_outcome) { // Shift the buffer for(int i = 0; i < performance_window - 1; i++) { performance_buffer[i] = performance_buffer[i + 1]; } // Add a new result (1.0 = accurate prediction, 0.0 = completely wrong) double accuracy = 1.0 - MathAbs(prediction - actual_outcome); performance_buffer[performance_window - 1] = accuracy; // Check if retraining is necessary if(ShouldRetrain()) { ScheduleRetraining(); } } bool ShouldRetrain() { // Condition 1: Enough time has passed bool time_condition = (TimeCurrent() - last_retrain_time) > 24 * 3600; // Condition 2: Performance has decreased 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)); // Retrain in a separate thread (simulate) RetrainModel(); last_retrain_time = TimeCurrent(); } }; ContinuousLearning g_learning_system;
N-BEATS是一个参数众多的复杂网络结构。在实盘环境中,每一毫秒的延迟都可能带来资金亏损,因此性能优化至关重要。
内存管理网络中的每一个矩阵都可能包含数千个参数。如果缺少合理的内存管理,系统很快就会耗尽资源:
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() { // Quick "release" without real deallocation 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 does not have high-precision timers, use GetMicrosecondCount() ulong start_time = GetMicrosecondCount(); // Search for an existing operation or create a new one 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; // Reuse to store time 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; // Usage: void OptimizedPredict(double &time_series[]) { g_profiler.StartTimer("PatchEmbedding"); // ... patch embedding code g_profiler.EndTimer("PatchEmbedding"); g_profiler.StartTimer("StackForward"); // ... forward pass via stacks g_profiler.EndTimer("StackForward"); }
性能剖析有助于我们定位瓶颈,把优化工作集中在真正关键的地方。
搭载神经网络的交易机器人并不是写完代码跑起来就可以置之不理的程序。它是一套复杂系统,需要持续监控、诊断与调参。缺少完善的监控体系,即便是设计最理想的模型架构,也会悄然失效并开始产生亏损。
预测质量实时监控第一道防线就是对预测精度进行不间断实时监测:
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; // Save the forecast for future evaluation // actual_outcomes will be filled later when the result is known } void RecordOutcome(double actual_movement) { if(current_index >= buffer_size) return; actual_outcomes[current_index] = actual_movement; // Calculate the forecast accuracy 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; // Periodically display statistics if(TimeCurrent() - last_evaluation > 3600) { // Every hour 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); // Degradation warning 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; // Base model accuracy drift_detected = false; last_drift_check = TimeCurrent(); } void AddPerformancePoint(double accuracy) { // Move the window for(int i = 0; i < window_size - 1; i++) { recent_performance[i] = recent_performance[i + 1]; } recent_performance[window_size - 1] = accuracy; // Check drift every 4 hours if(TimeCurrent() - last_drift_check > 4 * 3600) { CheckForDrift(); last_drift_check = TimeCurrent(); } } void CheckForDrift() { // Calculate the average performance in the window 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; // Not enough data window_mean /= valid_count; // Statistical test for significant deviation 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 test to determine the significance of the deviation 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"); // Automatically start 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) { // Look for a free slot for an alert int slot = -1; for(int i = 0; i < max_alerts; i++) { if(active_alerts[i].timestamp == 0) { slot = i; break; } } if(slot == -1) { // Replace the oldest alert datetime oldest = TimeCurrent(); for(int i = 0; i < max_alerts; i++) { if(active_alerts[i].timestamp < oldest) { oldest = active_alerts[i].timestamp; slot = i; } } } // Create an alert active_alerts[slot].level = level; active_alerts[slot].message = message; active_alerts[slot].timestamp = TimeCurrent(); active_alerts[slot].acknowledged = false; // Display in the log with the corresponding prefixes string prefix = GetAlertPrefix(level); Print(prefix, " ", message); // Critical alerts require immediate action if(level >= CRITICAL) { HandleCriticalAlert(message); } // Limit the frequency of notifications if(TimeCurrent() - last_notification > 300) { // No more than once every 5 minutes 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) { // Automatic actions for critical alerts if(StringFind(message, "accuracy") >= 0) { // Accuracy issues - reducing position size ReducePositionSizes(0.5); } else if(StringFind(message, "memory") >= 0) { // Memory issues - forced cleanup g_memory_manager.ResetTempPool(); } else if(StringFind(message, "drift") >= 0) { // Conceptual drift - stop trading until retraining DisableTrading("Concept drift detected"); } } void SendNotification(ALERT_LEVEL level, string message) { // In a real system this could be: // - Send email via web requests // - Push notifications via Telegram Bot API // - SMS via external services string notification = StringFormat("[%s] %s: %s", TimeToString(TimeCurrent()), EnumToString(level), message); // For demonstration purposes, we simply output it to the terminal Print("📱 NOTIFICATION SENT: ", notification); } }; AlertSystem g_alerts;
N‑BEATS系统开发完成后,接下来就要在真实交易环境下测试它的实际效果。我们使用MetaTrader 5所提供的专业测试工具开展测试。
N‑BEATS交易机器人测试结果
遗憾的是,与本系列此前能够有效运行的模型 —— Mamba与PatchTST不同,尽管该架构前景可观,但在2025年1月至8月的测试区间上,我们未能实现盈利。

研究了数十篇关于该算法的文章,并开发了第二版EA,但情况并未得到明显改善。

虽然整套实现方案技术复杂度高、设计精巧,但在2025年1至8月的历史数据回测表明,该系统无法产生稳定收益。与此前表现成功的Mamba和PatchTST架构相比,N-BEATS未能有效应对实际交易环境。
结论
本次研究表明:直接将N‑BEATS架构迁移应用于金融时序预测,无法取得预期效果。主要原因包括外汇数据噪声极高、模型没有纳入市场微观结构,以及算力开销和交易收益提升不匹配。
尽管如此,本项目依然具备很高的实用价值。项目搭建了一套可在MQL5中开展神经网络实验的底层框架,从零实现了反向传播,并开发了基于预测不确定性的风控工具。除此之外,还落地了监控与诊断系统,包含概念漂移检测和模型性能衰减自动响应模块,这对多种基于机器学习的交易方案都具有参考价值。
文中提供的代码可作为后续研究与定制算法开发的基础,用来构建更贴合金融市场特性的模型。研究结果强调,机器学习架构与方法论必须针对市场动态特性做适配,同时印证了想要在该领域取得成功,既需要工程方案,也离不开对经济规律的深刻理解。
| 文件名 | 文件使用 |
|---|---|
| NBeats_TimeSeriesNet.mqh | 模型第一版,放置在/Include文件夹中。 |
| NBeats_TimeSeriesNetV2.mqh | 模型第二版,放置在/Include文件夹中。 |
| NBeats_EA.mq5 | EA第一版,放置在/Experts文件夹中。 |
| NBeats_EAV2.mq5 | EA第二版,放置在/Experts文件夹中。 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/19317
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。