1
0
Files
leopard-analysis/openspec/changes/archive/2026-01-27-refactor-backtest-script/design.md
2026-01-27 18:30:41 +08:00

442 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 等工具函数
```