1
0

完成回测脚本

This commit is contained in:
2026-01-27 18:30:41 +08:00
parent 53e72e2f84
commit 5c4a70d7f0
14 changed files with 2739 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-27

View File

@@ -0,0 +1,441 @@
# 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 等工具函数
```

View File

@@ -0,0 +1,83 @@
# Proposal: Refactor Backtest Script
## Why
当前回测系统使用 Jupyter Notebook (`backtest.ipynb`) 手动执行,存在以下问题:
- 不支持自动化批量回测,无法通过命令行调用
- 策略逻辑与数据获取混在一起,难以复用和切换
- 缺乏参数化配置,每次回测需要手动修改代码
- 无法方便地对比不同策略在同一股票、不同时间段的表现
通过将回测流程重构为命令行工具,可以实现:
- 支持命令行参数化调用,便于批量执行
- 策略模块化,支持动态加载不同的策略文件
- 简化数据加载逻辑,专注于回测核心流程
- 提高代码可维护性和可扩展性
## What Changes
### 新增文件
1. **backtest.py** - 主流程脚本
- 命令行参数解析 (`--code`, `--start-date`, `--end-date`, `--strategy-file`, `--cash`, `--commission`, `--output`)
- 数据库连接与数据加载(查询复权后的价格数据)
- 动态加载策略文件(通过 `importlib`
- 执行回测(使用 `backtesting` 库)
- 结果输出:
- 控制台中文格式化统计信息
- HTML 图表文件(可选,通过 `--output` 参数控制)
2. **strategy.py** - 策略模板文件
- `calculate_indicators(data)` 函数:计算策略所需的技术指标(如 SMA、MACD
- `get_strategy()` 函数:返回策略类
- `SmaCross` 类:继承 `backtesting.Strategy`,实现交易逻辑(金叉买入、死叉卖出)
### 主要功能特性
- **动态策略加载**:通过 `--strategy-file` 参数指定任意策略文件
- **简化的数据库访问**:直接 SQL 查询获取数据,不引入额外抽象
- **指标计算策略化**:每个策略文件自己定义需要计算的指标
- **结果输出控制**:默认控制台输出,通过 `--output` 参数生成 HTML 图表
### 现有文件变更
- 无(新增文件,不修改现有 Notebook
## Capabilities
### New Capabilities
- **backtest-cli**: 命令行回测工具,支持通过参数化方式执行量化回测
- **strategy-loading**: 动态加载策略模块,支持从指定路径导入策略类和指标计算函数
- **data-fetching**: 从 PostgreSQL 数据库获取股票历史价格数据,自动处理复权
### Modified Capabilities
- 无(不涉及现有规范级别的需求变更)
## Impact
### 代码影响
- 新增 `backtest.py` 作为主入口文件
- 新增 `strategy.py` 作为策略模板
- 可选新增 `strategies/` 目录存放其他策略文件
### 依赖影响
**新增依赖**
- `sqlalchemy` - 数据库连接
- `backtesting` - 回测引擎
- `pandas`, `numpy` - 数据处理(已存在于 Notebook 中)
### API/系统影响
- 无外部 API 变更
- 数据库查询逻辑从 Notebook 迁移到 Python 脚本
- 输出从 Notebook 交互式展示改为命令行 + HTML 文件
### 用户影响
- 用户可以通过命令行执行回测,无需打开 Jupyter Notebook
- 策略开发者可以独立开发策略文件,通过约定接口集成到主流程
- 回测结果以 HTML 文件形式保存,便于分享和查看

View File

@@ -0,0 +1,195 @@
# Spec: Backtest CLI
## ADDED Requirements
### Requirement: 命令行参数解析
回测脚本 SHALL 通过命令行参数接收用户输入,参数 SHALL 包含股票代码、时间范围、策略文件、回测参数等。
#### Scenario: 基础回测执行
- **WHEN** 用户执行 `python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py`
- **THEN** 系统解析所有必需参数,无错误提示
- **THEN** 开始执行回测流程
- **THEN** 回测完成后输出统计信息到控制台
#### Scenario: 可选参数未指定
- **WHEN** 用户未指定 `--cash` 参数
- **THEN** 系统使用默认值 100000 作为初始资金
- **WHEN** 用户未指定 `--commission` 参数
- **THEN** 系统使用默认值 0.002 作为手续费率
- **WHEN** 用户未指定 `--output` 参数
- **THEN** 系统不生成 HTML 图表文件
#### Scenario: 必需参数缺失
- **WHEN** 用户未提供 `--code` 参数
- **THEN** 系统输出错误信息:"错误: 需要以下参数: --code"
- **THEN** 系统退出并返回非零状态码
- **WHEN** 用户未提供 `--start-date``--end-date` 参数
- **THEN** 系统输出对应的错误信息
- **THEN** 系统退出并返回非零状态码
#### Scenario: 自定义参数值
- **WHEN** 用户指定 `--cash 500000 --commission 0.001 --output result.html`
- **THEN** 系统使用指定的 500000 作为初始资金
- **THEN** 系统使用指定的 0.001 作为手续费率
- **THEN** 回测完成后生成 HTML 图表到 result.html
---
### Requirement: 数据库数据加载
回测脚本 SHALL 从 PostgreSQL 数据库加载指定股票的历史价格数据,并自动处理复权。
#### Scenario: 成功加载数据
- **WHEN** 用户指定有效的股票代码和时间范围
- **THEN** 系统连接数据库并执行查询
- **THEN** 返回 DataFrame包含列: [Open, High, Low, Close, Volume, factor]
- **THEN** DataFrame 的索引为 trade_date (DatetimeIndex)
- **THEN** 数据已应用复权计算price * factor
#### Scenario: 数据库连接失败
- **WHEN** 数据库连接失败(凭证错误、网络问题等)
- **THEN** 系统捕获异常并输出错误信息:"数据库连接失败: {error}"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 未找到股票数据
- **WHEN** 指定的股票代码或时间范围内无数据
- **THEN** 系统抛出 ValueError: "未找到股票 {code} 在指定时间范围内的数据"
- **THEN** 主流程捕获异常并输出友好错误信息
- **THEN** 系统退出并返回非零状态码
#### Scenario: 数据验证
- **WHEN** 数据库返回的 DataFrame 为空
- **THEN** 系统提示数据为空并退出
- **WHEN** 数据库返回的 DataFrame 少于 10 条记录
- **THEN** 系统提示数据不足并退出
---
### Requirement: 策略动态加载
回测脚本 SHALL 支持动态加载指定路径的策略文件,并验证策略接口。
#### Scenario: 加载有效策略文件
- **WHEN** 用户指定 `--strategy-file strategy.py`
- **THEN** 系统通过 importlib 加载该模块
- **THEN** 系统获取模块的 `calculate_indicators` 函数
- **THEN** 系统调用模块的 `get_strategy()` 函数获取策略类
- **THEN** 系统返回 (calculate_indicators, strategy_class) 元组
#### Scenario: 策略文件不存在
- **WHEN** 用户指定的策略文件路径不存在
- **THEN** 系统捕获 FileNotFoundError
- **THEN** 输出错误信息:"策略文件 {file} 不存在"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 策略接口不完整
- **WHEN** 策略文件缺少 `calculate_indicators` 函数
- **THEN** 系统捕获 AttributeError
- **THEN** 输出错误信息:"策略文件 {file} 缺少 calculate_indicators 函数"
- **THEN** 系统退出并返回非零状态码
- **WHEN** 策略文件缺少 `get_strategy` 函数
- **THEN** 系统捕获 AttributeError
- **THEN** 输出错误信息:"策略文件 {file} 缺少 get_strategy 函数"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 加载子目录中的策略
- **WHEN** 用户指定 `--strategy-file strategies/macd_strategy.py`
- **THEN** 系统正确加载子目录中的策略模块
- **THEN** 系统成功获取策略类和指标计算函数
---
### Requirement: 指标计算
回测脚本 SHALL 在执行回测前调用策略的指标计算函数,将技术指标添加到数据集中。
#### Scenario: 成功计算指标
- **WHEN** 系统调用 `calculate_indicators(data)`
- **THEN** 函数接收包含 [Open, High, Low, Close, Volume, factor] 的 DataFrame
- **THEN** 函数计算策略所需的指标(如 SMA, MACD, RSI
- **THEN** 函数返回添加了指标列的 DataFrame
- **THEN** DataFrame 保留原始列,新增指标列
#### Scenario: 指标计算产生 NaN 值
- **WHEN** 滚动窗口计算导致前 N 行的指标值为 NaN
- **THEN** DataFrame 包含 NaN 值(系统不自动删除)
- **THEN** Backtest 框架在回测时会跳过 NaN 值的行
#### Scenario: 指标计算函数抛出异常
- **WHEN** `calculate_indicators(data)` 执行时抛出异常
- **THEN** 主流程捕获异常
- **THEN** 输出错误信息:"指标计算失败: {error}"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 回测执行
回测脚本 SHALL 使用 backtesting 库执行回测,传入数据、策略和参数。
#### Scenario: 成功执行回测
- **WHEN** 系统调用 `Backtest(data, strategy_class, cash=..., commission=...).run()`
- **THEN** Backtest 初始化时调用策略类的 `init()` 方法
- **THEN** Backtest 逐个时间步调用策略类的 `next()` 方法
- **THEN** 系统返回包含回测统计信息的 stats 对象
#### Scenario: 回测参数传递
- **WHEN** 用户指定 `--cash 500000 --commission 0.001`
- **THEN** Backtest 实例化时使用 cash=500000
- **THEN** Backtest 实例化时使用 commission=0.001
- **THEN** Backtest 实例化时使用 finalize_trades=True
#### Scenario: 回测运行时错误
- **WHEN** 策略的 `next()` 方法执行时抛出异常
- **THEN** backtesting 库捕获异常
- **THEN** 系统输出错误信息和堆栈跟踪
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 结果输出
回测脚本 SHALL 将回测统计信息格式化输出到控制台,并可选生成 HTML 图表文件。
#### Scenario: 控制台输出
- **WHEN** 回测成功完成
- **THEN** 系统调用 `print_stats(stats)` 函数
- **THEN** 系统输出回测统计信息,使用中文标签
- **THEN** 输出内容包括:最终收益、总收益率、年化收益率、最大回撤、胜率等
- **THEN** 数值格式化(保留 2 位小数)
#### Scenario: 生成 HTML 图表
- **WHEN** 用户指定 `--output result.html`
- **THEN** 系统调用 `bt.plot(filename='result.html', show=False)`
- **THEN** 系统生成 HTML 文件到 result.html
- **THEN** 系统输出提示:"图表已保存到: result.html"
- **THEN** 图表包含价格曲线、资金曲线、买卖信号等
#### Scenario: 不生成 HTML 图表
- **WHEN** 用户未指定 `--output` 参数
- **THEN** 系统不调用 bt.plot() 方法
- **THEN** 系统不生成任何图表文件
- **THEN** 系统仅输出控制台统计信息
#### Scenario: 图表生成失败
- **WHEN** bt.plot() 方法执行时抛出异常
- **THEN** 系统捕获异常
- **THEN** 系统输出警告:"图表生成失败,但回测已完成: {error}"
- **THEN** 系统不影响控制台统计信息的输出
- **THEN** 系统正常退出(返回状态码 0
---
### Requirement: 错误处理
回测脚本 SHALL 对所有可能的错误进行捕获和处理,提供友好的错误提示。
#### Scenario: 数据库错误
- **WHEN** 数据库操作抛出 sqlalchemy.exc.SQLAlchemyError
- **THEN** 系统输出错误信息:"数据库错误: {error}"
- **THEN** 系统退出并返回状态码 2
#### Scenario: 文件操作错误
- **WHEN** 图表文件保存失败(权限、磁盘空间等)
- **THEN** 系统输出错误信息:"文件操作错误: {error}"
- **THEN** 系统退出并返回状态码 3
#### Scenario: 未预期的错误
- **WHEN** 发生其他未捕获的异常
- **THEN** 系统输出错误信息:"未知错误: {error}"
- **THEN** 系统输出完整的堆栈跟踪
- **THEN** 系统退出并返回状态码 1

View File

@@ -0,0 +1,280 @@
# Spec: Data Fetching
## ADDED Requirements
### Requirement: 数据库连接配置
系统 SHALL 通过硬编码常量管理数据库连接参数(开发环境)。
#### Scenario: 使用硬编码常量
- **WHEN** 系统在 backtest.py 中定义数据库配置
- **THEN** 系统定义 DB_HOST, DB_NAME, DB_USER, DB_PASSWORD 常量
- **THEN** DB_HOST 值 SHALL 为数据库主机地址(如 '81.71.3.24'
- **THEN** DB_NAME 值 SHALL 为数据库名称(如 'leopard_dev'
- **THEN** DB_USER 值 SHALL 为数据库用户名
- **THEN** DB_PASSWORD 值 SHALL 为数据库密码
#### Scenario: 构建连接字符串
- **WHEN** 系统创建 SQLAlchemy 连接
- **THEN** 系统使用硬编码的常量构建连接字符串
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 不从环境变量读取任何凭证
#### Scenario: 修改数据库凭证
- **WHEN** 开发人员需要更换数据库或凭证
- **THEN** 开发人员直接修改 backtest.py 中的常量值
- **THEN** 修改后脚本使用新凭证连接数据库
---
### Requirement: 数据库连接建立
系统 SHALL 使用 SQLAlchemy 创建 PostgreSQL 数据库连接。
#### Scenario: 成功建立连接
- **WHEN** 凭证正确且数据库可访问
- **THEN** 系统使用 `sqlalchemy.create_engine(conn_str)` 创建引擎
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 系统成功创建引擎对象
- **THEN** 系统可用于执行查询
#### Scenario: 连接字符串构建
- **WHEN** 系统构建 PostgreSQL 连接字符串
- **THEN** 连接字符串 SHALL 正确编码特殊字符(密码中的 @, : 等)
- **THEN** 连接字符串 SHALL 使用标准 URI 格式
- **THEN** 连接字符串 SHALL 不包含额外选项(仅基础连接参数)
#### Scenario: 数据库连接失败
- **WHEN** 凭证错误或数据库不可达
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.OperationalError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"数据库连接失败: {error}"
- **THEN** 系统退出并返回状态码 2
#### Scenario: 连接池管理
- **WHEN** 系统创建引擎对象
- **THEN** SQLAlchemy SHALL 自动管理连接池
- **THEN** 查询后连接 SHALL 自动返回池中
- **THEN** 系统 SHALL 在查询完成后调用 `engine.dispose()` 清理
---
### Requirement: SQL 查询构建
系统 SHALL 构建参数化的 SQL 查询以获取股票历史数据。
#### Scenario: 基础查询结构
- **WHEN** 系统构建查询
- **THEN** 查询 SHALL 选择 trade_date, Open, High, Low, Close, Volume, factor
- **THEN** 查询 SHALL 连接 leopard_daily 和 leopard_stock 表
- **THEN** 查询 SHALL 按 stock.code 过滤
- **THEN** 查询 SHALL 按 trade_date 范围过滤
- **THEN** 查询 SHALL 按 trade_date 升序排序
#### Scenario: 复权价格计算
- **WHEN** 系统计算复权价格
- **THEN** Open SHALL 计算为 `open * factor`
- **THEN** Close SHALL 计算为 `close * factor`
- **THEN** High SHALL 计算为 `high * factor`
- **THEN** Low SHALL 计算为 `low * factor`
- **THEN** Volume SHALL 直接使用原始值(不复权)
- **THEN** factor SHALL 使用 `COALESCE(factor, 1.0)` 处理 NULL 值
#### Scenario: 参数化股票代码
- **WHEN** 用户指定股票代码(如 '000001.SZ'
- **THEN** 查询 WHERE 子句 SHALL 使用 `stock.code = '{code}'`
- **THEN** 代码 SHALL 精确匹配(不使用 LIKE
- **THEN** 查询 SHALL 返回匹配股票的所有日线数据
#### Scenario: 参数化日期范围
- **WHEN** 用户指定开始日期 '2024-01-01' 和结束日期 '2025-12-31'
- **THEN** 查询 WHERE 子句 SHALL 使用 `BETWEEN '{start_date} 00:00:00' AND '{end_date} 23:59:59'`
- **THEN** 00:00:00 和 23:59:59 SHALL 覆盖全天
- **THEN** 日期格式 SHALL 为 YYYY-MM-DD HH:MM:SS
#### Scenario: 完整 SQL 查询
- **WHEN** 系统执行数据加载
- **THEN** 查询 SHALL 为:
```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
```
---
### Requirement: 数据查询执行
系统 SHALL 使用 pandas 的 `read_sql` 函数执行 SQL 查询并返回 DataFrame。
#### Scenario: 成功执行查询
- **WHEN** SQL 查询有效且数据存在
- **THEN** 系统调用 `pd.read_sql(query, engine)`
- **THEN** 系统返回 DataFrame 对象
- **THEN** DataFrame SHALL 包含查询结果的所有列
- **THEN** DataFrame 行数 SHALL 匹配数据库返回的记录数
#### Scenario: 数据类型处理
- **WHEN** pandas 读取 SQL 结果
- **THEN** trade_date SHALL 自动转换为 datetime 类型
- **THEN** Open, High, Low, Close, Volume SHALL 为 float 类型
- **THEN** factor SHALL 为 float 类型
- **THEN** 系统不需要手动类型转换(除日期索引设置)
#### Scenario: 查询返回空结果
- **WHEN** 指定股票代码或日期范围无数据
- **THEN** `read_sql` 返回空 DataFrame0 行)
- **THEN** 系统检查 `len(df) == 0`
- **THEN** 系统抛出 ValueError: "未找到股票 {code} 在指定时间范围内的数据"
#### Scenario: SQL 语法错误
- **WHEN** SQL 查询包含语法错误
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.ProgrammingError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"SQL 查询错误: {error}"
- **THEN** 系统退出并返回状态码 2
---
### Requirement: 数据格式转换
系统 SHALL 将查询结果转换为 backtesting 库要求的格式。
#### Scenario: 设置日期索引
- **WHEN** DataFrame 加载完成
- **THEN** 系统调用 `df.set_index('trade_date', inplace=True)`
- **THEN** DataFrame 的索引 SHALL 为 DatetimeIndex
- **THEN** 索引 SHALL 不再是数值索引
- **THEN** backtesting 库 SHALL 能正确处理日期范围
#### Scenario: 列名格式化
- **WHEN** DataFrame 加载完成
- **THEN** 列名 SHALL 为 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor']
- **THEN** 列名 SHALL 遵循 backtesting 库要求(首字母大写)
- **THEN** 列名 SHALL 与 SQL 查询中的别名一致
#### Scenario: 数据验证
- **WHEN** 系统准备返回 DataFrame
- **THEN** 系统验证 DataFrame 包含必需列
- **THEN** 系统验证 'Open', 'High', 'Low', 'Close', 'Volume' 列存在
- **THEN** 系统验证索引为 DatetimeIndex
- **WHEN** 验证失败
- **THEN** 系统抛出 ValueError: "数据格式不符合要求"
---
### Requirement: 数据清理
系统 SHALL 清理数据以确保回测质量。
#### Scenario: 删除 NULL 值行
- **WHEN** DataFrame 包含 NULL 或 NaN 值
- **THEN** 系统调用 `df.dropna()` 删除
- **THEN** 任何包含 NaN 的行 SHALL 被删除
- **THEN** 返回的 DataFrame SHALL 不包含 NULL 值
#### Scenario: 数据完整性检查
- **WHEN** DataFrame 加载完成
- **THEN** 系统检查 trade_date 连续性
- **THEN** 系统检查无重复日期
- **WHEN** 发现异常
- **THEN** 系统输出警告:"数据存在异常: {detail}"
#### Scenario: 最小数据量验证
- **WHEN** DataFrame 行数少于 10
- **THEN** 系统输出错误:"数据不足,至少需要 10 天数据"
- **THEN** 系统抛出 ValueError
- **THEN** 主流程捕获并退出
---
### Requirement: 资源管理
系统 SHALL 正确管理数据库连接和内存资源。
#### Scenario: 引擎创建和清理
- **WHEN** 系统开始数据加载
- **THEN** 系统创建 SQLAlchemy 引擎对象
- **THEN** 系统使用引擎执行查询
- **WHEN** 查询完成
- **THEN** 系统调用 `engine.dispose()` 关闭连接池
- **THEN** 系统释放所有数据库连接
#### Scenario: 异常情况下的资源清理
- **WHEN** 查询过程中抛出异常
- **THEN** 系统在 finally 块中调用 `engine.dispose()`
- **THEN** 所有连接 SHALL 被正确关闭
- **THEN** 系统不会泄漏数据库连接
---
### Requirement: 错误处理和日志
系统 SHALL 提供清晰的错误信息和调试支持。
#### Scenario: 连接错误信息
- **WHEN** 数据库连接失败
- **THEN** 错误信息 SHALL 包含数据库主机和端口
- **THEN** 错误信息 SHALL 区分网络错误和认证错误
- **THEN** 系统提示用户检查凭证和网络连接
#### Scenario: 查询错误信息
- **WHEN** SQL 查询失败
- **THEN** 错误信息 SHALL 包含失败的 SQL 语句
- **THEN** 错误信息 SHALL 包含数据库返回的错误详情
- **THEN** 系统提示用户检查表结构和数据
#### Scenario: 数据格式错误信息
- **WHEN** 返回的 DataFrame 不符合要求
- **THEN** 错误信息 SHALL 列出缺失的列
- **THEN** 错误信息 SHALL 提示期望的格式
- **THEN** 系统建议用户检查数据库表结构
---
### Requirement: 函数接口
`load_data_from_db` 函数 SHALL 提供清晰的调用接口。
#### Scenario: 函数签名
- **WHEN** 主流程调用 `load_data_from_db(code, start_date, end_date)`
- **THEN** 函数接收三个字符串参数
- **THEN** `code` 为股票代码(如 '000001.SZ'
- **THEN** `start_date` 为开始日期(如 '2024-01-01'
- **THEN** `end_date` 为结束日期(如 '2025-12-31'
#### Scenario: 返回值
- **WHEN** 数据加载成功
- **THEN** 函数返回 pandas.DataFrame
- **THEN** DataFrame 索引为 DatetimeIndextrade_date
- **THEN** DataFrame 包含 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor'] 列
#### Scenario: 异常抛出
- **WHEN** 数据加载失败
- **THEN** 函数 SHALL 抛出异常(不捕获)
- **THEN** 异常类型 SHALL 为 ValueError业务逻辑错误
- **THEN** 主流程负责捕获和处理异常
---
### Requirement: 性能考虑
系统 SHALL 优化数据加载性能以支持大数据集。
#### Scenario: 使用 pandas 向量化操作
- **WHEN** 执行复权计算
- **THEN** 计算 SHALL 使用 pandas 向量化操作
- **THEN** 不使用循环逐行计算
- **THEN** 10 年数据(约 2500 行) SHALL 在 1 秒内加载
#### Scenario: 索引优化
- **WHEN** 设置 DataFrame 索引
- **THEN** `set_index()` 操作 SHALL 高效(使用底层数组拷贝)
- **THEN** 日期索引 SHALL 支持快速范围查询
#### Scenario: 内存管理
- **WHEN** 加载大数据集
- **THEN** 系统 SHALL 及时调用 `engine.dispose()` 释放连接
- **THEN** DataFrame SHALL 使用 pandas 内部优化存储
- **THEN** 内存占用 SHALL 合理10 年数据约几 MB

View File

@@ -0,0 +1,225 @@
# Spec: Strategy Loading
## ADDED Requirements
### Requirement: 策略文件接口
策略文件 SHALL 提供两个必需的接口:指标计算函数和策略类获取函数。
#### Scenario: 标准策略文件结构
- **WHEN** 用户创建策略文件
- **THEN** 文件 SHALL 包含 `calculate_indicators(data)` 函数
- **THEN** 文件 SHALL 包含 `get_strategy()` 函数
- **THEN** 文件 SHALL 包含一个继承 `backtesting.Strategy` 的类
- **THEN** 所有三个组件 SHALL 在同一文件中
#### Scenario: calculate_indicators 函数签名
- **WHEN** 主流程调用 `calculate_indicators(data)`
- **THEN** 函数接收一个参数data (pandas.DataFrame)
- **THEN** 函数返回一个 pandas.DataFrame
- **THEN** 返回的 DataFrame SHALL 包含原始列和新增的指标列
- **THEN** 函数 SHALL 修改输入的 DataFrame不创建副本
#### Scenario: get_strategy 函数签名
- **WHEN** 主流程调用 `get_strategy()`
- **THEN** 函数不接收参数
- **THEN** 函数返回一个类对象
- **THEN** 返回的类 SHALL 继承自 `backtesting.Strategy`
---
### Requirement: 指标计算函数
`calculate_indicators` 函数 SHALL 计算策略所需的技术指标,并将结果添加到 DataFrame 中。
#### Scenario: SMA 指标计算
- **WHEN** 策略需要简单移动平均线指标
- **THEN** 函数使用 `data['Close'].rolling(window=N).mean()` 计算
- **THEN** 函数将结果存储为 `data['smaN']`
- **THEN** N 为具体的周期(如 10, 30, 60, 120
#### Scenario: MACD 指标计算
- **WHEN** 策略需要 MACD 指标
- **THEN** 函数使用 `data['Close'].ewm(span=12).mean()` 计算 EMA12
- **THEN** 函数使用 `data['Close'].ewm(span=26).mean()` 计算 EMA26
- **THEN** 函数计算 MACD = EMA12 - EMA26
- **THEN** 函数计算 Signal = MACD.ewm(span=9).mean()
- **THEN** 函数将结果存储为 `data['macd']`, `data['macd_signal']`, `data['macd_hist']`
#### Scenario: RSI 指标计算
- **WHEN** 策略需要 RSI 指标
- **THEN** 函数计算价格变化 delta = data['Close'].diff()
- **THEN** 函数计算 gain = delta.where(delta > 0, 0)
- **THEN** 函数计算 loss = -delta.where(delta < 0, 0)
- **THEN** 函数计算平均收益和平均损失
- **THEN** 函数计算 RS = average_gain / average_loss
- **THEN** 函数计算 RSI = 100 - (100 / (1 + RS))
- **THEN** 函数将结果存储为 `data['rsi']`
#### Scenario: 多指标计算
- **WHEN** 策略需要多个技术指标
- **THEN** 函数按顺序计算每个指标
- **THEN** 函数将所有指标列添加到 DataFrame
- **THEN** DataFrame 最终包含原始列 + 所有指标列
- **THEN** 计算顺序 SHALL 遵循指标间的依赖关系(如 MACD 依赖 EMA
#### Scenario: 指标列命名约定
- **WHEN** 函数添加指标列到 DataFrame
- **THEN** 列名 SHALL 使用小写和下划线(如 `sma10`, `macd_signal`
- **THEN** 列名 SHALL 与策略类的 `init()` 方法中引用的名称一致
- **THEN** 列名 SHALL 避免与原始列冲突
---
### Requirement: 策略类定义
策略类 SHALL 继承 `backtesting.Strategy`,并实现 `init()``next()` 方法。
#### Scenario: 策略类继承
- **WHEN** 用户定义策略类
- **THEN** 类 SHALL 显式继承 `backtesting.Strategy`
- **THEN** 类 SHALL 定义类属性作为可配置参数
- **THEN** 类名 SHALL 使用大驼峰命名(如 `SmaCross`, `MacdStrategy`
#### Scenario: init 方法实现
- **WHEN** Backtest 框架初始化策略时
- **THEN** 系统调用策略类的 `init()` 方法
- **THEN** `init()` 方法 SHALL 使用 `self.I()` 注册指标
- **THEN** `self.I(lambda x: x, self.data.column_name)` SHALL 引用 DataFrame 中的指标列
- **THEN** `init()` 方法 SHALL 不执行数据计算
#### Scenario: next 方法实现 - 金叉买入
- **WHEN** 短期均线上穿长期均线(金叉)
- **THEN** `next()` 方法 SHALL 调用 `self.position.close()` 平仓
- **THEN** `next()` 方法 SHALL 调用 `self.buy()` 开多仓
- **THEN** `next()` 方法 SHALL 使用 `crossover()` 函数检测交叉
#### Scenario: next 方法实现 - 死叉卖出
- **WHEN** 短期均线下穿长期均线(死叉)
- **THEN** `next()` 方法 SHALL 调用 `self.position.close()` 平仓
- **THEN** `next()` 方法 SHALL 调用 `self.sell()` 开空仓
- **THEN** `next()` 方法 SHALL 使用 `crossover()` 函数检测交叉
#### Scenario: next 方法实现 - 避免重复开仓
- **WHEN** 策略已持有多仓,且买入信号触发
- **THEN** `next()` 方法 SHALL 先调用 `self.position.close()`
- **THEN** `next()` 方法 SHALL 再调用 `self.buy()`
- **THEN** 系统 SHALL 自动处理仓位管理(不重复开仓)
#### Scenario: 可配置策略参数
- **WHEN** 策略类定义类属性
- **THEN** 类属性 SHALL 作为策略参数(如 `short_period = 10`
- **THEN** Backtest 框架 SHALL 自动访问这些属性
- **THEN** 参数 SHALL 可通过 Backtest 构造函数覆盖
---
### Requirement: 策略类指标引用
策略类的 `init()` 方法 SHALL 正确引用 DataFrame 中计算好的指标列。
#### Scenario: 引用 SMA 指标
- **WHEN** DataFrame 包含 `sma10``sma30`
- **THEN** `init()` 方法注册 `self.sma_short = self.I(lambda x: x, self.data.sma10)`
- **THEN** `init()` 方法注册 `self.sma_long = self.I(lambda x: x, self.data.sma30)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.sma10``self.data.sma30` 访问指标
#### Scenario: 引用 MACD 指标
- **WHEN** DataFrame 包含 `macd``macd_signal`
- **THEN** `init()` 方法注册 `self.macd = self.I(lambda x: x, self.data.macd)`
- **THEN** `init()` 方法注册 `self.signal = self.I(lambda x: x, self.data.macd_signal)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.macd``self.data.macd_signal` 访问指标
#### Scenario: 引用 RSI 指标
- **WHEN** DataFrame 包含 `rsi`
- **THEN** `init()` 方法注册 `self.rsi = self.I(lambda x: x, self.data.rsi)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.rsi` 访问指标
- **THEN** 策略逻辑 SHALL 使用 RSI 阈值生成信号(如 RSI > 70 超买)
#### Scenario: 指标列不存在
- **WHEN** 策略类引用的列名不存在于 DataFrame
- **THEN** Backtest 框架抛出 KeyError
- **THEN** 主流程捕获异常并输出错误信息:"指标列 {column} 不存在"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 动态加载机制
主流程 SHALL 使用 importlib 动态加载策略文件模块。
#### Scenario: 加载顶层策略文件
- **WHEN** 用户指定 `--strategy-file strategy.py`
- **THEN** 系统使用 `spec_from_file_location('strategy', 'strategy.py')` 创建规范
- **THEN** 系统使用 `module_from_spec(spec)` 创建模块对象
- **THEN** 系统使用 `spec.loader.exec_module(module)` 执行模块
- **THEN** 系统成功获取 `module.calculate_indicators``module.get_strategy`
#### Scenario: 加载子目录策略文件
- **WHEN** 用户指定 `--strategy-file strategies/macd_strategy.py`
- **THEN** 系统使用 `spec_from_file_location('strategies.macd_strategy', 'strategies/macd_strategy.py')`
- **THEN** 模块名使用点号分隔(反映目录结构)
- **THEN** 系统成功加载子目录中的策略模块
#### Scenario: 模块命名空间隔离
- **WHEN** 系统动态加载多个策略文件
- **THEN** 每个策略模块 SHALL 有独立的命名空间
- **THEN** 模块间 SHALL 不共享全局变量
- **THEN** 系统通过 `getattr(module, name)` 明确访问函数和类
#### Scenario: 策略文件导入错误
- **WHEN** 策略文件包含语法错误或导入错误
- **THEN** `exec_module()` 抛出 ImportError 或 SyntaxError
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"策略文件 {file} 加载失败: {error}"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 策略接口验证
主流程 SHALL 验证策略文件是否符合接口要求。
#### Scenario: 验证 calculate_indicators 存在
- **WHEN** 系统加载策略模块
- **THEN** 系统使用 `hasattr(module, 'calculate_indicators')` 检查函数
- **WHEN** 函数不存在
- **THEN** 系统抛出 AttributeError
- **THEN** 主流程捕获并输出:"策略文件 {file} 缺少 calculate_indicators 函数"
#### Scenario: 验证 get_strategy 存在
- **WHEN** 系统加载策略模块
- **THEN** 系统使用 `hasattr(module, 'get_strategy')` 检查函数
- **WHEN** 函数不存在
- **THEN** 系统抛出 AttributeError
- **THEN** 主流程捕获并输出:"策略文件 {file} 缺少 get_strategy 函数"
#### Scenario: 验证 get_strategy 返回类
- **WHEN** 系统调用 `get_strategy()`
- **THEN** 系统使用 `isinstance(returned, type)` 检查返回值
- **WHEN** 返回值不是类
- **THEN** 系统抛出 TypeError
- **THEN** 主流程捕获并输出:"get_strategy() 必须返回一个类"
#### Scenario: 验证策略类继承
- **WHEN** 系统获取策略类
- **THEN** 系统使用 `issubclass(strategy_class, backtesting.Strategy)` 检查继承
- **WHEN** 策略类未继承 `backtesting.Strategy`
- **THEN** 系统抛出 TypeError
- **THEN** 主流程捕获并输出:"策略类必须继承 backtesting.Strategy"
---
### Requirement: 策略文件示例
系统 SHALL 提供策略模板文件作为开发者参考。
#### Scenario: 提供策略模板
- **WHEN** 用户查看 strategy.py 文件
- **THEN** 文件 SHALL 包含完整的策略示例SMA 双均线交叉)
- **THEN** 文件 SHALL 包含清晰的注释说明每个接口的用途
- **THEN** 文件 SHALL 包含代码示例指标计算函数、get_strategy、策略类
#### Scenario: 策略文件文档
- **WHEN** 策略文件开头有文档字符串
- **THEN** 文档 SHALL 描述策略逻辑
- **THEN** 文档 SHALL 列出需要的指标
- **THEN** 文档 SHALL 说明参数含义(如 `short_period`, `long_period`
#### Scenario: 策略参数说明
- **WHEN** 策略类定义类属性
- **THEN** 每个属性 SHALL 有注释说明(如 `short_period = 10 # 短期均线周期`
- **THEN** 参数 SHALL 使用有意义的名称(不是 param1, param2

View File

@@ -0,0 +1,366 @@
# Tasks: Refactor Backtest Script
## 1. 项目设置和依赖
- [x] 1.1 创建 requirements.txt 文件,列出所有必需的 Python 包pandas, numpy, backtesting, sqlalchemy
- [ ] 1.2 安装项目依赖pip install -r requirements.txt
- [x] 1.3 配置数据库凭证(在 backtest.py 中硬编码)
- 设置 DB_HOST = '81.71.3.24'
- 设置 DB_NAME = 'leopard_dev'
- 设置 DB_USER = 'your_username'
- 设置 DB_PASSWORD = 'your_password'
- 根据实际开发环境修改这些值
---
## 3. 策略模板实现
- [x] 3.1 创建 strategy.py 文件,包含策略模板和示例
- [x] 3.2 实现 calculate_indicators(data) 函数
- 计算 SMA10, SMA30, SMA60, SMA120 指标
- 使用 data['Close'].rolling(window=N).mean() 方法
- 将结果添加到 DataFramedata['sma10'] 等)
- 返回添加了指标列的 DataFrame
- [x] 3.3 实现 get_strategy() 函数
- 返回 SmaCross 类
- 添加函数文档字符串说明用途
- [x] 3.4 实现 SmaCross 策略类
- 继承 backtesting.Strategy
- 定义类属性short_period = 10, long_period = 30
- 实现 init() 方法:使用 self.I() 注册 sma10 和 sma30 指标
- 实现 next() 方法:使用 crossover() 检测金叉和死叉,执行买卖操作
- [x] 3.5 添加详细的代码注释和文档字符串
- 文件开头描述策略逻辑
- 每个函数添加参数和返回值说明
- 策略类参数添加注释(如 short_period 的含义)
## 4. 策略动态加载功能
- [x] 4.1 在 backtest.py 中实现 load_strategy(strategy_file) 函数
- 使用 importlib.util.spec_from_file_location() 加载模块
- 使用 importlib.util.module_from_spec() 创建模块对象
- 使用 spec.loader.exec_module() 执行模块
- [x] 4.2 实现接口验证逻辑
- 检查模块是否有 calculate_indicators 属性hasattr 检查)
- 检查模块是否有 get_strategy 属性
- 验证 get_strategy() 返回的是类对象isinstance 检查)
- 验证策略类继承自 backtesting.Strategyissubclass 检查)
- [x] 4.3 实现异常处理
- 捕获 FileNotFoundError策略文件不存在
- 捕获 ImportError模块导入失败
- 捕获 AttributeError接口不完整
- 输出清晰的错误信息:"策略文件 {file} 加载失败: {error}"
- [x] 4.4 返回策略组件
- 返回元组:(calculate_indicators 函数, strategy_class)
## 5. 命令行参数解析
- [x] 5.1 实现 parse_arguments() 函数
- 使用 argparse.ArgumentParser 创建解析器
- 添加 --code 参数必需help: 股票代码)
- 添加 --start-date 参数必需help: 回测开始日期)
- 添加 --end-date 参数必需help: 回测结束日期)
- 添加 --strategy-file 参数必需help: 策略文件路径)
- 添加 --cash 参数可选default=100000help: 初始资金)
- 添加 --commission 参数可选default=0.002help: 手续费率)
- 添加 --output 参数可选help: HTML 输出文件路径)
- 添加 --warmup-days 参数可选default=365help: 预热天数,默认一年)
- [x] 5.2 实现参数验证
- 检查日期格式YYYY-MM-DD使用 datetime.strptime() 验证
- 检查策略文件是否存在os.path.isfile()
- 验证数值参数为正数cash, commission
- [x] 5.3 添加友好的错误提示
- 参数错误时显示帮助信息
- 日期格式错误时提示正确格式
## 6. 结果输出功能
- [x] 6.1 实现 print_stats(stats) 函数
- 创建 INDICATOR_MAPPING 字典(英文键 → 中文标签)
- 遍历 stats 对象的键值对
- 使用中文标签格式化输出
- [x] 6.2 实现格式化逻辑
- 实现 format_value(value, cn_name, key) 辅助函数
- 百分比和比率类值保留 2 位小数
- 金额类值保留 2 位小数
- 次数类值取整
- 其他值保留 4 位小数
- [x] 6.3 添加输出格式化
- 输出标题:"回测结果"(使用 "=" * 60 分隔)
- 每个指标独占一行
- 确保中英文对齐美观
## 7. 主流程编排
- [x] 7.1 实现 main() 函数,编排完整流程
- 调用 parse_arguments() 解析参数
- 调用 load_data_from_db() 加载数据
- 调用 load_strategy() 加载策略
- 调用 calculate_indicators() 计算指标
- 创建 Backtest 对象并执行
- 调用 print_stats() 输出结果
- [x] 7.2 添加进度提示信息
- 数据加载前:输出 "加载股票数据: {code} ({start_date} ~ {end_date})"
- 数据加载后:输出 "数据加载完成,共 {N} 条记录"
- 策略加载前:输出 "加载策略: {strategy_file}"
- 指标计算后:输出 "指标计算完成"
- 回测开始:输出 "开始回测..."
- 回测完成:输出 "回测完成!"
- [x] 7.3 实现回测执行
- 使用 Backtest(data, strategy_class, cash=args.cash, commission=args.commission, finalize_trades=True)
- 调用 bt.run() 执行回测
- 保存返回的 stats 对象
## 8. HTML 图表生成
- [x] 8.1 实现可选的图表生成逻辑
- 检查 args.output 参数是否指定
- 仅当指定时才调用 bt.plot()
- [x] 8.2 生成 HTML 图表文件
- 使用 bt.plot(filename=args.output, show=False) 生成文件
- show=False 确保在无头环境中也能生成
- 输出提示:"图表已保存到: {filepath}"
- [x] 8.3 添加异常处理
- 捕获图表生成异常
- 输出警告:"图表生成失败,但回测已完成: {error}"
- 不影响统计信息的正常输出
- 确保主流程正常退出(状态码 0
## 9. 全局错误处理
- [x] 9.1 在 main() 函数外层添加 try-except
- 捕获所有未预期的异常
- 输出错误信息和堆栈跟踪traceback.print_exc()
- 使用非零状态码退出
- [x] 9.2 实现特定错误的状态码映射
- 数据库错误:状态码 2
- 文件操作错误:状态码 3
- 参数错误:状态码 4
- 其他错误:状态码 1
- [x] 9.3 添加 `if __name__ == '__main__':` 入口
- 调用 main() 函数
- 确保脚本可直接执行和作为模块导入
## 10. 文档和示例(可选)
- [ ] 10.1 创建 README.md 文档(可选)
- [ ] 10.2 添加内联文档到 backtest.py
- [ ] 10.3 添加使用示例到 README
## 11. 测试和验证
- [ ] 11.1 测试基础回测流程
- [ ] 11.2 测试 HTML 图表生成
- [ ] 11.3 测试错误处理
- [ ] 11.4 测试不同策略
- [ ] 11.5 验证输出格式
## 12. 代码质量检查
- [ ] 12.1 运行代码检查工具(可选)
- [ ] 12.2 验证依赖版本兼容性
- [ ] 12.3 最终代码审查
---
## 3. 策略模板实现
- [x] 3.1 创建 strategy.py 文件,包含策略模板和示例
- [x] 3.2 实现 calculate_indicators(data) 函数
- 计算 SMA10, SMA30, SMA60, SMA120 指标
- 使用 data['Close'].rolling(window=N).mean() 方法
- 将结果添加到 DataFramedata['sma10'] 等)
- 返回添加了指标列的 DataFrame
- [x] 3.3 实现 get_strategy() 函数
- 返回 SmaCross 类
- 添加函数文档字符串说明用途
- [x] 3.4 实现 SmaCross 策略类
- 继承 backtesting.Strategy
- 定义类属性short_period = 10, long_period = 30
- 实现 init() 方法:使用 self.I() 注册 sma10 和 sma30 指标
- 实现 next() 方法:使用 crossover() 检测金叉和死叉,执行买卖操作
- [x] 3.5 添加详细的代码注释和文档字符串
- 文件开头描述策略逻辑
- 每个函数添加参数和返回值说明
- 策略类参数添加注释(如 short_period 的含义)
---
## 4. 策略动态加载功能
- [x] 4.1 在 backtest.py 中实现 load_strategy(strategy_file) 函数
- 使用 importlib.util.spec_from_file_location() 加载模块
- 使用 importlib.util.module_from_spec() 创建模块对象
- 使用 spec.loader.exec_module() 执行模块
- [x] 4.2 实现接口验证逻辑
- 检查模块是否有 calculate_indicators 属性hasattr 检查)
- 检查模块是否有 get_strategy 属性
- 验证 get_strategy() 返回的是类对象isinstance 检查)
- 验证策略类继承自 backtesting.Strategyissubclass 检查)
- [x] 4.3 实现异常处理
- 捕获 FileNotFoundError策略文件不存在
- 捕获 ImportError模块导入失败
- 捕获 AttributeError接口不完整
- 输出清晰的错误信息:"策略文件 {file} 加载失败: {error}"
- [x] 4.4 返回策略组件
- 返回元组:(calculate_indicators 函数, strategy_class)
---
## 5. 命令行参数解析
- [x] 5.1 实现 parse_arguments() 函数
- 使用 argparse.ArgumentParser 创建解析器
- 添加 --code 参数必需help: 股票代码)
- 添加 --start-date 参数必需help: 回测开始日期)
- 添加 --end-date 参数必需help: 回测结束日期)
- 添加 --strategy-file 参数必需help: 策略文件路径)
- 添加 --cash 参数可选default=100000help: 初始资金)
- 添加 --commission 参数可选default=0.002help: 手续费率)
- 添加 --output 参数可选help: HTML 输出文件路径)
- 添加 --warmup-days 参数可选default=365help: 预热天数,默认一年)
- [x] 5.2 实现参数验证
- 检查日期格式YYYY-MM-DD使用 datetime.strptime() 验证
- 检查策略文件是否存在os.path.isfile()
- 验证数值参数为正数cash, commission
- [x] 5.3 添加友好的错误提示
- 参数错误时显示帮助信息
- 日期格式错误时提示正确格式
---
## 6. 结果输出功能
- [x] 6.1 实现 print_stats(stats) 函数
- 创建 INDICATOR_MAPPING 字典(英文键 → 中文标签)
- 遍历 stats 对象的键值对
- 使用中文标签格式化输出
- [x] 6.2 实现格式化逻辑
- 实现 format_value(value, cn_name, key) 辅助函数
- 百分比和比率类值保留 2 位小数
- 金额类值保留 2 位小数
- 次数类值取整
- 其他值保留 4 位小数
- [x] 6.3 添加输出格式化
- 输出标题:"回测结果"(使用 "=" * 60 分隔)
- 每个指标独占一行
- 确保中英文对齐美观
---
## 7. 主流程编排
- [x] 7.1 实现 main() 函数,编排完整流程
- 调用 parse_arguments() 解析参数
- 调用 load_data_from_db() 加载数据
- 调用 load_strategy() 加载策略
- 调用 calculate_indicators() 计算指标
- 创建 Backtest 对象并执行
- 调用 print_stats() 输出结果
- [x] 7.2 添加进度提示信息
- 数据加载前:输出 "加载股票数据: {code} ({start_date} ~ {end_date})"
- 数据加载后:输出 "数据加载完成,共 {N} 条记录"
- 策略加载前:输出 "加载策略: {strategy_file}"
- 指标计算后:输出 "指标计算完成"
- 回测开始:输出 "开始回测..."
- 回测完成:输出 "回测完成!"
- [x] 7.3 实现回测执行
- 使用 Backtest(data, strategy_class, cash=args.cash, commission=args.commission, finalize_trades=True)
- 调用 bt.run() 执行回测
- 保存返回的 stats 对象
- [x] 8.1 实现可选的图表生成逻辑
- 检查 args.output 参数是否指定
- 仅当指定时才调用 bt.plot()
- [x] 8.2 生成 HTML 图表文件
- 使用 bt.plot(filename=args.output, show=False) 生成文件
- show=False 确保在无头环境中也能生成
- 输出提示:"图表已保存到: {filepath}"
- [x] 8.3 添加异常处理
- 捕获图表生成异常
- 输出警告:"图表生成失败,但回测已完成: {error}"
- 不影响统计信息的正常输出
- 确保主流程正常退出(状态码 0
---
## 9. 全局错误处理
- [ ] 9.1 在 main() 函数外层添加 try-except
- 捕获所有未预期的异常
- 输出错误信息和堆栈跟踪traceback.print_exc()
- 使用非零状态码退出
- [ ] 9.2 实现特定错误的状态码映射
- 数据库错误:状态码 2
- 文件操作错误:状态码 3
- 参数错误:状态码 4
- 其他错误:状态码 1
- [ ] 9.3 添加 `if __name__ == '__main__':` 入口
- 调用 main() 函数
- 确保脚本可直接执行和作为模块导入
---
## 10. 文档和示例
- [ ] 10.1 创建 README.md 文档(可选)
- 说明项目用途和功能
- 提供安装步骤pip install -r requirements.txt
- 提供使用示例(基础用法、自定义参数、不同策略)
- 说明策略文件接口规范
- 说明环境变量配置DB_USER, DB_PASSWORD
- [ ] 10.2 添加内联文档到 backtest.py
- 文件开头添加模块文档字符串
- 说明命令行参数和用法
- 提供使用示例
- [ ] 10.3 添加使用示例到 README
```bash
# 基础用法
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py
# 自定义参数
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py --cash 500000 --commission 0.001 --output result.html
```
---
## 11. 测试和验证
- [ ] 11.1 测试基础回测流程
- 执行 `python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py`
- 验证数据加载成功
- 验证策略加载成功
- 验证回测执行成功
- 验证统计信息输出正确
- [ ] 11.2 测试 HTML 图表生成
- 执行带 `--output` 参数的命令
- 验证 HTML 文件成功生成
- 验证图表内容正确(价格曲线、资金曲线等)
- [ ] 11.3 测试错误处理
- 测试无效股票代码(应提示未找到数据)
- 测试无效日期格式(应提示格式错误)
- 测试策略文件不存在(应提示文件不存在)
- 测试数据库连接失败(应提示连接错误)
- 测试策略接口不完整(应提示缺少函数)
- [ ] 11.4 测试不同策略
- 创建 strategies/macd_strategy.py
- 使用新策略执行回测
- 验证动态加载功能正常
- [ ] 11.5 验证输出格式
- 检查控制台输出使用中文标签
- 检查数值格式化正确(小数位数)
- 检查 HTML 文件可正常打开
---
## 12. 代码质量检查
- [ ] 12.1 运行代码检查工具(可选)
- 使用 pylint 或 flake8 检查代码风格
- 修复警告和错误
- [ ] 12.2 验证依赖版本兼容性
- 检查 backtesting 库版本兼容性
- 检查 pandas 和 numpy 版本要求
- [ ] 12.3 最终代码审查
- 对照设计文档检查实现是否完整
- 对照规范文档检查所有场景是否覆盖
- 确保代码遵循设计决策

View File

@@ -0,0 +1,195 @@
# Spec: Backtest CLI
## ADDED Requirements
### Requirement: 命令行参数解析
回测脚本 SHALL 通过命令行参数接收用户输入,参数 SHALL 包含股票代码、时间范围、策略文件、回测参数等。
#### Scenario: 基础回测执行
- **WHEN** 用户执行 `python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py`
- **THEN** 系统解析所有必需参数,无错误提示
- **THEN** 开始执行回测流程
- **THEN** 回测完成后输出统计信息到控制台
#### Scenario: 可选参数未指定
- **WHEN** 用户未指定 `--cash` 参数
- **THEN** 系统使用默认值 100000 作为初始资金
- **WHEN** 用户未指定 `--commission` 参数
- **THEN** 系统使用默认值 0.002 作为手续费率
- **WHEN** 用户未指定 `--output` 参数
- **THEN** 系统不生成 HTML 图表文件
#### Scenario: 必需参数缺失
- **WHEN** 用户未提供 `--code` 参数
- **THEN** 系统输出错误信息:"错误: 需要以下参数: --code"
- **THEN** 系统退出并返回非零状态码
- **WHEN** 用户未提供 `--start-date``--end-date` 参数
- **THEN** 系统输出对应的错误信息
- **THEN** 系统退出并返回非零状态码
#### Scenario: 自定义参数值
- **WHEN** 用户指定 `--cash 500000 --commission 0.001 --output result.html`
- **THEN** 系统使用指定的 500000 作为初始资金
- **THEN** 系统使用指定的 0.001 作为手续费率
- **THEN** 回测完成后生成 HTML 图表到 result.html
---
### Requirement: 数据库数据加载
回测脚本 SHALL 从 PostgreSQL 数据库加载指定股票的历史价格数据,并自动处理复权。
#### Scenario: 成功加载数据
- **WHEN** 用户指定有效的股票代码和时间范围
- **THEN** 系统连接数据库并执行查询
- **THEN** 返回 DataFrame包含列: [Open, High, Low, Close, Volume, factor]
- **THEN** DataFrame 的索引为 trade_date (DatetimeIndex)
- **THEN** 数据已应用复权计算price * factor
#### Scenario: 数据库连接失败
- **WHEN** 数据库连接失败(凭证错误、网络问题等)
- **THEN** 系统捕获异常并输出错误信息:"数据库连接失败: {error}"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 未找到股票数据
- **WHEN** 指定的股票代码或时间范围内无数据
- **THEN** 系统抛出 ValueError: "未找到股票 {code} 在指定时间范围内的数据"
- **THEN** 主流程捕获异常并输出友好错误信息
- **THEN** 系统退出并返回非零状态码
#### Scenario: 数据验证
- **WHEN** 数据库返回的 DataFrame 为空
- **THEN** 系统提示数据为空并退出
- **WHEN** 数据库返回的 DataFrame 少于 10 条记录
- **THEN** 系统提示数据不足并退出
---
### Requirement: 策略动态加载
回测脚本 SHALL 支持动态加载指定路径的策略文件,并验证策略接口。
#### Scenario: 加载有效策略文件
- **WHEN** 用户指定 `--strategy-file strategy.py`
- **THEN** 系统通过 importlib 加载该模块
- **THEN** 系统获取模块的 `calculate_indicators` 函数
- **THEN** 系统调用模块的 `get_strategy()` 函数获取策略类
- **THEN** 系统返回 (calculate_indicators, strategy_class) 元组
#### Scenario: 策略文件不存在
- **WHEN** 用户指定的策略文件路径不存在
- **THEN** 系统捕获 FileNotFoundError
- **THEN** 输出错误信息:"策略文件 {file} 不存在"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 策略接口不完整
- **WHEN** 策略文件缺少 `calculate_indicators` 函数
- **THEN** 系统捕获 AttributeError
- **THEN** 输出错误信息:"策略文件 {file} 缺少 calculate_indicators 函数"
- **THEN** 系统退出并返回非零状态码
- **WHEN** 策略文件缺少 `get_strategy` 函数
- **THEN** 系统捕获 AttributeError
- **THEN** 输出错误信息:"策略文件 {file} 缺少 get_strategy 函数"
- **THEN** 系统退出并返回非零状态码
#### Scenario: 加载子目录中的策略
- **WHEN** 用户指定 `--strategy-file strategies/macd_strategy.py`
- **THEN** 系统正确加载子目录中的策略模块
- **THEN** 系统成功获取策略类和指标计算函数
---
### Requirement: 指标计算
回测脚本 SHALL 在执行回测前调用策略的指标计算函数,将技术指标添加到数据集中。
#### Scenario: 成功计算指标
- **WHEN** 系统调用 `calculate_indicators(data)`
- **THEN** 函数接收包含 [Open, High, Low, Close, Volume, factor] 的 DataFrame
- **THEN** 函数计算策略所需的指标(如 SMA, MACD, RSI
- **THEN** 函数返回添加了指标列的 DataFrame
- **THEN** DataFrame 保留原始列,新增指标列
#### Scenario: 指标计算产生 NaN 值
- **WHEN** 滚动窗口计算导致前 N 行的指标值为 NaN
- **THEN** DataFrame 包含 NaN 值(系统不自动删除)
- **THEN** Backtest 框架在回测时会跳过 NaN 值的行
#### Scenario: 指标计算函数抛出异常
- **WHEN** `calculate_indicators(data)` 执行时抛出异常
- **THEN** 主流程捕获异常
- **THEN** 输出错误信息:"指标计算失败: {error}"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 回测执行
回测脚本 SHALL 使用 backtesting 库执行回测,传入数据、策略和参数。
#### Scenario: 成功执行回测
- **WHEN** 系统调用 `Backtest(data, strategy_class, cash=..., commission=...).run()`
- **THEN** Backtest 初始化时调用策略类的 `init()` 方法
- **THEN** Backtest 逐个时间步调用策略类的 `next()` 方法
- **THEN** 系统返回包含回测统计信息的 stats 对象
#### Scenario: 回测参数传递
- **WHEN** 用户指定 `--cash 500000 --commission 0.001`
- **THEN** Backtest 实例化时使用 cash=500000
- **THEN** Backtest 实例化时使用 commission=0.001
- **THEN** Backtest 实例化时使用 finalize_trades=True
#### Scenario: 回测运行时错误
- **WHEN** 策略的 `next()` 方法执行时抛出异常
- **THEN** backtesting 库捕获异常
- **THEN** 系统输出错误信息和堆栈跟踪
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 结果输出
回测脚本 SHALL 将回测统计信息格式化输出到控制台,并可选生成 HTML 图表文件。
#### Scenario: 控制台输出
- **WHEN** 回测成功完成
- **THEN** 系统调用 `print_stats(stats)` 函数
- **THEN** 系统输出回测统计信息,使用中文标签
- **THEN** 输出内容包括:最终收益、总收益率、年化收益率、最大回撤、胜率等
- **THEN** 数值格式化(保留 2 位小数)
#### Scenario: 生成 HTML 图表
- **WHEN** 用户指定 `--output result.html`
- **THEN** 系统调用 `bt.plot(filename='result.html', show=False)`
- **THEN** 系统生成 HTML 文件到 result.html
- **THEN** 系统输出提示:"图表已保存到: result.html"
- **THEN** 图表包含价格曲线、资金曲线、买卖信号等
#### Scenario: 不生成 HTML 图表
- **WHEN** 用户未指定 `--output` 参数
- **THEN** 系统不调用 bt.plot() 方法
- **THEN** 系统不生成任何图表文件
- **THEN** 系统仅输出控制台统计信息
#### Scenario: 图表生成失败
- **WHEN** bt.plot() 方法执行时抛出异常
- **THEN** 系统捕获异常
- **THEN** 系统输出警告:"图表生成失败,但回测已完成: {error}"
- **THEN** 系统不影响控制台统计信息的输出
- **THEN** 系统正常退出(返回状态码 0
---
### Requirement: 错误处理
回测脚本 SHALL 对所有可能的错误进行捕获和处理,提供友好的错误提示。
#### Scenario: 数据库错误
- **WHEN** 数据库操作抛出 sqlalchemy.exc.SQLAlchemyError
- **THEN** 系统输出错误信息:"数据库错误: {error}"
- **THEN** 系统退出并返回状态码 2
#### Scenario: 文件操作错误
- **WHEN** 图表文件保存失败(权限、磁盘空间等)
- **THEN** 系统输出错误信息:"文件操作错误: {error}"
- **THEN** 系统退出并返回状态码 3
#### Scenario: 未预期的错误
- **WHEN** 发生其他未捕获的异常
- **THEN** 系统输出错误信息:"未知错误: {error}"
- **THEN** 系统输出完整的堆栈跟踪
- **THEN** 系统退出并返回状态码 1

View File

@@ -0,0 +1,280 @@
# Spec: Data Fetching
## ADDED Requirements
### Requirement: 数据库连接配置
系统 SHALL 通过硬编码常量管理数据库连接参数(开发环境)。
#### Scenario: 使用硬编码常量
- **WHEN** 系统在 backtest.py 中定义数据库配置
- **THEN** 系统定义 DB_HOST, DB_NAME, DB_USER, DB_PASSWORD 常量
- **THEN** DB_HOST 值 SHALL 为数据库主机地址(如 '81.71.3.24'
- **THEN** DB_NAME 值 SHALL 为数据库名称(如 'leopard_dev'
- **THEN** DB_USER 值 SHALL 为数据库用户名
- **THEN** DB_PASSWORD 值 SHALL 为数据库密码
#### Scenario: 构建连接字符串
- **WHEN** 系统创建 SQLAlchemy 连接
- **THEN** 系统使用硬编码的常量构建连接字符串
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 不从环境变量读取任何凭证
#### Scenario: 修改数据库凭证
- **WHEN** 开发人员需要更换数据库或凭证
- **THEN** 开发人员直接修改 backtest.py 中的常量值
- **THEN** 修改后脚本使用新凭证连接数据库
---
### Requirement: 数据库连接建立
系统 SHALL 使用 SQLAlchemy 创建 PostgreSQL 数据库连接。
#### Scenario: 成功建立连接
- **WHEN** 凭证正确且数据库可访问
- **THEN** 系统使用 `sqlalchemy.create_engine(conn_str)` 创建引擎
- **THEN** 连接字符串格式 SHALL 为 `postgresql://{user}:{password}@{host}/{database}`
- **THEN** 系统成功创建引擎对象
- **THEN** 系统可用于执行查询
#### Scenario: 连接字符串构建
- **WHEN** 系统构建 PostgreSQL 连接字符串
- **THEN** 连接字符串 SHALL 正确编码特殊字符(密码中的 @, : 等)
- **THEN** 连接字符串 SHALL 使用标准 URI 格式
- **THEN** 连接字符串 SHALL 不包含额外选项(仅基础连接参数)
#### Scenario: 数据库连接失败
- **WHEN** 凭证错误或数据库不可达
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.OperationalError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"数据库连接失败: {error}"
- **THEN** 系统退出并返回状态码 2
#### Scenario: 连接池管理
- **WHEN** 系统创建引擎对象
- **THEN** SQLAlchemy SHALL 自动管理连接池
- **THEN** 查询后连接 SHALL 自动返回池中
- **THEN** 系统 SHALL 在查询完成后调用 `engine.dispose()` 清理
---
### Requirement: SQL 查询构建
系统 SHALL 构建参数化的 SQL 查询以获取股票历史数据。
#### Scenario: 基础查询结构
- **WHEN** 系统构建查询
- **THEN** 查询 SHALL 选择 trade_date, Open, High, Low, Close, Volume, factor
- **THEN** 查询 SHALL 连接 leopard_daily 和 leopard_stock 表
- **THEN** 查询 SHALL 按 stock.code 过滤
- **THEN** 查询 SHALL 按 trade_date 范围过滤
- **THEN** 查询 SHALL 按 trade_date 升序排序
#### Scenario: 复权价格计算
- **WHEN** 系统计算复权价格
- **THEN** Open SHALL 计算为 `open * factor`
- **THEN** Close SHALL 计算为 `close * factor`
- **THEN** High SHALL 计算为 `high * factor`
- **THEN** Low SHALL 计算为 `low * factor`
- **THEN** Volume SHALL 直接使用原始值(不复权)
- **THEN** factor SHALL 使用 `COALESCE(factor, 1.0)` 处理 NULL 值
#### Scenario: 参数化股票代码
- **WHEN** 用户指定股票代码(如 '000001.SZ'
- **THEN** 查询 WHERE 子句 SHALL 使用 `stock.code = '{code}'`
- **THEN** 代码 SHALL 精确匹配(不使用 LIKE
- **THEN** 查询 SHALL 返回匹配股票的所有日线数据
#### Scenario: 参数化日期范围
- **WHEN** 用户指定开始日期 '2024-01-01' 和结束日期 '2025-12-31'
- **THEN** 查询 WHERE 子句 SHALL 使用 `BETWEEN '{start_date} 00:00:00' AND '{end_date} 23:59:59'`
- **THEN** 00:00:00 和 23:59:59 SHALL 覆盖全天
- **THEN** 日期格式 SHALL 为 YYYY-MM-DD HH:MM:SS
#### Scenario: 完整 SQL 查询
- **WHEN** 系统执行数据加载
- **THEN** 查询 SHALL 为:
```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
```
---
### Requirement: 数据查询执行
系统 SHALL 使用 pandas 的 `read_sql` 函数执行 SQL 查询并返回 DataFrame。
#### Scenario: 成功执行查询
- **WHEN** SQL 查询有效且数据存在
- **THEN** 系统调用 `pd.read_sql(query, engine)`
- **THEN** 系统返回 DataFrame 对象
- **THEN** DataFrame SHALL 包含查询结果的所有列
- **THEN** DataFrame 行数 SHALL 匹配数据库返回的记录数
#### Scenario: 数据类型处理
- **WHEN** pandas 读取 SQL 结果
- **THEN** trade_date SHALL 自动转换为 datetime 类型
- **THEN** Open, High, Low, Close, Volume SHALL 为 float 类型
- **THEN** factor SHALL 为 float 类型
- **THEN** 系统不需要手动类型转换(除日期索引设置)
#### Scenario: 查询返回空结果
- **WHEN** 指定股票代码或日期范围无数据
- **THEN** `read_sql` 返回空 DataFrame0 行)
- **THEN** 系统检查 `len(df) == 0`
- **THEN** 系统抛出 ValueError: "未找到股票 {code} 在指定时间范围内的数据"
#### Scenario: SQL 语法错误
- **WHEN** SQL 查询包含语法错误
- **THEN** SQLAlchemy 抛出 `sqlalchemy.exc.ProgrammingError`
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"SQL 查询错误: {error}"
- **THEN** 系统退出并返回状态码 2
---
### Requirement: 数据格式转换
系统 SHALL 将查询结果转换为 backtesting 库要求的格式。
#### Scenario: 设置日期索引
- **WHEN** DataFrame 加载完成
- **THEN** 系统调用 `df.set_index('trade_date', inplace=True)`
- **THEN** DataFrame 的索引 SHALL 为 DatetimeIndex
- **THEN** 索引 SHALL 不再是数值索引
- **THEN** backtesting 库 SHALL 能正确处理日期范围
#### Scenario: 列名格式化
- **WHEN** DataFrame 加载完成
- **THEN** 列名 SHALL 为 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor']
- **THEN** 列名 SHALL 遵循 backtesting 库要求(首字母大写)
- **THEN** 列名 SHALL 与 SQL 查询中的别名一致
#### Scenario: 数据验证
- **WHEN** 系统准备返回 DataFrame
- **THEN** 系统验证 DataFrame 包含必需列
- **THEN** 系统验证 'Open', 'High', 'Low', 'Close', 'Volume' 列存在
- **THEN** 系统验证索引为 DatetimeIndex
- **WHEN** 验证失败
- **THEN** 系统抛出 ValueError: "数据格式不符合要求"
---
### Requirement: 数据清理
系统 SHALL 清理数据以确保回测质量。
#### Scenario: 删除 NULL 值行
- **WHEN** DataFrame 包含 NULL 或 NaN 值
- **THEN** 系统调用 `df.dropna()` 删除
- **THEN** 任何包含 NaN 的行 SHALL 被删除
- **THEN** 返回的 DataFrame SHALL 不包含 NULL 值
#### Scenario: 数据完整性检查
- **WHEN** DataFrame 加载完成
- **THEN** 系统检查 trade_date 连续性
- **THEN** 系统检查无重复日期
- **WHEN** 发现异常
- **THEN** 系统输出警告:"数据存在异常: {detail}"
#### Scenario: 最小数据量验证
- **WHEN** DataFrame 行数少于 10
- **THEN** 系统输出错误:"数据不足,至少需要 10 天数据"
- **THEN** 系统抛出 ValueError
- **THEN** 主流程捕获并退出
---
### Requirement: 资源管理
系统 SHALL 正确管理数据库连接和内存资源。
#### Scenario: 引擎创建和清理
- **WHEN** 系统开始数据加载
- **THEN** 系统创建 SQLAlchemy 引擎对象
- **THEN** 系统使用引擎执行查询
- **WHEN** 查询完成
- **THEN** 系统调用 `engine.dispose()` 关闭连接池
- **THEN** 系统释放所有数据库连接
#### Scenario: 异常情况下的资源清理
- **WHEN** 查询过程中抛出异常
- **THEN** 系统在 finally 块中调用 `engine.dispose()`
- **THEN** 所有连接 SHALL 被正确关闭
- **THEN** 系统不会泄漏数据库连接
---
### Requirement: 错误处理和日志
系统 SHALL 提供清晰的错误信息和调试支持。
#### Scenario: 连接错误信息
- **WHEN** 数据库连接失败
- **THEN** 错误信息 SHALL 包含数据库主机和端口
- **THEN** 错误信息 SHALL 区分网络错误和认证错误
- **THEN** 系统提示用户检查凭证和网络连接
#### Scenario: 查询错误信息
- **WHEN** SQL 查询失败
- **THEN** 错误信息 SHALL 包含失败的 SQL 语句
- **THEN** 错误信息 SHALL 包含数据库返回的错误详情
- **THEN** 系统提示用户检查表结构和数据
#### Scenario: 数据格式错误信息
- **WHEN** 返回的 DataFrame 不符合要求
- **THEN** 错误信息 SHALL 列出缺失的列
- **THEN** 错误信息 SHALL 提示期望的格式
- **THEN** 系统建议用户检查数据库表结构
---
### Requirement: 函数接口
`load_data_from_db` 函数 SHALL 提供清晰的调用接口。
#### Scenario: 函数签名
- **WHEN** 主流程调用 `load_data_from_db(code, start_date, end_date)`
- **THEN** 函数接收三个字符串参数
- **THEN** `code` 为股票代码(如 '000001.SZ'
- **THEN** `start_date` 为开始日期(如 '2024-01-01'
- **THEN** `end_date` 为结束日期(如 '2025-12-31'
#### Scenario: 返回值
- **WHEN** 数据加载成功
- **THEN** 函数返回 pandas.DataFrame
- **THEN** DataFrame 索引为 DatetimeIndextrade_date
- **THEN** DataFrame 包含 ['Open', 'High', 'Low', 'Close', 'Volume', 'factor'] 列
#### Scenario: 异常抛出
- **WHEN** 数据加载失败
- **THEN** 函数 SHALL 抛出异常(不捕获)
- **THEN** 异常类型 SHALL 为 ValueError业务逻辑错误
- **THEN** 主流程负责捕获和处理异常
---
### Requirement: 性能考虑
系统 SHALL 优化数据加载性能以支持大数据集。
#### Scenario: 使用 pandas 向量化操作
- **WHEN** 执行复权计算
- **THEN** 计算 SHALL 使用 pandas 向量化操作
- **THEN** 不使用循环逐行计算
- **THEN** 10 年数据(约 2500 行) SHALL 在 1 秒内加载
#### Scenario: 索引优化
- **WHEN** 设置 DataFrame 索引
- **THEN** `set_index()` 操作 SHALL 高效(使用底层数组拷贝)
- **THEN** 日期索引 SHALL 支持快速范围查询
#### Scenario: 内存管理
- **WHEN** 加载大数据集
- **THEN** 系统 SHALL 及时调用 `engine.dispose()` 释放连接
- **THEN** DataFrame SHALL 使用 pandas 内部优化存储
- **THEN** 内存占用 SHALL 合理10 年数据约几 MB

View File

@@ -0,0 +1,225 @@
# Spec: Strategy Loading
## ADDED Requirements
### Requirement: 策略文件接口
策略文件 SHALL 提供两个必需的接口:指标计算函数和策略类获取函数。
#### Scenario: 标准策略文件结构
- **WHEN** 用户创建策略文件
- **THEN** 文件 SHALL 包含 `calculate_indicators(data)` 函数
- **THEN** 文件 SHALL 包含 `get_strategy()` 函数
- **THEN** 文件 SHALL 包含一个继承 `backtesting.Strategy` 的类
- **THEN** 所有三个组件 SHALL 在同一文件中
#### Scenario: calculate_indicators 函数签名
- **WHEN** 主流程调用 `calculate_indicators(data)`
- **THEN** 函数接收一个参数data (pandas.DataFrame)
- **THEN** 函数返回一个 pandas.DataFrame
- **THEN** 返回的 DataFrame SHALL 包含原始列和新增的指标列
- **THEN** 函数 SHALL 修改输入的 DataFrame不创建副本
#### Scenario: get_strategy 函数签名
- **WHEN** 主流程调用 `get_strategy()`
- **THEN** 函数不接收参数
- **THEN** 函数返回一个类对象
- **THEN** 返回的类 SHALL 继承自 `backtesting.Strategy`
---
### Requirement: 指标计算函数
`calculate_indicators` 函数 SHALL 计算策略所需的技术指标,并将结果添加到 DataFrame 中。
#### Scenario: SMA 指标计算
- **WHEN** 策略需要简单移动平均线指标
- **THEN** 函数使用 `data['Close'].rolling(window=N).mean()` 计算
- **THEN** 函数将结果存储为 `data['smaN']`
- **THEN** N 为具体的周期(如 10, 30, 60, 120
#### Scenario: MACD 指标计算
- **WHEN** 策略需要 MACD 指标
- **THEN** 函数使用 `data['Close'].ewm(span=12).mean()` 计算 EMA12
- **THEN** 函数使用 `data['Close'].ewm(span=26).mean()` 计算 EMA26
- **THEN** 函数计算 MACD = EMA12 - EMA26
- **THEN** 函数计算 Signal = MACD.ewm(span=9).mean()
- **THEN** 函数将结果存储为 `data['macd']`, `data['macd_signal']`, `data['macd_hist']`
#### Scenario: RSI 指标计算
- **WHEN** 策略需要 RSI 指标
- **THEN** 函数计算价格变化 delta = data['Close'].diff()
- **THEN** 函数计算 gain = delta.where(delta > 0, 0)
- **THEN** 函数计算 loss = -delta.where(delta < 0, 0)
- **THEN** 函数计算平均收益和平均损失
- **THEN** 函数计算 RS = average_gain / average_loss
- **THEN** 函数计算 RSI = 100 - (100 / (1 + RS))
- **THEN** 函数将结果存储为 `data['rsi']`
#### Scenario: 多指标计算
- **WHEN** 策略需要多个技术指标
- **THEN** 函数按顺序计算每个指标
- **THEN** 函数将所有指标列添加到 DataFrame
- **THEN** DataFrame 最终包含原始列 + 所有指标列
- **THEN** 计算顺序 SHALL 遵循指标间的依赖关系(如 MACD 依赖 EMA
#### Scenario: 指标列命名约定
- **WHEN** 函数添加指标列到 DataFrame
- **THEN** 列名 SHALL 使用小写和下划线(如 `sma10`, `macd_signal`
- **THEN** 列名 SHALL 与策略类的 `init()` 方法中引用的名称一致
- **THEN** 列名 SHALL 避免与原始列冲突
---
### Requirement: 策略类定义
策略类 SHALL 继承 `backtesting.Strategy`,并实现 `init()``next()` 方法。
#### Scenario: 策略类继承
- **WHEN** 用户定义策略类
- **THEN** 类 SHALL 显式继承 `backtesting.Strategy`
- **THEN** 类 SHALL 定义类属性作为可配置参数
- **THEN** 类名 SHALL 使用大驼峰命名(如 `SmaCross`, `MacdStrategy`
#### Scenario: init 方法实现
- **WHEN** Backtest 框架初始化策略时
- **THEN** 系统调用策略类的 `init()` 方法
- **THEN** `init()` 方法 SHALL 使用 `self.I()` 注册指标
- **THEN** `self.I(lambda x: x, self.data.column_name)` SHALL 引用 DataFrame 中的指标列
- **THEN** `init()` 方法 SHALL 不执行数据计算
#### Scenario: next 方法实现 - 金叉买入
- **WHEN** 短期均线上穿长期均线(金叉)
- **THEN** `next()` 方法 SHALL 调用 `self.position.close()` 平仓
- **THEN** `next()` 方法 SHALL 调用 `self.buy()` 开多仓
- **THEN** `next()` 方法 SHALL 使用 `crossover()` 函数检测交叉
#### Scenario: next 方法实现 - 死叉卖出
- **WHEN** 短期均线下穿长期均线(死叉)
- **THEN** `next()` 方法 SHALL 调用 `self.position.close()` 平仓
- **THEN** `next()` 方法 SHALL 调用 `self.sell()` 开空仓
- **THEN** `next()` 方法 SHALL 使用 `crossover()` 函数检测交叉
#### Scenario: next 方法实现 - 避免重复开仓
- **WHEN** 策略已持有多仓,且买入信号触发
- **THEN** `next()` 方法 SHALL 先调用 `self.position.close()`
- **THEN** `next()` 方法 SHALL 再调用 `self.buy()`
- **THEN** 系统 SHALL 自动处理仓位管理(不重复开仓)
#### Scenario: 可配置策略参数
- **WHEN** 策略类定义类属性
- **THEN** 类属性 SHALL 作为策略参数(如 `short_period = 10`
- **THEN** Backtest 框架 SHALL 自动访问这些属性
- **THEN** 参数 SHALL 可通过 Backtest 构造函数覆盖
---
### Requirement: 策略类指标引用
策略类的 `init()` 方法 SHALL 正确引用 DataFrame 中计算好的指标列。
#### Scenario: 引用 SMA 指标
- **WHEN** DataFrame 包含 `sma10``sma30`
- **THEN** `init()` 方法注册 `self.sma_short = self.I(lambda x: x, self.data.sma10)`
- **THEN** `init()` 方法注册 `self.sma_long = self.I(lambda x: x, self.data.sma30)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.sma10``self.data.sma30` 访问指标
#### Scenario: 引用 MACD 指标
- **WHEN** DataFrame 包含 `macd``macd_signal`
- **THEN** `init()` 方法注册 `self.macd = self.I(lambda x: x, self.data.macd)`
- **THEN** `init()` 方法注册 `self.signal = self.I(lambda x: x, self.data.macd_signal)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.macd``self.data.macd_signal` 访问指标
#### Scenario: 引用 RSI 指标
- **WHEN** DataFrame 包含 `rsi`
- **THEN** `init()` 方法注册 `self.rsi = self.I(lambda x: x, self.data.rsi)`
- **THEN** `next()` 方法 SHALL 通过 `self.data.rsi` 访问指标
- **THEN** 策略逻辑 SHALL 使用 RSI 阈值生成信号(如 RSI > 70 超买)
#### Scenario: 指标列不存在
- **WHEN** 策略类引用的列名不存在于 DataFrame
- **THEN** Backtest 框架抛出 KeyError
- **THEN** 主流程捕获异常并输出错误信息:"指标列 {column} 不存在"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 动态加载机制
主流程 SHALL 使用 importlib 动态加载策略文件模块。
#### Scenario: 加载顶层策略文件
- **WHEN** 用户指定 `--strategy-file strategy.py`
- **THEN** 系统使用 `spec_from_file_location('strategy', 'strategy.py')` 创建规范
- **THEN** 系统使用 `module_from_spec(spec)` 创建模块对象
- **THEN** 系统使用 `spec.loader.exec_module(module)` 执行模块
- **THEN** 系统成功获取 `module.calculate_indicators``module.get_strategy`
#### Scenario: 加载子目录策略文件
- **WHEN** 用户指定 `--strategy-file strategies/macd_strategy.py`
- **THEN** 系统使用 `spec_from_file_location('strategies.macd_strategy', 'strategies/macd_strategy.py')`
- **THEN** 模块名使用点号分隔(反映目录结构)
- **THEN** 系统成功加载子目录中的策略模块
#### Scenario: 模块命名空间隔离
- **WHEN** 系统动态加载多个策略文件
- **THEN** 每个策略模块 SHALL 有独立的命名空间
- **THEN** 模块间 SHALL 不共享全局变量
- **THEN** 系统通过 `getattr(module, name)` 明确访问函数和类
#### Scenario: 策略文件导入错误
- **WHEN** 策略文件包含语法错误或导入错误
- **THEN** `exec_module()` 抛出 ImportError 或 SyntaxError
- **THEN** 主流程捕获异常
- **THEN** 系统输出错误信息:"策略文件 {file} 加载失败: {error}"
- **THEN** 系统退出并返回非零状态码
---
### Requirement: 策略接口验证
主流程 SHALL 验证策略文件是否符合接口要求。
#### Scenario: 验证 calculate_indicators 存在
- **WHEN** 系统加载策略模块
- **THEN** 系统使用 `hasattr(module, 'calculate_indicators')` 检查函数
- **WHEN** 函数不存在
- **THEN** 系统抛出 AttributeError
- **THEN** 主流程捕获并输出:"策略文件 {file} 缺少 calculate_indicators 函数"
#### Scenario: 验证 get_strategy 存在
- **WHEN** 系统加载策略模块
- **THEN** 系统使用 `hasattr(module, 'get_strategy')` 检查函数
- **WHEN** 函数不存在
- **THEN** 系统抛出 AttributeError
- **THEN** 主流程捕获并输出:"策略文件 {file} 缺少 get_strategy 函数"
#### Scenario: 验证 get_strategy 返回类
- **WHEN** 系统调用 `get_strategy()`
- **THEN** 系统使用 `isinstance(returned, type)` 检查返回值
- **WHEN** 返回值不是类
- **THEN** 系统抛出 TypeError
- **THEN** 主流程捕获并输出:"get_strategy() 必须返回一个类"
#### Scenario: 验证策略类继承
- **WHEN** 系统获取策略类
- **THEN** 系统使用 `issubclass(strategy_class, backtesting.Strategy)` 检查继承
- **WHEN** 策略类未继承 `backtesting.Strategy`
- **THEN** 系统抛出 TypeError
- **THEN** 主流程捕获并输出:"策略类必须继承 backtesting.Strategy"
---
### Requirement: 策略文件示例
系统 SHALL 提供策略模板文件作为开发者参考。
#### Scenario: 提供策略模板
- **WHEN** 用户查看 strategy.py 文件
- **THEN** 文件 SHALL 包含完整的策略示例SMA 双均线交叉)
- **THEN** 文件 SHALL 包含清晰的注释说明每个接口的用途
- **THEN** 文件 SHALL 包含代码示例指标计算函数、get_strategy、策略类
#### Scenario: 策略文件文档
- **WHEN** 策略文件开头有文档字符串
- **THEN** 文档 SHALL 描述策略逻辑
- **THEN** 文档 SHALL 列出需要的指标
- **THEN** 文档 SHALL 说明参数含义(如 `short_period`, `long_period`
#### Scenario: 策略参数说明
- **WHEN** 策略类定义类属性
- **THEN** 每个属性 SHALL 有注释说明(如 `short_period = 10 # 短期均线周期`
- **THEN** 参数 SHALL 使用有意义的名称(不是 param1, param2