# Design: Refactor Backtest Script

## Context

### 当前状态

现有的回测系统基于 Jupyter Notebook (`backtest.ipynb`)，包含以下手动执行步骤：
1. 通过 SQL magic 查询数据库获取股票价格数据（含复权）
2. 数据预处理（重命名列、设置索引）
3. 计算技术指标（SMA10, SMA30, SMA60, SMA120）
4. 定义策略类（SmaCross，金叉买入、死叉卖出）
5. 执行回测并打印结果
6. 生成交互式图表（Bokeh）

### 约束条件

- 数据库：PostgreSQL (leopard_dev@81.71.3.24)
- 数据表：`leopard_daily` (日线数据), `leopard_stock` (股票信息)
- 回测引擎：`backtesting` Python 库
- 复权逻辑：`price * factor`（factor 从数据库获取）
- 输出格式：中文标签 + Bokeh HTML 图表

### 利益相关者

- 量化研究员：需要快速测试不同策略、不同股票的回测表现
- 策略开发者：需要独立开发策略，通过标准接口集成
- 运维人员：需要支持批量自动化回测任务

## Goals / Non-Goals

### Goals

1. **命令行化执行** - 通过命令行参数完成回测，无需交互式环境
2. **策略模块化** - 策略逻辑与主流程分离，支持动态加载不同策略文件
3. **参数化配置** - 支持股票代码、时间范围、初始资金、手续费率等参数
4. **简化的数据访问** - 保持简单的数据库连接逻辑，不引入过度抽象
5. **清晰的结果输出** - 控制台中文统计 + 可选的 HTML 图表文件

### Non-Goals

- ❌ 不支持多时间周期（仅日线）
- ❌ 不支持多股票组合回测（仅单股票）
- ❌ 不支持参数优化（固定策略参数）
- ❌ 不支持实盘交易接口
- ❌ 不引入复杂的依赖注入或插件系统
- ❌ 不实现 Web UI 或 API 接口

## Decisions

### D1: 文件结构 - 单一入口文件 + 策略文件

**决策**:
- `backtest.py` - 包含所有主流程逻辑（参数解析、数据加载、回测执行、结果输出）
- `strategy.py` - 策略模板（指标计算函数 + 策略类）
- 可选 `strategies/` 目录 - 存放其他策略文件

**理由**:
- 用户要求简化文件数量，保持流程集中
- 单一入口文件便于理解和维护
- 策略文件独立，便于多人协作开发

**替代方案**:
- 将数据加载、结果输出拆分为独立模块 - 被用户拒绝（"设计的文件太多了，需要简化"）

---

### D2: 策略接口 - 两个必需函数 + 策略类

**决策**: 策略文件必须提供：

1. **`calculate_indicators(data)` 函数**
   ```python
   def calculate_indicators(data: pd.DataFrame) -> pd.DataFrame:
       """计算策略所需的技术指标，返回添加了指标列的 DataFrame"""
   ```

2. **`get_strategy()` 函数**
   ```python
   def get_strategy() -> type:
       """返回策略类（Strategy 的子类）"""
   ```

3. **策略类定义**
   ```python
   from backtesting import Strategy

   class MyStrategy(Strategy):
       def init(self):
           """注册指标到 backtesting 框架"""
           pass

       def next(self):
           """每个时间步的决策逻辑"""
           pass
   ```

**理由**:
- 将指标计算与交易逻辑分离，主流程可以预处理所有数据
- `get_strategy()` 函数提供清晰的加载接口
- 遵循 `backtesting` 库的接口规范

**替代方案**:
- 将 `calculate_indicators` 作为策略类的方法 - 问题：主流程无法先计算指标，必须在 Strategy 类中注册

---

### D3: 策略动态加载 - 使用 `importlib`

**决策**:
```python
import importlib.util

def load_strategy(strategy_file):
    """动态加载策略文件"""
    spec = importlib.util.spec_from_file_location(module_name, strategy_file)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)

    calculate_indicators = module.calculate_indicators
    strategy_class = module.get_strategy()
    return calculate_indicators, strategy_class
```

**理由**:
- 支持任意路径的策略文件（如 `strategy.py`, `strategies/macd.py`）
- 无需预定义策略列表或配置文件
- Python 标准库，无额外依赖

**替代方案**:
- 约定式加载（所有策略放在 `strategies/` 目录） - 灵活性不足
- 配置文件映射策略名称和文件路径 - 增加维护成本

---

### D4: 数据库连接 - 简化 SQLAlchemy 连接

**决策**:
```python
import sqlalchemy

conn_str = f"postgresql://{user}:{password}@{host}/{database}"
engine = sqlalchemy.create_engine(conn_str)
df = pd.read_sql(query, engine)
engine.dispose()
```

**理由**:
- 用户要求"数据库访问保持简单，不需要太多抽象"
- SQLAlchemy 提供基础连接池和 SQL 注入防护
- 支持参数化查询（未来扩展）

**SQL 查询**:
```sql
SELECT
    trade_date,
    open * factor AS Open,
    close * factor AS Close,
    high * factor AS High,
    low * factor AS Low,
    volume AS Volume,
    COALESCE(factor, 1.0) AS factor
FROM leopard_daily daily
LEFT JOIN leopard_stock stock ON stock.id = daily.stock_id
WHERE stock.code = '{code}'
  AND daily.trade_date BETWEEN '{start_date} 00:00:00'
                          AND '{end_date} 23:59:59'
ORDER BY daily.trade_date
```

**替代方案**:
- 直接使用 `psycopg2` - 需要手动处理游标和类型转换
- 引入 ORM 模型 - 过度抽象，与"保持简单"要求矛盾

---

### D5: 执行顺序 - 先计算指标，再执行回测

**决策**:
```
1. load_data_from_db() → 获取原始价格数据
2. calculate_indicators(data) → 添加指标列到 DataFrame
3. Backtest(data, strategy_class) → 执行回测
```

**理由**:
- 指标计算与回测分离，便于调试和验证
- 避免在 Strategy 类的 `init()` 中重复计算
- 支持可视化指标（如果需要）

**示例流程**:
```python
data = load_data_from_db('000001.SZ', '2024-01-01', '2025-12-31')
# data 包含: Open, High, Low, Close, Volume, factor

data = calculate_indicators(data)
# data 新增: sma10, sma30, sma60, sma120

bt = Backtest(data, SmaCross, cash=100000, commission=0.002)
stats = bt.run()
```

**替代方案**:
- 在 Strategy 类的 `init()` 中计算指标 - 导致指标逻辑分散，难以调试

---

### D6: 输出格式 - 控制台 + 可选 HTML 文件

**决策**:

**控制台输出**:
- 始终打印回测统计信息（中文格式化）
- 使用 notebook 中定义的 `INDICATOR_MAPPING` 映射

**HTML 输出**:
- 仅当指定 `--output` 参数时生成
- 使用 `backtesting` 库的 `bt.plot(filename=..., show=False)` 方法
- 生成独立的 HTML 文件，无需浏览器环境

**理由**:
- 用户要求"输出包括命令行输出和 html 文件输出，使用一个参数控制"
- 控制台输出便于快速查看，HTML 文件便于分享和详细分析
- `show=False` 确保在无头环境中也能生成文件

**示例用法**:
```bash
# 仅控制台输出
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py

# 控制台 + HTML 文件
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py --output result.html
```

**替代方案**:
- 始终生成 HTML 文件 - 增加不必要的磁盘 I/O
- 自动在浏览器打开 - 不适用于服务器环境

---

### D8: 预热天数 - 命令行参数控制

**决策**:
```python
parser.add_argument('--warmup-days', type=int, default=365,
                   help='预热天数（默认: 365，约一年）')
```

**执行逻辑**:
1. 用户从数据库查询的日期范围：`--start-date` 到 `--end-date`
2. 回测前，从数据中截取最后 N 天（由 `--warmup-days` 指定）
3. 截取的数据用于指标计算和回测

**理由**:
- 用户明确要求："如果命令行参数指定了，就用参数指定的时长，否则默认预热时长为一年"
- 简化实现，不需要自动计算各策略所需的最长预热期
- 灵活性高，用户可根据需要调整预热天数
- 避免复杂化：不解析策略代码以确定最长指标周期

**示例**:
```python
# 查询 2024-01-01 到 2025-12-31 的数据（2 年）
data = load_data_from_db('000001.SZ', '2024-01-01', '2025-12-31')  # 约 500 条记录

# 默认预热 365 天，取最后 1 年的数据用于回测
data = data.iloc[-365:]  # 2025-01-01 到 2025-12-31

# 用户指定预热 180 天
data = data.iloc[-180:]  # 2025-07-01 到 2025-12-31
```

**替代方案**:
- 自动计算策略所需的最长指标周期 - 需要解析策略代码，复杂度高
- 不截取数据，依赖策略自己处理 NaN - 但用户明确要求预热天数控制

---

### D7: 数据库凭证 - 环境变量

**决策**:
```python
# 数据库配置（开发环境，直接硬编码）
DB_HOST = '81.71.3.24'
DB_NAME = 'leopard_dev'
DB_USER = 'your_username'
DB_PASSWORD = 'your_password'
```

**理由**:
- 用户明确要求："数据库凭证不使用环境变量，开发人员直接硬编码到代码里即可"
- 开发环境仅内部使用，无安全风险
- 简化实现，无需环境变量管理
- 不引入额外的配置文件或库

**替代方案**:
- 使用环境变量 - 用户明确拒绝
- 使用配置文件 - 增加维护成本，用户明确不需要

---

## Risks / Trade-offs

### R1: SQL 注入风险

**风险**: 当前查询使用字符串拼接，存在 SQL 注入风险

**缓解措施**:
- 用户要求"数据库访问保持简单"，暂不实现参数化查询
- 文档中明确说明输入格式（股票代码、日期）
- 后续可在 `load_data_from_db()` 中添加输入验证

---

### R2: 策略文件加载失败

**风险**: 动态加载策略文件时，文件不存在或代码错误会导致运行时崩溃

**缓解措施**:
- 使用 `try-except` 捕获 `ImportError` 和 `AttributeError`
- 提供清晰的错误信息："策略文件 {file} 加载失败: {error}"
- 在文档中说明策略文件的标准接口

---

### R3: 指标计算性能

**风险**: 大数据集（如 10 年日线数据）计算指标可能较慢

**缓解措施**:
- 使用 pandas 的向量化操作（已实现）
- 考虑在文档中提示：首次运行可能较慢，后续可缓存指标数据
- 当前不优化（属于非目标范围）

---

### R4: 策略接口兼容性

**风险**: 用户编写的策略文件可能不符合接口要求（缺少 `calculate_indicators` 或 `get_strategy`）

**缓解措施**:
- 提供 `strategy.py` 作为标准模板
- 在 `load_strategy()` 中进行接口检查
- 运行时捕获 `AttributeError` 并提示缺失的函数

---

### R5: 图表生成失败

**风险**: Bokeh 生成 HTML 文件时可能因数据格式或依赖问题失败

**缓解措施**:
- 仅在用户指定 `--output` 参数时才尝试生成图表
- 使用 `try-except` 捕获异常，不影响统计信息输出
- 错误提示："图表生成失败，但回测已完成: {error}"

---

### R6: 时区和日期处理

**风险**: 数据库中的日期与用户输入的日期可能存在时区差异

**缓解措施**:
- 当前 SQL 查询使用 `BETWEEN 'start_date 00:00:00' AND 'end_date 23:59:59'` 覆盖全天
- 假设数据库和用户输入使用相同的时区（本地时间）
- 文档中说明日期格式为 `YYYY-MM-DD`

---

## Resolved Decisions

1. **数据库凭证管理**: ✅ 已决定 - 直接硬编码在代码中
   - 实现方式：在 backtest.py 中定义 DB_HOST, DB_NAME, DB_USER, DB_PASSWORD 常量
   - 不使用环境变量、不使用配置文件
   - 开发人员可直接修改代码中的凭证
   - 无安全风险（仅开发环境内部使用）

2. **错误处理详细程度**: ✅ 已决定 - 仅打印到控制台，不写入日志文件
   - 实现方式：所有错误信息直接使用 `print()` 输出到 stdout/stderr
   - 不引入日志库（logging）
   - 保持输出简洁，便于管道处理

3. **指标预热期**: ✅ 已决定 - 通过 `--warmup-days` 命令行参数控制
   - 实现方式：默认 365 天（约 1 年），用户可指定其他值
   - 不自动计算策略所需的最长指标周期
   - 使用 `data.iloc[-warmup_days:]` 截取数据

4. **多策略并行**: ✅ 已决定 - 不支持一次回测运行多个策略
   - 实现方式：每次命令执行只支持单个策略文件
   - 如需对比策略，用户需多次执行命令
   - 不实现多进程/多线程并行回测

---

## Implementation Overview

### 核心流程

```
main()
  ├─ parse_arguments()            # 解析命令行参数
  ├─ load_data_from_db()         # 从数据库获取价格数据
  │   └─ 返回 DataFrame: [Open, High, Low, Close, Volume, factor]
  ├─ load_strategy()             # 动态加载策略文件
  │   └─ 返回: (calculate_indicators, strategy_class)
  ├─ calculate_indicators(data)   # 计算技术指标
  │   └─ 返回添加了指标列的 DataFrame
  ├─ Backtest(data, strategy)    # 执行回测
  │   └─ 返回 stats 对象
  ├─ print_stats(stats)          # 控制台输出中文统计
  └─ bt.plot(filename=..., show=False)  # 可选：生成 HTML 图表
```

### 文件结构

```
leopard_analysis/
├── backtest.py                 # 主流程脚本
├── strategy.py                 # SMA 策略模板
├── strategies/                # 其他策略（可选）
│   ├── macd_strategy.py
│   ├── rsi_strategy.py
│   └── ...
├── .env                      # 数据库凭证（可选）
├── requirements.txt            # 依赖列表
└── README.md                 # 使用说明（可选）
```

### 依赖关系

```
backtest.py
  ├─ argparse           # 命令行参数解析
  ├─ sqlalchemy        # 数据库连接
  ├─ pandas            # 数据处理
  ├─ importlib        # 动态模块加载
  └─ backtesting       # 回测引擎

strategy.py
  ├─ pandas            # DataFrame 操作
  ├─ backtesting       # Strategy 基类
  └─ backtesting.lib   # crossover 等工具函数
```
