11 KiB
Context
Current State:
backtest.py (284 lines) 是单一文件,包含:
- 命令行参数解析
- 数据库连接和数据加载
- 策略动态加载和验证
- 回测执行逻辑
- 结果格式化输出
- 图表生成
Constraints:
- 需要保持现有功能完整性(数据加载、策略加载、回测执行、结果展示)
- 需要支持多股票回测(串行执行)
- 不考虑并发实现(保持简单)
- 错误处理采用立即失败策略
- 数据库配置明文存储,不考虑环境变量
Stakeholders:
- 开发者:需要清晰的模块划分和可复用的接口
- 终端用户:需要友好的 CLI 输出(进度条、表格化结果)
Goals / Non-Goals
Goals:
- 分离核心逻辑与 CLI 界面,提升代码复用性
- 提供标准化函数接口,供其他模块调用回测功能
- 支持多股票批量回测(串行执行)
- 集中管理配置(数据库、参数、配色)
- 优化 CLI 输出体验(tabulate 表格化、tqdm 进度条)
Non-Goals:
- 并行执行多股票回测(性能优化非目标)
- 环境变量管理配置(配置明文存储即可)
- 复杂的聚合统计(仅单股票结果拼接)
- 图表文件合并(每个股票生成独立 HTML)
- 配置文件热重载(启动时加载一次)
Decisions
Decision 1: 三层模块划分
选择: 分离为 config.py、backtest_core.py、backtest_command.py 三个文件
理由:
- config.py:集中管理所有配置,避免硬编码分散
- backtest_core.py:纯粹的业务逻辑,提供可复用的函数接口
- backtest_command.py:CLI 界面,负责参数解析和结果展示
替代方案:
- 方案 A:保留单一文件,但改进内部结构(函数分离)
- 拒绝理由:仍无法复用,CLI 和业务逻辑耦合
- 方案 B:使用类封装(如
BacktestEngine类)- 拒绝理由:增加复杂度,函数接口已足够
Decision 2: BacktestResult 数据类
选择: 使用 dataclasses.dataclass 定义 BacktestResult
理由:
- 结构化返回结果,便于序列化和导出
- 类型提示支持,提升代码可读性
- 自动生成
__init__、__repr__等方法,减少样板代码
替代方案:
- 方案 A:直接返回原始
stats对象(backtesting 库返回)- 拒绝理由:依赖 backtesting 库内部结构,耦合度高
- 方案 B:返回字典
- 拒绝理由:缺乏类型提示,容易拼写错误
Decision 3: 批量回测策略
选择: 串行执行(for 循环),立即失败
理由:
- 简单可靠,易于调试
- 错误处理清晰(第一个失败就停止)
- 避免并发带来的资源竞争和复杂度
替代方案:
- 方案 A:并行执行(ThreadPoolExecutor)
- 拒绝理由:性能非目标,并发增加复杂度
- 方案 B:继续执行其他股票,最后统一报告错误
- 拒绝理由:用户需求是立即失败
Decision 4: CLI 参数设计
选择: --codes 多值参数(nargs='+'),--output-dir 目录参数
理由:
--codes支持传入多个股票代码,如--codes 000001.SZ 600000.SH--output-dir为每个股票生成{code}.html,如output/000001.SZ.html- 保持原有参数(
--start-date、--end-date、--strategy-file、--cash、--commission、--warmup-days)
替代方案:
- 方案 A:
--code逗号分隔(如--code 000001.SZ,600000.SH)- 拒绝理由:需要额外解析逻辑,不直观
- 方案 B:
--code多次调用(如--code 000001.SZ --code 600000.SH)- 拒绝理由:argparse 的
nargs='+'更符合习惯
- 拒绝理由:argparse 的
Decision 5: 输出优化库
选择: 使用 tabulate 表格化批量结果,使用 tqdm 显示进度条
理由:
- tabulate:提供美观的表格输出,支持多种格式(grid、simple 等)
- tqdm:提供实时进度条,提升用户体验
- 两个库都是轻量级,不引入复杂依赖
替代方案:
- 方案 A:手动格式化表格(字符串拼接)
- 拒绝理由:代码冗余,格式不够美观
- 方案 B:不使用进度条(仅输出完成提示)
- 拒绝理由:多股票回测耗时较长,用户需要进度反馈
Decision 6: 结果展示策略
选择: 单股票使用详细格式(现有),多股票使用表格格式(新增)
理由:
- 单股票:保持原有的详细输出(每个指标单独一行)
- 多股票:使用
tabulate表格横向对比,节省垂直空间
替代方案:
- 方案 A:所有情况都使用详细格式(拼接)
- 拒绝理由:多股票时输出过长,难以阅读
- 方案 B:所有情况都使用表格格式
- 拒绝理由:单股票时表格优势不明显,详细格式更清晰
Decision 7: 配置管理方式
选择: 明文常量存储在 config.py
理由:
- 满足用户需求(不考虑信息安全)
- 避免引入
python-dotenv依赖 - 代码简洁,修改直接
替代方案:
- 方案 A:环境变量(
os.getenv)- 拒绝理由:用户明确不需要
- 方案 B:配置文件(JSON/YAML)
- 拒绝理由:增加文件管理和解析复杂度
Decision 8: 数据访问接口
选择: load_data_from_db(code, start_date, end_date) 函数签名保持不变
理由:
- 现有接口已满足需求(单次查询一个股票)
- 迁移成本低,直接复制到
backtest_core.py
替代方案:
- 方案 A:批量查询(
load_data_from_db(codes, start_date, end_date))- 拒绝理由:需要修改 SQL 为
IN子句,且结果聚合复杂
- 拒绝理由:需要修改 SQL 为
- 方案 B:连接池复用(全局 engine 对象)
- 拒绝理由:每次创建引擎的开销可接受(串行执行)
Decision 9: 策略加载接口
选择: load_strategy(strategy_file) 返回 (calculate_indicators, strategy_class) 元组
理由:
- 保持现有接口,迁移成本低
- 函数返回两个值符合 Python 惯例
替代方案:
- 方案 A:返回类对象(策略类自带指标计算方法)
- 拒绝理由:现有策略文件结构分离了两者,修改成本高
- 方案 B:返回命名空间对象(封装两个属性)
- 拒绝理由:增加复杂度,元组足够
Decision 10: 错误处理策略
选择: 立即失败(不捕获部分错误继续执行)
理由:
- 符合用户需求
- 简化错误追踪(第一个错误直接暴露)
- 避免"部分成功"的歧义状态
替代方案:
- 方案 A:捕获错误但继续执行,最后统一报告
- 拒绝理由:用户明确要求立即失败
Risks / Trade-offs
Risk 1: CLI 命令变化导致用户习惯中断
风险: 用户习惯使用 python backtest.py,需要切换到 uv run python backtest_command.py
缓解:
- 在项目根目录创建软链接
backtest.py -> backtest_command.py(可选) - 或在 README 中明确说明新的使用方式
- 提供迁移指南(参数变化说明)
Risk 2: 多股票串行执行耗时较长
风险: 10 个股票可能需要 10 倍时间(每个 30 秒 → 总计 5 分钟)
缓解:
- 使用
tqdm进度条提供实时反馈 - 在 README 中说明性能限制
- 未来可扩展为并行执行(非当前目标)
Risk 3: BacktestResult 字段可能与 backtesting 库不兼容
风险: backtesting 库升级后,stats 对象的键名可能变化
缓解:
- 使用
.get(key, default)方法访问,避免 KeyError - 提供默认值(0 或空字符串)
- 在文档中说明依赖的 backtesting 版本
Risk 4: tabulate/tqdm 依赖未安装
风险: 用户运行时缺少依赖,导致 ImportError
缓解:
- 使用
uv add明确添加依赖到 pyproject.toml - 在 README 中说明安装步骤
- 错误信息中提示安装命令(
uv add tabulate tqdm)
Risk 5: 策略文件路径处理不一致
风险: 策略文件路径可能是相对路径或绝对路径,导致加载失败
缓解:
- 使用
os.path.abspath()转换为绝对路径 - 在错误信息中提示用户检查路径
- 测试相对路径和绝对路径两种情况
Risk 6: 图表输出目录不存在
风险: 用户指定的 --output-dir 不存在,导致保存失败
缓解:
- 使用
os.makedirs(output_dir, exist_ok=True)自动创建 - 在错误信息中提示用户检查目录权限
Risk 7: 内存占用(多股票同时加载数据)
风险: 如果同时加载多个股票数据,内存占用可能较高
缓解:
- 串行执行确保一次只加载一个股票的数据
- 单个股票的数据量可控(10 年约几 MB)
- future 可考虑流式处理(非当前目标)
Migration Plan
Step 1: 创建 config.py
- 从
backtest.py提取数据库配置 - 添加默认回测参数
- 添加图表配色配置
- 测试导入无错误
Step 2: 创建 backtest_core.py
- 迁移
load_data_from_db()函数(导入 config) - 迁移
load_strategy()函数 - 迁移
apply_color_scheme()函数(使用 config 配置) - 定义
BacktestResult数据类 - 实现
run_backtest()函数 - 实现
run_batch_backtest()函数 - 单元测试核心函数
Step 3: 创建 backtest_command.py
- 实现
parse_arguments()函数(支持--codes) - 实现
format_single_result()函数(详细格式) - 实现
format_batch_results()函数(使用 tabulate) - 实现
main()函数(调用run_batch_backtest()) - 测试单股票回测
- 测试多股票回测
Step 4: 更新依赖
- 运行
uv add tabulate添加依赖 - 运行
uv add tqdm添加依赖 - 运行
uv sync同步依赖
Step 5: 删除 backtest.py
- 确认新功能完整(单股票、多股票、图表输出)
- 确认错误处理正确(立即失败)
- 删除
backtest.py文件 - 更新 README 说明新的使用方式
Rollback Strategy
如果迁移过程中发现问题:
- 保留
backtest.py直到backtest_command.py完全可用 - 使用
git版本控制,可随时回退 - 逐步迁移(先核心函数,后 CLI),确保每步可验证
Open Questions
-
BacktestResult 字段完整性: 是否需要包含所有 backtesting.stats 的键,或仅包含当前用到的字段?
- 倾向:仅包含当前用到的字段(未来可扩展)
-
表格格式选择: tabulate 支持多种格式(grid、simple、pipe、html),多股票结果使用哪种?
- 倾向:grid(美观的边框格式)
-
进度条粒度: tqdm 进度条应该显示每个股票的回测进度,还是仅显示批量回测的总进度?
- 倾向:仅显示批量回测的总进度(股票 N/M)
-
图表输出目录结构: 多股票图表是平铺在
output/下,还是按日期/策略分组?- 倾向:平铺在
output/下(简单)
- 倾向:平铺在