数据科学和机器学习(第 28 部分):使用 AI 预测 EURUSD 的多个期货·进阶篇
📘

数据科学和机器学习(第 28 部分):使用 AI 预测 EURUSD 的多个期货·进阶篇

第 2/3 篇

◍ 多模型直推的隐性成本

直接多步预测要求为每一个预测步单独训练并维护一个模型,算下来训练与调参的耗时和算力开销明显偏高,对 MT5 上跑 EA 的个人交易者尤其不友好。 它不像递归一步推一步那样会把前一步的误差带进后一步,这算双刃剑:好处是单步错不连锁放大,坏处是各步预测可能互相打架,出现前后不一致的价格路径。 由于每个模型各自独立,横向几个预测区间之间的依赖关系大概率抓不牢,比起统一建模的方法,容易在震荡段给出割裂的信号。外汇与贵金属波动受事件驱动强,这种割裂会放大误判风险。

「迭代喂回的线性回归预测链」

递归多步预测(也叫迭代预测)只用同一个模型做一步 ahead 推断,把刚算出的那个值塞回输入,再推下一步,循环到目标步数。我们用线性回归拿前一根收盘价猜下一根收盘价,猜出的价直接当下一轮自变量,单特征下结构很轻。 训练前按时间顺序切 70% 训练、30% 测试,不做随机打乱,否则时态依赖(下一根收价受前一根影响)会被破坏。测试集上普通一步预测的准度约 98%,但那是直推不是递归。 真要递归得写个函数把预测值不断喂回。对 EURUSD H1 跑 10 步递归,全样本回测准确率掉到 91%——误差随步数累积是这类方法的硬伤。模型最终存成 ONNX 供 MT5 调用。 MT5 侧把 ONNX 以 resource 嵌进 exe,OnInit 里初始化 CLinearRegression,取前一根已收收盘价即可向外推 10 根;用 TrendCreate 画短水平线把预测值铺在图上,肉眼核对偏离。外汇与贵金属杠杆高,递归偏差会放大,仅作辅助参考。

MQL5 / C++
new_df = pd.DataFrame({
    &class="macro">#x27;Close&class="macro">#x27;: df[&class="macro">#x27;Close&class="macro">#x27;],
    &class="macro">#x27;target close&class="macro">#x27;: df[&class="macro">#x27;Close&class="macro">#x27;].shift(-class="num">1) # next bar closing price
})
new_df = new_df.dropna() # after shifting we want to drop all NaN values
X = new_df[["Close"]].values # Assigning close values into a 2D x array
y = new_df["target close"].values
print(new_df.shape)
new_df.head(class="num">10)
model = Pipeline([
    ("scaler", StandardScaler()),
    ("linear_regression", LinearRegression())
])
# Split the data into training and test sets
train_size = class="type">int(len(new_df) * class="num">0.7)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Train the model
model.fit(X_train, y_train)
# Testing the Model
test_pred = model.predict(X_test) # Make predictions on the test set
# Plot the actual vs predicted values
plt.figure(figsize=(class="num">7.5, class="num">5))
plt.plot(y_test, label=&class="macro">#x27;Actual Values&class="macro">#x27;)
plt.plot(test_pred, label=&class="macro">#x27;Predicted Values&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Samples&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Close Prices&class="macro">#x27;)
plt.title(&class="macro">#x27;Actual vs Predicted Values&class="macro">#x27;)
plt.legend()
plt.show()
# Function for recursive forecasting
def recursive_forecast(model, initial_value, steps):
    predictions = []
    current_input = np.array([[initial_value]])
    
    for _ in range(steps):
        prediction = model.predict(current_input)[class="num">0]
        predictions.append(prediction)
        
        # Update the input for the next prediction
        current_input = np.array([[prediction]])
    
    class="kw">return predictions
current_close = X[-class="num">1][class="num">0]  # Use the last value in the array
# Number of future steps to forecast
steps = class="num">10
# Forecast future values
forecasted_values = recursive_forecast(model, current_close, steps)
print("Forecasted Values:")
print(forecasted_values)
Forecasted Values:
[class="num">1.0854623040804965, class="num">1.0853751608200348, class="num">1.0852885667357617, class="num">1.0852025183667728, class="num">1.0851170122739744, class="num">1.085032045039946, class="num">1.0849476132688034, class="num">1.0848637135860637, class="num">1.0847803426385094, class="num">1.0846974970940555]
predicted = []
for i in range(class="num">0, X_test.shape[class="num">0], steps):
    
    current_close = X_test[i][class="num">0]  # Use the last value in the test array
    
    forecasted_values = recursive_forecast(model, current_close, steps)
    predicted.extend(forecasted_values)
    
print(len(predicted))
# Convert the trained pipeline to ONNX
initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([None, class="num">1]))]
onnx_model = convert_sklearn(model, initial_types=initial_type)
# Save the ONNX model to a file
with open("Lr.EURUSD.h1.pred_close.onnx", "wb") as f:
    f.write(onnx_model.SerializeToString())
print("Model saved to Lr.EURUSD.h1.pred_close.onnx")
class="macro">#resource "\Files\Lr.EURUSD.h1.pred_close.onnx" as class="type">uchar lr_model[]
class="macro">#include <MALE5\Linear Models\Linear Regression.mqh>
CLinearRegression lr;

递归多步预测的代码骨架与误差累积

在 MT5 里做递归多步预测,核心是把上一步的输出直接塞回模型当下一步输入。下面这段 EA 片段展示了 OnInit 里加载线性回归模型、OnTick 里取前一根 H1 收盘价为起点、连续推 10 步的逻辑。 [CODE] int OnInit() { //--- if (!lr.Init(lr_model)) return INIT_FAILED; //--- ArraySetAsSeries(rates, true); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+

//Expert tick function

//+------------------------------------------------------------------+ void OnTick() { //--- CopyRates(Symbol(), PERIOD_H1, 1, 1, rates); vector input_x = {rates[0].close}; //get the previous closed bar close price vector predicted_close(10); //predicted values for the next 10 timestepps for (int i=0; i<10; i++) { predicted_close[i] = lr.predict(input_x); input_x[0] = predicted_close[i]; //The current predicted value is the next input } Print(predicted_close); } OR 0 16:39:37.018 Recursive-Multi step forecasting (EURUSD,H4) [1.084011435508728,1.083933353424072,1.083855748176575,1.083778619766235,1.083701968193054,1.083625793457031,1.083550095558167,1.08347487449646,1.083400130271912,1.083325862884521] if (NewBar()) { for (int i=0; i<10; i++) { predicted_close[i] = lr.predict(input_x); input_x[0] = predicted_close[i]; //The current predicted value is the next input //--- ObjectDelete(0, "step"+string(i+1)+"-prediction"); //delete an object if it exists TrendCreate("step"+string(i+1)+"-prediction",rates[0].time, predicted_close[i], rates[0].time+(10*60*60), predicted_close[i], clrBlack); //draw a line starting from the previous candle to 10 hours forward } } [/CODE] 逐行拆一下关键点:OnInit 中 lr.Init(lr_model) 失败就返回 INIT_FAILED,模型只加载一次;ArraySetAsSeries(rates, true) 让 rates 按时间倒序,rates[0] 即最新根。OnTick 里 CopyRates 取 1 根 H1 数据,input_x 初值设为前收,for 循环里每步把 predicted_close[i] 写回 input_x[0] 再做下一轮预测。日志里 EURUSD H4 实测输出从 1.08401 递减到 1.08333,10 步跨度约 68 点。 NewBar 触发的分支会在图表画 10 条水平预测线,ObjectDelete 先清旧对象避免重叠,TrendCreate 从上一根 K 线时间拉到 +10 小时位置。外汇与贵金属属高风险品种,这类线性外推在震荡市可能快速失真。 只维护单个模型确实省资源,横向预测一致性也更好。但早期步的误差会一步步传下去放大,整体准确度倾向下滑;模型假设的关系在 10 步内稳定,这一前提未必总成立。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//---

   if (!lr.Init(lr_model))
      class="kw">return INIT_FAILED;

class=class="str">"cmt">//---

   ArraySetAsSeries(rates, true);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---

   CopyRates(Symbol(), PERIOD_H1, class="num">1, class="num">1, rates);
   vector input_x = {rates[class="num">0].close}; class=class="str">"cmt">//get the previous closed bar close price

   vector predicted_close(class="num">10); class=class="str">"cmt">//predicted values for the next class="num">10 timestepps

   for (class="type">int i=class="num">0; i<class="num">10; i++)
     {
       predicted_close[i] = lr.predict(input_x);
       input_x[class="num">0] = predicted_close[i]; class=class="str">"cmt">//The current predicted value is the next input
     }

   Print(predicted_close);
  }
OR       class="num">0       class="num">16:class="num">39:class="num">37.018    Recursive-Multi step forecasting(EURUSD,H4) [class="num">1.084011435508728,class="num">1.083933353424072,class="num">1.083855748176575,class="num">1.083778619766235,class="num">1.083701968193054,class="num">1.083625793457031,class="num">1.083550095558167,class="num">1.08347487449646,class="num">1.083400130271912,class="num">1.083325862884521]
   if (NewBar())
     {
       for (class="type">int i=class="num">0; i<class="num">10; i++)
        {
          predicted_close[i] = lr.predict(input_x);
          input_x[class="num">0] = predicted_close[i]; class=class="str">"cmt">//The current predicted value is the next input

          class=class="str">"cmt">//---

            ObjectDelete(class="num">0, "step"+class="type">class="kw">string(i+class="num">1)+"-prediction"); class=class="str">"cmt">//class="kw">delete an object if it exists
            TrendCreate("step"+class="type">class="kw">string(i+class="num">1)+"-prediction",rates[class="num">0].time, predicted_close[i], rates[class="num">0].time+(class="num">10*class="num">60*class="num">60), predicted_close[i], clrBlack); class=class="str">"cmt">//draw a line starting from the previous candle to class="num">10 hours forward
        }
     }

◍ 一次出多根K线:多输出神经网络的训练路数

多输出模型跟递归预测不是一回事。它一次前向计算就吐出一串未来步的预测向量,比如同时给第1到第5根收盘价,而不是拿第1根预测去推第2根。神经网络天生支持多输出,这种结构让它直接在训练里学到不同未来步之间的依赖,而不是假设它们相互独立。 做数据集时,目标变量要一次性铺开。下面这个函数把 Close 往后 shift 1~5 根,拼成 5 列 target_close_1 到 target_close_5,再跟原始 OHLC 合并、去 NaN。示例跑出来 X 形状 (995,4)、y 形状 (995,5),也就是 995 条样本、4 个输入特征、5 个输出头。 [CODE] # Create target variables for multiple future steps def create_target(df, future_steps=10): target = pd.concat([df['Close'].shift(-i) for i in range(1, future_steps + 1)], axis=1) # using close prices for the next i bar target.columns = [f'target_close_{i}' for i in range(1, future_steps + 1)] # naming the columns return target # Combine features and targets new_df = pd.DataFrame({ 'Open': df['Open'], 'High': df['High'], 'Low': df['Low'], 'Close': df['Close'] }) future_steps = 5 target_columns = create_target(new_df, future_steps).dropna() combined_df = pd.concat([new_df, target_columns], axis=1) #concatenating the new pandas dataframe with the target columns combined_df = combined_df.dropna() #droping rows with NaN values caused by shifting values target_cols_names = [f'target_close_{i}' for i in range(1, future_steps + 1)] X = combined_df.drop(columns=target_cols_names).values #dropping all target columns from the x array y = combined_df[target_cols_names].values # creating the target variables print(f"x={X.shape} y={y.shape}") combined_df.head(10) x=(995, 4) y=(995, 5) [/CODE] 模型用 Sequential 堆三层 Dense:256、128、再 5 个单元对应 5 步,总参数 34821。切分数据时用 random_state=42 做随机拆分,不按时间顺序——因为这里赌的是网络抓非线性关系,不指望它学时序形态。训练加 EarlyStopping(patience=5) 防过拟合。 [CODE] # Defining the neural network model model = Sequential([ Input(shape=(X.shape[1],)), Dense(units = 256, activation='relu'), Dense(units = 128, activation='relu'), Dense(units = future_steps) ]) # Compiling the model adam = Adam(learning_rate=0.01) model.compile(optimizer=adam, loss='mse') # Mmodel summary model.summary() ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense (Dense) │ (None, 256) │ 1,280 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 128) │ 32,896 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 5) │ 645 │ └─────────────────────────────────┴────────────────────────┴───────────────┘ Total params: 34,821 (136.02 KB) Trainable params: 34,821 (136.02 KB) Non-trainable params: 0 (0.00 B) # Split the data into training and test sets X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42) scaler = MinMaxScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # Training the model early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True) # stop training when 5 epochs doesn't improve [/CODE] 训完把模型存成 ONNX、定标器存二进制,就能进 MT5 用。实测同样数据上,多输出网络画出的预测线比递归线性回归顺眼,外汇和贵金属波动大、样本外表现可能漂移,上实盘前先用历史数据复验。

「把训练好的网络塞进 MT5 里跑」

模型在 Python 端训完,20 个 epoch、验证集切 0.2、batch_size 32,早停回调兜底。测试集上逐步看拟合:第 1 步 R² 0.866,第 2 步冲到 0.938,第 3 步 0.904,后两步滑到 0.849 和 0.846,越远越掉精度,这是多步预测的正常衰减。 拿测试集首行做前向,得到 5 步预测 [1.0892788, 1.0895394, 1.0892794, 1.0883198, 1.0884078],对应 EURUSD H1 的归一化收盘价偏移。外汇和贵金属杠杆高,这种输出只是概率倾向,不是方向保证。 真正落盘是把 Keras 转 ONNX:tf2onnx 按 input_signature 导出,opset 13,权重写 NN.EURUSD.h1.onnx;缩放器极值存成 .bin,MT5 侧用 #resource 挂进来,Init 失败直接 INIT_FAILED, scaler 用训练好的 min/max 重建,行情数组倒序后交给推理。开 MT5 把这几个文件丢进 Files,编译含 NeuralNets 和 preprocessing 的 EA 就能验证。

MQL5 / C++
class="macro">#resource "\Files\NN.EURUSD.h1.onnx" as class="type">uchar onnx_model[]; class=class="str">"cmt">//rnn model in onnx format
class="macro">#resource "\Files\NN.EURUSD.h1.min_max.max.bin" as class="type">class="kw">double min_max_max[];
class="macro">#resource "\Files\NN.EURUSD.h1.min_max.min.bin" as class="type">class="kw">double min_max_min[];
class="macro">#include <MALE5\Neural Networks\Regressor Neural Nets.mqh>
class="macro">#include <MALE5\preprocessing.mqh>
CNeuralNets nn;
MinMaxScaler *scaler;
class="type">MqlRates rates[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if (!nn.Init(onnx_model))
     class="kw">return INIT_FAILED;

   scaler = new MinMaxScaler(min_max_min, min_max_max); class=class="str">"cmt">//Initializing the scaler, populating it with trained values

class=class="str">"cmt">//---

   ArraySetAsSeries(rates, true);

   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

多模型直推会重复跑特征工程与前向计算,隐性成本随预测步数线性放大;建议合并为单网络多输出,或限制递归步数控制在 5 根内。
会累积且偏误单向放大,递归超过 8 步后均线偏离常超 20 点;实盘前用历史样本测误差曲线再定步长。
可以,小布盯盘已内置递归预测诊断,打开 EURUSD 品种页能直接看多根K线置信带和误差预警,不用自己写回测。
把未来 N 根收盘价同时作为输出向量监督训练,损失函数用多输出 MSE;数据需去均值防尺度压制短步预测。
将权重导出为轻量结构,在客户端用本地计算每根收盘后刷新预测;注意外汇高风险,信号仅作概率参考。