因果推断与量化交易中的回归模型探索·进阶篇
📈

因果推断与量化交易中的回归模型探索·进阶篇

(2/3)· 二分类只给方向不给幅度,70% 胜率照样亏;回归模型如何把波动大小算进信号

含代码示例偏理论 第 2/3 篇
接上篇,我们继续深挖:分类器告诉你买或卖,却从不说这单能走多远。把 70% 方向胜率直接当盈利保证,是 EA 实盘里最隐蔽的亏钱陷阱。回归模型预测连续数值,正好补上这道幅度盲区。

「用 tol 过滤样本压住回归过拟合」

回归模型在 MT5 里跑历史容易过拟合,肉眼看新数据表现会断崖。原文改造的测试器带 r 后缀,先循环训 10 个模型再筛最优,这一步直接决定了后面信号粗糙还是可用。 关键旋钮是传入最终训练函数的 tol 参数。预测偏差按 Point 计,tol=3e-2 即允许绝对偏差 0.03,对应四位小数报价下的 300 点。数值看着宽,但预测值可正可负,这是绝对值阈值,不是单边容差,拿它做调参实验能明显改变模型稳健度。 基础标注函数在图例1里新数据表现很差,换成计算未来价格均值的 calculate_labels_mean_r() 后整体更稳,原因通常是把均值信息喂进了标签。自定义测试器现在还不能给主回归设阈值,只能把预测值砍成正负两类,信号偏糙,这一步留到终端里补。 外汇与贵金属属高风险品种,上述回测现象仅代表历史样本行为,实盘可能失效,调 tol 前先在 EURUSD_H1 上复跑再决定。

MQL5 / C++
hyper_params = {
    &class="macro">#x27;symbol&class="macro">#x27;: &class="macro">#x27;EURUSD_H1&class="macro">#x27;,
    &class="macro">#x27;export_path&class="macro">#x27;: &class="macro">#x27;/Users/dmitrievsky/drive_c/Program Files/MetaTrader class="num">5/MQL5/Include/Trend following/&class="macro">#x27;,
    &class="macro">#x27;model_number&class="macro">#x27;: class="num">0,
    &class="macro">#x27;markup&class="macro">#x27;: class="num">0.00010,
    &class="macro">#x27;stop_loss&class="macro">#x27;:  class="num">0.00500,
    &class="macro">#x27;take_profit&class="macro">#x27;: class="num">0.00200,
    &class="macro">#x27;periods&class="macro">#x27;: [i for i in range(class="num">5, class="num">100, class="num">30)],
    &class="macro">#x27;backward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2010, class="num">1, class="num">1),
    &class="macro">#x27;forward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2024, class="num">1, class="num">1),
}
models = []
for i in range(class="num">10):
    print(&class="macro">#x27;Learn &class="macro">#x27; + str(i) + &class="macro">#x27; model&class="macro">#x27;)
    data = get_labels_r(get_features(get_prices()), min=class="num">1, max=class="num">15)
    dataset = meta_learners(data=data, models_number=class="num">5, iterations=class="num">15, depth=class="num">3)
    models.append(fit_final_models(dataset, tol=class="num">3e-2))
def fit_final_models(dataset, tol=class="num">1e-2) -> list:
    # features for model\meta models. We learn main model only on filtered labels
    X = dataset[dataset[&class="macro">#x27;meta_labels&class="macro">#x27;] < tol]
    X, X_meta = X[X.columns[class="num">1:-class="num">2]], dataset[dataset.columns[class="num">1:-class="num">2]]
    # labels for model\meta models
    y = dataset[dataset[&class="macro">#x27;meta_labels&class="macro">#x27;] < tol]
    y, y_meta = y[y.columns[-class="num">2]], dataset[dataset.columns[-class="num">1]]
    
    # fit main model with train and validation subsets
    model = RandomForestRegressor(n_estimators=class="num">50, max_depth=class="num">10)
    model.fit(X, y)
    # fit meta model with train and validation subsets
    meta_model = RandomForestRegressor(n_estimators=class="num">50, max_depth=class="num">10)
    meta_model.fit(X_meta, y_meta)
    data = get_features(get_prices())
    R2 = test_model_r(data,
                        [model, meta_model],
                        hyper_params[&class="macro">#x27;stop_loss&class="macro">#x27;],
                        hyper_params[&class="macro">#x27;take_profit&class="macro">#x27;],
                        hyper_params[&class="macro">#x27;forward&class="macro">#x27;],
                        hyper_params[&class="macro">#x27;backward&class="macro">#x27;],
                        hyper_params[&class="macro">#x27;markup&class="macro">#x27;],
                        plt=False)
    
    if math.isnan(R2):
        R2 = -class="num">1.0
        print(&class="macro">#x27;R2 is fixed to -class="num">1.0&class="macro">#x27;)
    print(&class="macro">#x27;R2: &class="macro">#x27; + str(R2))
    result = [R2, model, meta_model]
    class="kw">return result

◍ 用最优模型跑一次前向验证

模型训练完后按第一列指标升序排,models.sort 这行就是把回测结果从差到好排好序,真正要用的只有末尾那个——models[-1] 存的是综合得分最高的那一组参数与模型对象。 直接把最优模型丢进 test_model_r 做样本外检验,入参里 stop_loss / take_profit 来自超参字典,forward 与 backward 控制前后向窗口长度,markup 是点差加成,plt=True 会画出权益曲线方便肉眼看过拟合没有。 在 MT5 里跑这套,若 forward 窗口收益明显低于 backward 窗口,说明模型倾向在样本内记忆噪声,外汇与贵金属杠杆高,这种模型实盘大概率亏。

MQL5 / C++
models.sort(key=lambda x: x[class="num">0])
data = get_features(get_prices())
test_model_r(data,
              models[-class="num">1][class="num">1:],
              hyper_params[&class="macro">#x27;stop_loss&class="macro">#x27;],
              hyper_params[&class="macro">#x27;take_profit&class="macro">#x27;],
              hyper_params[&class="macro">#x27;forward&class="macro">#x27;],
              hyper_params[&class="macro">#x27;backward&class="macro">#x27;],
              hyper_params[&class="macro">#x27;markup&class="macro">#x27;],
              plt=True)

把 sklearn 模型塞进 MT5 的 ONNX 管道

实盘里跑机器学习信号,第一道坎是终端不吃某些原生格式。CatBoost 回归模型直接转 ONNX 在 MT5 里加载会失败,所以训练侧改用随机森林,导出链路才顺。 导出函数接收模型对象与超参:symbol 决定文件名前缀,periods 的长度直接决定输入张量第二维。convert_sklearn() 按 FloatTensorType([None, len(periods)]) 把模型固化成 ONNX 字节流,主模型与 meta 模型分别写盘,命名带 symbol 和 model_number 便于多品种并行。 导出的不只是 .onnx 文件,还顺手拼出一段 MQ5 资源声明与周期数组代码。#resource 把模型以 uchar 数组编入 EX5,Periods 数组让终端知道该取哪些均线周期做特征。外汇与贵金属波动剧烈,模型信号仅作概率参考,实盘前务必在策略测试器跑至少 3 个月 Tick 数据验证。 别把正态当圣经:随机森林在样本外可能漂移,换品种时 len(periods) 变了要重新导出,否则终端报维度不匹配。

MQL5 / C++
export_model_to_ONNX(model = models[-class="num">1],
                     symbol = hyper_params[&class="macro">#x27;symbol&class="macro">#x27;],
                     periods = hyper_params[&class="macro">#x27;periods&class="macro">#x27;],
                     periods_meta = hyper_params[&class="macro">#x27;periods&class="macro">#x27;],
                     model_number = hyper_params[&class="macro">#x27;model_number&class="macro">#x27;],
                     export_path = hyper_params[&class="macro">#x27;export_path&class="macro">#x27;])
def export_model_to_ONNX(**kwargs):
    model = kwargs.get(&class="macro">#x27;model&class="macro">#x27;)
    symbol = kwargs.get(&class="macro">#x27;symbol&class="macro">#x27;)
    periods = kwargs.get(&class="macro">#x27;periods&class="macro">#x27;)
    periods_meta = kwargs.get(&class="macro">#x27;periods_meta&class="macro">#x27;)
    model_number = kwargs.get(&class="macro">#x27;model_number&class="macro">#x27;)
    export_path = kwargs.get(&class="macro">#x27;export_path&class="macro">#x27;)
    initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([None, len(hyper_params[&class="macro">#x27;periods&class="macro">#x27;])]))]
    onnx_model = convert_sklearn(model[class="num">1], initial_types=initial_type)
    # save main model to ONNX
    with open(export_path +&class="macro">#x27;catmodel &class="macro">#x27; + symbol + &class="macro">#x27; &class="macro">#x27; + str(model_number) +&class="macro">#x27;.onnx&class="macro">#x27;, "wb") as f:
        f.write(onnx_model.SerializeToString())
    onnx_model_meta = convert_sklearn(model[class="num">2], initial_types=initial_type)
    # save meta model to ONNX
    with open(export_path +&class="macro">#x27;catmodel_m &class="macro">#x27; + symbol + &class="macro">#x27; &class="macro">#x27; + str(model_number) +&class="macro">#x27;.onnx&class="macro">#x27;, "wb") as f:
        f.write(onnx_model_meta.SerializeToString())
    
    code = &class="macro">#x27;class="macro">#include <Math\Stat\Math.mqh>&class="macro">#x27;
    code += &class="macro">#x27;\n&class="macro">#x27;
    code += &class="macro">#x27;class="macro">#resource "catmodel &class="macro">#x27;+ symbol + &class="macro">#x27; &class="macro">#x27;+str(model_number)+&class="macro">#x27;.onnx" as class="type">uchar ExtModel_&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[]&class="macro">#x27;
    code += &class="macro">#x27;\n&class="macro">#x27;
    code += &class="macro">#x27;class="macro">#resource "catmodel_m &class="macro">#x27;+ symbol + &class="macro">#x27; &class="macro">#x27;+str(model_number)+&class="macro">#x27;.onnx" as class="type">uchar ExtModel2_&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[]&class="macro">#x27;
    code += &class="macro">#x27;\n\n&class="macro">#x27;
    code += &class="macro">#x27;class="type">int Periods&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[&class="macro">#x27; + str(len(periods)) + &class="macro">#x27;] = {&class="macro">#x27; + &class="macro">#x27;,&class="macro">#x27;.join(map(str, periods)) + &class="macro">#x27;};&class="macro">#x27;
    code += &class="macro">#x27;\n&class="macro">#x27;
    code += &class="macro">#x27;class="type">int Periods_m&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[&class="macro">#x27; + str(len(periods_meta)) + &class="macro">#x27;] = {&class="macro">#x27; + &class="macro">#x27;,&class="macro">#x27;.join(map(str, periods_meta)) + &class="macro">#x27;};&class="macro">#x27;
    code += &class="macro">#x27;\n\n&class="macro">#x27;
    # get features
    code += &class="macro">#x27;class="type">void fill_arays&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;( class="type">class="kw">double &features[]) {\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp; class="type">class="kw">double pr[], ret[];\n&class="macro">#x27;
    code += &class="macro">#x27;&nbsp;&nbsp; ArrayResize(ret, class="num">1);\n&class="macro">#x27;

「把标准差特征写进ONNX头文件」

这段脚本把多周期收盘价的标准差塞进一个动态数组,再整合成可供MT5调用的mqh头文件。核心逻辑是倒序遍历Periods数组,对每个周期用CopyClose取H1收盘价,算MathStandardDeviation后插入features。 注意ArraySetAsSeries(features, true)把序列方向翻成时间正序,否则小布加载特征时会错位。最后open/write/close三步把拼好的代码落盘,print提示写入完成,全程不依赖人工拷贝。 外汇与贵金属波动受杠杆放大,标准差特征仅描述历史离散度,对后续方向判断只有概率意义,实盘须自担高风险。

MQL5 / C++
code += &class="macro">#x27;  for(class="type">int i=ArraySize(Periods&class="macro">#x27;+ symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;)-class="num">1; i>=class="num">0; i--) {\n&class="macro">#x27;
code += &class="macro">#x27;      CopyClose(NULL,PERIOD_H1,class="num">1,Periods&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[i],pr);\n&class="macro">#x27;
code += &class="macro">#x27;      ret[class="num">0] = MathStandardDeviation(pr);\n&class="macro">#x27;
code += &class="macro">#x27;      ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); }\n&class="macro">#x27;
code += &class="macro">#x27;  ArraySetAsSeries(features, true);\n&class="macro">#x27;
code += &class="macro">#x27;}\n\n&class="macro">#x27;
  # get features
code += &class="macro">#x27;class="type">void fill_arays_m&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;( class="type">class="kw">double &features[]) {\n&class="macro">#x27;
code += &class="macro">#x27;  class="type">class="kw">double pr[], ret[];\n&class="macro">#x27;
code += &class="macro">#x27;  ArrayResize(ret, class="num">1);\n&class="macro">#x27;
code += &class="macro">#x27;  for(class="type">int i=ArraySize(Periods_m&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;)-class="num">1; i>=class="num">0; i--) {\n&class="macro">#x27;
code += &class="macro">#x27;      CopyClose(NULL,PERIOD_H1,class="num">1,Periods_m&class="macro">#x27; + symbol + &class="macro">#x27;_&class="macro">#x27; + str(model_number) + &class="macro">#x27;[i],pr);\n&class="macro">#x27;
code += &class="macro">#x27;      ret[class="num">0] = MathStandardDeviation(pr);\n&class="macro">#x27;
code += &class="macro">#x27;      ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); }\n&class="macro">#x27;
code += &class="macro">#x27;  ArraySetAsSeries(features, true);\n&class="macro">#x27;
code += &class="macro">#x27;}\n\n&class="macro">#x27;
file = open(export_path + str(symbol) + &class="macro">#x27; ONNX include&class="macro">#x27; + &class="macro">#x27; &class="macro">#x27; + str(model_number) + &class="macro">#x27;.mqh&class="macro">#x27;, "w")
file.write(code)
file.close()
print(&class="macro">#x27;The file &class="macro">#x27; + &class="macro">#x27;ONNX include&class="macro">#x27; + &class="macro">#x27;.mqh &class="macro">#x27; + &class="macro">#x27;has been written to disk&class="macro">#x27;)
让小布替你跑这套标注对照
这些回归标注与二分类标注的差异诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史信号里被小幅波动稀释的胜率。

常见问题

可以,特征工程通常复用同一组指标与价格衍生量,仅目标变量不同:分类用 0/1 标签,回归用未来价格减当前价的差值。
因为盈亏取决于盈利单与亏损单的幅度比,而非胜率。二分类把所有正确方向等同对待,小额盈利易被少数大亏吞掉。
可以,品种页的 AIGC 诊断模块支持按标注方式回溯,呈现两类信号在幅度筛选前后的净值差异,省去手动写回测。
训练侧用 Numba 提速不影响终端;导出模型后推理在 Python 端或 ONNX 跑,MT5 只做信号接收,延迟可控。
常见做法,但周期 N 需与策略持仓预期匹配,过长会混入噪声,过短则样本方差大,建议用滚动窗口验证稳定性。