## 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.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='+'` 更符合习惯 ### 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/` 下(简单)