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