程序员量化交易实战 10:跑通第一个最小回测闭环
程序员量化交易实战 10:跑通第一个最小回测闭环
古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。这个专栏会把一个 A 股量化平台从 0 到 1 拆开写:数据、策略、回测、模拟盘、提醒和生产化,尽量用真实代码和真实运行结果说话。更完整的更新也会同步到微信公众号「字与码」。
前面几篇终于可以串起来了。
第 4 篇有 A 股订单规则,第 8 篇有清洗后的 K 线,第 9 篇有因子信号。第 10 篇做第一个最小回测闭环:看到 buy_watch 时买入,看到 risk_watch 时卖出,过程中记录现金、持仓、交易和权益曲线。

这不是完整回测引擎
先说边界。
这一篇的回测不是事件驱动引擎,也不处理撮合队列、分钟线、滑点模型和多标的组合优化。它只是一个可解释的最小闭环。
它要证明几件事:
- 清洗后的 K 线可以进入因子计算。
- 因子信号可以驱动买卖动作。
- 买入前会经过 A 股 100 股手和现金校验。
- 卖出会计算费用。
- 每天都有权益曲线。
- 回测结果可以通过测试复现。

回测结果对象
第 10 章新增 app/mini_backtest.py。
交易明细对象:
@dataclass(frozen=True)
class MiniBacktestTrade:
trade_date: date
side: str
price: float
shares: int
amount: float
fee: float
reason: str
回测结果对象:
@dataclass(frozen=True)
class MiniBacktestResult:
symbol: str
initial_cash: float
final_equity: float
cash: float
shares: int
total_return: float
max_drawdown: float
trades: tuple[MiniBacktestTrade, ...] = field(default_factory=tuple)
equity_curve: tuple[dict[str, object], ...] = field(default_factory=tuple)
reason 很重要。交易不是只有买卖方向,还要知道为什么买、为什么卖。后面做策略诊断时,这个字段会非常有用。
买入时接入 A 股规则
买入逻辑里没有直接扣钱,而是复用第 4 篇的 check_a_share_order():
check = check_a_share_order(
side="buy",
price=bar.close,
shares=raw_shares,
available_cash=cash,
)
if check.accepted:
cash = round(cash - check.amount - check.fee.total, 2)
shares += check.normalized_shares
这一步会处理 100 股手、手续费和现金不足。
如果这里绕过交易规则,回测里可能买出 37 股、买到负现金,最后收益看起来很好,但那不是 A 股可执行结果。
卖出也要扣费用
卖出时目前直接清仓,并计算卖出费用:
fee = estimate_a_share_fee(round(bar.close * shares, 2), "sell")
amount = round(bar.close * shares, 2)
cash = round(cash + amount - fee.total, 2)
第 4 篇里卖出费用包含佣金、过户费和印花税。这里直接复用,避免回测和模拟盘各写一套规则。
每天记录权益曲线
无论当天有没有交易,都记录权益:
equity = round(cash + shares * bar.close, 2)
equity_curve.append({
"trade_date": bar.trade_date.isoformat(),
"cash": cash,
"shares": shares,
"equity": equity,
"signal": factor.signal,
})
回测不是只看最后收益。权益曲线能告诉我们中间经历了什么,最大回撤也从这里计算。
def _max_drawdown(equity_values: list[float]) -> float:
peak = None
worst = 0.0
for value in equity_values:
peak = value if peak is None else max(peak, value)
if peak:
worst = min(worst, value / peak - 1)
return round(worst, 6)
测试第一个闭环
第 10 章测试覆盖四个场景。
没有数据时保持现金不变:
result = run_signal_backtest("600519.SH", [], initial_cash=100000)
assert result.final_equity == 100000
assert result.trades == ()
稳定上行时产生买入:
closes = [10 + index * 0.1 for index in range(40)]
result = run_signal_backtest("600519.SH", _bars("600519.SH", closes), initial_cash=100000)
assert result.trades[0].side == "buy"
assert result.trades[0].shares % 100 == 0
先涨后跌时产生买入和卖出:
assert [trade.side for trade in result.trades] == ["buy", "sell"]
assert result.shares == 0
标的不匹配时不交易。这能防止多标的回测里把别的股票行情误用到当前 symbol。
本篇实战任务
拉取第 10 章代码:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-10
uv sync --extra dev
uv run pytest
只跑回测测试:
uv run pytest tests/test_mini_backtest.py
第 10 章全量测试通过:168 passed,仍只有既有 FastAPI deprecation warning。
第 6-10 篇阶段 review
第二组五篇完成了“数据到最小回测”的闭环。
第 6 篇把 PostgreSQL schema 和 Alembic 迁移做成可检查对象,避免平台表结构只靠人工确认。
第 7 篇把 A 股股票池构建拆成纯函数,处理代码规范化、ST/退市过滤、去重、限制规模和来源摘要。
第 8 篇把原始 K 线清洗成统一的 CleanMarketBar,显式处理日期、数字、OHLC 自洽、重复行和成交量单位。
第 9 篇在干净 K 线上计算收益、均线、动量和波动率,并生成第一版可解释信号。
第 10 篇把信号接入 A 股订单规则,跑通买入、卖出、现金、持仓、费用、权益曲线和最大回撤。
这一组文章没有跳到“策略收益优化”,而是把可运行基础补齐。后面第三组会继续扩展:多标的回测、股票池遍历、回测指标、策略参数和结果持久化。
本章更新与代码仓库
本章更新内容:
- 新增
app/mini_backtest.py。 - 串联第 8 章行情清洗、第 9 章因子信号和第 4 章 A 股订单规则。
- 新增
tests/test_mini_backtest.py,覆盖空数据、买入、卖出和 symbol 隔离。 - 完成第 6-10 篇阶段 review。
代码仓库:
https://github.com/ax2/zi-quant-platform
本章代码:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-10
uv sync --extra dev
uv run pytest tests/test_mini_backtest.py
本篇小结
第一个回测闭环不需要复杂,但必须真实。
它要尊重股票池、行情清洗、因子窗口、A 股手数、费用、现金和持仓。只有这些基本约束都进了代码,后面谈策略优化才有意义。