程序员量化交易实战 10:跑通第一个最小回测闭环

前面几篇终于可以串起来了。

第 4 篇有 A 股订单规则,第 8 篇有清洗后的 K 线,第 9 篇有因子信号。第 10 篇做第一个最小回测闭环:看到 buy_watch 时买入,看到 risk_watch 时卖出,过程中记录现金、持仓、交易和权益曲线。

这不是完整回测引擎

先说边界。

这一篇的回测不是事件驱动引擎,也不处理撮合队列、分钟线、滑点模型和多标的组合优化。它只是一个可解释的最小闭环。

它要证明几件事:

  • 清洗后的 K 线可以进入因子计算。
  • 因子信号可以驱动买卖动作。
  • 买入前会经过 A 股 100 股手和现金校验。
  • 卖出会计算费用。
  • 每天都有权益曲线。
  • 回测结果可以通过测试复现。

这里也顺手把几个常见边界说清楚。回测不是“用历史数据跑一遍策略”这么简单,至少要区分四件事:信号生成、订单约束、成交假设和账户记账。本文只做日线收盘价成交假设,不做分钟线撮合;只处理单标的持仓,不处理组合层现金共享;但它已经接入 A 股手数和费用,避免出现明显不可执行的交易。

回测结果对象

第 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 篇可以直接运行当前主线的联动示例:

uv run python -m scripts.chapter_examples factor-backtest --source sample

下面这张截图来自同一条命令的最小回测部分:

输出里 000001.SZ 初始资金为 100000,最终权益为 105031.28,总收益约 5.03%,最大回撤约 -2.58%,产生 1 笔买入交易。这里的收益不是重点,重点是能看到信号已经变成了真实账户字段:买入日期、股数、价格、原因、现金、持仓和权益曲线都能被代码复现。

测试第一个闭环

第 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 隔离。
  • 补充当前主线联动示例的真实运行截图,展示第 9 篇信号如何进入第 10 篇回测。
  • 补充信号生成、订单约束、成交假设和账户记账的回测边界说明。
  • 完成第 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 股手数、费用、现金和持仓。只有这些基本约束都进了代码,后面谈策略优化才有意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值