数据科学和机器学习(第 16 部分):全新面貌的决策树(基础篇)
「用决策树给价格行为建模的起点」
在 MT5 里做价格行为量化,决策树是一类容易被低估但很实用的监督学习模型。它不依赖复杂网络结构,靠特征切分就能把行情状态分层,适合做趋势 / 震荡的概率判别。 把历史 K 线的收盘价变化、波幅、动量等作为特征,决策树会递归地找到使样本纯度提升最大的切分点。这种结构天然可解释:哪一根阈值线把多头样本和空头样本分开,打开模型就能看到。 外汇与贵金属市场高杠杆、高波动,用决策树做信号判别只代表历史样本下的概率倾向,实盘前务必在 MT5 策略测试器用自有数据回测验证。
◍ 为什么重做决策树
上一轮写的决策树文章虽然把天气数据分类的算法跑通了,但代码和讲解都不够利落,后台陆续收到读者要更干净实现的请求。 这一篇直接重写一版更紧凑的决策树代码,目的不是炫技,而是给后面的随机森林铺路——把单棵树的逻辑啃透, ensemble 那套才好懂。 外汇与贵金属行情里用树模型做状态分类同样高风险,过拟合一棵歪树可能比肉眼误判还快,所以底层实现必须能逐行核对。
先把决策树的结构拆开看
决策树本质上是一张倒置的流程图:最上方是根节点,往下每一层内部节点都对某个输入特征做一次判定,比如「波动率是否大于 2%」「RSI 是否低于 30」。 判定结果顺着分支走,直到落到叶节点。叶节点不给过程,只给结论——要么是分类标签(如「偏多」/「偏空」),要么是连续数值(如预测下一根 K 线的波动幅度)。 在 MT5 里用 AI 模块做行情分类时,这套结构直接决定了你喂给模型的特征顺序。特征选错,根节点一刀切歪,后面分支再细也救不回样本划分质量。外汇与贵金属杠杆高,模型误分带来的回撤可能远超股票,验证前务必用历史 tick 跑一遍。
「决策树里的节点到底长什么样」
决策树拆开看就是两类节点在干活。内部节点是树里的决策点,对某个特征做条件测试,比如判断特征值是否大于某个阈值,测试结果决定往左还是往右走分支;叶节点是终点,分类任务里给出类标签,回归任务里给出预测值,本身不再往外长分支。 实际编码时节点通常存成双精度相关的结构体。和从头写的某些 ML 算法不同,决策树用递归类和函数才顺手,用 Python 之外的语言写容易绕晕——MT5 上的 MQL5 就是典型。 下面这段 MQL5 的 Node 类把两类节点揉在一个结构里:feature_index 和 threshold 管内部节点怎么切分,leaf_value 管叶节点输出啥,left_child / right_child 两个指针挂子树。开 MT5 新建一个类粘进去,就能自己跑一棵简化树。 别把递归当可选项 MQL5 没有内建树容器,Node 里 left_child 和 right_child 是指针,构造和释放都要手写。漏了析构去 delete 子树,回测跑几万根 K 线后内存占用可能悄悄涨到几百 MB。
class Node { class="kw">public: class=class="str">"cmt">// for decision node class="type">uint feature_index; class="type">class="kw">double threshold; class="type">class="kw">double info_gain; class=class="str">"cmt">// for leaf node class="type">class="kw">double leaf_value; Node *left_child; class=class="str">"cmt">//left child Node Node *right_child; class=class="str">"cmt">//right child Node Node() : left_child(NULL), right_child(NULL) {} class=class="str">"cmt">// class="kw">default constructor Node(class="type">uint feature_index_, class="type">class="kw">double threshold_=NULL, Node *left_=NULL, Node *right_=NULL, class="type">class="kw">double info_gain_=NULL, class="type">class="kw">double value_=NULL) : left_child(left_), right_child(right_) { this.feature_index = feature_index_; this.threshold = threshold_; this.info_gain = info_gain_; this.value = value_; } class="type">void Print() { printf("feature_index: %d \nthreshold: %f \ninfo_gain: %f \nleaf_value: %f",feature_index,threshold, info_gain, value); } };
◍ 节点里到底装了什么
决策树的节点不是空壳,内部节点必须带一个测试条件:基于某个特征和阈值(或类别)来判断数据往哪边拆。你在 Node 类定义里直接翻是看不到这个条件的,它被塞进了 build_tree 的返回逻辑里——函数构造完节点实例时顺手把拆分规则集成进去。 具体落到字段,节点要记下两样东西:feature_index 标明当前在测第几个特征,threshold 存的是拆分用的阈值或类别边界。下面这段声明就是骨架,特征索引是无符号整型,阈值是双精度浮点。 外汇与贵金属行情里用这类树做状态划分时,阈值取错一个 tick 都可能让分支完全反转,属于高风险建模,建议先在 MT5 历史数据上跑小规模树验证。
Node *build_tree(matrix &data, class="type">uint curr_depth=class="num">0); class="type">uint feature_index; class="type">class="kw">double threshold;
叶节点里到底存了什么
决策树跑到底,叶子节点不会给你过程,只给你一个结果。分类任务下它存的是类标签,回归任务下存的是连续数值,这决定了你后面是做方向判断还是做价格预测。 在 MQL5 的树结构实现里,这个落点被压成一个 double 成员。无论你训练的是判别突破还是拟合均线残差,最终落到叶子上的就是 leaf_value 这一个量。 实操上,开 MT5 自建树模型时,先确认你的叶子是分类标签还是回归值,再决定下游怎么接。外汇与贵金属波动剧烈,回归值容易过拟合历史,验证时务必用样本外数据跑一遍。
class="type">class="kw">double leaf_value;