1
0
Files
2026-01-27 18:30:41 +08:00

226 lines
9.9 KiB
Markdown
Raw Permalink Blame History

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