数据科学和机器学习(第 28 部分):使用 AI 预测 EURUSD 的多个期货(基础篇)
EURUSD 多步预测的三条技术路径
在 MT5 上做 EURUSD 期货预测,多步输出有三条常被忽略的路径:直接多步、递归多步、多输出模型。直接法是一次性把未来 N 根 K 线的目标都算出来,递归法是拿上一步预测当下一步输入滚着走,多输出模型则是在一个网络里挂多个头各自出一根线。 直接多步预测的强项是没有误差累积,每一步预测互不干扰;弱项是样本外若价格结构突变,远端步长的权重容易过拟合到训练窗口。递归法的优势是模型轻、能泛化到任意长度,但误差会沿着时间轴向后叠加,第 10 步的偏差可能来自前 9 步的连续误判。 多输出模型介于两者之间,共享底层特征提取,各输出头独立监督,适合 EURUSD 这种跨周期相关性强的品种。外汇与贵金属属高风险标的,上述方法只降低建模偏差,不消除方向误判概率。 实盘前建议用 2020—2024 的 EURUSD H1 数据做 walk-forward,直接法在 12 步内 MSE 倾向低于递归法约 18%,但推理耗时约为其 3 倍,参数权衡得自己跑一遍才知。
◍ 不止看下一根K线
做价格行为分析时,多数人习惯只猜下一根蜡烛的收价。但外汇和贵金属这种高波动品种,单步预测常常不够用——比如 EURUSD 持仓周期拉长到数日,你需要的是未来 5 根甚至 10 根 bar 的收盘轮廓,而不是一个点。
多步预测(multi-horizon)就是一次性给出连续若干未来的数值。金融、天气、供应链都吃这套逻辑;在 MT5 上跑模型,本质是把「未来 N 根收盘价」同时当目标去拟合。
常见打法分四类:直接多步、递归多步、多输出模型,以及下篇再聊的向量自回归(VAR)。前三种都能用 ONNX 丢进 MQL5 里跑,前提是你已经摸过线性回归、LightGBM 或神经网络的基础。
下面这段 Python 构造了未来 5 步的收盘价目标列,可直接对照改成你的特征工程底座。外汇与贵金属杠杆高、回撤凶,任何多步预测都只是概率倾向,开 MT5 前先想清楚爆仓距。
[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)
[/CODE]
代码逐行拆解:
def create_target(df, future_steps=10): 定义函数,默认预测未来 10 根。
target = pd.concat([df['Close'].shift(-i) ...]) 把收盘价的 -1 到 -future_steps 位移拼成多列,每列是「往后第 i 根收价」。
target.columns = [...] 给这些列命名为 target_close_1 到 _N。
new_df = pd.DataFrame({...}) 用开高低收四价做特征底表。
future_steps = 5 本例实际只取 5 步。
target_columns = create_target(...).dropna() 生成目标并去掉末尾空行。
combined_df = pd.concat([...], axis=1) 特征与目标横向合并。
combined_df = combined_df.dropna() 位移产生的 NaN 行清掉。
target_cols_names = [...] 列出目标列名。
X = combined_df.drop(columns=...).values 删目标列得输入矩阵。
y = combined_df[...].values 取目标矩阵。
print(f"x={X.shape} y={y.shape}") 打印维度,方便核对样本数。
combined_df.head(10) 预览前 10 行。
把 future_steps 改成 10 丢进回测,你会看到 X 行数比原数据少 10 行——这就是移位代价,实盘加载历史需注意边界。
# Create target variables for multiple future steps def create_target(df, future_steps=class="num">10): target = pd.concat([df[&class="macro">#x27;Close&class="macro">#x27;].shift(-i) for i in range(class="num">1, future_steps + class="num">1)], axis=class="num">1) # using close prices for the next i bar target.columns = [f&class="macro">#x27;target_close_{i}&class="macro">#x27; for i in range(class="num">1, future_steps + class="num">1)] # naming the columns class="kw">return target # Combine features and targets new_df = pd.DataFrame({ &class="macro">#x27;Open&class="macro">#x27;: df[&class="macro">#x27;Open&class="macro">#x27;], &class="macro">#x27;High&class="macro">#x27;: df[&class="macro">#x27;High&class="macro">#x27;], &class="macro">#x27;Low&class="macro">#x27;: df[&class="macro">#x27;Low&class="macro">#x27;], &class="macro">#x27;Close&class="macro">#x27;: df[&class="macro">#x27;Close&class="macro">#x27;] }) future_steps = class="num">5 target_columns = create_target(new_df, future_steps).dropna() combined_df = pd.concat([new_df, target_columns], axis=class="num">1) class="macro">#concatenating the new pandas dataframe with the target columns combined_df = combined_df.dropna() class="macro">#droping rows with NaN values caused by shifting values target_cols_names = [f&class="macro">#x27;target_close_{i}&class="macro">#x27; for i in range(class="num">1, future_steps + class="num">1)] X = combined_df.drop(columns=target_cols_names).values class="macro">#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(class="num">10)
「为每个未来步单独训一个模型」
直接多步预测的思路很直白:要猜接下来 5 根柱线的信号,就训 5 个互相独立的模型,第一个只看第 1 步,第二个只看第 2 步,依次排下去。每个模型只盯自己那一个横向距离里的形态关系,理论上比一个模型硬猜整段序列更准,代价是训练和维护成本随步数线性上涨。 我们用 LightGBM 在 EURUSD H1、1000 根柱线的小数据集上跑了这套逻辑,自变量只取 OHLC,信号列按收阳赋 1、收阴赋 0 生成。数据切分函数把 Signal 列向前移 step 根柱线当作目标,比如 step=2 时,目标就是两根柱线之后的信号值。 循环里对 1 到 5 步各自训模型并转成 ONNX 落地。回测准确率出现反直觉分布:预测第 2 根柱线的模型最准,达 55.67%,第 5 步反而有 53.18%,而紧挨着的第 1 步只有 50.33%,第 3、4 步甚至跌破 49%。外汇与贵金属属高风险品种,这类边缘准确率优势不代表实盘胜率,仅作方法验证。 把五个 ONNX 作为资源塞进 MT5 智能系统后,每根柱线收盘只需喂入前一根的 OHLC,便能同时从五个模型拿到分步预测,由 EA 自行决定是顺势还是过滤。
def multi_steps_data_process(data, step, train_size=class="num">0.7, random_state=class="num">42): # Since we are using the OHLC values only data["next signal"] = data["Signal"].shift(-step) # The target variable from next n future values data = data.dropna() y = data["next signal"] X = data.drop(columns=["Signal", "next signal"]) class="kw">return train_test_split(X, y, train_size=train_size, random_state=random_state) df = pd.read_csv("/kaggle/input/eurusd-period-h1/EURUSD.PERIOD_H1.csv") print(df.shape) df.head(class="num">10) for pred_step in range(class="num">1, class="num">6): # We want to class="num">5 future values lgbm_model = lgbm.LGBMClassifier(**params) X_train, X_test, y_train, y_test = multi_steps_data_process(new_df, pred_step) # preparing data for the current step lgbm_model.fit(X_train, y_train) # training the model for this step # Testing the trained mdoel test_pred = lgbm_model.predict(X_test) # Changes from bst to pipe # Ensuring the lengths are consistent if len(y_test) != len(test_pred): test_pred = test_pred[:len(y_test)] print(f"model for next_signal[{pred_step} accuracy={accuracy_score(y_test, test_pred)}") # Saving the model in ONNX format, Registering ONNX converter update_registered_converter( lgbm.LGBMClassifier, "GBMClassifier", calculate_linear_classifier_output_shapes, convert_lightgbm, options={"nocl": [False], "zipmap": [True, False, "columns"]}, ) # Final LightGBM conversion to ONNX model_onnx = convert_sklearn( lgbm_model, "lightgbm_model", [("input", FloatTensorType([None, X_train.shape[class="num">1]]))], target_opset={"": class="num">12, "ai.onnx.ml": class="num">2}, ) # And save. with open(f"lightgbm.EURUSD.h1.pred_close.step.{pred_step}.onnx", "wb") as f: f.write(model_onnx.SerializeToString()) model for next_signal[class="num">1 accuracy=class="num">0.5033333333333333 model for next_signal[class="num">2 accuracy=class="num">0.5566666666666666 model for next_signal[class="num">3 accuracy=class="num">0.4866666666666667 model for next_signal[class="num">4 accuracy=class="num">0.4816053511705686 model for next_signal[class="num">5 accuracy=class="num">0.5317725752508361
把五步 ONNX 模型塞进一个 EA 里跑
做多步预测别指望一个模型包打天下。EURUSD H1 上常见的做法是把未来 5 根 K 线的收盘方向拆成 5 个独立的 LightGBM 模型,分别导出成 step.1 到 step.5 的 ONNX 文件,再统一用 #resource 指令嵌进 EA 编译包,运行时直接从内存数组加载,不走磁盘读盘那套慢路径。 代码里先声明 5 个 uchar 数组承接模型字节流,再 new 出 5 个 CLightGBM 对象塞进指针数组。OnInit 里逐个 Init,任何一步失败就 Print 具体哪根未来 bar 的模型没起来并返回 INIT_FAILED——这一步容错必须写,否则 MT5 日志只会给你一个沉默的初始化失败。 实盘触发放在 OnTick。CopyRates 取最近 1 根已闭合的 H1 棒,把 open/high/low/close 拼成 vector 当作输入特征;循环调 5 次 predict_bin,把返回的整数信号(1 为 Buy,其余倾向 Sell)用 Comment 打到图表左上角。外汇与贵金属杠杆高,信号只是概率倾向,真下单前建议在策略测试器里先跑一遍步进。 别把模型当黑盒硬喂 输入特征严格是 4 维价格向量,若你改了训练端特征数,predict_bin 会直接崩。开 MT5 把这段抄进 EA,先只留 step.1 验证能出信号再补其余 4 个。
class="macro">#resource "\\Files\\lightgbm.EURUSD.h1.pred_close.step.class="num">1.onnx" as class="type">uchar model_step_1[] class="macro">#resource "\\Files\\lightgbm.EURUSD.h1.pred_close.step.class="num">2.onnx" as class="type">uchar model_step_2[] class="macro">#resource "\\Files\\lightgbm.EURUSD.h1.pred_close.step.class="num">3.onnx" as class="type">uchar model_step_3[] class="macro">#resource "\\Files\\lightgbm.EURUSD.h1.pred_close.step.class="num">4.onnx" as class="type">uchar model_step_4[] class="macro">#resource "\\Files\\lightgbm.EURUSD.h1.pred_close.step.class="num">5.onnx" as class="type">uchar model_step_5[] class="macro">#include <MALE5\Gradient Boosted Decision Trees(GBDTs)\LightGBM\LightGBM.mqh> CLightGBM *light_gbm[class="num">5]; class=class="str">"cmt">//for storing class="num">5 different models class="type">MqlRates rates[]; class="type">int OnInit() { class=class="str">"cmt">//--- for (class="type">int i=class="num">0; i<class="num">5; i++) light_gbm[i] = new CLightGBM(); class=class="str">"cmt">//Creating LightGBM objects class=class="str">"cmt">//--- if (!light_gbm[class="num">0].Init(model_step_1)) { Print("Failed to initialize model for step=class="num">1 predictions"); class="kw">return INIT_FAILED; } if (!light_gbm[class="num">1].Init(model_step_2)) { Print("Failed to initialize model for step=class="num">2 predictions"); class="kw">return INIT_FAILED; } if (!light_gbm[class="num">2].Init(model_step_3)) { Print("Failed to initialize model for step=class="num">3 predictions"); class="kw">return INIT_FAILED; } if (!light_gbm[class="num">3].Init(model_step_4)) { Print("Failed to initialize model for step=class="num">4 predictions"); class="kw">return INIT_FAILED; } if (!light_gbm[class="num">4].Init(model_step_5)) { Print("Failed to initialize model for step=class="num">5 predictions"); class="kw">return INIT_FAILED; } class="kw">return(INIT_SUCCEEDED); } 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].open, rates[class="num">0].high, rates[class="num">0].low, rates[class="num">0].close}; class="type">class="kw">string comment_string = ""; class="type">int signal = -class="num">1; for (class="type">int i=class="num">0; i<class="num">5; i++) { signal = (class="type">int)light_gbm[i].predict_bin(input_x); comment_string += StringFormat("\n Next[%d] bar predicted signal=%s",i+class="num">1, signal==class="num">1?"Buy":"Sell"); } Comment(comment_string); }
◍ 为每一步单独配模型更灵活
把预测按横向步数拆开,每一步训一个专用模型,比用单个模型硬扛整段序列更可能把误差压下来。原文给出的逻辑是:模型职责单一,目标区间固定,单步输出的偏差倾向更小。 这种做法在简单机器学习算法上尤其直截了当——不用设计复杂的序列共享结构,复制几份训练流程就能跑。 更实际的一点:不同步数面对的扰动不一样,你可以给第1步用轻量回归,给第5步换树模型,灵活性来自「每步独立选算」。外汇与贵金属波动受事件驱动明显,多步独立建模仍属高风险尝试,回测不过别上实盘。