数据科学和机器学习(第 17 部分):摇钱树?外汇交易中随机森林的艺术与科学·综合运用
(3/3)·单棵决策树在震荡行情里常掉链子,十棵树投票能否真的抬高命中率
「EURUSD 随机森林回测的日志现场」
在 MT5 策略测试器里跑 RandomForest Test(EURUSD,H1),日志首屏会吐出一组带前缀的分裂与拟合记录,读懂它们才知道模型在第一层怎么切样本。 MI 行显示根节点按特征 X_4 做阈值判断:当 X_4<=0.0483159 时走向右枝,输出原始值约 4.45;IS 行给出左枝均值 109.44,GQ 行给出右枝均值 104.84,说明左右两堆样本的平均响应差了约 4.6 个点。 PH 行最关键——r-squared 0.5937,意味着这套森林在样本内解释了约 59.4% 的 EURUSD 小时线波动方差;外汇与贵金属杠杆高,样本内拟合强不等于实盘能复现,需换时段交叉验证。 直接把上面日志贴进小布盯盘的回测解析窗,比对你自己特征工程下的 X_4 阈值,若右枝均值异常偏低,优先查特征归一化是否串了周期。
MI class="num">0 class="num">00:class="num">04:class="num">11.402 RandomForest Test(EURUSD,H1) --->--->---> right: X_4<=class="num">0.0483159?class="num">4.450134400476193 IS class="num">0 class="num">00:class="num">04:class="num">11.402 RandomForest Test(EURUSD,H1) --->--->--->---> left: class="num">109.44371428571428 GQ class="num">0 class="num">00:class="num">04:class="num">11.402 RandomForest Test(EURUSD,H1) --->--->--->---> right: class="num">104.84033333333332 PH class="num">0 class="num">00:class="num">04:class="num">11.403 RandomForest Test(EURUSD,H1) r-squared: class="num">0.5937442611327515
◍ 随机森林凭什么进交易模型
做 MT5 上的行情状态分类或波动率回归,随机森林比单棵决策树更扛造:多棵树投票把方差压下来,对报价里的毛刺和过拟合没那么敏感。 它另一个实用点是能直接吐出特征重要性排序。你拿 15 分钟图的 ATR、RSI、成交量变化喂进去,跑完就能看见哪个因子对多空判别贡献大,省掉拍脑袋选指标的功夫。 对外汇和贵金属这种高杠杆、跳空频繁的品种,模型鲁棒性直接关系实盘存活率,随机森林对特征量纲不挑剔,不同周期数值混用也不用先标准化,接数据更快。 不过它只是降低过拟合概率,不等于免死金牌;贵金属夜盘流动性断裂时,任何集成模型都可能集体误判,上实盘前务必用历史分段时间交叉验证。
把百棵树塞进一个分类器里
随机森林分类器本质上就是一组决策树分类器的集合,核心思路是用多棵弱树投票降低单棵树的过拟合倾向。在 MT5 的自定义实现里,它内部维护一个名为 forest[] 的 CDecisionTreeClassifier 对象数组,森林规模由 n_trees 控制,默认值设为 100,也就是一次预测要聚合 100 棵树的判断。 每棵树的生长受两个参数约束:min_split 决定节点最少要有多少样本才允许再分叉,max_depth 限制树枝的最大层数,这两个值在上一节已经拆过。random_state 用来固定随机种子,方便你复跑时拿到一致的结果。 下面这段是分类器的类声明骨架,直接贴出来方便你在 MT5 里对照建文件: class CRandomForestClassifier { CMetrics metrics; protected: uint m_ntrees; uint m_maxdepth; uint m_minsplit; int m_random_state; CMatrixutils matrix_utils; CDecisionTreeClassifier *forest[]; string ConvertTime(double seconds); public: CRandomForestClassifier(uint n_trees=100, uint minsplit=NULL, uint max_depth=NULL, int random_state=-1); ~CRandomForestClassifier(void); void fit(matrix &x, vector &y, bool replace=true); double predict(vector &x); vector predict(matrix &x); }; 逐行看:class 开头定义森林分类器;protected 区里 m_ntrees / m_maxdepth / m_minsplit / m_random_state 分别是树数量、最大深度、最小分裂样本、随机种子;forest[] 就是那 100 棵树的指针数组;public 里的 fit 用特征和标签训练,predict 有两个重载,单样本返回 double、批量返回 vector。 外汇与贵金属波动剧烈、杠杆高风险大,用这类模型做信号过滤仅能提升概率,不构成方向保证。实盘前先把 n_trees 改成 10 跑一次 fit,看编译和耗时再放大到 100。
class CRandomForestClassifier { CMetrics metrics; class="kw">protected: class="type">uint m_ntrees; class="type">uint m_maxdepth; class="type">uint m_minsplit; class="type">int m_random_state; CMatrixutils matrix_utils; CDecisionTreeClassifier *forest[]; class="type">class="kw">string ConvertTime(class="type">class="kw">double seconds); class="kw">public: CRandomForestClassifier(class="type">uint n_trees=class="num">100, class="type">uint minsplit=NULL, class="type">uint max_depth=NULL, class="type">int random_state=-class="num">1); ~CRandomForestClassifier(class="type">void); class="type">void fit(matrix &x, vector &y, class="type">bool replace=true); class="type">class="kw">double predict(vector &x); vector predict(matrix &x); };
「森林怎么把单棵树拼起来」
随机森林分类器的核心在 fit 函数:构造函数里定好的 n_trees 棵树,就是在这里一棵棵训练并塞进 forest 数组的。 代码先用 concatenate 把特征矩阵 x 和标签向量 y 按列拼成 data,再靠 matrix_utils.Randomize 做自助采样(replace=true 时是有放回抽取),生成每棵树的训练子集。若某次拆分失败,会直接 ArrayRemove 把那棵无效树删掉,并打印失败位置。 每棵树都是 new 出来的 CDecisionTreeClassifier,用当前子集 fit 完立刻在训练集上 predict,顺手用 metrics.accuracy_score 算出该树准确率。控制台会打印类似「Tree <3> Rand Seed <42> Accuracy Score: 0.917 Time taken: 00:00:01」的行,你能直接看到单树耗时和精度。 循环结束把 m_ntrees 重置为 ArraySize(forest),也就是实际成功建成的树数——这个数可能比最初设定的少,回测前最好 Print 出来确认,避免以为跑满了 500 棵其实只活了 487 棵。外汇与贵金属行情高波动,样本随机性会导致森林稳定性漂移,实盘前务必在 MT5 策略测试器里复跑确认。
class="type">void CRandomForestClassifier::fit(matrix &x, vector &y, class="type">bool replace=true) { matrix x_subset; vector y_subset; matrix data = this.matrix_utils.concatenate(x, y, class="num">1); matrix temp_data = data; vector preds; class="type">class="kw">datetime time_start = GetTickCount(), current_time; Print("[ Classifier Random Forest Building ]"); for (class="type">uint i=class="num">0; i<m_ntrees; i++) class=class="str">"cmt">//Build a given x number of trees { time_start = GetTickCount(); temp_data = data; matrix_utils.Randomize(temp_data, m_random_state, replace); class=class="str">"cmt">//Get randomized subsets if (!this.matrix_utils.XandYSplitMatrices(temp_data, x_subset, y_subset)) class=class="str">"cmt">//split the random subset into x and y subsets { ArrayRemove(forest,i,class="num">1); class=class="str">"cmt">//Delete the invalid tree in a forest printf("%s %d Failed to split data for a tree ",__FUNCTION__,__LINE__); class="kw">continue; } forest[i] = new CDecisionTreeClassifier(this.m_minsplit, this.m_maxdepth); class=class="str">"cmt">//Add the tree to the forest forest[i].fit(x_subset, y_subset); class=class="str">"cmt">//Add the trained tree to the forest preds = forest[i].predict(x_subset); current_time = GetTickCount(); printf(" ==> Tree <%d> Rand Seed <%s> Accuracy Score: %.3f Time taken: %s",i+class="num">1,m_random_state==-class="num">1?"None":class="type">class="kw">string(m_random_state),metrics.accuracy_score(y_subset, preds), ConvertTime((current_time - time_start) / class="num">1000.0)); } m_ntrees = ArraySize(forest); class=class="str">"cmt">//The successfully build trees }
◍ 随机森林未必压得过单棵决策树
用同一组交易数据跑了 5 轮对比,决策树训练集准确率固定在 73.8%、测试集 40%,而随机森林测试集全是 45%,训练集从 78% 到 83% 不等(78%、83%、80%、78.8%、78.8%)。 表面看随机森林测试集比决策树高 5 个百分点,但训练集波动说明它没稳定碾压;实战里拿随机森林做行情分类,常会撞见整体准确性并不比单棵决策树更好的情况。 这类失效通常来自数据噪声、特征区分度低或树间相关性偏高——具体诱因下节拆。外汇与贵金属波动剧烈,模型准确率浮动属高风险常态,别把回测数字当实盘保底。
随机森林跑不赢单棵树的十个技术坑
随机森林的精度优势来自基学习器的差异度。若每棵树高度相似,集成几乎无法带来边际提升,这时它和单独一棵决策树没本质区别。 随机性必须真正注入训练过程:对特征做随机子集抽取,或对训练样本做不同子采样(bagging)。若每棵树都吃完整数据集,容易把噪声当信号,反而拖垮泛化。 超参层面要动手试:每次分裂考虑的特征数 m_max_features、内部节点最小样本数 m_minsplit、最大深度 m_maxdepth。网格或随机搜索能帮你定位更优配置,而不是凭直觉定死。 交叉验证是必做项。它给出模型对新数据普适性的可靠估计,也能暴露过拟合或欠拟合。特征量纲差异大时,做归一或标准化虽非树模型强制要求,但便于单树与融合结果横向对比。 评估度量要对口:回归看 R² 或调整 R²,分类看准确率。fit() 末尾的 error 参数就是选每棵树精度度量的入口。森林规模也得实验,树多了精度可能上行,但训练与推理耗时会陡增。 最后两块容易被忽略:数据质量(异常值、缺失值直接拉低性能)和随机种子。种子固定且相同,所有树会算出完全一致的精度——那就真退回单棵树的水平了。 下面这段枚举定义了分类与回归各自的误差度量标签,对应 fit() 里 error 参数的取值入口: [CODE] <span class="keyword">enum</span> errors_classifier { ERR_ACCURACY }; <span class="keyword">enum</span> errors_regressor { ERR_R2_SCORE, ERR_ADJUSTED_R }; [/CODE] 逐行拆解:第一行声明分类器误差枚举类型 errors_classifier;花括号内 ERR_ACCURACY 表示用准确率作分类评估。随后声明回归器误差枚举 errors_regressor;其中 ERR_R2_SCORE 对应 R² 分数,ERR_ADJUSTED_R 对应调整 R²,供回归任务区分拟合度量。开 MT5 接 CRandomForest 类时,这两个枚举直接决定森林里每棵树的误差怎么算。
<span class="keyword">enum</span> errors_classifier { ERR_ACCURACY }; <span class="keyword">enum</span> errors_regressor { ERR_R2_SCORE, ERR_ADJUSTED_R };
「测试器里随机森林反而跑输单棵决策树」
把两套模型丢进 MT5 策略测试器,周期取 2022.01.01–2023.02.01,默认参数不动。测试环境设为:随机延迟、仅限开盘价建模、本金 1000 美元、杠杆 1:100。外汇与贵金属杠杆交易属高风险,以下仅为历史回测现象,不预示未来。 单看训练集准确率,随机森林压过决策树;但实盘式测试画出来的净值曲线很难看。随机森林那版全程只有 46% 盈利交易,图形持续下沉。 决策树反而更抗跌:盈利交易占比 44%,曲线形态优于由 100 棵树集成的随机森林。这说明纸面精度高不等于测试期生存力强。 做一次快优,把两者止损锁 960、止盈 1295、最小拆分设 2,结果分化更明显。决策树分类器测试期 47.68% 交易盈利,净赚 52 美元;随机森林分类器仅 33.99% 盈利,净亏 72 美元。 机器学习里「高级模型必胜」是错觉。随机森林虽是决策树的后继集成,但简单模型常在特定数据上反杀。随机森林仍有调参余地,换叶子数或树深重跑优化,可能翻盘。
◍ 实盘前先看清随机森林的硬伤
把随机森林搬进 MT5 做外汇或贵金属信号,先得接受它的工程代价。森林里树越多,CPU 与内存占用越夸张:原文提到一次训练 100 棵树,竟让交易动作延迟了 10 分钟——这对秒级出入场的黄金剥头皮策略基本是致命的。 模型本身也不是省心的黑匣子。多树融合后,单棵决策树那点可解释性荡然无存;类分布不平衡时它会偏向主导类别,劣势品种的预测精度会被拖垮;噪声或异常值仍能诱发过拟合,超参数虽稳却并非不敏感。 真要验证,把 forest.mqh 与 RandomForest Test.mq5 丢进 include 和 EA 目录,先用小样本跑通再谈扩树。外汇与贵金属杠杆高、滑点凶,任何 ML 信号都只是概率倾斜,实盘前务必在策略测试器里把延迟和过拟合曲线拉出来看。