回测策略的 copy-paste bug:sell 分支 in_buy=True 让信号死锁

本文最后更新于 2026年8月11日 凌晨

回测跑出来一条只涨不跌的净值曲线,胜率 100%,最大回撤 0%。

这不是圣杯,这是 bug。

策略里买入后该卖出的信号一次都没触发,仓位从开仓那天起就锁死在账户里。净值曲线”看起来”只涨不跌,只是因为它从来没结算过亏损。最讽刺的是,这个 bug 的源头只是一个 copy-paste:把 buy 分支的代码复制到 sell 分支,忘了把一个布尔标志从 True 改成 False

copy-paste 为什么这么危险

人在写第二段相似代码时,大脑会切到”自动驾驶”模式。第一段(buy 分支)是你逐字敲出来的,每行都过脑子。第二段(sell 分支)你只是复制、粘贴、改几个词。问题在于,你改了显眼的变量名,却漏了那个不起眼的状态标志。

心理学上叫”无意视盲”(inattentional blindness)——注意力聚焦在”改对函数名和方向”上,就看不见那个藏在第五行的 in_buy = True 应该改成 False

代码结构上更危险。copy-paste 出来的两段代码高度相似但语义相反,这种”对称结构”会让后续阅读者也产生惯性,扫一眼觉得”差不多对”就跳过了。

状态机不变量被悄悄打破

一个典型的回测策略本质上是个状态机:两个状态(空仓 flat、持仓 in_position),两个事件(买入信号、卖出信号),四条转移规则。

不变量只有一条:任何时候,持仓标志 in_position 必须与实际的持仓方向一致——有仓位时为 True,没仓位时为 False,不能停在中间态。

1
2
# 不变量:开仓次数 - 平仓次数 = 当前持仓状态
assert (buy_count - sell_count) == (1 if in_position else 0)

buy 分支正常工作时,状态流转是 flat → in_position。但如果 sell 分支因为 copy-paste 把守卫条件也复制了过去——本该 if in_position 却写成了 if not in_position——状态机就卡住了。持仓后遇到 SELL 信号,not in_position 为假,整个分支被跳过,卖出操作永远不执行,仓位从开仓那天起就锁死。

死锁的隐蔽性在于它不报错。没有异常,没有崩溃,策略安静地”持仓不动”。如果你只看净值曲线而不看交易明细,根本发现不了。

最小可复现的 Python 示例

把问题缩到最小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SimpleStrategy:
def __init__(self):
self.in_position = False
self.trades = []

def on_signal(self, signal, price):
if signal == "BUY" and not self.in_position:
self.in_position = True
self.trades.append(("BUY", price))

elif signal == "SELL" and not self.in_position:
# ← copy-paste 灾难现场
# 整行从 buy 分支复制过来,改了 signal 和 action,
# 但守卫条件里的 not self.in_position 没改,应该是 self.in_position
self.in_position = False
self.trades.append(("SELL", price))

跑一下回测,模拟一组 BUY-SELL-BUY-SELL 信号:

1
2
3
4
5
6
7
8
9
strat = SimpleStrategy()
signals = [("BUY", 100), ("SELL", 110), ("BUY", 105), ("SELL", 108)]

for signal, price in signals:
strat.on_signal(signal, price)

print(f"交易笔数: {len(strat.trades)}")
print(f"最终持仓状态: {strat.in_position}")
print(f"交易记录: {strat.trades}")

输出:

1
2
3
交易笔数: 1
最终持仓状态: True
交易记录: [('BUY', 100)]

四条信号只执行了第一条买入。SELL 信号在持仓时遇到守卫 not self.in_position,结果为假,整个 elif 分支被跳过——卖出操作根本没执行,状态也没翻转。整条策略链从第二个信号起就完全失效,却没有任何报错。

三道防线

第一道:状态断言

在每个状态转移操作后加一行断言,把不变量焊死在代码里:

1
2
3
4
5
6
7
8
9
10
11
12
13
def _check_invariant(self):
"""状态机不变量:交易计数与持仓状态一致"""
buys = sum(1 for s, _ in self.trades if s == "BUY")
sells = sum(1 for s, _ in self.trades if s == "SELL")
expected_position = (buys - sells) == 1
assert self.in_position == expected_position, \
f"状态冲突: in_position={self.in_position}, 但交易记录 implies {expected_position}"

def on_signal(self, signal, price):
if signal == "BUY" and not self.in_position:
self.in_position = True
self.trades.append(("BUY", price))
self._check_invariant() # ← 立刻捕获异常状态

copy-paste 把 in_position = True 带进 sell 分支后,这行断言会立刻在测试阶段炸出来,而不是让 bug 潜伏到实盘。

第二道:对称性检查

buy 和 sell 是镜像操作,用对称结构写,让两个分支的差异一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
def _enter_position(self, price):
self.in_position = True
self.trades.append(("BUY", price))

def _exit_position(self, price):
self.in_position = False # ← 与 _enter_position 严格对称
self.trades.append(("SELL", price))

def on_signal(self, signal, price):
if signal == "BUY" and not self.in_position:
self._enter_position(price)
elif signal == "SELL" and self.in_position:
self._exit_position(price)

把状态转移收敛到两个方法里,buy 和 sell 不再各写一份。copy-paste 的机会从”复制整个分支”降到了零。即使要改状态逻辑,也只需要改一个地方。

第三道:属性测试

属性测试(property-based testing)不预设具体用例,而是让框架自动生成大量随机信号序列,验证策略始终满足不变量。用 hypothesis 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from hypothesis import given, strategies as st, settings

@given(st.lists(
st.tuples(st.sampled_from(["BUY", "SELL"]), st.floats(1, 1000)),
max_size=50
))
@settings(max_examples=200)
def test_strategy_always_consistent(signals):
"""无论什么信号序列,策略状态机永远满足不变量"""
strat = SimpleStrategy()
for signal, price in signals:
strat.on_signal(signal, price)
# 不变量:持仓状态与交易记录一致
expected = len([s for s in strat.trades if s[0] == "BUY"]) == \
len([s for s in strat.trades if s[0] == "SELL"]) + (1 if strat.in_position else 0)
assert expected, f"状态不一致: 持仓={strat.in_position}, 交易={strat.trades}"

200 组随机信号序列里,只要有任何一组让 buy 次数和 sell 次数与持仓状态对不上,测试就挂。copy-paste 遗留的 bug 在这种暴力扫描下无处藏身。

交易明细才是真相

净值曲线会骗人,交易明细不会。

每次回测完,先看交易笔数。如果一轮回测只有一个 BUY 没有对应的 SELL,策略从一开始就死了。再看持仓时间分布,如果某个仓位从开仓一直拿到回测结束,那不是”长期持有策略”,那是卖出信号没生效。

最后核对状态终值。回测结束时 in_position 应该和最后一次交易方向一致——如果最后一笔是 BUY 但 in_position == False,或者反过来,状态机就出问题了。

把这三个检查写成回测报告的必检项,比任何花哨的风控逻辑都管用。

copy-paste 不可怕,可怕的是它制造的错误是”静默正确”——代码能跑,结果能出,只是结果全是假的。状态断言、对称结构、属性测试,三道防线把这种沉默的 bug 变成响亮的报错。


回测策略的 copy-paste bug:sell 分支 in_buy=True 让信号死锁
https://normdist.com/2026/08/11/ND-20260811-001-copy-paste-state-machine-deadlock/
作者
小瑞
发布于
2026年8月11日
许可协议