数据科学和机器学习(第 28 部分):使用 AI 预测 EURUSD 的多个期货·进阶篇
◍ 多模型直推的隐性成本
直接多步预测要求为每一个预测步单独训练并维护一个模型,算下来训练与调参的耗时和算力开销明显偏高,对 MT5 上跑 EA 的个人交易者尤其不友好。 它不像递归一步推一步那样会把前一步的误差带进后一步,这算双刃剑:好处是单步错不连锁放大,坏处是各步预测可能互相打架,出现前后不一致的价格路径。 由于每个模型各自独立,横向几个预测区间之间的依赖关系大概率抓不牢,比起统一建模的方法,容易在震荡段给出割裂的信号。外汇与贵金属波动受事件驱动强,这种割裂会放大误判风险。
「迭代喂回的线性回归预测链」
递归多步预测(也叫迭代预测)只用同一个模型做一步 ahead 推断,把刚算出的那个值塞回输入,再推下一步,循环到目标步数。我们用线性回归拿前一根收盘价猜下一根收盘价,猜出的价直接当下一轮自变量,单特征下结构很轻。 训练前按时间顺序切 70% 训练、30% 测试,不做随机打乱,否则时态依赖(下一根收价受前一根影响)会被破坏。测试集上普通一步预测的准度约 98%,但那是直推不是递归。 真要递归得写个函数把预测值不断喂回。对 EURUSD H1 跑 10 步递归,全样本回测准确率掉到 91%——误差随步数累积是这类方法的硬伤。模型最终存成 ONNX 供 MT5 调用。 MT5 侧把 ONNX 以 resource 嵌进 exe,OnInit 里初始化 CLinearRegression,取前一根已收收盘价即可向外推 10 根;用 TrendCreate 画短水平线把预测值铺在图上,肉眼核对偏离。外汇与贵金属杠杆高,递归偏差会放大,仅作辅助参考。
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 步内稳定,这一前提未必总成立。
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 就能验证。
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">//+------------------------------------------------------------------+