# 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)