前面四十六篇,每个坑都在对应章节的「常见坑」小节里出现过一次。但坑这种东西,分散讲一遍和集中列一遍,记忆效果完全不同——就像体检报告单,分项看都是小事,汇总看才知道哪里亮了红灯。本篇把全系列的坑集中成一份「血泪清单」,按「数据 → 回测 → 策略 → 工程 → 实盘」的生命周期排序,每条给出现象(你怎么发现踩坑了)、原因(为什么会这样)、避免方法(下次怎么做)。建议打印贴在显示器旁边。
本篇目标
- 能逐条核对自己当前的量化流程,找出已经踩过或正在踩的坑
- 能把每条坑回指到合集里对应的篇章,知道去哪里找系统解法
- 能在代码评审时(自己的或别人的)快速定位「这代码有没有坑」
- 能理解坑的共同根源:数据、时间、成本、人性四类系统性来源
核心概念
坑的四大根源
把 20 条坑摊开看,绝大多数源自四类系统性问题:
数据类:数据本身错了(复权、幸存者偏差、缺失)——地基歪了楼必塌; 时间类:未来函数、信号与成交时序错位——用到了不该知道的信息; 成本类:手续费、滑点、换手被低估——利润被摩擦力吃光; 人性类:过拟合、频繁调参、情绪干预——聪明反被聪明误。
划重点:新手踩坑的顺序往往是「先踩时间类(回测爆赚实则未来函数),再踩成本类(上实盘发现利润没了),然后踩数据类(换个数据源结果全变),最后一直在踩人性类」。提前知道顺序,至少每一类坑只踩一次。
怎么用这份清单
三个使用场景:写完回测后,从第 1 条过到第 20 条,每条问自己「我的代码有没有这个问题」;评审他人代码时,按数据→时间→成本顺序快速扫一遍;上实盘前,配合第 46 篇的检查清单做双重核对。清单不保证你不再踩坑,但能保证同一个坑不踩第二遍。
完整清单:20 个坑
数据篇(坑 1–5)
1. 未来函数(详解见第 17、18 篇)
现象:回测年化高得离谱(50%+),资金曲线几乎只涨不跌。原因:当日收盘后才确定的信号被用于当日成交,或指标窗口里混入了未来数据。避免:信号一律 shift(1);写代码时自问「这一行在当年那天,真的算得出来吗」。
2. 幸存者偏差(第 17 篇) 现象:回测只用了「今天还活着」的股票池,退市股全缺席,收益系统性虚高。原因:多数免费数据源只提供当前上市股票。避免:意识优先——A 股三十年退市数以百计,样本含退市股才接近真实;教学阶段至少在结论里注明此局限。
3. 复权价格用错(第 7、8 篇) 现象:回测里某天价格暴跌 40%,但当天并没有跌停。原因:用了未复权或后复权混用的价格,分红送转日出现价格跳空。避免:策略回测统一用前复权(或后复权,但全流程一致);真实下单时切换回真实价格。
4. 停牌与涨跌停处理缺失(第 17、20 篇) 现象:回测在涨停日「买入成功」,实盘根本排不到队。原因:向量化回测默认任何价格都能成交。避免:加涨停不买、跌停不卖的过滤规则;停牌日用前收盘价记账并冻结交易。
5. 数据源单点依赖(第 42、43 篇) 现象:接口改版或限流,某天数据悄悄缺失,策略用残缺数据照常出信号。原因:没有校验与降级。避免:数据管道三件套——行数校验、日期连续性检查、备用源降级。
回测篇(坑 6–10)
6. 成交价口径混乱(第 18、20 篇) 现象:回测用收盘价成交,实盘下单一开盘就滑出一个点。原因:回测撮合假设与实盘执行时点不一致。避免:统一口径——信号收盘算、次日开盘价成交是较保守的教学标准;并在实盘小资金阶段测量真实滑点。
7. 忽略手续费与滑点(第 22、23 篇) 现象:回测赚钱、实盘亏钱,差额正好等于成本。原因:高频调参下 0.1% 双边成本会累积成吞噬利润的大数。避免:回测必含成本(本系列统一双边 0.1%);换手率作为策略的常规输出指标。
8. 样本太短(第 25 篇) 现象:两年数据调出的参数,第三年失效。原因:样本没覆盖完整牛熊周期,参数只适配了单一市场状态。避免:回测区间至少一轮牛熊(A 股 5 年起步);报告结果时注明覆盖了哪些市场状态。
9. 过拟合(第 17、25、40 篇) 现象:样本内夏普 2.0、样本外 0.2。原因:参数自由度相对数据量太大,模型记住了噪声。避免:Walk-Forward 验证、参数平原选择、规则宁少勿多——三条防线缺一不可。
10. 只看收益不看回撤(第 19 篇) 现象:两条策略年化都是 12%,一条最大回撤 −8%、另一条 −35%,新手只看年化。原因:回撤决定你能不能拿得住策略。避免:年化、夏普、最大回撤、Calmar 四件套一起看;回撤超过心理承受线(如 20%)的策略再赚钱也别碰。
策略篇(坑 11–14)
11. 频繁调参(第 25、44 篇) 现象:每周末「优化」一次参数,回测越来越好,实盘越来越差。原因:每轮调参都是一次过拟合采样。避免:参数冻结制度——用 git commit 锁定参数(第 44 篇),改参数要走「分支实验 → Walk-Forward → 评审」流程。
12. 策略过多且互相重叠(第 36 篇) 现象:组合里五个策略曲线高度相似,以为分散了,熊市一起亏。原因:多数自研策略共享同一风险因子(如都是 A 股 beta)。避免:策略层面也做相关性分析(第 36 篇),重叠的策略留一个最好的即可。
13. 用趋势策略的止损管均值回归策略(第 35 篇) 现象:回归策略一入场就被止损扫出,胜率惨跌。原因:趋势止损距离对回归策略太紧——回归策略的价值恰恰在「先亏后赚」。避免:退出规则匹配策略类型:回归看偏离度、趋势看结构破位。
14. 相信「圣杯」(第 49 篇) 现象:花大价钱买「胜率 90% 年化翻倍」的策略/课程。原因:如果它真存在,卖它不如用它。避免:任何不给出「样本外验证方法」的收益宣称都当故事听;回到本系列的方法论自己复现。
工程篇(坑 15–17)
15. 代码没版本管理(第 44 篇) 现象:这周回测比上周好,但说不清改了什么。原因:没有 git,改动无据可查。避免:最小工作流 init/add/commit;每次出报告先 commit,报告与版本绑定。
16. 数据没备份、模拟与实盘逻辑不一致(第 41、42 篇) 现象:硬盘一坏数据全没;纸面与实盘两套代码渐行渐远。原因:单点存储 + 逻辑复制而非复用。避免:数据进 git ignore 但有独立备份目录;策略逻辑单一来源(strategies/ 模块),回测/纸面/实盘只换执行层。
17. 实盘系统没有监控与对账(第 43、45 篇) 现象:策略挂了三天才发现,期间该平的仓没平。原因:没有心跳、告警、每日对账。避免:第 43 篇三件套(logging、心跳、告警)+ 第 45 篇的对账器,上实盘前缺一不可。
实盘篇(坑 18–20)
18. 情绪干预系统(第 46、48 篇) 现象:浮亏 5% 时手动砍仓、错过信号后手动追单,纪律失效。原因:实盘的亏损是真实的,人性反应接管了系统。避免:仓位定在自己「亏了也睡得着」的水平(第 48 篇);把「手动干预」本身设为需要书面记录的异常事件。
19. all-in 单一策略(第 36、48 篇) 现象:全部资金押一个策略,遇到它的专属熊市一无所有。原因:对策略的信任超过了对不确定性的敬畏。避免:单策略资金上限(如总资金 30%);用策略组合分散「策略风险」本身。
20. 裸奔实盘无风控(第 35、48 篇) 现象:亏损从 −5% 一路滑到 −40%,全程没有强制退出机制。原因:策略只有入场逻辑,没有退出与熔断。避免:三层风控铁三角——单笔止损、组合回撤熔断、最大仓位限制(第 35、48 篇),任何一层触发都必须是无条件执行。
完整代码(本篇的「代码」是清单的机械化:一份快速体检脚本)
# ---------- 策略代码的「坑位」快速体检 ----------
import re
from pathlib import Path
def pitfall_scan(path):
"""对策略源码做静态扫描,标出可疑模式(教学级,不追求完备)"""
code = Path(path).read_text(encoding="utf-8")
findings = []
# 坑1: 信号未shift就直接用( heuristic:有rolling/pct_change但全文无shift)
has_indicator = bool(re.search(r"rolling|ewm|pct_change", code))
has_shift = "shift(1)" in code or "shift(-1)" in code
if has_indicator and not has_shift:
findings.append("坑1嫌疑: 计算了指标但未见 shift —— 检查信号时序")
if "shift(-" in code and "label" not in code and "y" not in code:
findings.append("坑1嫌疑: shift(负数) 出现在特征侧 —— 未来数据?")
# 坑7: 无手续费
if "fee" not in code and "commission" not in code and "0.001" not in code:
findings.append("坑7嫌疑: 未见到费率变量 —— 回测含成本了吗?")
# 坑15: 报告与版本
if "commit" not in code and "version" not in code:
findings.append("坑15提示: 结果输出未绑定代码版本(第44篇)")
# 坑18: 硬编码的全仓
if re.search(r"all_in|全仓", code):
findings.append("坑19嫌疑: 出现全仓字样 —— 单笔仓位控制?")
if not findings:
findings.append("未发现明显坑位模式(不代表没有,人工复核仍必要)")
for f in findings:
print(f"[{path}] {f}")
return findings
# 用法示例(对第18篇的回测脚本跑一遍):
# pitfall_scan("posts/../backtest_ma.py")
示例输出(示意,对一个未加shift的旧脚本):
[old_backtest.py] 坑1嫌疑: 计算了指标但未见 shift —— 检查信号时序
[old_backtest.py] 坑7嫌疑: 未见到费率变量 —— 回测含成本了吗?
代码逐段解释
体检脚本的五组检查各对应一条坑:rolling|ewm|pct_change 与 shift 的共存检测抓坑 1(信号时序);费率变量缺失抓坑 7(回测成本);commit/version 缺失抓坑 15(版本绑定);全仓 字样抓坑 19(仓位控制)。它是启发式的——只能抓「模式缺失」级别的低级错误,抓不出逻辑性的坑(比如 shift 了但移错方向)。它的价值是提交前的最后一道机械检查:三秒钟,零成本。把它接进第 44 篇的工作流(commit 前自动跑),20 条清单里最机械的部分就自动化了。
常见坑与小练习
常见坑(本篇自身的使用之坑)
- 清单背了但不用:知道 20 条坑和避开 20 条坑之间隔着「每次回测前过一遍」的习惯。把它做成 checklist 文件放进项目,让流程替记忆干活。
- 只查自己想查的:自查时对某条坑「感觉没有」就跳过——每条都要有物证(代码行号、输出截图)。
- 用清单否定一切:查出一两条嫌疑就全盘推翻策略。先确认是不是真坑(很多是误报),再决定改不改。
小练习
- 把
pitfall_scan扩展出坑 4 的检测:找出代码里pct_change相邻是否有涨跌停过滤(如abs(ret) < 0.095),没有则报嫌疑。思路提示:与坑 7 的检测同构——检查特定模式的存在性;A 股主板 ±10%、创业板 ±20%,阈值可配置。
- 对你自己的某个历史回测脚本跑一遍 20 条人工核对,输出一份「坑位审计表」(坑号/是否存在/证据/整改动作)。
思路提示:表格用第 8 篇的方式存 CSV;「整改动作」空的行就是下一步的工作清单。
- 给 20 条坑按你自己踩过的频率排个序,找出你的「Top 3 个人坑」,并在对应的篇章里重做那一篇的练习。
思路提示:个人坑的分布暴露你的知识短板——数据弱的先补第 7/8 篇,时间概念弱的精读第 17 篇。
本文策略仅用于教学,不构成投资建议,实盘前请充分测试。