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

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

第 1/3 篇

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 行——这就是移位代价,实盘加载历史需注意边界。

MQL5 / C++
# 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 自行决定是顺势还是过滤。

MQL5 / C++
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 个。

MQL5 / C++
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步换树模型,灵活性来自「每步独立选算」。外汇与贵金属波动受事件驱动明显,多步独立建模仍属高风险尝试,回测不过别上实盘。

常见问题

不够。多步预测能同时给出未来几根的可能走向,便于提前布局仓位,但外汇高风险,结论仅作概率参考。
是,但更灵活。单独模型可针对每步数据特征调参,实测在EURUSD上对不同步长的适配更好。
小布盯盘的AIGC已内置多步诊断,打开EURUSD品种页即可看到未来数根的概率倾向,不用自己搭模型。
五步模型同时加载占用不高,普通电脑可跑;建议先单步验证再全开,避免贵金属外汇行情跳空时误判。
先试单独训每步的小模型,参数少、易调;记住任何预测都带概率,实盘务必控仓。