回测引擎最小模型:现金、持仓与成交
写出买卖规则以后,很多人会立刻问:“那到底赚不赚钱?”别急,先要有一台能按规则记账的机器。就像开小卖部,不是喊一句“今天卖得不错”就行,现金还剩多少、货架上还有多少货、每一笔买卖有没有记清,都要对得上。
这是什么
回测引擎,就是用程序模拟现金、持仓、成交和账户净值变化的一套记账机器。它不负责替你发明好策略,只负责按你写好的量化策略规则,一天一天往前走:该买就扣现金加持仓,该卖就减持仓加现金,每天算一次账户还值多少钱。
如果把量化策略规则比作“进货和清货的规矩”,回测引擎就是收银台加库存本。规矩再漂亮,收银台记错钱、库存本记错货,最后看到的盈利和亏损都不可信。
一个最小回测引擎至少要管四张账:
| 账本 | 记录什么 | 为什么重要 |
|---|---|---|
| cash | 现金还剩多少 | 没现金就不能继续买入 |
| position | 持仓数量和成本 | 不知道持仓,就不知道能不能卖 |
| trades | 每一笔成交 | 方便复盘买卖是否按规则发生 |
| equity | 账户净值 | 现金 + 持仓按当日价格估算后的总价值 |
经验
第一版回测引擎越小越好。先让一只教学样本股票、几天行情、几笔买卖完整跑通,再扩大范围。
为什么不能只看买卖信号
买卖信号只是“想做什么”,回测引擎要处理的是“做完以后账户变成什么”。这两个差得很远。
比如策略在 2024-01-10 给出买入信号,如果账户只有 1 万元现金,就不能按 5 万元去买;如果买入当天普通股票遵守 T+1,就不能假装当天又卖掉;如果已经满仓,再出现买入信号,也不能继续凭空加仓。
散户手动交易时,经常忽略这些细节,感觉“这里买、那里卖”好像挺顺。程序一旦认真记账,很多漂亮想法会露馅:现金不够、仓位超了、卖出日期不对、持仓数量对不上。所以最小回测引擎的存在意义,不是让曲线好看,而是把交易规则和仓位控制落到每一天的账户变化里。
用一笔买卖看清账怎么走
还是用贵州茅台(600519,白酒行业,上交所主板,2001 年 8 月 27 日上市)作为真实 A 股公司例子;下面数字是教学样本,不代表真实历史行情。
假设账户初始现金 10 万元,策略规定单只股票最多 20% 仓位。教学样本数据如下:
| date | code | close | signal | can_sell |
|---|---|---|---|---|
| 2024-01-10 | SAMPLE | 100.00 | buy | no |
| 2024-01-11 | SAMPLE | 102.00 | hold | yes |
| 2024-01-12 | SAMPLE | 98.00 | sell | yes |
这里为了展示记账流程,假设第一天的信号可在同日按 100.00 元成交;真实回测如果使用收盘后数据生成信号,通常应在下一交易日执行,避免偷看当天收盘价。
第一天出现买入信号,20% 仓位就是 2 万元。按 100.00 元买入,先不考虑额外成本,能买 200 股。账户变化是:现金从 10 万元变成 8 万元,持仓从 0 股变成 200 股。
第二天没有卖出信号,持仓不变。账户净值大约是 8 万元现金 + 200 股 × 102.00 元 = 10.04 万元。
第三天出现卖出信号,且 can_sell 是 yes,说明这笔持仓已经不是当天刚买入,可以卖。按 98.00 元卖出 200 股,现金变成 9.96 万元,持仓归零。这次从买入到卖出亏了 400 元。
这就是回测引擎最朴素的价值:它把“我觉得这里可以买卖”变成一串能核对的账。
最小代码长什么样
先别追求复杂结构。一个入门版可以把账户状态放在一个小字典里:
account = {
"cash": 100_000,
"shares": 0,
"buy_date": None,
}每天读一行行情和信号,然后按顺序处理:
def run_one_day(account, row):
price = row["close"]
if row["signal"] == "buy" and account["shares"] == 0:
budget = account["cash"] * 0.2
shares = int(budget // (price * 100)) * 100
account["cash"] -= shares * price
account["shares"] += shares
account["buy_date"] = row["date"]
elif row["signal"] == "sell" and account["shares"] > 0 and row["can_sell"]:
account["cash"] += account["shares"] * price
account["shares"] = 0
account["buy_date"] = None
equity = account["cash"] + account["shares"] * price
return equity这段代码故意很直白:先看现金,决定能买多少;再看持仓和 can_sell,决定能不能卖;最后用当天收盘价估算账户净值。这里把买入数量按 100 股整数倍取整,是为了更接近 A 股普通股票的买入习惯;更严格的版本还要预留交易费用,不能把每一分钱都假装能买成股票。
这里有三个关键点。
第一,买入要先扣现金。不能只记录“我买了”,却不减少现金。
第二,卖出要先检查持仓。没有持仓就不能卖,普通股票当天买入也不能假装当天卖出。
第三,每天都要算账户净值。只看最后一次卖出结果,会漏掉中间的浮盈、浮亏和回撤压力。
新手最容易踩的坑
第一个坑,是只记录信号,不记录成交。信号说买,不代表一定买得到;现金不够、仓位限制、交易规则,都可能让这笔买入不能发生。
第二个坑,是忘了 T+1。当天买入的普通股票,通常不能当天卖出。如果回测引擎不检查 can_sell,就可能做出真实交易里做不到的动作。
第三个坑,是账户净值算错。有人只看现金,忘了持仓还值钱;有人只看持仓市值,忘了现金也属于账户。正确的最小口径是:现金 + 持仓按当日价格估算的价值。
风险
方向性做法:第一版回测引擎建议先保守,宁愿少算一点机会,也不要让程序做出现实里无法成交的动作。适用条件是你在搭最小模型;反例是为了让结果更好看,放宽 T+1、忽略仓位、让现金变成负数;风险是你会把程序漏洞误认为策略能力。
大坑
不要让回测引擎“替你圆梦”。只要出现现金为负、没有持仓却能卖出、当天买当天卖普通股票这类情况,结果再漂亮也要先停下来查账。
总结
回测引擎是量化研究里的记账机器:它按量化策略规则处理买入、卖出和仓位,把现金、持仓、成交和账户净值一天天算清楚。第一版不用复杂,重点是账要对:买入扣现金,卖出减持仓,T+1 不乱来,账户净值每天能核对。
自检 3 问:
- 为什么只有买卖信号还不够,必须记录现金和持仓?
- 普通股票 T+1 会怎样影响回测引擎里的卖出判断?
- 账户净值为什么要同时包含现金和持仓价值?
股市有风险,入市需谨慎。本文不构成任何投资建议。
本章新学到:回测引擎。