完成回测脚本
This commit is contained in:
310
backtest.py
Normal file
310
backtest.py
Normal 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()
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
@@ -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 等工具函数
|
||||
```
|
||||
@@ -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 文件形式保存,便于分享和查看
|
||||
@@ -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
|
||||
@@ -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` 返回空 DataFrame(0 行)
|
||||
- **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 索引为 DatetimeIndex(trade_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)
|
||||
@@ -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)
|
||||
@@ -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() 方法
|
||||
- 将结果添加到 DataFrame(data['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.Strategy(issubclass 检查)
|
||||
- [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=100000,help: 初始资金)
|
||||
- 添加 --commission 参数(可选,default=0.002,help: 手续费率)
|
||||
- 添加 --output 参数(可选,help: HTML 输出文件路径)
|
||||
- 添加 --warmup-days 参数(可选,default=365,help: 预热天数,默认一年)
|
||||
- [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() 方法
|
||||
- 将结果添加到 DataFrame(data['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.Strategy(issubclass 检查)
|
||||
- [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=100000,help: 初始资金)
|
||||
- 添加 --commission 参数(可选,default=0.002,help: 手续费率)
|
||||
- 添加 --output 参数(可选,help: HTML 输出文件路径)
|
||||
- 添加 --warmup-days 参数(可选,default=365,help: 预热天数,默认一年)
|
||||
- [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 最终代码审查
|
||||
- 对照设计文档检查实现是否完整
|
||||
- 对照规范文档检查所有场景是否覆盖
|
||||
- 确保代码遵循设计决策
|
||||
195
openspec/specs/backtest-cli/spec.md
Normal file
195
openspec/specs/backtest-cli/spec.md
Normal 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
|
||||
280
openspec/specs/data-fetching/spec.md
Normal file
280
openspec/specs/data-fetching/spec.md
Normal 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` 返回空 DataFrame(0 行)
|
||||
- **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 索引为 DatetimeIndex(trade_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)
|
||||
225
openspec/specs/strategy-loading/spec.md
Normal file
225
openspec/specs/strategy-loading/spec.md
Normal 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)
|
||||
@@ -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
87
strategy.py
Normal 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
49
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user