English Русский Español Português
preview
基于N-BEATS网络的外汇EA

基于N-BEATS网络的外汇EA

MetaTrader 5 — 交易系统 |
36 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

现代外汇交易机器人仍在沿用二十年前的指标体系。当移动平均线发生交叉、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

附加的文件 |
NBeats_EA.mq5 (29.3 KB)
NBeats_EAV2.mq5 (53.44 KB)
最近评论 | 前往讨论 (2)
Aleksey Vyazmikin
Aleksey Vyazmikin | 3 9月 2025 在 22:30
请详细介绍一下“概念漂移检测”——这个想法的要旨是什么?
secret
secret | 3 9月 2025 在 23:42
哎呀,这些GPT文章啊)
开发自定义市场情绪指标 开发自定义市场情绪指标
在本文中,我们开发一个自定义市场情绪指标,用于把市场状况分类为看涨、看跌、风险偏好(risk-on)、风险规避(risk-off)或中性。该指标采用多时间框架,帮助交易者更清晰地把握整体市场偏向并获得短期确认。
基于PatchTST的神经网络交易EA 基于PatchTST的神经网络交易EA
本文介绍了PatchTST这一革新性架构,它是专为金融时序分析定制的Transformer模型,将市场数据切分为16根K线的补丁块,以此实现高效运算。我们将讲解基于MQL5的交易机器人完整实现方案,涵盖从数学原理、数据结构,到自带风控与持续学习系统的成品EA。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MQL5交易策略自动化(第三十部分):开发具备可视化反馈的价格行为AB-CD谐波形态交易系统 MQL5交易策略自动化(第三十部分):开发具备可视化反馈的价格行为AB-CD谐波形态交易系统
在本文中,我们会在MQL5中开发一款AB=CD形态EA,借助枢轴点与斐波那契比例识别看涨、看跌AB=CD谐波形态,并按照精准的入场、止损、止盈价位执行交易。我们通过图表对象实现可视化反馈,帮助交易者更直观地研判行情。