使用机器学习开发趋势交易策略·综合运用
◍ 用元标签稀释掉噪声簇
这段脚本在做均值回归模型的集群训练,核心在 use_meta_dilution 开关:当某个样本在聚类标签里等于 2.0(被判定为噪声簇),就把它在 meta_data 里的 clusters 标记强制置 0,不进入后续拟合。 具体做法是先遍历 clustered_data 的索引,若 labels==2.0 且时间点在 meta_data 中存在,就把对应行的 clusters 清掉,随后删掉 clustered_data 中标签为 2.0 的所有行,保证元特征与清洗后的数据同步。 hyper_params 里 EURUSD_H1 的回测窗口从 2000-01-01 到 2024-01-01,止损 0.00500、止盈 0.00200、markup 0.00010,n_clusters=10,processing 跑 10 次迭代。首轮迭代 Cluster 0 的 R2 达到 0.9837,说明该簇内特征解释力很强。 外汇与贵金属属高风险品种,R2 高只代表样本内拟合优度,实盘可能随机制切换而衰减,开 MT5 把 export_path 改成本机 Include 目录才能复跑。
markup=hyper_params[&class="macro">#x27;markup&class="macro">#x27;]) print(f&class="macro">#x27;Iteration: {i}, Cluster: {clust}&class="macro">#x27;) clustered_data = clustered_data.drop([&class="macro">#x27;close&class="macro">#x27;, &class="macro">#x27;clusters&class="macro">#x27;], axis=class="num">1) meta_data = data.copy() meta_data[&class="macro">#x27;clusters&class="macro">#x27;] = meta_data[&class="macro">#x27;clusters&class="macro">#x27;].apply(lambda x: class="num">1 if x == clust else class="num">0) if use_meta_dilution: for dt in clustered_data.index: if clustered_data.loc[dt, &class="macro">#x27;labels&class="macro">#x27;] == class="num">2.0: if dt in meta_data.index: # Check if class="type">class="kw">datetime exists in meta_data meta_data.loc[dt, &class="macro">#x27;clusters&class="macro">#x27;] = class="num">0 clustered_data = clustered_data.drop(clustered_data[clustered_data.labels == class="num">2.0].index) # Синхронизация meta_data с bad_data models.append(fit_final_models(clustered_data, meta_data.drop([&class="macro">#x27;close&class="macro">#x27;], axis=class="num">1))) models.sort(key=lambda x: x[class="num">0]) class="kw">return models 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/Library/Containers/com.isaacmarovitz.Whisky/Bottles/54CFA88F-36A3-47F7-915A-D09B24E89192/drive_c/Program Files/MetaTrader class="num">5/MQL5/Include/Mean reversion/&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">300, class="num">30)], &class="macro">#x27;periods_meta&class="macro">#x27;: [class="num">100], &class="macro">#x27;backward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2000, 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), &class="macro">#x27;full forward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2026, class="num">1, class="num">1), &class="macro">#x27;n_clusters&class="macro">#x27;: class="num">10, &class="macro">#x27;rolling&class="macro">#x27;: [class="num">10], } dataset = get_features(get_prices()) models = processing(iterations = class="num">10, threshold=class="num">0.001, polyorder=class="num">3, vol_window=class="num">100, use_meta_dilution = True) Iteration: class="num">0, Cluster: class="num">0 R2: class="num">0.9837358133371028
聚类拟合优度与模型导出的实跑片段
这组输出来自某次聚类训练的第 0 轮迭代,每个簇都报了 R2。簇 1 到簇 8(缺 6)的 R2 落在 0.9002 到 0.9934 之间,其中簇 8 的 0.9934 最高,簇 1 的 0.9002 最低,说明不同价格形态子集对回归目标的解释力差异明显。
日志里有一行 too few samples: 471,意味着有簇的样本数低于阈值 471 条,这类簇的拟合结果在外汇或贵金属这种高噪声市场里可能过拟合,上 MT5 回测时要单独剔除或降权。
迭代结束后直接调 test_model 用上一组模型跑验证,传入止损、止盈、-forward 参数并把图打开;紧接着 export_model_to_ONNX 把模型按 symbol、periods 等元信息导出,方便脱离训练环境做轻量推理。外汇贵金属波动剧烈,模型历史拟合好不等于实盘概率占优,参数务必小样本先验。
test_model(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;], plt=True) 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_meta&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;])
「降复杂度换平稳性,但 EURUSD 趋势模型先天挑食」
把模型导进 MT5 终端的流程和上篇一致,不用重述。训练段取 2020–2024,测试段从 2019 起跑,目的就是看看训练前那段也不是死水——净值曲线显示 2024 年后训练数据比测试数据更平滑,说明样本外泛化偏脆。 在 EURUSD 上做趋势跟踪,这套算法对新数据比较挑剔,直接训出来的策略效果并不理想。反复调超参数能拿到还行的模型,但始终没找到哪一组超参数具有普适重要性,印象里算法本身在挖掘稳定模式上就偏弱,或者根本不存在那种模式。 一个实在的补偿点:模型常在极短止损内给出结果,比如 20 个四位点数(即 2 个点差)内就能判生死。这给风控留了口子——一旦样本外开始失效,可以直接停用,外汇和贵金属的高波动下这种快刀逻辑能少亏些。 防过拟合就压模型复杂度。原 fit_final_models() 里迭代 1000,降到 100,提前停止设连续 15 轮验证集分类错误不改善就停。代价是净值曲线更噪、更匀,没原来‘好看’,但过拟合风险降了。
def fit_final_models(clustered, meta) -> list: # features for model\meta models. We learn main model only on filtered labels X, X_meta = clustered[clustered.columns[:-class="num">1]], meta[meta.columns[:-class="num">1]] X = X.loc[:, ~X.columns.str.contains(&class="macro">#x27;meta_feature&class="macro">#x27;)] X_meta = X_meta.loc[:, X_meta.columns.str.contains(&class="macro">#x27;meta_feature&class="macro">#x27;)] # labels for model\meta models y = clustered[&class="macro">#x27;labels&class="macro">#x27;] y_meta = meta[&class="macro">#x27;clusters&class="macro">#x27;] y = y.astype(&class="macro">#x27;int16&class="macro">#x27;) y_meta = y_meta.astype(&class="macro">#x27;int16&class="macro">#x27;) # train\test split train_X, test_X, train_y, test_y = train_test_split( X, y, train_size=class="num">0.7, test_size=class="num">0.3, shuffle=True) train_X_m, test_X_m, train_y_m, test_y_m = train_test_split( X_meta, y_meta, train_size=class="num">0.7, test_size=class="num">0.3, shuffle=True) # learn main model with train and validation subsets model = CatBoostClassifier(iterations=class="num">100, custom_loss=[&class="macro">#x27;Accuracy&class="macro">#x27;], eval_metric=&class="macro">#x27;Accuracy&class="macro">#x27;, verbose=False, use_best_model=True, task_type=&class="macro">#x27;CPU&class="macro">#x27;, thread_count=-class="num">1) model.fit(train_X, train_y, eval_set=(test_X, test_y), early_stopping_rounds=class="num">15, plot=False) # learn meta model with train and validation subsets meta_model = CatBoostClassifier(iterations=class="num">100, custom_loss=[&class="macro">#x27;F1&class="macro">#x27;], eval_metric=&class="macro">#x27;F1&class="macro">#x27;, verbose=False, use_best_model=True, task_type=&class="macro">#x27;CPU&class="macro">#x27;,
◍ 两层模型训练与回测评分的落地写法
这段 Python 风格的伪代码把 stacking 的两层模型串成了可回测闭环:先以 thread_count=-1 吃满所有 CPU 核训练基模型,再用 early_stopping_rounds=15 在验证集上掐掉过拟合的后续迭代,省得手动数轮次。 meta_model.fit 里 eval_set 直接挂测试集,plot=False 关掉图形输出,适合在 MT5 外接 Python 环境里批量跑参数。early_stopping_rounds=15 意味着连续 15 轮没提升就停,回测时能把训练耗时压低一截。 R2 拿到后先判 NaN,若是则强行置为 -1.0 并打印 'R2 is fixed to -1.0',避免优化器把无效结果当真实适应度。最后返回 [R2, model, meta_model],上层遗传算法就能拿 R2 当适应度去筛超参。 外汇与贵金属波动有跳空和滑点,这套 R2 仅反映样本内拟合,实盘概率层面可能衰减,开 MT5 接 Python 跑前建议先缩小样本窗口验证稳定性。
thread_count=-class="num">1) meta_model.fit(train_X_m, train_y_m, eval_set=(test_X_m, test_y_m), early_stopping_rounds=class="num">15, plot=False) R2 = test_model([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;full forward&class="macro">#x27;]) 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)) class="kw">return [R2, model, meta_model]
一点提醒
用聚类和二元分类硬做趋势跟踪,难点不在算法而在分布漂移:盘整品种的新价大多落在训练区间内,趋势品种却常跑出模型没见过的价格带,靠价格差特征喂进去泛化立刻垮掉。 附件里 EURUSD_H1_ONNX_include_0.mqh 配 trend_following.ex5 可直接挂 H1 欧美验证;评论区有人 2025 年 4 月实测说欧美单向单基本失效,只有黄金这类强趋势品种在条形图上买入还能跑。 外汇和贵金属杠杆高、跳空频繁,这套 ONNX 模型没有内置市场状态监督,行情一变就容易变半手动。模型重训快、几分钟出新的,但真上线前先在策略测试器跑几个月不同样本,别把某段行情的拟合当保本。