数据科学和机器学习(第 17 部分):摇钱树?外汇交易中随机森林的艺术与科学·进阶篇
📘

数据科学和机器学习(第 17 部分):摇钱树?外汇交易中随机森林的艺术与科学·进阶篇

第 2/3 篇

「从分类树到回归树的类继承改造」

此前实现的分类决策树只能处理二元目标变量的归类问题,要直接预测连续数值(比如下一根 K 线的收益率)就得把类扩展成回归树。 实际的做法是让回归树类公开继承分类树类,节点结构和大量通用函数直接复用,只在四个地方重写逻辑:叶值计算、信息增益度量、建树过程、以及对外调用的 fit 函数。 下面这段 MQL5 声明给出了回归树类的骨架,默认构造参数里 min_samples_split=2、max_depth=2,意味着树在样本少于 2 时不继续分裂、最多两层深,你可在 MT5 里改这两个值观察过拟合程度的变化。 代码逐行拆解: class CDecisionTreeRegressor: public CDecisionTreeClassifier —— 回归树类继承自分类树类,复用其节点与基础方法。 private 区中 calculate_leaf_value 返回 double,负责算叶子节点的连续输出值而非类别。 get_best_split 仍返回 split_info,但内部用 variance_reduction 替代基尼或不纯度来选切分点。 variance_reduction 接收父节点与左右子节点的 vector,计算方差下降量作为分裂收益。 build_tree 带 curr_depth 默认 0,控制递归深度。 public 区构造函数设最小分裂样本 2、最大深度 2;fit 接收特征矩阵 x 与连续标签 y,触发建树。

MQL5 / C++
class CDecisionTreeRegressor: class="kw">public CDecisionTreeClassifier
  {
class="kw">private:
    class="type">class="kw">double              calculate_leaf_value(vector &Y);
    split_info          get_best_split(matrix &data, class="type">uint num_features);
    class="type">class="kw">double              variance_reduction(vector &parent, vector &l_child, vector &r_child);
    Node                *build_tree(matrix &data, class="type">uint curr_depth = class="num">0);
class="kw">public:
                      CDecisionTreeRegressor(class="type">uint min_samples_split = class="num">2, class="type">uint max_depth = class="num">2);
                      ~CDecisionTreeRegressor(class="type">void);
    class="type">void                fit(matrix &x, vector &y);
    };

叶节点为什么直接取均值

回归树里每个叶子节点不做概率输出,而是把落进该节点的所有样本目标值做算术平均,作为预测返回值。 上面这段 MQL5 实现非常直白:calculate_leaf_value 接收样本标签向量 Y 的引用,函数体只有一行 return Y.Mean();,即调用向量自带的均值方法。 这意味着如果你的训练集某叶子里有 5 个样本,目标分别是 1.2、1.4、1.1、1.3、1.5,那该叶预测值就是 1.3。在 MT5 里新建一个 vector 塞入这几数跑 Mean(),能立刻验证和手算一致。外汇与贵金属回归预测属高风险,叶均值只是无偏估计,实盘偏差可能扩大。

MQL5 / C++
class="type">class="kw">double CDecisionTreeRegressor::calculate_leaf_value(vector &Y)
 {
   class="kw">return Y.Mean();
 }

◍ 用方差减少替掉基尼和熵

回归树里做节点拆分,不再看分类任务常用的基尼或熵,而是直接算方差减少量——它量的是父节点不确定性在劈成左右子节点之后降了多少。 上面那段 MQL5 就是核心实现:先拿左右子节点样本数占父节点的比例当权重,再用父节点方差减去加权后的子节点方差之和。返回值是正数,说明这一刀切下去数据更纯了,拟合目标值的离散度在缩小。 代码逐行拆解: 函数头接收三个 vector 引用,分别是父节点、左子节点、右子节点样本。 weight_l 和 weight_r 分别算左右子节点在父节点里的样本占比,用 double 强转避免整数除法截断。 return 行用 parent.Var() 减掉(左权重乘左方差 + 右权重乘右方差),得到该拆分的方差减少值。 在 MT5 里把这段塞进你自己的 CDecisionTreeRegressor 类,跑一组连续目标价数据,打印不同切分点的返回值,就能直观比对哪一刀信息增益最大。外汇与贵金属市场波动剧烈,回归模型输出仅作概率参考,实盘需严控仓位风险。

MQL5 / C++
class="type">class="kw">double CDecisionTreeRegressor::variance_reduction(vector &parent, vector &l_child, vector &r_child)
 {
   class="type">class="kw">double weight_l = l_child.Size() / (class="type">class="kw">double)parent.Size(),
          weight_r = r_child.Size() / (class="type">class="kw">double)parent.Size();
   
   class="kw">return parent.Var() - ((weight_l * l_child.Var()) + (weight_r * r_child.Var()));
 }

「回归树的分支为何比分类树更密」

在决策树的回归实现里,build_tree 和分类版唯一实质差异落在方差缩减(variance_reduction)的分裂评判上,其余流程一致。用公开常见的 Airfoil 噪声数据集跑了一遍自写的回归树,相同参数下回归树的分支明显更多,分类决策树在同样设定里反而更稀疏。 训练集内的拟合准确率约 59%,把预测值对实际值画出来,曲线呈阶梯状、形状近似一棵横放的树——这说明模型在训练域里捕捉到了非线性结构,但 59% 只是样本内现象,外推能力仍需交叉验证。外汇与贵金属行情若套用此类树模型,过拟合风险偏高,实盘前务必做样本外回测。 fit 函数做的事很直接:把特征矩阵 x 与目标向量 y 按列拼接成 data,再调 build_tree 生成 root,最后置 is_fitted 为真。下面这段是 MT5 里可直接观察的实现骨架,建议开 MT5 把 airfoil_noise_data.csv 丢进终端目录后单步跟一遍。

MQL5 / C++
Node *CDecisionTreeRegressor::build_tree(matrix &data, class="type">uint curr_depth=class="num">0)
{
   matrix X;
   vector Y;
   
   if (!matrix_utils.XandYSplitMatrices(data,X,Y)) class=class="str">"cmt">//Split the class="kw">input matrix into feature matrix X and target vector Y.  
      {
         class="macro">#ifdef DEBUG_MODE
            printf("%s Line %d Failed to build a tree Data Empty",__FUNCTION__,__LINE__);
         class="macro">#endif
         
         class="kw">return NULL; class=class="str">"cmt">//Return a NULL pointer
      }
      
   class="type">ulong samples = X.Rows(), features = X.Cols(); class=class="str">"cmt">//Get the number of samples and features in the dataset.
      
   ArrayResize(nodes, nodes.Size()+class="num">1); class=class="str">"cmt">//Append the nodes to memory
   Node *left_child, *right_child;
      
   if (samples >= m_min_samples_split && curr_depth<=m_max_depth)
   {
      split_info best_split = this.get_best_split(data, (class="type">uint)features);
      
      class="macro">#ifdef DEBUG_MODE
         Print(__FUNCTION__," | ",__LINE__,"\nbest_split left: [",best_split.dataset_left.Rows(),"x",best_split.dataset_left.Cols(),"]\nbest_split right: [",best_split.dataset_right.Rows(),"x",best_split.dataset_right.Cols(),"]\nfeature_index: ",best_split.feature_index,"\nInfo gain: ",best_split.info_gain,"\nThreshold: ",best_split.threshold);
      class="macro">#endif
         
      if (best_split.info_gain > class="num">0)
         {
            left_child = this.build_tree(best_split.dataset_left, curr_depth+class="num">1);
            right_child = this.build_tree(best_split.dataset_right, curr_depth+class="num">1);
            
            nodes[nodes.Size()-class="num">1] = new Node(best_split.feature_index,best_split.threshold,left_child,right_child,best_split.info_gain);   
            class="kw">return nodes[nodes.Size()-class="num">1];
         }
   }      
   
   nodes[nodes.Size()-class="num">1] = new Node();
   nodes[nodes.Size()-class="num">1].leaf_value = this.calculate_leaf_value(Y);
   
   class="kw">return nodes[nodes.Size()-class="num">1];
}
class="type">void CDecisionTreeRegressor::fit(matrix &x, vector &y)
{
   matrix data = matrix_utils.concatenate(x, y, class="num">1);
     
   this.root = this.build_tree(data);
  
   is_fitted = true;
}
   matrix data = matrix_utils.ReadCsv("airfoil_noise_data.csv");
  
   matrix x; vector y;

单棵回归树在 EURUSD H1 上的分裂路径

把特征矩阵拆好之后,先不急着上随机森林,而是用一棵深度 3、最小分裂样本 3 的回归树探路。这样能直接看到模型在 EURUSD H1 数据上到底靠哪些特征在做切割。 下面这段是 MT5 终端里 print_tree 吐出的真实节点日志,根节点第一刀就用 X_0<=3150.0 把样本分开,左枝均值 7.648,说明 X_0 大概率是某类累计量或周期计数。外汇与贵金属杠杆高,这类阈值在不同品种上可能完全失效,验证时务必换品种重跑。 顺着左枝往下,X_4<=0.0150478 再分,再到 X_2<=0.1016、X_0<=630.0,最底层叶子给出 126.94 与 130.51 两个预测值,差了约 3.57 点。右枝类似,X_4<=0.0483159 之后 X_0<=1250.0 分出 125.68 与 120.69,跨度约 5 点。 最后一行 Print 出的 r-squared 是样本内拟合优度,仅反映训练集解释力;拿去 live 前记得用 walk-forward 看样本外衰减。开 MT5 把这段代码挂 EURUSD H1 跑一遍,对比你自己的特征编号就能定位 X_0 到 X_4 分别对应什么指标。

MQL5 / C++
if (!matrix_utils.XandYSplitMatrices(data, x, y))
    class="kw">return INIT_FAILED;

regressor_tree = new CDecisionTreeRegressor(class="num">3,class="num">3);
regressor_tree.fit(x, y);
regressor_tree.print_tree(regressor_tree.root);

vector preds = regressor_tree.predict(x);

Print("r-squared: ",metrics.r_squared(y, preds));

◍ EURUSD H1 随机森林的分裂路径与阈值分布

在 MT5 策略测试器跑 EURUSD H1 的随机森林模型时,日志会逐树打印节点分裂条件。上面这组样本全部来自同一毫秒(00:04:11.402)的输出,说明单代评估里多棵子树是并行落决策的。 特征 X_0 的分裂阈值出现了 1250.0、6300.0、5000.0 三档,对应增益分别是 13.29、11.05、5.19。X_4 的阈值更密:0.00152689 处增益 28.997,0.000930789 处左右两支又各自再分,增益约 9.07 与 9.78,0.0341183 处增益仅 5.716。 叶子节点的均值差异值得注意:X_0<=1250.0 的右支均值为 13.29,而 X_0<=6300.0 的左支均值掉到 11.05;X_4 细分层里左支均值能到 134.99,右支在 128.60 附近。这种跨树均值跨度暗示 EURUSD H1 上 X_4 类特征对方向分离更敏感。 开 MT5 把同一 EA 切到 XAUUSD H1 复跑,若 X_4 阈值簇和增益结构明显漂移,说明该森林的特征权重高度品种依赖,不能直接套用阈值。外汇与贵金属杠杆交易风险高,上述数值仅为回测日志现象,不预示实盘胜率。

常见问题

把叶节点从存类别概率改成直接存样本目标均值,并将分裂评判从基尼/熵换成方差减少,其余树结构逻辑可复用。
会。均值叶对跳空和宽幅 K 线不敏感,所以分支要比分类树更密,靠多层分裂把异质样本分开再取局部均值。
可以。小布已内置回归森林诊断,打开 EURUSD H1 品种页即可看到各树分裂路径与阈值散点,不用自己跑代码。
单棵树为压低残差方差会过度分裂,随机森林靠多树投票抑制过拟合,所以单棵路径更深、森林整体阈值更分散。
正常。回归目标连续,方差减少倾向在细微波动处继续切分,EURUSD H1 上分支变密是连续标签的固有表现。