因果推断与量化交易中的回归模型探索·进阶篇
(2/3)· 二分类只给方向不给幅度,70% 胜率照样亏;回归模型如何把波动大小算进信号
「用 tol 过滤样本压住回归过拟合」
回归模型在 MT5 里跑历史容易过拟合,肉眼看新数据表现会断崖。原文改造的测试器带 r 后缀,先循环训 10 个模型再筛最优,这一步直接决定了后面信号粗糙还是可用。 关键旋钮是传入最终训练函数的 tol 参数。预测偏差按 Point 计,tol=3e-2 即允许绝对偏差 0.03,对应四位小数报价下的 300 点。数值看着宽,但预测值可正可负,这是绝对值阈值,不是单边容差,拿它做调参实验能明显改变模型稳健度。 基础标注函数在图例1里新数据表现很差,换成计算未来价格均值的 calculate_labels_mean_r() 后整体更稳,原因通常是把均值信息喂进了标签。自定义测试器现在还不能给主回归设阈值,只能把预测值砍成正负两类,信号偏糙,这一步留到终端里补。 外汇与贵金属属高风险品种,上述回测现象仅代表历史样本行为,实盘可能失效,调 tol 前先在 EURUSD_H1 上复跑再决定。
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 窗口,说明模型倾向在样本内记忆噪声,外汇与贵金属杠杆高,这种模型实盘大概率亏。
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) 变了要重新导出,否则终端报维度不匹配。
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; class="type">class="kw">double pr[], ret[];\n&class="macro">#x27; code += &class="macro">#x27; ArrayResize(ret, class="num">1);\n&class="macro">#x27;
「把标准差特征写进ONNX头文件」
这段脚本把多周期收盘价的标准差塞进一个动态数组,再整合成可供MT5调用的mqh头文件。核心逻辑是倒序遍历Periods数组,对每个周期用CopyClose取H1收盘价,算MathStandardDeviation后插入features。 注意ArraySetAsSeries(features, true)把序列方向翻成时间正序,否则小布加载特征时会错位。最后open/write/close三步把拼好的代码落盘,print提示写入完成,全程不依赖人工拷贝。 外汇与贵金属波动受杠杆放大,标准差特征仅描述历史离散度,对后续方向判断只有概率意义,实盘须自担高风险。
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;)