自己搭一个最小回测:从数据到净值
读完你会知道
- 最小回测只需要六个环节:数据、信号、仓位、成交、成本、净值。
- 成交必须发生在决策之后的下一交易日,并检查停牌和涨跌停。
- 先跑通一个简单策略,再谈参数优化和因子组合。
回测框架听起来很重,其实最小版本可以很短:读数据、算信号、定仓位、模拟成交、扣成本、记净值。真正难的不是写循环,而是让每个环节都符合交易约束。
下面用一个双均线例子说明结构。代码只是伪代码,重点是顺序和约束。
一、六个环节
- 数据:对齐交易日历,处理复权、停牌和缺失值。
- 信号:只用当日及之前的数据,生成买入、卖出或持有。
- 仓位:把信号转成目标仓位,决定用多少资金。
- 成交:在下一交易日按可成交价格执行,检查涨跌停和停牌。
- 成本:扣佣金、印花税、滑点,必要时加冲击成本。
- 净值:每日按收盘价重估持仓,记录现金、市值和净值。
顺序不能乱。先决定再成交,成交之后才算成本,最后才更新净值。任何一步提前或延后,都会引入前视偏差或高估收益。
二、最小伪代码
下面是一个简化到不能再简化的循环:
for t in days:
signal = ma_fast[t] > ma_slow[t] # 只用 t 及之前的数据
target = 1.0 if signal else 0.0
order = target - position[t-1]
if abs(order) > tolerance:
price = next_open[t+1] # 下一交易日成交
if tradable(t+1):
fill(order, price, cost(t+1))
equity[t] = cash[t] + shares[t] * close[t]
这段伪代码里最重要的两行是「下一交易日成交」和「tradable 检查」。少了任何一个,回测都会偏乐观。
三、T+1 与成交约束
A 股股票当日买入次日才能卖出,回测必须遵守。除此之外,还要检查:当天是否停牌、是否一字涨停买不进、是否一字跌停卖不出、成交量是否足够。处理不了时,选择顺延或放弃,不要假装成交。
| 约束 | 错误做法 | 正确做法 |
|---|---|---|
| 决策与成交 | 当日收盘决策当日成交 | 下一交易日成交 |
| T+1 | 当天买当天卖 | 至少持有到下一交易日 |
| 一字板 | 按收盘价成交 | 视为不可成交 |
| 停牌 | 沿用上一日价格 | 顺延或放弃 |
四、成本怎么加
最低限度要加佣金和卖出印花税,再加一点滑点。小资金容易忽略最低佣金:一笔交易金额太小,实际费率可能远高于名义费率。高换手策略还要考虑冲击成本,否则回测会系统性高估收益。
一个实用做法是把成本设为可调参数,跑 0 倍、1 倍、2 倍成本,看策略收益对成本有多敏感。如果 1 倍成本就把超额收益吃光,实盘很难做。
五、净值与指标
净值等于现金加持仓市值。每天记录净值后,再算年化收益、最大回撤、夏普比率和换手率。指标要和基准比,也要和不同成本假设比。只看一条漂亮的净值曲线,说明不了策略有效。
还要检查持仓数量、单票权重和交易频率。一个回测如果一年只交易两次、持仓五只,却宣称年化 30%,先怀疑数据而不是相信策略。
先固定一个简单策略,把数据、成交、成本和净值跑通,再谈参数优化。最小版本的正确性比复杂版本的收益率更重要。等你能解释每一笔成交为什么发生、成本从哪里扣、净值为什么变化,再往上加因子和风控,才不会在错误的底座上堆功能。
最后,把回测结果和实盘差距当成常态。回测是排除错误假设的工具,不是收益预告。
六、常见错误
- 当日决策当日成交。用了收盘价,又假设能在收盘前成交。
- 忽略停牌和涨跌停。在不可成交的价格上成交。
- 不扣成本。高换手策略的收益被严重高估。
- 参数调到最优。在同一段数据上反复试参数,等于过拟合。
- 没有样本外。训练集表现好,不代表未来有效。
数据与口径
- 伪代码省略了分红、拆股和部分风控逻辑,只用于说明最小结构。
- A 股交易规则以交易所公告为准;不同板块的涨跌停和 T+1 规则可能不同。
- 本文不构成任何策略推荐或投资建议。