1
0
Files

11 KiB
Raw Permalink Blame History

Context

Current State: backtest.py (284 lines) 是单一文件,包含:

  • 命令行参数解析
  • 数据库连接和数据加载
  • 策略动态加载和验证
  • 回测执行逻辑
  • 结果格式化输出
  • 图表生成

Constraints:

  • 需要保持现有功能完整性(数据加载、策略加载、回测执行、结果展示)
  • 需要支持多股票回测(串行执行)
  • 不考虑并发实现(保持简单)
  • 错误处理采用立即失败策略
  • 数据库配置明文存储,不考虑环境变量

Stakeholders:

  • 开发者:需要清晰的模块划分和可复用的接口
  • 终端用户:需要友好的 CLI 输出(进度条、表格化结果)

Goals / Non-Goals

Goals:

  1. 分离核心逻辑与 CLI 界面,提升代码复用性
  2. 提供标准化函数接口,供其他模块调用回测功能
  3. 支持多股票批量回测(串行执行)
  4. 集中管理配置(数据库、参数、配色)
  5. 优化 CLI 输出体验tabulate 表格化、tqdm 进度条)

Non-Goals:

  1. 并行执行多股票回测(性能优化非目标)
  2. 环境变量管理配置(配置明文存储即可)
  3. 复杂的聚合统计(仅单股票结果拼接)
  4. 图表文件合并(每个股票生成独立 HTML
  5. 配置文件热重载(启动时加载一次)

Decisions

Decision 1: 三层模块划分

选择: 分离为 config.pybacktest_core.pybacktest_command.py 三个文件

理由:

  • config.py:集中管理所有配置,避免硬编码分散
  • backtest_core.py:纯粹的业务逻辑,提供可复用的函数接口
  • backtest_command.pyCLI 界面,负责参数解析和结果展示

替代方案:

  • 方案 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='+' 更符合习惯

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 子句,且结果聚合复杂
  • 方案 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

  1. backtest.py 提取数据库配置
  2. 添加默认回测参数
  3. 添加图表配色配置
  4. 测试导入无错误

Step 2: 创建 backtest_core.py

  1. 迁移 load_data_from_db() 函数(导入 config
  2. 迁移 load_strategy() 函数
  3. 迁移 apply_color_scheme() 函数(使用 config 配置)
  4. 定义 BacktestResult 数据类
  5. 实现 run_backtest() 函数
  6. 实现 run_batch_backtest() 函数
  7. 单元测试核心函数

Step 3: 创建 backtest_command.py

  1. 实现 parse_arguments() 函数(支持 --codes
  2. 实现 format_single_result() 函数(详细格式)
  3. 实现 format_batch_results() 函数(使用 tabulate
  4. 实现 main() 函数(调用 run_batch_backtest()
  5. 测试单股票回测
  6. 测试多股票回测

Step 4: 更新依赖

  1. 运行 uv add tabulate 添加依赖
  2. 运行 uv add tqdm 添加依赖
  3. 运行 uv sync 同步依赖

Step 5: 删除 backtest.py

  1. 确认新功能完整(单股票、多股票、图表输出)
  2. 确认错误处理正确(立即失败)
  3. 删除 backtest.py 文件
  4. 更新 README 说明新的使用方式

Rollback Strategy

如果迁移过程中发现问题:

  1. 保留 backtest.py 直到 backtest_command.py 完全可用
  2. 使用 git 版本控制,可随时回退
  3. 逐步迁移(先核心函数,后 CLI确保每步可验证

Open Questions

  1. BacktestResult 字段完整性: 是否需要包含所有 backtesting.stats 的键,或仅包含当前用到的字段?

    • 倾向:仅包含当前用到的字段(未来可扩展)
  2. 表格格式选择: tabulate 支持多种格式grid、simple、pipe、html多股票结果使用哪种

    • 倾向grid美观的边框格式
  3. 进度条粒度: tqdm 进度条应该显示每个股票的回测进度,还是仅显示批量回测的总进度?

    • 倾向:仅显示批量回测的总进度(股票 N/M
  4. 图表输出目录结构: 多股票图表是平铺在 output/ 下,还是按日期/策略分组?

    • 倾向:平铺在 output/ 下(简单)