# Spec: Batch Backtest

## ADDED Requirements

### Requirement: 多股票回测参数
系统 SHALL 支持通过命令行参数传入多个股票代码进行批量回测。

#### Scenario: 传入多个股票代码
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ 600000.SH --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
- **THEN** 系统解析所有股票代码到列表 `['000001.SZ', '600000.SH']`
- **THEN** 系统按顺序依次执行每个股票的回测
- **THEN** 系统为每个股票生成独立的回测结果

#### Scenario: 传入单个股票代码
- **WHEN** 用户执行 `python backtest_command.py --codes 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategies/macd_strategy.py`
- **THEN** 系统解析为单个股票代码列表 `['000001.SZ']`
- **THEN** 系统执行单个股票回测
- **THEN** 系统输出详细格式的回测结果

#### Scenario: 缺少 --codes 参数
- **WHEN** 用户未提供 `--codes` 参数
- **THEN** 系统输出错误信息："错误: 需要以下参数: --codes"
- **THEN** 系统退出并返回非零状态码

---

### Requirement: 批量回测执行
系统 SHALL 串行执行多个股票的回测，每次加载一个股票的数据并执行回测。

#### Scenario: 成功执行多个股票回测
- **WHEN** 用户传入 N 个股票代码
- **THEN** 系统循环 N 次，每次加载一个股票的数据
- **THEN** 系统每次执行完整的回测流程（数据加载、指标计算、回测执行）
- **THEN** 系统每次执行完成后生成 `BacktestResult` 对象
- **THEN** 系统返回包含 N 个 `BacktestResult` 的列表

#### Scenario: 每个股票独立预热期
- **WHEN** 系统执行第 i 个股票的回测
- **THEN** 系统使用 `start_date - warmup_days` 计算该股票的预热开始日期
- **THEN** 系统独立加载该股票的预热期数据
- **THEN** 不同股票的预热期互不影响

#### Scenario: 第一个股票回测失败
- **WHEN** 系统执行第一个股票回测时发生错误（数据库连接失败、策略加载失败等）
- **THEN** 系统捕获异常并输出错误信息
- **THEN** 系统停止执行后续股票的回测
- **THEN** 系统退出并返回非零状态码（立即失败策略）

#### Scenario: 中间股票回测失败
- **WHEN** 系统执行第 i 个股票回测时发生错误
- **THEN** 系统输出错误信息（包含股票代码）
- **THEN** 系统停止执行后续股票的回测
- **THEN** 系统退出并返回非零状态码

#### Scenario: 资源管理
- **WHEN** 系统完成第 i 个股票的回测
- **THEN** 系统关闭该股票的数据库连接（`engine.dispose()`）
- **THEN** 系统释放该股票的数据内存
- **THEN** 系统开始加载第 i+1 个股票的数据

---

### Requirement: 批量回测进度显示
系统 SHALL 使用 tqdm 显示批量回测的实时进度，提供用户反馈。

#### Scenario: 显示进度条
- **WHEN** 系统开始执行 N 个股票的批量回测
- **THEN** 系统显示进度条格式：`回测进度: 25%|█████▌  | 1/4 [00:30<01:30,  12.5s/it]`
- **THEN** 系统在完成每个股票回测后更新进度条
- **THEN** 进度条显示当前进度（i/N）、已用时间、预计剩余时间
- **THEN** 进度条在所有股票回测完成后消失

#### Scenario: 单股票回测不显示进度条
- **WHEN** 用户传入单个股票代码
- **THEN** 系统不显示 tqdm 进度条
- **THEN** 系统直接输出回测结果

#### Scenario: 进度条描述文本
- **WHEN** 系统显示批量回测进度
- **THEN** 进度条描述 SHALL 为 "回测进度"（中文）
- **THEN** 进度条显示已完成/总数（如 "1/4", "2/4"）

---

### Requirement: 批量回测结果展示
系统 SHALL 使用 tabulate 将多个股票的回测结果格式化为表格，便于横向对比。

#### Scenario: 表格化输出多股票结果
- **WHEN** 用户传入多个股票代码且回测成功
- **THEN** 系统使用 tabulate 生成表格
- **THEN** 表格格式 SHALL 为 grid（带边框）
- **THEN** 表格列 SHALL 包含：股票代码、收益率%、胜率%、最大回撤%、交易次数、SQN
- **THEN** 系统在表格上方显示表头（中文列名）
- **THEN** 数值保留 2 位小数（交易次数为整数）

#### Scenario: 表格内容填充
- **WHEN** 系统格式化第 i 个股票的结果
- **THEN** 系统从 `BacktestResult` 对象提取字段
- **THEN** "股票代码" 列填充 `result.code`
- **THEN** "收益率%" 列填充 `result.return_pct`
- **THEN** "胜率%" 列填充 `result.win_rate`
- **THEN** "最大回撤%" 列填充 `result.max_drawdown`
- **THEN** "交易次数" 列填充 `result.trades`
- **THEN** "SQN" 列填充 `result.sqn`

#### Scenario: 单股票回测不使用表格
- **WHEN** 用户传入单个股票代码
- **THEN** 系统不使用 tabulate 生成表格
- **THEN** 系统使用详细格式输出（每个指标单独一行）
- **THEN** 系统保持原有 `print_stats()` 的输出格式

#### Scenario: 表格示例输出
- **WHEN** 用户传入 2 个股票代码
- **THEN** 系统输出格式 SHALL 为：
  ```
  +-------------+-----------+--------+------------+----------+-------+
  | 股票代码    | 收益率%  | 胜率%  | 最大回撤%  | 交易次数 | SQN   |
  +-------------+-----------+--------+------------+----------+-------+
  | 000001.SZ   | 20.35    | 55.00  | -8.50      | 45       | 1.85  |
  | 600000.SH   | 15.00    | 48.00  | -12.30     | 38       | 1.42  |
  +-------------+-----------+--------+------------+----------+-------+
  ```

---

### Requirement: 多股票图表输出
系统 SHALL 为每个股票生成独立的 HTML 图表文件，文件名格式为 `{code}.html`。

#### Scenario: 指定 --output-dir 参数
- **WHEN** 用户传入 `--output-dir output/`
- **THEN** 系统为每个股票生成 HTML 文件到 `output/{code}.html`
- **THEN** 文件名 SHALL 为股票代码，如 `000001.SZ.html`, `600000.SH.html`
- **THEN** 系统自动创建 `output/` 目录（`exist_ok=True`）
- **THEN** 系统在完成后输出提示："图表已保存到目录: output/" 后列出所有文件

#### Scenario: 未指定 --output-dir 参数
- **WHEN** 用户未传入 `--output-dir` 参数
- **THEN** 系统不为任何股票生成图表文件
- **THEN** 系统仅输出控制台统计信息

#### Scenario: 图表文件覆盖
- **WHEN** 系统再次执行相同的批量回测
- **THEN** 系统覆盖已存在的 HTML 文件
- **THEN** 系统不提示文件已存在

---

### Requirement: 结构化回测结果
系统 SHALL 返回标准化的 `BacktestResult` 对象，包含所有关键指标。

#### Scenario: BacktestResult 对象创建
- **WHEN** 系统完成单股票回测
- **THEN** 系统从 `stats` 对象提取指标到 `BacktestResult`
- **THEN** `BacktestResult.code` 设置为股票代码
- **THEN** `BacktestResult.start_date` 设置为回测开始日期
- **THEN** `BacktestResult.end_date` 设置为回测结束日期
- **THEN** `BacktestResult.equity_final` 设置为最终权益
- **THEN** `BacktestResult.equity_peak` 设置为峰值收益
- **THEN** `BacktestResult.return_pct` 设置为总收益率
- **THEN** `BacktestResult.buy_hold_return` 设置为买入持有收益率
- **THEN** `BacktestResult.return_annual` 设置为年化收益率
- **THEN** `BacktestResult.volatility_annual` 设置为年化波动率
- **THEN** `BacktestResult.max_drawdown` 设置为最大回撤
- **THEN** `BacktestResult.avg_drawdown` 设置为平均回撤
- **THEN** `BacktestResult.max_drawdown_duration` 设置为最大回撤持续时长
- **THEN** `BacktestResult.avg_drawdown_duration` 设置为平均回撤持续时长
- **THEN** `BacktestResult.sortino_ratio` 设置为索提诺比率
- **THEN** `BacktestResult.calmar_ratio` 设置为卡尔玛比率
- **THEN** `BacktestResult.trades` 设置为交易次数
- **THEN** `BacktestResult.win_rate` 设置为胜率
- **THEN** `BacktestResult.sqn` 设置为系统质量数
- **THEN** `BacktestResult.cash` 设置为初始资金
- **THEN** `BacktestResult.commission` 设置为手续费率

#### Scenario: BacktestResult 列表返回
- **WHEN** 系统完成批量回测
- **THEN** 系统返回 `List[BacktestResult]`
- **THEN** 列表顺序 SHALL 与输入股票代码顺序一致
- **THEN** 列表长度 SHALL 等于输入股票代码数量（成功时）

#### Scenario: BacktestResult 数据类型
- **WHEN** 系统创建 `BacktestResult` 对象
- **THEN** 数值字段 SHALL 为 float 类型（除 `trades`, `max_drawdown_duration` 为 int）
- **THEN** 日期字段 SHALL 为 str 类型（YYYY-MM-DD 格式）
- **THEN** 系统支持 `result.to_dict()` 方法（dataclass 自动生成）

---

### Requirement: 可复用回测引擎接口
系统 SHALL 提供标准化的函数接口，供其他模块调用回测功能。

#### Scenario: run_backtest 函数调用
- **WHEN** 其他模块调用 `run_backtest(code, start_date, end_date, strategy_file, cash, commission, warmup_days, output_file)`
- **THEN** 函数接收股票代码、日期范围、策略文件、回测参数、输出文件路径
- **THEN** 函数执行完整回测流程（数据加载、策略加载、指标计算、回测执行）
- **THEN** 函数返回 `BacktestResult` 对象
- **THEN** 函数不打印任何输出（纯函数）

#### Scenario: run_batch_backtest 函数调用
- **WHEN** 其他模块调用 `run_batch_backtest(codes, start_date, end_date, strategy_file, cash, commission, warmup_days, output_dir)`
- **THEN** 函数接收股票代码列表、日期范围、策略文件、回测参数、输出目录
- **THEN** 函数串行执行每个股票的回测
- **THEN** 函数返回 `List[BacktestResult]`
- **THEN** 函数显示 tqdm 进度条（批量时）

#### Scenario: 函数参数默认值
- **WHEN** 调用者不指定可选参数
- **THEN** `cash` 默认为 100000
- **THEN** `commission` 默认为 0.002
- **THEN** `warmup_days` 默认为 365
- **THEN** `output_file` 默认为 None（不生成图表）
- **THEN** `output_dir` 默认为 None（不生成图表）

#### Scenario: 函数异常抛出
- **WHEN** `run_backtest` 或 `run_batch_backtest` 执行时发生错误
- **THEN** 函数 SHALL 抛出异常（不捕获）
- **THEN** 异常类型 SHALL 为 ValueError、TypeError 或原始异常
- **THEN** 异常信息 SHALL 包含具体错误原因
- **THEN** 调用者负责捕获和处理异常

---

### Requirement: 集中配置管理
系统 SHALL 在 config.py 中集中管理数据库配置、默认回测参数、图表配色。

#### Scenario: 数据库配置访问
- **WHEN** backtest_core.py 需要数据库连接参数
- **THEN** 模块从 config 导入 `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
- **THEN** 模块使用这些常量构建连接字符串
- **THEN** 模块不重复定义数据库配置

#### Scenario: 默认参数访问
- **WHEN** backtest_core.py 需要默认回测参数
- **THEN** 模块从 config 导入 `DEFAULT_CASH`, `DEFAULT_COMMISSION`, `DEFAULT_WARMUP_DAYS`
- **THEN** 模块使用这些常量作为函数默认值
- **THEN** 模块不重复定义默认参数

#### Scenario: 图表配色访问
- **WHEN** backtest_core.py 需要设置图表配色
- **THEN** 模块从 config 导入 `BULL_COLOR`, `BEAR_COLOR`
- **THEN** 模块使用这些颜色设置 `plotting.BULL_COLOR` 和 `plotting.BEAR_COLOR`
- **THEN** 模块不重复定义颜色配置

#### Scenario: 配置文件内容
- **WHEN** 查看 config.py 文件
- **THEN** 文件包含数据库配置（DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD）
- **THEN** 文件包含默认回测参数（DEFAULT_CASH, DEFAULT_COMMISSION, DEFAULT_WARMUP_DAYS）
- **THEN** 文件包含图表配色（BULL_COLOR, BEAR_COLOR）
- **THEN** 所有配置使用明文常量（不使用环境变量）

---

### Requirement: 错误处理策略
系统 SHALL 在批量回测失败时立即停止执行，不继续处理后续股票。

#### Scenario: 数据加载失败
- **WHEN** 系统加载第 i 个股票数据时失败（数据库错误、数据不存在）
- **THEN** 系统捕获异常
- **THEN** 系统输出错误信息："回测失败 [{code}]: {error}"
- **THEN** 系统停止执行后续股票的回测
- **THEN** 系统退出并返回非零状态码

#### Scenario: 策略加载失败
- **WHEN** 系统加载策略文件时失败（文件不存在、接口不完整）
- **THEN** 系统捕获异常
- **THEN** 系统输出错误信息："策略加载失败: {error}"
- **THEN** 系统停止执行所有股票的回测
- **THEN** 系统退出并返回非零状态码

#### Scenario: 回测执行失败
- **WHEN** 系统执行第 i 个股票回测时失败（策略逻辑错误）
- **THEN** 系统捕获异常
- **THEN** 系统输出错误信息和完整堆栈跟踪
- **THEN** 系统停止执行后续股票的回测
- **THEN** 系统退出并返回非零状态码

#### Scenario: 图表生成失败
- **WHEN** 系统生成第 i 个股票图表时失败
- **THEN** 系统捕获异常
- **THEN** 系统输出警告："图表生成失败 [{code}]: {error}，但回测已完成"
- **THEN** 系统继续执行后续股票的回测
- **THEN** 系统在返回的 `BacktestResult` 中设置 `error` 字段（如果设计支持）

---

### Requirement: 依赖管理
系统 SHALL 在 pyproject.toml 中添加 tabulate 和 tqdm 依赖。

#### Scenario: 添加 tabulate 依赖
- **WHEN** 查看 pyproject.toml 文件
- **THEN** 文件包含 `tabulate` 依赖
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
- **THEN** 系统可以导入 `import tabulate` 无错误

#### Scenario: 添加 tqdm 依赖
- **WHEN** 查看 pyproject.toml 文件
- **THEN** 文件包含 `tqdm` 依赖
- **THEN** 依赖版本 SHALL 为兼容当前 Python 版本的版本
- **THEN** 系统可以导入 `from tqdm import tqdm` 无错误

#### Scenario: 依赖安装
- **WHEN** 用户运行 `uv sync` 或 `pip install -e .`
- **THEN** 系统自动安装 tabulate 和 tqdm
- **THEN** 系统显示依赖安装进度
- **THEN** 系统完成安装后可以正常使用回测工具

#### Scenario: 依赖缺失提示
- **WHEN** 系统导入 tabulate 或 tqdm 时失败
- **THEN** 系统输出友好错误信息："缺少依赖: {package_name}，请运行: uv add {package_name}"
- **THEN** 系统退出并返回非零状态码
