完成回测脚本
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user