当脚本算出一个"物理上不可能"的数字——比如收益率 −100%、t 值 −26——你会先怀疑什么?数据脏了?还是代码写错了?这篇文章的核心观点很短:先怀疑代码。
现象
回测收益率里出现了 −100% 这种不可能值,以及荒谬到离谱的统计量:
最大回撤: -1.00 (-100%,意味着本金全部归零)
t-statistic: -26
−100% 意味着"持仓价值归零",而这只标的明明还在正常交易;t = −26 则说明检验结果"极端显著地反向"——这些都是不该出现的数值。它们共同的特征是:从业务常识上说不通。
根因
这类"不可能的数值"几乎总是来自四类代码错误。逐个说清楚:
第一类:公式多减了一次 1。 把涨跌幅公式写成了多减一次的形式:
# 正确
ret = (p1 / p2 - 1) * 100
# 错误:多减了一个 1
ret = (p1 / p2 - 1 - 1) * 100
当 p1/p2 接近 1 时,这个多减的 1 会让收益率直接逼近 −100%——那个"不可能值"就是这么来的。
第二类:循环变量复用。 两层循环里,内层循环不小心用了外层残留的循环变量,或者反过来:
for i in range(len(prices)):
for j in range(len(volumes)):
# 错误:本意用 j,却写成了 i
total += prices[i] * volumes[i]
第二个循环里用了上一个循环残留的变量,结果每次都在用同一个值,统计量自然错得离谱。给循环变量起互不冲突、有含义的名字(比如 i_price / j_vol),能有效避免。
第三类:键错位。 一侧按 A 键分组、对照组按 B 键分组,两边窗口没对齐。比如信号组按"信号日"对齐,对照组却按"自然日"对齐,两个窗口错开几天,算出来的差值毫无意义。
第四类:日历混用。 把很久以前的日期带进了当前窗口——比如某处用了"上一个交易日"的逻辑,但没判断节假日,于是一个跨假期的窗口把几天甚至几周前的数据也算了进来。
解决
针对四类问题,逐个修正:
# 1) 修正公式(去掉多余的一次减 1)
ret = (p1 / p2 - 1) * 100
# 2) 循环变量改为语义化的独立命名
for idx_sig, sig in enumerate(signals):
for idx_ref, ref in enumerate(references):
total += sig.price * ref.volume
# 3) 统一键的口径:两侧都按"交易日"对齐
sig_by_day = signals.groupby("trade_date")
ref_by_day = references.groupby("trade_date")
# 4) 用统一的交易日历有效集再统计
valid_days = calendar.intersection(set(sig_by_day.groups))
除了修代码,还要建立一个数值合理性断言的习惯,把这些"不可能的数值"拦在报告生成之前:
assert df["ret"].min() > -0.21, "收益率超出单日理论下限"
assert df["ret"].max() < 0.21, "收益率超出单日理论上限"
assert abs(t_stat) < 20, "t 值异常,检查样本独立性或键对齐"
延伸与预防
一句话:出现"不可能的数值"时,先怀疑代码,而不是先怀疑数据。 数据脏了通常是"局部离谱",而"整批结果都违背常识"往往是代码的系统性错误。
几个可操作的预防措施:
- 给关键指标设合理区间断言,越界即报错,别让错误值流到报告里;
- 循环变量语义化命名,两层以上循环尤其要注意;
- 两侧对比的键必须显式统一,宁可在代码里多写一行对齐逻辑,也不要有"默认对齐"的心照不宣;
- 日历用统一的有效交易日集,所有窗口裁剪都基于它。
这类 bug 的特点是"数学上合法、业务上荒谬"。养成对荒谬值的敏感度,并且第一时间往代码里找原因,能省下大量在数据上瞎找的时间。