手上有一堆零散的日线数据想尽快验证一个关于股票趋势预测的想法最省事的路径往往不是从零搭一套 Python 工程而是打开 MATLAB 写一个几百行的脚本——我把它叫 gupiao.m。这个 MATLAB 股票趋势预测工具包不是高频交易系统也不承诺任何收益它做的事情非常具体读入一份行情表算出若干技术指标和统计特征构造未来一段时间的涨跌方向标签训练一个分类模型做滚动回测最后把资金曲线、混淆矩阵、特征重要性画成几张结果图。整套源码就是一个主脚本加六个辅助函数跑完自动落盘 PNG方便反复对比不同参数下的结果图。我最初写它纯粹是为了省时间。做量化研究的人都清楚一个想法从脑子里闪过到看到图上有没有信号中间隔着的全是脏活复权、对齐、缺失值、标签对齐、训练测试切分。这些东西每次重写一遍非常折磨人所以干脆固化成模板换数据只改配置。适合读这篇内容的人大概有三类一是刚接触量化、想找个能跑通的最小闭环练手的学生二是有 MATLAB 授权、懒得再装一套环境的研究人员三是想拿它做教学演示或者内部技术分享的从业者。下面我按数据、算法、代码、读图、排错的顺序把踩过的坑和最后的定稿方案完整拆一遍。1. 整体设计与技术选型这个工具包到底解决什么问题动手写代码之前最该想清楚的是边界。我见过太多人一上来就说我要预测股价然后花三个月做出来的东西既不能回测也不能解释最后不了了之。所以 gupiao.m 从第一版起就给自己划了三条线不预测具体价格点位只预测方向不做日内只用日线不做全市场选股只对单一标的或一小组标的做时序判断。这三条限制看起来是自我阉割实际上是让整个流程能在几百行代码里闭环的关键也是我后面所有设计取舍的出发点。1.1 需求拆解预测什么、不预测什么预测价格的具体数值是一个几乎不可能做好的任务。股价是非平稳序列均值和方差随时间漂移你今天拟合出来的回归系数下个月就失效了。所以工具包把问题降维成二分类或三分类未来 N 个交易日的累计收益是涨、跌、还是横盘。降维之后模型输出的不再是明天收盘 12.37 元这种容易被证伪的绝对值而是一个概率比如上涨概率 0.58这个概率还能画成分布图一眼看出模型是自信还是在瞎猜。这个拆解带来的第二个好处是可评估。方向预测可以用混淆矩阵、AUC、命中率来衡量这些指标的口径非常清楚不像价格回归那样容易被平均绝对误差看起来还行但其实全靠猜均值骗过去。工具包里我固定输出三个口径整体命中率、上涨类的精确率与召回率、以及扣除成本后的资金曲线。三个口径必须同时看只看其中一个必然误判。注意方向预测的准确率天花板很低。日线级别上能稳定做到 55% 以上就已经算有信息量任何声称 80% 以上命中率的日线模型第一反应应该是去查未来函数而不是高兴。1.2 技术选型为什么是 MATLAB 而不是别的选 MATLAB 的理由很实在不是情怀。第一这个工具包处理的是时间 × 特征的二维表本质就是矩阵MATLAB 的 table 和 timetable 类型对时间索引、重采样、对齐的支持非常顺手一行synchronize或retime就能解决很多麻烦。第二绘图成本极低plot、histogram、confusionchart几行代码就能出能贴进汇报材料的图这对需要频繁看结果图的研究流程是巨大加成。第三统计与机器学习工具箱里现成的分类器够用逻辑回归、决策树集成、支持向量机都能直接调交叉验证和超参数搜索也有封装好的函数不用自己造轮子。第四脚本天然适合改一行跑一次的探索式工作不需要处理包管理和虚拟环境这对非软件工程背景的使用者友好得多。代价也要说清楚MATLAB 是商业软件没有授权就没法跑源码发给别人对方未必能执行部署成服务也不方便它更适合做研究原型而不是生产系统另外社区里现成的量化库远不如另一种主流语言丰富很多轮子得自己写。所以我的定位很明确gupiao.m 是研究工具跑通想法、验证信号、产出图表验证成功了再考虑用别的技术栈重写。1.3 工具包的整体结构与 gupiao.m 的职责边界最终的目录结构我调整过三次最后一次定稿是把配置、数据、特征、标签、回测、绘图彻底分开主脚本只负责编排流程不写具体逻辑。这样做的原因是探索阶段最常改的是特征和参数如果这些都塞在主脚本里改一次要翻两百行很容易改错地方。拆开之后改特征只需动一个文件回测逻辑完全不受影响出错的概率大幅下降。文件名职责改动频率gupiao.m主流程编排串起所有环节低config_init.m路径、窗口长度、阈值、成本参数高load_price.m读取行情、复权、对齐交易日中build_features.m生成技术指标与统计特征高make_labels.m构造前瞻收益与分类标签中walk_forward.m滚动训练与预测输出概率序列低plot_result.m统一出图并保存 PNG低职责边界划清之后还有一个隐性好处可复现。每次实验只要把 config_init.m 里的参数和随机种子记录下来配合固定的数据文件理论上任何一次结果图都能原样重现。我吃过亏——早期版本里随机种子没固定同一个配置跑两次结果差 3 个百分点我以为是数据问题查了大半天才发现是树模型每次随机抽样不同。从那以后种子固定写进配置成了铁律。2. 数据层脏活干不好后面全是白费这一章是我最想强调的部分因为它决定了后面所有工作的上限。我见过太多人把 80% 的精力花在调模型上数据只花 20%结果模型怎么调都不灵。真实情况恰恰相反日线数据里能挖出的信息本来就稀薄一点点处理不当——复权错了、停牌没对齐、除权跳空当成信号——就足以把本来微弱的信号彻底淹没。数据层的目标不是洗干净而是不制造假信号。2.1 字段口径与复权处理最小可用的字段集合只有七个日期、开盘、最高、最低、收盘、成交量、成交额。有这七个就能算出绝大部分常用指标。麻烦在于复权。原始行情在除权除息日会出现价格跳空比如某标的实施送转价格从 30 元直接变成 15 元如果你不做处理均线会瞬间跌破各种突破信号集体触发而这些全是假的。这里必须区分两种复权。前复权是以当前价格为基准向前调整历史价格好处是图形连续、当前价格与实际报价一致适合做可视化和近期分析缺点是每次出现新的除权历史序列会被整体重算导致你今天跑出的历史信号和上个月跑出的不一样回测结果无法复现。后复权是以历史某点为基准向后调整历史序列一旦生成就不再变化适合做严谨的滚动回测。我的选择是做回测和训练时用后复权画图给人看时用前复权两份数据分开存文件名后缀区分。注意成交量在复权时也要同步调整否则量价关系类特征比如量比、换手率在除权日前后会失真。经验做法是成交量按价格复权因子的倒数缩放很多人只调价格忘了调量这个坑非常隐蔽。2.2 交易日对齐、停牌与缺失值真实数据里一定会遇到停牌。停牌期间没有行情记录如果你直接按行号索引计算滚动窗口等于把停牌前后的日子硬拼在一起时间跨度被压缩指标全错。解决办法是构造一份完整的交易日历用 timetable 把每只标的重新索引到日历上缺失的日期显式标记为 NaN这样滚动窗口的时间跨度就永远是真实的交易日数。缺失值的填补要分情况。价格类的短缺口一到三天我允许前向填充因为停牌复牌后价格连续性大体还在超过五天的缺口我不填直接把该窗口标记为无效在训练时剔除。成交量类缺口一律不填用 NaN 表示当日无交易让模型自己学习这个状态——实际上无交易本身就是信息停牌前后往往伴随波动率变化。这条经验是我在对比实验里发现的把停牌日当成交量零处理比前向填充的模型表现稳定得多。另外还要处理涨跌停造成的不可成交。回测里如果信号触发在涨停当天实际是买不进去的把这种交易算进收益就等于自我欺骗。工具包里我用当日最高价等于最低价、且涨幅达到限制这一条件来做粗略判定命中就把该笔交易标记为不可执行并跳过。这个判定不完美但足以过滤掉大部分虚假收益。2.3 特征工程的务实做法特征的数量控制是新手最容易犯的错误。我刚做这件事的时候一口气写了六十多个特征什么都能算一点结果模型在训练集上 AUC 到 0.9测试集掉到 0.5典型的过拟合。后来把特征砍到二十个左右效果反而稳定。判断标准很简单这个特征背后有没有一个能说清楚的行为逻辑说得清就留说不清就砍。留下来的特征大致分四组。趋势组5/10/20/60 日均线及其相对位置、均线斜率动量组5/20 日收益率、相对强弱指标、MACD 柱状值波动组20 日已实现波动率、真实波幅均值、最高最低价区间宽度量能组成交量相对 20 日均量的比值、成交额变化率。每组三到六个总数控制在二十上下。注意所有需要标准化的特征比如收益率、成交量比值标准化参数必须只用训练窗口内的数据计算然后应用到测试窗口。如果在全样本上算一次均值和标准差再切分测试集的信息就泄漏进训练过程了这是最经典也最难自查的错误之一。3. 核心算法三条路线怎么选、参数怎么算算法部分我不想写成教科书重点讲取舍。这个工具包里实际上并存三条路线不是让它们互相打架而是让它们互为参照技术指标路线给可解释性统计模型给基线树模型给非线性能力。三条路线跑同一份数据、同一种标签结果图叠在一起看如果三条路线给出的信号高度一致可信度就高一些如果只有某一条特别亮眼大概率是过拟合。3.1 技术指标路线先有一个能说人话的基线技术指标路线的价值不在于赚钱而在于提供一个说人话的基线。均线金叉、MACD 柱由负转正、相对强弱指标从超卖区回升这些东西拿给任何人看都能理解而且不需要训练。工具包里我把它们组合成一个简单的评分三个条件各贡献一分总分大于等于二就给出看多信号。这个评分不做任何优化参数就用最常见的设置故意保持朴素。为什么要刻意保持朴素因为它是检验复杂模型的标尺。如果花了两天调出来的机器学习模型回测表现还不如一条 20 日均线那基本可以断定模型学的是噪声而不是规律。我遇到过一次某个加了十二个特征的模型年化比均线策略还低两个点一开始以为是自己代码有 bug后来把特征重要性打出来看发现排前两位的全是滞后一期的收盘价衍生物等于模型在抄昨天的答案。纯指标策略也有明显的短板。它的规则是硬的一旦市场结构变化就整体失效它无法输出概率只能给二值信号不利于做仓位管理参数比如均线周期稍有改动结果差异很大说明它对参数敏感。所以它只能作为基线不能作为最终方案。3.2 统计与机器学习路线从线性到树模型统计路线的第一步是逻辑回归。它的好处是系数可解释正负号和大小直接告诉你某个特征在往哪个方向推。我一般先把全部特征标准化后跑一遍逻辑回归看系数符号是否符合常识如果波动率特征的系数是正的意味着波动越大越看涨那就要回头检查标签构造是不是有问题因为在多数市场状态下波动放大伴随的是下行风险。第二步上树模型。决策树集成方法对特征的非线性关系和交互效应捕捉得更好也不太需要精细的标准化代价是可解释性下降只能靠特征重要性图来找补。工具包里我限制树的最大深度在 3 到 4 层叶子节点最小样本数不低于 30目的就是压住过拟合。这两条限制看起来保守但实测下来放开深度的收益几乎全在训练集上测试集反而更差。至于时间序列专用的模型我也试过。自回归类方法在纯价格序列上的短期拟合能力不差但它对平稳性的要求很严日线价格本身不平稳做一阶差分之后噪声又太大实际用下来不如直接把滞后收益率当特征喂给分类器来得省事。所以最终版本里自回归模型只作为一条对照线保留不参与主流程。3.3 标签设计把趋势翻译成可学习的目标标签设计是整个工具包里最需要琢磨的一环。定义太松正负样本混在一起模型学不到东西定义太紧样本量骤减又容易过拟合。我用的是三段式先算未来 N 个交易日的累计收益再设定一个阈值带收益高于正阈值为上涨类低于负阈值为下跌类中间的不参与训练。阈值怎么定不能拍脑袋。我的做法是用过去 250 个交易日的日收益率标准差估计当前波动水平再按 √N 缩放得到 N 日累计收益的标准差阈值取这个标准差的一半。举例说明假设某标的近一年日收益率标准差为 1.8%N 取 20那么 20 日累计收益的标准差约等于 1.8% × √20 ≈ 8.0%。阈值取一半就是 ±4.0%。用正态近似估算落在 ±0.5 个标准差内的概率大约是 38%也就是说上涨类约占 31%下跌类约占 31%中间类约 38%。这个比例相对平衡既保留了足够的训练样本又让类别之间有明显区分。这个设计有一个重要的隐含假设波动率是慢变的。在绝大多数时段这个假设成立但在波动率骤升的阶段比如市场情绪剧烈变化固定窗口估计的阈值会偏小导致大量样本被划入极端类。所以工具包里加了一层保护如果近 20 日的已实现波动率超过近 250 日均值的 1.8 倍就暂停开新仓只做预测不做交易。这条规则不是最优的但它挡住了几次明显的失效。3.4 滚动窗口与样本量估算训练测试切分绝对不能随机打乱。时间序列打乱之后模型在训练时见过未来测试集的表现会虚高到离谱。我采用严格的滚动前推用第 1 到第 500 个交易日训练预测第 501 到第 520 个交易日窗口整体前移 20 天再用第 21 到第 520 天训练预测第 521 到第 540 天如此循环。样本量要提前算清楚。五年日线大约 1215 个交易日减去标签前瞻损失的 20 天可用样本约 1195 个。训练窗 500 天每折前进 20 天大约能滚 34 折累计得到约 680 个样本外预测点。这个数量对于评估一个模型来说是够的但要注意这 680 个预测点之间高度相关因为它们共享重叠的训练数据和相邻的标签窗口所以有效样本量远小于 680统计显著性会比表面看起来弱得多。注意二十个特征、五百个训练样本样本参数比约 25 比 1。这个比例对线性模型勉强够用对树模型就得靠深度限制和叶子最小样本数来约束。如果你想把特征加到四十个训练窗至少要拉到一千天否则别指望样本外表现。4. gupiao.m 的实操流程与关键代码前面讲的是设计这一章讲怎么落地。我按主脚本的执行顺序走一遍把关键的几段代码贴出来并说明每段为什么这么写。代码风格上我刻意保持朴素不用任何高级语法糖目的是让别人拿到源码能直接读懂、直接改。所有函数名和变量名都用英文注释用中文这个混搭方式在团队里接受度最高。4.1 主流程骨架把七步串成一条线主脚本只做编排不写业务逻辑。它的结构是一条直线初始化配置、读数据、建特征、造标签、滚动回测、算指标、出图。我特意把它写成直线而不是包成一个大函数是因为探索阶段需要频繁在中间打断点看变量包成函数之后调试会很不方便。% gupiao.m clc; clear; close all; rng(20240101); % 固定随机种子保证可复现 cfg config_init(); % 路径、窗口、阈值、成本 raw load_price(cfg); % 读行情 复权 交易日对齐 feat build_features(raw, cfg); lab make_labels(raw, cfg); res walk_forward(feat, lab, cfg); fprintf(样本外命中率: %.2f%%\n, res.hitRate * 100); fprintf(上涨类精确率: %.2f%%, 召回率: %.2f%%\n, ... res.precUp * 100, res.recUp * 100); plot_result(res, cfg); % 统一出图自动存 PNG这段代码里唯一值得多说一句的是rng。放在最前面而不是函数内部是因为只要放一次整个会话的随机性就全都固定住了如果放在函数里不同函数调用顺序变化还是会引入差异。4.2 数据读取与指标计算读取环节我要求所有数据源都先转成统一的七列格式字段名固定为 date、open、high、low、close、volume、amount日期统一转成 datetime 类型并设为 timetable 的时间索引。这一步看似繁琐实际上是整个工具包可移植性的基础——换数据源只改前面的转换代码后面全部不动。function raw load_price(cfg) t readtable(cfg.file); t.date datetime(t.date, InputFormat, yyyy-MM-dd); t sortrows(t, date); tt table2timetable(t(:, 2:end), RowTimes, t.date); % 交易日历对齐缺失日期显式置 NaN fullCal (cfg.calStart : caldays(1) : cfg.calEnd); fullCal fullCal(isbusday(fullCal)); % 剔除周末节假日由数据本身决定 tt retime(tt, fullCal, fillwithmissing); % 价格短缺口前向填充长缺口保留 NaN raw.tt fillmissing(tt, previous, MaxGap, caldays(3)); raw.ret [NaN; diff(log(raw.tt.close))]; raw.valid ~isnan(raw.tt.close) ~isnan(raw.ret); endfillmissing的MaxGap参数是关键。不加这个限制前向填充会把长达半年的停牌期全部填成同一个价格波动率特征直接变成零模型会误以为这段时间极度稳定。加上三天限制之后长缺口保持 NaN后面在特征构造阶段用valid掩码整体剔除。指标计算我全部手写没有用现成的技术分析工具箱函数。原因是手写版本能精确控制 NaN 的处理方式而且窗口长度是变量方便做参数敏感性测试。以均线为例用movmean一行就能算但要注意窗口不足时的输出是 NaN必须在前瞻对齐之前处理干净。4.3 特征与标签的构建特征构建函数返回的是一张与 timetable 行对齐的矩阵每一行对应一个交易日每一列对应一个特征。标签则是一个分类向量取值 1 表示上涨、-1 表示下跌、0 表示中性不参与训练。两者必须严格对齐到同一时间索引否则会出现用未来特征预测过去这种致命错误。function feat build_features(raw, cfg) c raw.tt.close; v raw.tt.volume; f struct(); for k [5 10 20 60] f.(sprintf(ma%d, k)) movmean(c, k) ./ c - 1; end f.mom5 c ./ circshift(c, 5) - 1; f.mom20 c ./ circshift(c, 20) - 1; f.vol20 movstd(raw.ret, 20, omitnan) * sqrt(252); f.vr20 v ./ movmean(v, 20, omitnan); f.range (raw.tt.high - raw.tt.low) ./ c; names fieldnames(f); M nan(height(raw.tt), numel(names)); for i 1:numel(names) M(:, i) f.(names{i}); end feat.X M; feat.names names; feat.idx raw.valid; end标签构造要注意两点。第一前瞻收益必须用未来 N 日的收盘价计算且最后 N 行没有完整标签要整行删除不能填零。第二阈值必须用滚动窗口估计不能用全样本的波动率否则又是一种信息泄漏。function lab make_labels(raw, cfg) n cfg.horizon; % 前瞻天数如 20 c raw.tt.close; fwd circshift(c, -n) ./ c - 1; % 未来 n 日累计收益 sigma movstd(raw.ret, 250, omitnan) * sqrt(n); thr 0.5 * sigma; % 阈值 半个标准差 y zeros(size(fwd)); y(fwd thr) 1; y(fwd -thr) -1; y(isnan(fwd) | isnan(thr)) NaN; lab.y y; end4.4 滚动回测这一段决定了结果可不可信滚动回测是整个工具包里代码最短但最容易写错的一段。核心约束只有一条任何在时点 t 做出的预测只能使用 t 及之前的信息。我把这条约束落实成一个循环每一折都重新训练、重新标准化、重新预测绝不跨折复用任何拟合结果。function res walk_forward(feat, lab, cfg) idx find(feat.idx ~isnan(lab.y)); X feat.X(idx, :); y lab.y(idx); nTr cfg.trainWin; nTe cfg.testWin; P nan(numel(y), 1); for s 1 : nTe : (numel(y) - nTr - nTe 1) tr s : s nTr - 1; te s nTr : s nTr nTe - 1; mu mean(X(tr, :), 1, omitnan); sg std(X(tr, :), 0, 1, omitnan); sg(sg 0 | isnan(sg)) 1; Xtr (X(tr, :) - mu) ./ sg; Xte (X(te, :) - mu) ./ sg; % 用训练窗口的参数标准化 mdl fitcensemble(Xtr, y(tr), Method, Bag, ... NumLearningCycles, 150, ... Learners, templateTree(MaxNumSplits, 12, ... MinLeafSize, 30)); [~, score] predict(mdl, Xte); P(te) score(:, 2); % 上涨类概率 end res.P P; res.y y; res.hitRate mean((P 0.5) (y 1), omitnan); end标准化那两行是重点。均值mu和标准差sg全部来自训练窗口然后原封不动地应用到测试窗口。如果图省事在循环外面用全样本算一次测试集的信息就漏进训练了模型在样本外会显得异常准而你真金白银照做的时候会发现完全不是那么回事。4.5 结果图每张图都要能回答一个问题绘图我统一收在 plot_result.m 里固定输出四张图资金曲线对比图、预测概率分布直方图、混淆矩阵、特征重要性条形图。每张图都对应一个具体问题——策略有没有超额、模型是自信还是模糊、错在哪一类、靠什么在做判断。四张图缺一张判断就会偏。function plot_result(res, cfg) fig1 figure(Visible, off); plot(res.equity, LineWidth, 1.4); hold on; plot(res.bench, LineWidth, 1.4); legend({策略, 基准}, Location, northwest); title(样本外累计净值含交易成本); exportgraphics(fig1, fullfile(cfg.outDir, equity.png), Resolution, 200); fig2 figure(Visible, off); histogram(res.P(res.y 1), 20, Normalization, probability); hold on; histogram(res.P(res.y -1), 20, Normalization, probability); legend({真实上涨, 真实下跌}); title(预测概率分布); exportgraphics(fig2, fullfile(cfg.outDir, probdist.png), Resolution, 200); end一个小细节Visible,off加上exportgraphics可以在不弹窗的情况下批量出图适合在服务器上跑长任务。早期我用saveas缺点是分辨率不可控插进文档里全是锯齿后来统一换成exportgraphics并固定 200 dpi出图质量稳定多了。5. 结果图怎么读别被一条漂亮的曲线骗了结果图是工具包的输出也是最容易误读的部分。我见过有人看到资金曲线一路向上就兴奋完全没注意到同期基准涨得更多也见过有人盯着 62% 的命中率沾沾自喜却没发现样本里本来就有 58% 是上涨类。读图需要一套固定的对照逻辑我把自己的检查顺序整理成下面这几步每次跑完新配置都按这个顺序过一遍。5.1 资金曲线与概率分布的正确读法资金曲线的第一眼不是看斜率而是看两条线的相对位置和回撤结构。如果策略曲线长期在基准下方说明它连什么都不做都没跑赢问题多半出在交易成本或者信号过于频繁如果策略曲线在基准上方但回撤更深说明它在放大风险暴露需要检查仓位规则。我通常会额外算两个数年化超额和最大回撤比两者一起看单看任何一个都会误判。概率分布直方图是我最喜欢的一张图。理想情况下真实上涨样本的预测概率分布应该整体偏向右侧真实下跌样本偏左两条分布有可见的重叠区但峰位分开。如果两条分布几乎完全重合说明模型什么都没学到特征和标签之间没有可利用的关系如果两条分布分得很开但峰位贴在最两端大量 0.0 和 1.0那要警惕过拟合这种极端自信在样本外几乎必然崩掉。还有一种情况值得单独说概率分布的双峰变成单峰且集中在 0.5 附近。这不是模型不行而是特征的信息量确实已经耗尽此时正确的做法是承认现实、回退到简单基线而不是继续加特征或者调参数那只会让你在训练集上越走越远。5.2 混淆矩阵与特征重要性暴露的问题混淆矩阵要按行看也就是看真实类别的召回情况。如果真实上涨的样本只有三成被识别出来而真实下跌的样本有八成被识别说明模型严重偏向看空这在下跌市里表现不错一旦市场转向就会连续打脸。反之亦然。两侧召回率的差距超过 25 个百分点就该检查标签的类别比例是不是失衡了或者阈值设置是否偏向一边。特征重要性的作用是查偷懒。正常情况下排在前面的应该是动量、波动这类有明确含义的特征且各特征的贡献分散没有哪一个占据绝对主导。如果某个特征的重要性超过 40%或者排在首位的是某个滞后价格衍生量那基本可以确定模型在走捷径这时候要么删掉这个特征重跑要么直接换一套特征体系。我踩过这个坑某次结果特别好一查重要性发现排第一的是收盘价相对 60 日均线的偏离而这个特征和标签在构造上有时间重叠等于把答案抄进去了。现象大概率原因处理方向训练集 AUC 0.9测试集 0.5过拟合或信息泄漏删特征、加正则、检查标准化范围两侧召回率差距超 25%类别失衡或阈值偏向调整阈值带宽度、改类别权重单一特征重要性超 40%特征与标签存在时间重叠检查该特征的窗口是否跨越标签期资金曲线平滑但贴地信号过于频繁成本吞噬收益加最小持仓期、提高开仓阈值概率分布全挤在 0.5特征信息量耗尽回退基线换特征体系而非调参5.3 回测里那些看不见的坑未来函数是头号杀手而且形式极多。最常见的一种是用当日收盘价算出指标然后假设当日收盘价成交。现实中你不可能在收盘的同一瞬间既知道收盘价又完成交易。工具包里我把成交价统一设为信号出现后的下一个交易日开盘价这个改动让很多策略的收益直接腰斩但它是真实的。幸存者偏差同样致命。如果你只用现在还活着的标的做回测那些中途退市、长期停牌的样本全被剔除了结果天然乐观。工具包的设计是单标的时序回测天然规避了这个问题但如果你打算扩展到多标的务必把历史全样本含已退出的都纳入。交易成本别省。佣金、印花税、滑点三项加起来在日线级别上通常要按单边千分之一到千分之二估算。我做过对比一个换手率极高的策略在零成本假设下年化超额 8 个百分点加上成本之后变成 -1 个百分点。成本参数的默认值我设在偏保守的一端宁可低估收益也不要高估。注意任何在样本外表现远超预期的配置第一件事是跑一次标签打乱对照实验——把标签随机重排后重新训练如果模型还能得到接近的命中率那说明你的评估流程里有结构性漏洞而不是模型厉害。6. 常见问题与排查速查表写到这里把这两年攒下来的报错和症状整理成一张表遇到问题先查表再动手。大部分问题其实反复就那么几个只是每次换了个面孔出现。下面分代码层和模型层两块另外补几条只能在实战里学到的经验。6.1 代码层面的典型报错报错或现象原因处理办法Undefined function isbusday未安装金融工具箱自己写工作日判断剔除周六日即可timetable 对齐后行数变多交易日历包含非交易日用数据自身日期构造日历别硬编码circshift结果时间错位位移方向与时间顺序相反明确约定数据按时间升序位移用负值取未来绘图中文变方框字体不支持指定系统内存在的字族或统一用英文标签predict输出列数不符类别编码与预期顺序不同显式读取模型类别属性按类别名取对应列内存暴涨timetable 未裁剪、表格复制过多只保留用到的时间段和列中间结果及时 clearpredict那一条我吃过不小的亏。二分类模型的评分矩阵列顺序是按类别排序来的如果标签是 1 和 -1那么第一列对应 -1、第二列对应 1这个顺序和直觉相反。早期我直接取第一列当上涨概率结果整个信号方向是反的资金曲线竟然是完美的反向指标。从那以后取列之前一定先读一遍模型类别按名字映射不再靠位置猜。6.2 模型层面的典型症状训练集和测试集表现差距过大八成是过拟合处理顺序是先砍特征再加深深度限制再提高叶子最小样本数最后才考虑加正则。这个顺序很重要因为正则只能压住参数幅度压不住特征本身携带的噪声。样本外结果时好时坏、波动极大通常是折数太少或者测试窗太短。测试窗设成 5 天每折只有 5 个预测点随机性主导一切。我的经验是测试窗至少 20 天累计样本外点不少于 300 个再谈结论。模型在某些时段集体失效、之后又恢复正常多半是市场结构变了不是模型坏了。这时候不要急着重新训练先看那段时期的波动率有没有异常放大。我加的那条波动率暂停规则就是为了应对这种情况代价是错过了部分反弹但避免了几次连续亏损。6.3 几条只能靠实战攒出来的心得第一条先跑通再加料。第一版代码我建议只用均线和动量两组特征、一个逻辑回归把整条流水线跑通看到一张正常的资金曲线再去加特征加模型。反过来做的话一旦结果不对你根本分不清是数据错了、特征错了还是模型错了。第二条把每次实验的参数和结果记在一个表里。我用的是一个简单的文本记录每行写日期、特征版本、窗口长度、阈值、命中率、年化超额累计记了两百多行。回头看的时候你会发现真正有效的改动其实只有那么两三次其余全是原地打转这份记录能帮你快速识别哪些方向不值得再试。第三条控制住自己的调参冲动。同一个配置反复试不同阈值本质上就是在测试集上做选择试到第一百次总能找到一个好看的但那个结果没有任何泛化意义。我给自己定的规矩是任何参数调整必须有逻辑理由不能因为刚好这次跑得好就定下来。第四条接受这个工具包的天花板。它是个研究原型用来验证想法、产出图表、支撑讨论不是印钞机。我个人在实际操作中的体会是价值最大的产出往往不是那个最终策略而是过程中发现的数据问题——比如某段历史的复权因子明显异常或者某个特征在特定市况下完全失效。这些发现比一个好看的净值曲线有用得多它们才是能沉淀下来的东西。最后分享一个小技巧每次定稿一个版本就把当次的配置、数据和四张结果图打包存一份快照。我吃过因为没有留快照、三个月后想复现某个结果却怎么都跑不出来的亏。存快照的成本几乎为零但它在你想回头对比到底是哪一步改动让效果变好的时候能救你一整天。