如果说这两年量化圈子里有什么被反复提起、又反复翻车的入门方向,“决策树做交易”绝对算一个。很多人一看这名字就觉得亲切:决策树嘛,sklearn 里几行代码就能跑,还能画成树状图看模型在琢磨什么,比神经网络那种黑箱强多了。可真把行情数据丢进去,让树老老实实训练完,再拿去回测、实盘走一圈,十个里有九个会皱眉——回测曲线漂亮得不像话,实盘一上手就开始还债,甚至换个时间段重新训练,同一套代码跑出来的结果完全是两个世界。这篇我打算把“训练一棵决策树做交易到底难在哪”这件事彻底掰开,从特征、标签、过拟合、市场状态四条主线一条条拆,最后附上我从零搭到能落地的完整实操过程,以及我踩过的那些能用钱买来的坑。
先说清楚它能干什么。决策树在交易里最常规的用法,是把它当成一个“信号发生器”:输入一堆跟行情相关的特征(比如过去几天的涨跌幅、均线差、波动率、成交量变化),让它学一个映射关系——这些特征组合出现时,未来一段时间行情更可能上涨还是下跌。模型输出的是方向判断,再叠加仓位管理、止损止盈,就凑成一套可以跑回测的策略。它解决的核心问题,是把“涨跌”这件事从人的直觉判断,变成一个可复现、可回测、可解释的规则集合。适合谁来参考?凡是刚进量化门、想拿机器学习做交易但还没被市场毒打过的新人,或者已经在用技术指标手工做交易、想把规则自动化验证一遍的人,这篇文章都值得你花十分钟读完。接下来我不讲教科书,只讲实战里那些真正卡住你的东西。
1. 内容整体设计与思路拆解:为什么偏偏是决策树,又为什么它这么容易翻车
1.1 决策树做交易的基本逻辑:把行情分类问题化
先建立一个统一框架。交易这件事,本质上是“基于当前已知信息,对未知未来做判断,并根据判断下注”。决策树做的事情高度吻合:它把当前已知信息(特征)按一系列“如果……那么……”的规则拆开,最后在每个分支末端给出一个结论(涨/跌),或者一个数值(未来收益预测)。
用生活中的例子类比:决策树就像你妈判断你要不要带伞的逻辑。“如果今天下雨概率超过70%,那么带伞;否则如果昨天天气预报说降温,那么加件外套;否则不带。”每一层都是一个条件判断,层层递进,最终落到一个行动建议。量化交易里的决策树一模一样,它不需要你预设“金叉买入、死叉卖出”这种死规则,而是从历史数据里自动长出一组条件,让样本落在不同分支后得到的“纯度”最高——也就是说,落在同一分支的样本,它们的未来收益方向尽可能一致。
所以你会发现,决策树最吸引人的点有三个:一是门槛低,sklearn 一行DecisionTreeClassifier()就能初始化;二是可解释性强,训练完可以tree.export_graphviz()导出来,每个节点的划分特征和阈值都清清楚楚;三是不用像神经网络那样做大量特征缩放、正则化调参,对异常值也不那么敏感。这三个优点放在别的领域没问题,放到交易里却恰恰埋下了翻车的种子。
1.2 为什么“数据丢进去就能训练”的想法在这里行不通
很多新人踩的第一个跟头,是把决策树当成一个万能分类器,行情数据下载下来,整理成结构化表格,特征选几个技术指标,标签设成“明天涨还是跌”,丢进训练集就开始调参。等你把max_depth、min_samples_leaf调得回测曲线一路向北,心里开始盘算“今年目标年化 80%”的时候,实盘往往会在两周内教会你做人。
问题出在哪?在于交易数据跟普通机器学习数据集有本质差别。你在 Kaggle 上做泰坦尼克号生存预测,训练集和测试集都是来自同一个人群分布、同一套统计规律的数据;而行情数据是典型的时间序列,它的统计规律不仅随时间漂移,还会因为市场参与者结构变化、政策环境变化、情绪周期变化而整体换一副面孔。你用 2015 年到 2020 年的数据训练出来的规律,拿到 2021 年可能就是反向指标。决策树这种“把特征空间切成一个个小格子,每格里统计样本多数决定输出”的模型,对特征的泛化能力依赖极强——但这些特征在训练期里建立的相关性,到了样本外可能完全不存在,甚至反向。
更要命的是,决策树本身是高方差模型。它有多容易记住训练集?如果你不限制深度,它会一直分裂到每个叶子节点只剩一个样本,把所有噪声都背下来。历史行情里的噪声含量又比其他领域高出一截,于是这棵树学到的“规律”里,相当一部分是随机波动的回声。这就是为什么同样一棵树,换个时间段滚样本,参数就全失效了。所以本文所有实操,第一条纪律就是:限制模型复杂度,比追求回测收益更重要。
2. 核心细节解析与难点拆解:四个真正卡住你的关键环节
2.1 特征工程:市场不是马尔可夫过程,光喂“历史价格”远远不够
第一个难点,特征到底怎么造。常见的新手做法是拿 OHLCV 数据直接开算:用close.pct_change()得到每日收益率,滚动 5 日均值、20 日均值、布林带位置、RSI、MACD,再加成交量变化率,十几个特征一股脑塞给模型。这样做不是不行,但很多人忽略了两个关键问题。
第一个问题是特征的信息量有限。决策树本质上只能学习“当前特征组合 → 未来结果”的条件概率,它没有任何记忆机制。如果特征里只有“今天的涨跌幅”而没有“过去一周的累计状态”,那模型学到的顶多是“今天涨了明天继续涨的概率略高”这种短视统计。市场不是马尔可夫过程——今天的行情并不能完全决定明天的走势,至少还要看趋势的斜率、动能强度、所处的位置等等。所以特征设计不能只做一阶差分,要把“状态”类特征一起放进去,比如均线相对关系(MA5-MA20 的差值)、价格在近 20 日区间的位置、波动率所处的分位水平。这些特征告诉模型一件事:当前市场处于什么状态。
第二个问题是特征不是越多越好。决策树的特征选择是局部的、贪心的,它每次只找一个“当下划分最纯”的特征,并不考虑特征组合的全局效应。当特征里混入大量噪声特征时,树分裂时很容易选中那些恰好能把当前样本分开、但毫无泛化能力的特征——这就是过拟合的源头之一。我自己的经验是,起始特征控制在 8~12 个以内,宁缺毋滥,并且要用滚动窗口验证特征在样本外是否依然有区分度。相关性太高的特征也要去重,比如 MA5 和 MA10 在短期趋势里高度相关,同时放进去会让分裂时选择不稳定,造成树的规则集在相近的时间窗口里反复横跳。
2.2 标签设计:你让模型学的“涨跌”,可能根本不适合交易
第二个难点,可能很多人压根没当回事:标签怎么定义。
如果是分类任务,最常见的是把 t+1 或 t+5 的收益率取符号作为标签:收益为正标 1,收益为负标 0。这个设计本身没错,但它隐藏着几个问题。一是分类任务会让模型牺牲收益幅度:它只关心方向对不对,不关心涨 1% 和涨 8% 的区别。决策树分裂时用的是基尼系数或熵,算的是“这个分支里正负样本更纯了没有”,于是模型很可能倾向预测“大概率涨一点点”,而对极端行情完全没有捕捉能力。实际操作中我会建议用回归树预测未来收益的数值,再在决策层把预测值转成信号——预测收益大于某个阈值才开仓,这样既能过滤低质量信号,又能保留幅度信息。
二是预测周期太短会导致交易成本过高。如果你用 t+1 收益做标签,模型天天给你信号,扣掉手续费和滑点,胜率就算 55% 也未必能赚钱。我踩过的坑就是这样:跑出来的回测看起来胜率 53%,年化收益 40%,但把双边万三手续费、千一滑点一加,收益直接打了个三折。所以标签的预测周期要跟持仓周期匹配——你想做隔日波段,标签至少用未来 3~5 日收益;想做趋势跟踪,标签可能是未来 10 日或 20 日收益。预测周期拉长,信号频率降下来,每笔交易能吃的波动更大,成本占比自然被摊薄。
还有一个容易被忽略的细节是标签的类别平衡度。A 股、美股指数这类数据,长期来看上涨的天数略多于下跌,但差异不大,基本在 50%~55% 范围内;但如果你加了一个“横盘”类标签(比如 |收益| < 0.5% 记为 0),样本分布往往会严重偏向横盘,模型学出来就是个“永远预测横盘”的废物。处理方式要么去掉横盘样本,要么用class_weight='balanced'给少数类加权。总之,标签设计直接决定模型在学什么,这一步想清楚了,后面所有环节才有意义。
2.3 过拟合与样本外失效:回测曲线越漂亮,越要警惕
第三个难点,也是最核心的难点:过拟合。决策树在交易数据上的过拟合严重程度,远超一般人的预期。我举一个实际例子:某次我用同样的特征、同样的标签,只改了max_depth,从 4 调到 6,回测年化从 28% 直接跳到 67%,最大回撤从 12% 变成 18%。懂行的人一看就明白,深度加大之后,树开始记细节了——它学的不再是“均线多头排列时上涨概率大”,而是“2020 年 7 月 3 日前后那段特征的精确组合曾经大涨过”。这种记忆没有任何泛化价值,换个时间窗口立刻失效。
处理过拟合,我的经验是三条线同时收紧。第一条是限制模型复杂度:max_depth控制在 3~5,min_samples_leaf至少 50,max_features可以用sqrt或者手动限制每次分裂只考虑一部分特征。这个组合会让模型更“钝”,回测曲线没那么性感,但至少是在学规律而不是背答案。第二条是样本外验证时必须做时间序列分割。普通的train_test_split随机打乱数据在时间序列场景下是禁区——它会把 2023 年的数据泄漏到训练集里,模型等于提前看了“未来”。正确做法是训练集用前 70% 的时间段,测试集用后 30%,甚至更严谨一点,用TimeSeriesSplit做滚动验证,每一折都保证训练集时间在测试集之前。第三条是观察参数敏感性:如果max_depth从 3 调到 4,回测结果发生剧烈波动,说明模型已经处于记忆状态,赶紧把复杂度压回去。
过拟合还有个隐蔽变种叫“前视偏差”。我见过最典型的例子是:用当天的收盘价计算信号,却在当天开盘价就买入/卖出,回测系统里看起来每天都有信号、每天都能吃到当天的波动,实盘根本不可能复现。任何用到“当日”数据的特征,最早只能在次日开盘执行。这类问题回测软件不会提醒你,全凭自己心里那根弦绷着。
2.4 市场状态切换与样本不均衡:为什么模型会突然“失灵”
第四个难点,来自市场本身的非平稳性。金融市场的规律在不同阶段完全不同:单边牛市里“回调就买”是王道,震荡市里“涨多了就卖”才能活命,熊市里最好的策略可能是不交易。决策树训练时面对的是整段历史的混合分布,它学出来的规则是“所有时期规律的加权平均”。问题是,如果训练期里牛市占了 70%,模型会严重偏向“乐观”,等到震荡市一来,它的高置信度信号大概率是错的。
这个问题的解法,一种是用滚动窗口训练:固定训练集长度为最近 1~2 年,每季度或每月重新训练一次,让模型始终在“最近的市场语言”里学习。另一种是做市场状态标记,把“均线多头排列 + 波动率中等”之类的状态作为特征喂进去,让树自己去学不同状态下的差异。我实操下来,滚动训练比静态训练稳健得多,但代价是代码复杂度上来了,还要处理每次训练后规则变化带来的信号不一致问题。
样本不均衡在交易里的具体表现是“趋势段样本稀少”。拿 A 股的主要指数来看,真正走出流畅趋势的时间可能只占全部交易日的 30%,剩下 70% 都是震荡。如果你用“未来 20 日涨幅 > 5%”作为多头标签,正样本比例可能不到 15%。决策树面对极度不均衡的数据,会倾向于把所有样本都归到多数类。这时候光靠class_weight不够,我更推荐直接按市场状态分层采样,或者干脆把震荡段数据剔除一部分,让模型集中学习有区分度的样本。不过这招要慎用——剔除太狠,等于你在教模型“只会在特定行情里玩”。
3. 实操过程与核心环节实现:从零到一回测一棵能用的决策树
3.1 数据准备与特征计算
实操部分我直接用 Python 给你走一遍完整流程。数据源自己准备,我用的是日线级别的行情数据,字段至少包含date, open, high, low, close, volume,指数或个股都能跑。先导入必要的库并读取数据:
import pandas as pd import numpy as np from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score import warnings warnings.filterwarnings('ignore') df = pd.read_csv('daily_data.csv', parse_dates=['date']) df = df.sort_values('date').reset_index(drop=True)特征计算是关键。我按前面说的思路,构造四类特征:动量类、均线类、波动类、量能类,外加一组时间哑变量。具体的计算逻辑如下:
# 动量类 df['ret_1'] = df['close'].pct_change(1) df['ret_5'] = df['close'].pct_change(5) df['ret_10'] = df['close'].pct_change(10) df['ret_20'] = df['close'].pct_change(20) # 均线类:短期均线相对长期均线的偏离度,量化“多空排列” df['ma5'] = df['close'].rolling(5).mean() df['ma20'] = df['close'].rolling(20).mean() df['ma_gap'] = (df['ma5'] - df['ma20']) / df['ma20'] # 波动类:20日滚动标准差(波动率) df['vol_20'] = df['close'].pct_change(20).rolling(20).std() # 量能类:当日成交量相对5日均量的变化 df['volume_ratio'] = df['volume'] / df['volume'].rolling(5).mean() # 价格位置:当前收盘价在近20日最高最低区间中的位置 df['pos_20'] = (df['close'] - df['low'].rolling(20).min()) / ( df['high'].rolling(20).max() - df['low'].rolling(20).min() + 1e-9 ) # 时间特征:星期几(周一至周五) df['weekday'] = df['date'].dt.weekday这里要特别解释一下pos_20这个特征。它代表当前价格在最近 20 日波动区间里的相对位置,取值 0 到 1 之间,数值越接近 0 说明价格越贴近区间底部,越接近 1 说明越接近顶部。这个特征对决策树有独特价值:树可以很容易地学出“位置大于 0.8 时,短期回调概率高”这类规则,而且这类规则在不同的震荡行情中都能复用。相比之下,纯价格类特征在不同标的上绝对值差很多,反而不好用。
标签我采用两手方案:一手是分类标签label_cls,定义成未来 5 日收益大于 0 为 1,否则为 0;另一手是回归标签label_reg,直接用未来 5 日收益率。先用分类做信号,再用回归预测值过滤低质量信号。构造方式如下:
# 未来5日收益率 df['future_ret_5'] = df['close'].shift(-5) / df['close'] - 1 # 分类标签:未来5日收益 > 0 为1,否则为0 df['label_cls'] = (df['future_ret_5'] > 0).astype(int) # 回归标签 df['label_reg'] = df['future_ret_5'] # 去掉末尾没有未来数据的行和NaN行 df = df.replace([np.inf, -np.inf], np.nan).dropna().reset_index(drop=True)3.2 模型训练与核心参数选择
特征和标签准备好之后,选择特征列,初始化决策树模型。参数这块我有固定的起步配置,大多数行情数据上都能直接跑出相对稳健的结果:
feature_cols = ['ret_1', 'ret_5', 'ret_10', 'ret_20', 'ma_gap', 'vol_20', 'volume_ratio', 'pos_20', 'weekday'] X = df[feature_cols] y_cls = df['label_cls'] y_reg = df['label_reg'] # 分类树:限制深度和叶子样本量,这是防过拟合的第一道防线 clf = DecisionTreeClassifier( max_depth=4, min_samples_leaf=50, max_features=0.7, class_weight='balanced', random_state=42 ) # 回归树:用同样限制辅助预测收益幅度 reg = DecisionTreeRegressor( max_depth=4, min_samples_leaf=50, max_features=0.7, random_state=42 )这几个参数值得逐个说清楚。max_depth=4意味着树最多只有 4 层分裂,最多产生 16 个叶子节点,对应最多 16 条“规则”。这个上限是刻意压低的——深度 3 到 5 之间,树学的是相对稳健的条件概率;超过 6,就开始逐条记忆训练集了。min_samples_leaf=50表示每个叶子节点至少要有 50 个训练样本才能成立,这保证了叶子节点上的统计结论不是靠一两个样本撑起来的,能显著提升样本外稳定性。max_features=0.7让每次分裂只随机考虑 70% 的特征,这是给树加随机性、降低方差的手段,借鉴了随机森林的思路。class_weight='balanced'则在样本不均衡时自动给少数类加权,防止树把全体样本都判成多数类。
回归树在这里扮演的角色是“置信度过滤器”。训练完成后,我让分类树先判断方向,再用回归树输出未来收益的预测值,只有两个模型都给出正向结论、且回归树预测收益超过阈值(比如 1%,视持仓周期而定)时,才生成做多信号。这样能过滤掉那些“方向可能对,但幅度太小、扣完成本不赚钱”的鸡肋信号。
3.3 回测框架与评估方法:先别急着看收益
训练和信号生成只是第一步,关键是回测怎么设计和评估。用时间序列分割做样本外验证,避免数据泄漏:
# 时间序列分割:训练集在前,测试集在后,且测试集严格晚于训练集 tscv = TimeSeriesSplit(n_splits=4) for fold, (train_idx, test_idx) in enumerate(tscv.split(X)): X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y_cls.iloc[train_idx], y_cls.iloc[test_idx] clf.fit(X_train, y_train) pred = clf.predict(X_test) acc = accuracy_score(y_test, pred) print(f'Fold {fold+1}: acc={acc:.4f}')回测策略逻辑我用最简单的“择时”方式:所有可用现金,信号为 1 时持有标的,信号为 0 时空仓。每天按信号调仓,次日开盘执行。这里有个关键点必须守住:当天的特征值只有在收盘后才能全部确认,所以信号只能用于次日开盘的交易,绝不能当天收盘买入。回测代码里要把信号向后平移一天:
# 用滚动训练方式生成样本外信号,并做次日开盘执行 signals = [] for fold, (train_idx, test_idx) in enumerate(tscv.split(X)): clf.fit(X.iloc[train_idx], y_cls.iloc[train_idx]) reg.fit(X.iloc[train_idx], y_reg.iloc[train_idx]) X_test = X.iloc[test_idx] cls_prob = clf.predict_proba(X_test)[:, 1] reg_pred = reg.predict(X_test) # 只有当分类概率 > 0.55 且回归预测收益 > 0.5% 时才出多头信号 signal = ((cls_prob > 0.55) & (reg_pred > 0.005)).astype(int) signals.extend(signal) df['signal_raw'] = pd.Series(signals, index=X.index) # 信号后移一天,模拟次日开盘执行 df['signal'] = df['signal_raw'].shift(1).fillna(0)这段代码里有两个阈值值得解释。cls_prob > 0.55意味着模型预测上涨的概率必须明显高于 50%——如果模型输出 51%,说明它自己都没多大把握,这种信号实盘里噪声极大。reg_pred > 0.005即预测未来 5 日收益要超过 0.5%,扣掉双边交易成本和滑点后还能剩一点。这两个阈值不能拍脑袋定,最好用训练集上的预测分布做分位数参考,但起步阶段用这两个经验值跑通流程没问题。
接着算策略净值曲线和关键指标:
df['strategy_ret'] = df['signal'] * df['ret_1'] # 持有标的时获得次日收益 df['strategy_ret'] = df['strategy_ret'] - 0.0006 * df['signal'].diff().abs() # 扣除换仓成本 df['cum_strategy'] = (1 + df['strategy_ret']).cumprod() df['cum_benchmark'] = (1 + df['ret_1']).cumprod() # 计算基础指标 total_return = df['cum_strategy'].iloc[-1] - 1 annual_return = (1 + total_return) ** (252 / len(df)) - 1 # 最大回撤 rolling_max = df['cum_strategy'].cummax() max_drawdown = (df['cum_strategy'] / rolling_max - 1).min() # 胜率 trade_days = df[df['signal'] == 1] win_rate = (trade_days['ret_1'] > 0).mean() print(f'策略累计收益: {total_return:.2%}, 年化: {annual_return:.2%}, 最大回撤: {max_drawdown:.2%}, 胜率: {win_rate:.2%}')这段代码已经是能直接跑通的雏形,但我要再三提醒:跑出来的回测结果,不代表未来能赚这么多。我用这个流程实测过多个标的和时间段,得出的结论是,决策树策略的样本外表现普遍比训练期内要差一截,年化收益打个五折到七折都是正常现象。更稳的做法是,把样本外结果分年度去看,如果某一年策略明显失效,别急着否定模型,先看看那一年是不是震荡市——这往往是模型适用范围的自然边界,而不是代码 bug。
3.4 策略稳定性和参数敏感性检查
回测能跑通之后,别急着上实盘,先做一件事:参数敏感性检查。具体做法是把max_depth从 3 调到 6,min_samples_leaf从 30 调到 100,观察策略的年化收益、最大回撤、胜率三个指标的变化幅度。如果收益曲线剧烈波动,比如深度 4 年化 30%、深度 5 年化 80%,说明模型在记忆历史噪声,这个策略的真实泛化能力存疑。如果指标在一个合理区间内小幅波动,比如年化始终在 20%~35% 之间徘徊,那至少说明策略核心逻辑对参数不那么敏感,是个相对健康的信号。
另外一定要对比两个基准:买入持有和随机信号。拿买入持有做对比,是为了回答“模型真的比躺平强吗”;拿随机信号做对比,是为了检验交易成本和信号频率是否侵蚀了收益。有个小技巧:把信号列df['signal']随机洗牌 100 次,跑 100 遍回测,看随机策略的收益分布。如果真实策略的收益落在随机分布的下半区,那这模型还不如扔硬币。这个检查我每次都会做,能过滤掉大量“看着厉害、实则运气”的假策略。
4. 实测踩坑记录与排查方法:把最常见的坑一次性列给你
4.1 实盘和回测不一致的三大隐形杀手
决策树交易策略从回测到实盘,最常见的“魔鬼”有三个。第一个就是前视偏差。之前提到过用当日收盘价计算特征、当日执行交易,回测里感觉每天都赚钱,实盘里每笔都是“收盘后看到信号,第二天开盘追进去,价格已经跳空”。排查方法很简单:把信号列shift(1)之后再算一次回测,如果收益差距巨大,说明原策略严重依赖当日不可执行的信号。第二个是滑点估计不足。回测里用收盘价成交,实盘集合竞价、大单冲击、停牌复牌等因素都会让实际成交价偏离理想价。我一般按成交额的千分之一到千分之二估算滑点,即便这样,在流动性差的标的上还是有偏差。第三个是真假交易成本的混淆。很多回测框架默认手续费为零,或者只算单边,实际来回一次的成本会让高频信号策略直接亏穿。排查方式是设置“生死线”:把单边成本调到 0.15% 再跑一遍回测,如果策略从盈利变亏损,说明它赚的其实是幻想出来的价差。
4.2 六大高频问题速查表
我在训练决策树做交易的过程中,积累了下面这张排查表。遇到问题先对着表格查一遍,能省下大量盲目调参的时间。
| 问题现象 | 可能原因 | 排查与对策 |
|---|---|---|
| 回测年化极高,换段时间就失效 | 模型过拟合,记住了训练期噪声 | 降低max_depth,提高min_samples_leaf,用时间序列分割重新验证 |
| 准确率 60% 但交易不赚钱 | 忽略了收益幅度和交易成本 | 改用回归树过滤信号,要求预测收益超过阈值才开仓 |
| 训练集和测试集收益差异巨大 | 训练/测试随机分割导致数据泄漏 | 改用TimeSeriesSplit,训练集必须时间上在前 |
| 信号过于频繁,手续费吞噬收益 | 标签预测周期太短 | 把标签从 t+1 改成 t+5 或 t+10,降低换仓频率 |
| 模型长期预测“横盘”或“不动” | 样本不均衡,多数类占主导 | 设置class_weight='balanced',或剔除过度震荡样本 |
| 跑的结果每次都不太一样 | 决策树分裂的随机性 | 固定random_state,或者用随机森林做集成降低方差 |
4.3 我踩过的三个具体坑
第一个坑是特征泄漏,而且非常隐蔽。我当时做了一个“当日涨跌幅”的特征,又在同一天用close计算持仓收益,回测系统里“今天上涨 → 今天买入 → 吃到今天的涨幅”,收益曲线完美得让我一度怀疑人生。后来把信号shift(1)之后,曲线直接从年化 60% 变成 15%。那一次给我的教训是:回测代码里,所有信号和收益的计算时间点必须分得清清楚楚,信号只允许用当日收盘前可得的数据,交易最早在次日开盘执行。
第二个坑是max_depth从 5 加到 7,回测收益翻了一倍,我差点以为自己发现了圣杯。后来用滚动样本外验证一看,深度 7 的模型在样本外的表现反而比深度 5 还要差。原因很简单:深度越深,树的规则越细碎,每条规则覆盖的样本越少,统计显著性越低,跨时间窗口的稳定性自然越差。所以我现在一律先从max_depth=3开始,确认样本外稳定再加到 4 或 5,绝不拍脑袋追求高深度高收益。
第三个坑跟时间特征有关。我把weekday作为数值特征放进去,树学会了“周三大概率涨”这种规则。一开始我觉得挺神奇,直到我意识到这可能只是历史某段时间的巧合,换标的基本就失效了。时间特征不是不能用,但要用周期编码(比如sin(2π * weekday / 5)和cos(...))或者做哑变量,更重要的是要验证它在滚动样本外是否保持有效。否则,树学到的可能是一堆没有逻辑支撑的统计巧合。
5. 更深一层的思考:决策树在交易里真正的定位
走到这一步,你可能已经意识到,用决策树直接预测价格方向,本质上是个胜率游戏——特征选得好、参数限制严、成本算得清,能做出一个勉强跑赢基准的策略,但天花板非常明显。交易市场是零和博弈,参与者都在进化,任何静态规则都会被市场反噬。那么决策树做交易,真的没有价值了吗?我个人看法不是这样。
我在实操中得出的体会是,决策树最大的价值不在于“作为主策略预测员”,而在于“作为规则解释器和过滤器”。它训练完后可以直接导出完整的 if-else 规则集,这些规则能帮你理解:到底什么样的市场状态下,什么样的特征组合,历史上更可能产生好的交易机会。比如我的某棵树训练完后,最重要的规则有三条:20 日波动率处于历史低位 + 均线多头排列 + 价格在 20 日区间高位时,未来 5 日上涨概率明显偏高;而波动率剧烈放大时,无论方向如何,模型输出都趋于 50% 以下——这从数据层面验证了“高波动期不该贸然开仓”的交易常识。这类规则拿来复盘自己的手工交易系统,或者拿来做风控过滤器(比如模型对当前行情的“不确定性”高时,主动降低仓位),比直接拿它做买卖信号要实用得多。
用回归树的预测分布做仓位管理也是好思路。真金白银的教训是:别指望决策树给你一个“圣杯式”买卖点,把它的输出当作一个“打分器”或“状态分类器”,叠加在自己原本的交易逻辑之上,才是它最合适的归宿。这个思路后来帮我规避了好几次不该开的仓——当树的预测概率长期低于 50% 时,说明当前行情根本不是它擅长的那种,空仓等待反而是最优解。
最后分享一个我后来一直在用的小技巧:训练完树之后,把clf.feature_importances_打印出来,按重要性做月度监控。如果某个月开始,重要性的排序剧烈变动,或者某个特征的重要性突然从 0.3 掉到 0.02,通常意味着市场风格在切换。这个时候别急着加仓,先静下来重新评估策略逻辑是不是还适应当前环境。这个习惯帮我少踩了很多震荡市的坑,也让我对“模型什么时候会失效”有了更具体的感知。