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

310
backtest.py Normal file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
"""
量化回测主程序
使用方法:
python backtest.py --code 000001.SZ --start-date 2024-01-01 --end-date 2025-12-31 --strategy-file strategy.py
"""
import argparse
import sys
import os
import importlib.util
import pandas as pd
from datetime import datetime
from backtesting import Backtest
# 数据库配置(直接硬编码,开发环境)
DB_HOST = "81.71.3.24"
DB_PORT = 6785
DB_NAME = "leopard_dev"
DB_USER = "leopard"
DB_PASSWORD = "9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X"
def load_data_from_db(code, start_date, end_date):
"""
从数据库加载历史数据
参数:
code: 股票代码(如 '000001.SZ'
start_date: 开始日期(如 '2024-01-01'
end_date: 结束日期(如 '2025-12-31'
返回:
DataFrame, 包含列: [Open, High, Low, Close, Volume, factor]
"""
import sqlalchemy
import urllib.parse
# 构建连接字符串URL 编码密码中的特殊字符)
encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
conn_str = (
f"postgresql://{DB_USER}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)
engine = sqlalchemy.create_engine(conn_str)
try:
# 构建 SQL 查询
query = f"""
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
"""
# 执行查询
df = pd.read_sql(query, engine)
if len(df) == 0:
raise ValueError(f"未找到股票 {code} 在指定时间范围内的数据")
# 潬换日期并设置为索引
df["trade_date"] = pd.to_datetime(df["trade_date"], format="%Y-%m-%d")
df.set_index("trade_date", inplace=True)
return df
finally:
# 清理连接
engine.dispose()
def load_strategy(strategy_file):
"""
动态加载策略文件
参数:
strategy_file: 策略文件路径 (如 'strategy.py''strategies/macd.py')
返回:
(calculate_indicators, strategy_class) 元组
"""
# 获取模块名
module_name = strategy_file.replace(".py", "").replace("/", ".")
spec = importlib.util.spec_from_file_location(module_name, strategy_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 接口验证
if not hasattr(module, "calculate_indicators"):
raise AttributeError(f"策略文件 {strategy_file} 缺少 calculate_indicators 函数")
if not hasattr(module, "get_strategy"):
raise AttributeError(f"策略文件 {strategy_file} 缺少 get_strategy 函数")
calculate_indicators = module.calculate_indicators
strategy_class = module.get_strategy()
# 验证 get_strategy 返回的是类
if not isinstance(strategy_class, type):
raise TypeError("get_strategy() 必须返回一个类")
# 验证策略类继承自 backtesting.Strategy
from backtesting import Strategy
if not issubclass(strategy_class, Strategy):
raise TypeError("策略类必须继承 backtesting.Strategy")
return calculate_indicators, strategy_class
def parse_arguments():
"""
解析命令行参数
返回:
args: 命名空间对象
"""
parser = argparse.ArgumentParser(
description="量化回测工具", formatter_class=argparse.RawDescriptionHelpFormatter
)
# 必需参数
parser.add_argument(
"--code", type=str, required=True, help="股票代码 (如: 000001.SZ)"
)
parser.add_argument(
"--start-date", type=str, required=True, help="回测开始日期 (格式: YYYY-MM-DD)"
)
parser.add_argument(
"--end-date", type=str, required=True, help="回测结束日期 (格式: YYYY-MM-DD)"
)
parser.add_argument(
"--strategy-file",
type=str,
required=True,
help="策略文件路径 (如: strategy.py)",
)
# 可选参数
parser.add_argument(
"--cash", type=float, default=100000, help="初始资金 (默认: 100000)"
)
parser.add_argument(
"--commission", type=float, default=0.002, help="手续费率 (默认: 0.002)"
)
parser.add_argument(
"--output", type=str, default=None, help="HTML 输出文件路径 (可选)"
)
parser.add_argument(
"--warmup-days", type=int, default=365, help="预热天数 (默认: 365约一年"
)
return parser.parse_args()
def format_value(value, cn_name, key):
"""
格式化数值显示
"""
if isinstance(value, (int, float)):
if "%" in cn_name or key in [
"Sharpe Ratio",
"Sortino Ratio",
"Calmar Ratio",
"Profit Factor",
]:
formatted_value = f"{value:.2f}"
elif "$" in cn_name:
formatted_value = f"{value:.2f}"
elif "次数" in cn_name:
formatted_value = f"{value:.0f}"
else:
formatted_value = f"{value:.4f}"
else:
formatted_value = str(value)
return formatted_value
def print_stats(stats):
"""
打印回测统计结果
参数:
stats: backtesting 库返回的统计对象
"""
print("\n" + "=" * 60)
print("回测结果")
print("=" * 60)
# 基本指标
metrics = [
("Return (%)", "总收益率", "Return [%]"),
("Return", "总收益", "Return"),
("Sharpe Ratio", "夏普比率", "Sharpe Ratio"),
("Sortino Ratio", "索提诺比率", "Sortino Ratio"),
("Calmar Ratio", "卡尔玛比率", "Calmar Ratio"),
("Max Drawdown (%)", "最大回撤 (%)", "Max. Drawdown [%]"),
("Avg Drawdown (%)", "平均回撤 (%)", "Avg. Drawdown [%]"),
("Max Drawdown Duration", "最大回撤持续天数", "Max. Drawdown Duration"),
("Avg Drawdown Duration", "平均回撤持续天数", "Avg. Drawdown Duration"),
]
for key, cn_name, en_name in metrics:
try:
value = getattr(stats, key, None)
if value is not None:
formatted = format_value(value, cn_name, key)
print(f"{cn_name:20s}: {formatted}")
except Exception:
pass
print()
# 交易统计
trade_metrics = [
("# Trades", "总交易次数", "# Trades"),
("Win Rate [%]", "胜率 (%)", "Win Rate [%]"),
("Best Trade", "最佳交易", "Best Trade"),
("Worst Trade", "最差交易", "Worst Trade"),
("Avg Trade", "平均交易", "Avg. Trade"),
("Avg Win Trade", "平均盈利交易", "Avg. Win Trade"),
("Avg Loss Trade", "平均亏损交易", "Avg. Loss Trade"),
("Profit Factor", "盈利因子", "Profit Factor"),
("Expectancy", "期望值", "Expectancy"),
]
for key, cn_name, en_name in trade_metrics:
try:
value = getattr(stats, key, None)
if value is not None:
formatted = format_value(value, cn_name, key)
print(f"{cn_name:20s}: {formatted}")
except Exception:
pass
print("=" * 60 + "\n")
def main():
"""
主函数:编排完整回测流程
"""
try:
# 解析参数
args = parse_arguments()
# 加载数据
print(f"加载股票数据: {args.code} ({args.start_date} ~ {args.end_date})")
data = load_data_from_db(args.code, args.start_date, args.end_date)
print(f"数据加载完成,共 {len(data)} 条记录")
# 截取预热数据
warmup_data = data.iloc[-args.warmup_days :]
print(f"使用预热数据范围: {warmup_data.index[0]} ~ {warmup_data.index[-1]}")
# 加载策略
print(f"加载策略: {args.strategy_file}")
calculate_indicators, strategy_class = load_strategy(args.strategy_file)
# 计算指标
print("计算指标...")
warmup_data = calculate_indicators(warmup_data)
print("指标计算完成")
# 执行回测
print("开始回测...")
from backtesting import Backtest
bt = Backtest(
warmup_data,
strategy_class,
cash=args.cash,
commission=args.commission,
finalize_trades=True,
)
stats = bt.run()
# 输出结果
print_stats(stats)
# 生成图表
if args.output:
print(f"\n生成图表: {args.output}")
bt.plot(filename=args.output, show=False)
print(f"图表已保存到: {args.output}")
print("\n回测完成!")
except Exception as e:
print(f"\n错误: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

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

View File

@@ -13,4 +13,5 @@ dependencies = [
"pandas-stubs~=2.3.3",
"peewee~=3.19.0",
"psycopg2-binary~=2.9.11",
"sqlalchemy>=2.0.46",
]

87
strategy.py Normal file
View File

@@ -0,0 +1,87 @@
"""
SMA 双均线交叉策略
策略逻辑:
- 当短期均线上穿长期均线时 (金叉),买入
- 当短期均线下穿长期均线时 (死叉),卖出
指标计算:
- SMA10: 10 日简单移动平均线
- SMA30: 30 日简单移动平均线
- SMA60: 60 日简单移动平均线
- SMA120: 120 日简单移动平均线
"""
import pandas as pd
from backtesting import Strategy
from backtesting.lib import crossover
def calculate_indicators(data):
"""
计算策略所需的技术指标
参数:
data: DataFrame, 包含 [Open, High, Low, Close, Volume, factor]
返回:
DataFrame, 添加了指标列
"""
data = data.copy()
# 计算不同周期的移动平均线
data["sma10"] = data["Close"].rolling(window=10).mean()
data["sma30"] = data["Close"].rolling(window=30).mean()
data["sma60"] = data["Close"].rolling(window=60).mean()
data["sma120"] = data["Close"].rolling(window=120).mean()
return data
def get_strategy():
"""
返回策略类
返回:
SmaCross 类
"""
return SmaCross
class SmaCross(Strategy):
"""
SMA 双均线交叉策略
参数:
short_period: 短期均线周期 (默认: 10)
long_period: 长期均线周期 (默认: 30)
"""
# 可配置参数
short_period = 10
long_period = 30
def init(self):
"""
初始化策略
注册指标到 backtesting 框架
"""
self.sma_short = self.I(lambda x: x, self.data.sma10)
self.sma_long = self.I(lambda x: x, self.data.sma30)
def next(self):
"""
每个时间步的决策逻辑
金叉: 短期均线上穿长期均线 → 买入
死叉: 短期均线下穿长期均线 → 卖出
"""
# 金叉:短期均线上穿长期均线
if crossover(self.data.sma10, self.data.sma30):
self.position.close() # 先平掉现有仓位
self.buy() # 开多仓
# 死叉:短期均线下穿长期均线
elif crossover(self.data.sma30, self.data.sma10):
self.position.close() # 先平掉现有仓位
self.sell() # 开空仓

49
uv.lock generated
View File

@@ -405,6 +405,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
]
[[package]]
name = "greenlet"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" },
{ url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" },
{ url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" },
{ url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" },
{ url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" },
{ url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" },
{ url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" },
{ url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" },
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" },
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -873,6 +896,7 @@ dependencies = [
{ name = "pandas-stubs" },
{ name = "peewee" },
{ name = "psycopg2-binary" },
{ name = "sqlalchemy" },
]
[package.metadata]
@@ -886,6 +910,7 @@ requires-dist = [
{ name = "pandas-stubs", specifier = "~=2.3.3" },
{ name = "peewee", specifier = "~=3.19.0" },
{ name = "psycopg2-binary", specifier = "~=2.9.11" },
{ name = "sqlalchemy", specifier = ">=2.0.46" },
]
[[package]]
@@ -1583,6 +1608,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/9a/b4450ccce353e2430621b3bb571899ffe1033d5cd72c9e065110f95b1a63/soupsieve-2.8.2-py3-none-any.whl", hash = "sha256:0f4c2f6b5a5fb97a641cf69c0bd163670a0e45e6d6c01a2107f93a6a6f93c51a", size = 37016, upload-time = "2026-01-18T16:21:29.7Z" },
]
[[package]]
name = "sqlalchemy"
version = "2.0.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" },
{ url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" },
{ url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" },
{ url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" },
{ url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" },
{ url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" },
{ url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" },
{ url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" },
{ url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" },
{ url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" },
{ url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" },
{ url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" },
]
[[package]]
name = "stack-data"
version = "0.6.3"