程序员量化交易实战 11:从单只股票扩展到多标的组合回测
原创 · 约 11 分钟阅读 · 阅读 --
Last updated on

程序员量化交易实战 11:从单只股票扩展到多标的组合回测

作者: Alex Xiang


程序员量化交易实战 11:从单只股票扩展到多标的组合回测

古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。这个专栏会把一个 A 股量化平台从 0 到 1 拆开写:数据、策略、回测、模拟盘、提醒和生产化,尽量用真实代码和真实运行结果说话。更完整的更新也会同步到微信公众号「字与码」。

第 10 篇跑通了单只股票的最小回测闭环。单标的回测能解释规则,但不能代表策略在股票池上的表现。

这一篇把回测从一只股票扩展到多标的组合。先不做复杂组合优化,只做等权资金分配、逐标的运行、权益曲线聚合和交易统计。简单,但足够把“策略在股票池上跑一遍”这件事落到代码。

程序员量化交易实战第十一篇封面

组合回测先从等权开始

组合回测有很多复杂问题:调仓周期、权重约束、行业暴露、相关性、成交量容量、现金复用。

这些都重要,但第一版先不要全塞进去。我们先做一个明确边界:

  • 输入一组股票代码。
  • 每只股票分到相同初始资金。
  • 每只股票调用第 10 篇的 run_signal_backtest()
  • 最后把各股票权益曲线按日期相加。

等权不是最优解,只是最容易验证的基线。它的好处是口径清楚:如果组合表现变化,先看每个标的自己的回测结果,再看聚合后的权益曲线,不会一开始就被优化器、约束矩阵和行业暴露绕进去。真正进入生产前,后面还要继续补仓位上限、行业集中度、停牌处理、成交容量和再平衡规则。

新增组合结果对象

第 11 章新增 app/portfolio_backtest.py

组合结果对象是:

@dataclass(frozen=True)
class PortfolioBacktestResult:
    initial_cash: float
    final_equity: float
    total_return: float
    max_drawdown: float
    symbol_results: tuple[MiniBacktestResult, ...]
    equity_curve: tuple[dict[str, object], ...]

这里保留 symbol_results,不是只保留聚合指标。因为后面排查策略表现时,必须能知道是哪只股票贡献了收益,哪只股票拖累了回撤。

等权资金分配

核心函数是 run_equal_weight_portfolio_backtest()

cash_per_symbol = initial_cash / len(selected)
results = tuple(
    run_signal_backtest(
        symbol,
        all_bars,
        initial_cash=cash_per_symbol,
        position_ratio=position_ratio,
    )
    for symbol in selected
)

这里有两个细节。

第一,symbols 会先去重:

selected = list(dict.fromkeys(symbols))

第二,可以用 max_symbols 限制回测规模,避免本地实验一次跑太大。

聚合权益曲线

单标的回测已经有每日权益。组合层做的是按日期求和:

def _sum_equity_by_date(results: Iterable[MiniBacktestResult]) -> list[dict[str, object]]:
    by_date: dict[str, float] = {}
    for result in results:
        for row in result.equity_curve:
            trade_date = str(row["trade_date"])
            by_date[trade_date] = by_date.get(trade_date, 0.0) + float(row["equity"])
    return [{"trade_date": trade_date, "equity": round(equity, 2)} for trade_date, equity in sorted(by_date.items())]

这仍然是简化版。真实组合回测要处理不同股票停牌、日期不齐、现金共享和调仓。但第一版先让组合权益能跑出来。

交易统计

组合层还加了 portfolio_trade_summary()

{
    "symbols": len(result.symbol_results),
    "traded_symbols": len(traded_symbols),
    "buy_count": buy_count,
    "sell_count": sell_count,
    "trade_count": buy_count + sell_count,
}

这个摘要很实用。策略如果在 100 只股票上只交易了 1 只,和在 80 只股票上都有信号,是完全不同的解释。

真实运行截图

当前主线的联动示例会在第 10 篇单标的回测之后继续跑组合回测:

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

下面是组合回测部分的真实输出:

第 11 篇组合回测真实运行截图

这次样例里有 2 个标的,最终组合权益为 102515.64,总收益约 2.52%,最大回撤约 -1.29%。trade_summary 显示只有 1 个标的发生交易。这个结果很有用:它提醒我们,组合收益看起来还可以,但信号覆盖并不广,后面做策略复盘时不能只盯组合总收益。

本章更新与代码仓库

本章更新内容:

  • 新增 app/portfolio_backtest.py
  • 实现等权多标的组合回测、权益曲线聚合、交易统计和日期裁剪。
  • 新增 tests/test_portfolio_backtest.py,覆盖资金分配、去重限额、交易摘要和日期过滤。
  • 补充当前主线联动示例的组合回测真实运行截图。
  • 补充等权基线、信号覆盖和后续组合约束的背景说明。

代码仓库:

https://github.com/ax2/zi-quant-platform

本章代码:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-11
uv sync --extra dev
uv run pytest tests/test_portfolio_backtest.py

第 11 章全量测试通过:172 passed,仍只有既有 FastAPI deprecation warning。

本篇小结

组合回测的第一步不是优化权重,而是把多标的运行链路接上。

等权分配、逐标的回测、权益聚合和交易摘要,让策略可以从单只股票走向股票池。下一篇继续补回测指标,让组合结果不只是一条收益曲线。

微信公众号

欢迎关注「字与码」

如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。

微信公众号字与码二维码