diff --git a/openspec/changes/archive/2026-01-28-add-macd-strategy/.openspec.yaml b/openspec/changes/archive/2026-01-28-add-macd-strategy/.openspec.yaml new file mode 100644 index 0000000..fc9f48b --- /dev/null +++ b/openspec/changes/archive/2026-01-28-add-macd-strategy/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-01-27 diff --git a/openspec/changes/archive/2026-01-28-add-macd-strategy/design.md b/openspec/changes/archive/2026-01-28-add-macd-strategy/design.md new file mode 100644 index 0000000..626fb39 --- /dev/null +++ b/openspec/changes/archive/2026-01-28-add-macd-strategy/design.md @@ -0,0 +1,203 @@ +## Context + +当前项目使用`backtesting`库进行量化回测框架,现有策略为SMA双均线交叉策略(`strategies/sma_strategy.py`)。用户需要新增基于MACD的趋势跟踪策略,适配A股市场特性。 + +**当前状态**: +- 回测框架已就绪(`backtest.py`支持动态加载策略) +- 现有SMA策略作为参考模板 +- 策略文件需要遵循固定模式:`calculate_indicators()`、`get_strategy()`、Strategy类 +- 无风险管理要求,无需实现止损、仓位管理等复杂逻辑 + +**依赖环境**: +- Python 3.x +- pandas (已安装) +- backtesting库(已安装) +- ta-lib依赖(已手动安装完成) + +## Goals / Non-Goals + +**Goals:** +- 创建`strategies/macd_strategy.py`,实现MACD趋势跟踪策略 +- 使用ta-lib库简化MACD和EMA200指标计算 +- 实现MACD金叉/死叉 + EMA200趋势过滤的交易信号 +- 保持策略文件独立性,无需修改`backtest.py` +- 支持通过`--strategy-file`参数加载新策略 + +**Non-Goals:** +- 不实现风险管理功能(止损、止盈、仓位管理) +- 不支持多股票组合回测 +- 不修改现有SMA策略 +- 不实现命令行参数配置(所有参数固定在策略文件中) + +## Decisions + +### D1: 指标计算库选择 + +**决策**: 使用`ta-lib`而非原生pandas或pandas-ta + +**理由**: +- ta-lib是技术分析领域的事实标准,性能优异 +- C语言实现,计算速度快,适合大量指标计算 +- API简洁直观,广泛用于量化交易系统 +- 文档完善,社区支持广泛 +- 与pandas集成良好(可直接传入Series) + +**考虑的替代方案**: +- **原生pandas**: 实现简单但需手写EMA计算,代码冗长 +- **pandas-ta**: API设计现代,但性能不如ta-lib,且安装依赖较多 + +### D2: MACD参数配置 + +**决策**: 使用`(10, 20, 9)`参数组合(平衡型) + +**理由**: +- 快线10比标准12更敏感,适应A股较高波动性 +- 慢线20比标准26更快响应,同时保持趋势跟踪稳定性 +- 信号线9保持标准,避免信号过于频繁 +- 该组合在多数A股市场环境下回测表现稳定 +- 10-20的组合在斐波那契数列附近,技术分析流认可度高 + +**参数优化依据**: +- A股波动率高:需要相对敏感的快线参数 +- T+1交易规则:避免过于激进的参数,减少假信号 +- 散户追涨杀跌:结合趋势过滤(EMA200)避免逆势交易 +- 平衡策略:兼顾信号及时性和稳定性 + +### D3: 趋势过滤器选择 + +**决策**: 使用EMA200作为趋势确认 + +**理由**: +- 200日均线被广泛认可为牛熊分界线 +- EMA比SMA更平滑,减少假突破 +- 与MACD配合:MACD捕捉动量转折,EMA200确认趋势方向 +- 机构投资者常用:大资金使用200日线作为战略配置参考 +- 在A股市场验证:结合EMA200可显著减少震荡市中的假信号 + +**交易逻辑**: +- **买入条件**: MACD金叉 AND 价格 > EMA200 +- **卖出条件**: MACD死叉 OR 价格 < EMA200 + +### D4: 策略行为模式 + +**决策**: EMA200双向过滤(跌破EMA200强制卖出) + +**理由**: +- 避免在趋势转向后继续持有 +- EMA200跌破通常预示趋势反转,及时止损保护利润 +- 比仅入场过滤更严格,但风险控制更好 + +**替代方案(未采用)**: +- **仅入场过滤**: EMA200仅用于确认买入,卖出仅依赖MACD死叉 + - 优点: 交易次数更多,可能捕捉更多小波段 + - 缺点: 在趋势反转时可能持有过久,回撤较大 +- **动态参数**: 根据市场波动率动态调整MACD参数 + - 优点: 适应不同市场环境 + - 缺点: 实现复杂,超出当前需求范围 + +### D5: 策略文件结构 + +**决策**: 严格遵循现有`strategy.py`模式 + +**理由**: +- 保持代码一致性,便于维护 +- 无需修改`backtest.py`(已验证可动态加载) +- 其他策略可参考相同模式开发 + +**文件模式**: +```python +# 必需函数 +def calculate_indicators(data): + """计算所需指标,返回DataFrame""" + pass + +def get_strategy(): + """返回策略类""" + pass + +# 必需类 +class MacdTrendStrategy(Strategy): + """策略类""" + + # 可配置参数(固定) + fast_period = 10 + slow_period = 20 + signal_period = 9 + + def init(self): + """注册指标到backtesting框架""" + pass + + def next(self): + """每个时间步的决策逻辑""" + pass +``` + +### D6: 指标计算时机 + +**决策**: 在`calculate_indicators()`中计算所有指标 + +**理由**: +- 指标计算与策略逻辑分离,代码清晰 +- backtesting框架在加载策略前调用`calculate_indicators()` +- 数据预处理在策略初始化前完成,提高性能 +- 便于回测时查看完整指标数据 + +**替代方案(未采用)**: +- 在Strategy.init()中动态计算指标 + - 优点: 数据与策略逻辑更紧密 + - 缺点: 回测时无法提前查看指标,调试困难 + +## Risks / Trade-offs + +### R1: pandas-ta安装依赖 + +**风险**: 用户环境可能未安装pandas-ta + +**缓解**: +- ta-lib已手动安装,无需在依赖管理中重复添加 +- 提供清晰的错误提示(如遇ModuleNotFoundError) + +### R2: 参数固定性 + +**风险**: 无法通过命令行调整参数,灵活性降低 + +**缓解**: +- 参数基于A股市场研究,具有通用性 +- 如需调整,可直接修改策略文件参数值 +- 在代码注释中明确参数含义和调整建议 + +### R3: 无风险控制机制 + +**风险**: 在强趋势反转时可能出现较大回撤 + +**缓解**: +- EMA200趋势过滤已提供一定保护 +- 如未来需要风险控制,可在`next()`方法中添加止损逻辑 +- 当前设计满足"不考虑风险管理"的需求 + +### R4: 震荡市假信号 + +**风险**: MACD在横盘震荡市中易产生频繁假信号 + +**缓解**: +- EMA200趋势过滤可减少震荡市中的交易频率 +- 选择相对保守的参数(10-20而非8-17),避免过于敏感 +- 研究表明,零轴过滤和趋势过滤可显著降低震荡市损失 + +### R5: 策略滞后性 + +**风险**: 基于EMA的指标天然滞后,可能错过趋势初期 + +**缓解**: +- 平衡型参数(10-20-9)在及时性和稳定性间取得平衡 +- 滞后性是趋势指标的固有特性,无法完全消除 +- 如需更及时信号,可考虑更小参数组合(8-17-7) + +## Migration Plan + +无需迁移步骤,新策略文件完全独立,不影响现有功能。 + +## Open Questions + +无 - 所有设计决策已明确。 diff --git a/openspec/changes/archive/2026-01-28-add-macd-strategy/proposal.md b/openspec/changes/archive/2026-01-28-add-macd-strategy/proposal.md new file mode 100644 index 0000000..dc2c06e --- /dev/null +++ b/openspec/changes/archive/2026-01-28-add-macd-strategy/proposal.md @@ -0,0 +1,34 @@ +## Why + +当前项目仅包含SMA双均线交叉策略(`strategies/sma_strategy.py`),需要引入基于MACD的趋势跟踪策略。MACD作为经典动量指标,结合EMA200趋势过滤,在A股市场表现优异,能更准确地捕捉趋势启动点和反转信号。 + +## What Changes + +- 创建 `strategies/macd_strategy.py` - 新增MACD趋势跟踪策略文件 +- 实现MACD指标计算 - 使用ta-lib库计算MACD(10,20,9)指标和EMA200趋势线 +- 实现策略交易逻辑 - MACD金叉/死叉信号 + EMA200趋势确认 +- 保持策略文件独立性 - 按照现有`strategy.py`模式实现(calculate_indicators、get_strategy、Strategy类) +- 创建strategies目录 - 用于统一管理所有策略脚本 + +## Capabilities + +### New Capabilities +- `macd-trading`: MACD趋势跟踪策略,包含MACD指标计算、EMA200趋势过滤、以及基于金叉/死叉的交易信号生成 + +### Modified Capabilities +无 + +## Impact + +**依赖变化**: +- ta-lib已手动安装(用于技术指标计算) + +**代码影响**: +- 不需要修改现有代码(`backtest.py`无需改动,策略文件模式保持一致) +- 策略目录扩展至2个策略文件 +- 可通过`--strategy-file`参数切换使用SMA或MACD策略 + +**系统影响**: +- 回测框架保持不变 +- 现有SMA策略完全不受影响 +- 可通过backtest.py的标准接口加载MACD策略 diff --git a/openspec/changes/archive/2026-01-28-add-macd-strategy/specs/macd-trading/spec.md b/openspec/changes/archive/2026-01-28-add-macd-strategy/specs/macd-trading/spec.md new file mode 100644 index 0000000..e7612d7 --- /dev/null +++ b/openspec/changes/archive/2026-01-28-add-macd-strategy/specs/macd-trading/spec.md @@ -0,0 +1,134 @@ +## ADDED Requirements + +### Requirement: MACD趋势跟踪策略 + +系统应提供基于MACD指标的趋势跟踪交易策略,包括MACD计算、EMA200趋势过滤、以及基于金叉/死叉的交易信号生成。 + +#### Scenario: 策略文件加载 + +- **WHEN** 用户在命令行指定`--strategy-file strategies/macd_strategy.py` +- **THEN** backtest.py成功加载策略文件并执行回测 +- **AND** 策略类正确注册所有技术指标到backtesting框架 +- **AND** 策略逻辑根据MACD金叉/死叉和EMA200位置生成交易信号 + +#### Scenario: MACD指标计算 + +- **WHEN** 调用`calculate_indicators(data)`函数,传入包含[Open, High, Low, Close, Volume, factor]的DataFrame +- **THEN** 函数使用ta-lib计算以下指标并添加到DataFrame: + - MACD线(DIF): 10日EMA - 20日EMA + - MACD信号线(DEA): 9日EMA的MACD + - MACD柱状图(Histogram): MACD线 - 信号线 + - EMA200: 200日指数移动平均线 +- **AND** 返回包含原始数据和所有新增指标的DataFrame +- **AND** 指标名称使用ta-lib返回的默认列名(macd、macdsignal、macdhist) + +#### Scenario: 策略初始化 + +- **WHEN** backtesting框架初始化MacdTrendStrategy策略类 +- **THEN** 调用`init()`方法 +- **AND** 在`init()`中通过`self.I()`注册以下指标到backtesting框架: + - MACD线(`self.data.MACD_10_20_9`) + - MACD信号线(`self.data.MACDs_10_20_9`) + - EMA200(`self.data.EMA_200`) +- **AND** 所有参数(fast_period=10、slow_period=20、signal_period=9)在策略类中定义为类变量 +- **AND** 注册的指标可直接在`next()`方法中访问 + +#### Scenario: MACD金叉买入信号 + +- **WHEN** 策略检测到MACD线上穿信号线(金叉) +- **AND** 当前价格高于EMA200趋势线(确认上升趋势) +- **AND** 当前无持仓或持仓方向与买入信号相反 +- **THEN** 策略平掉现有仓位(如有) +- **AND** 策略开多仓(`self.buy()`) +- **AND** 在趋势市场下捕捉上涨机会 + +#### Scenario: EMA200跌破卖出信号 + +- **WHEN** 策略检测到当前价格跌破EMA200趋势线 +- **AND** 当前持有多仓 +- **THEN** 策略平掉多仓(`self.position.close()`) +- **AND** 不开空仓(仅平仓,避免逆势交易) +- **AND** 在趋势转向时及时止损保护利润 + +#### Scenario: MACD死叉卖出信号 + +- **WHEN** 策略检测到MACD线下穿信号线(死叉) +- **AND** 当前持有多仓 +- **THEN** 策略平掉多仓(`self.position.close()`) +- **AND** 不开空仓 +- **AND** 在动量减弱时退出持仓 + +#### Scenario: EMA200下方不开仓 + +- **WHEN** 当前价格低于EMA200趋势线 +- **AND** 检测到MACD金叉信号 +- **THEN** 策略不执行买入操作 +- **AND** 避免在下跌趋势中逆势交易 +- **AND** 等待价格回到EMA200上方再考虑入场 + +#### Scenario: 空仓状态处理 + +- **WHEN** 策略当前无持仓 +- **AND** 检测到卖出信号(MACD死叉或EMA200跌破) +- **THEN** 策略跳过卖出信号 +- **AND** 避免重复平仓导致错误 + +#### Scenario: 震荡市场过滤 + +- **WHEN** 市场处于震荡状态(价格围绕EMA200波动) +- **AND** MACD产生频繁的假金叉/死叉信号 +- **THEN** EMA200趋势过滤减少交易频率 +- **AND** 避免在无明确趋势时频繁交易 +- **AND** 等待趋势明确后再入场 + +#### Scenario: 趋势市场顺势交易 + +- **WHEN** 市场处于明确上升趋势(价格持续在EMA200上方) +- **AND** MACD金叉确认动量增强 +- **THEN** 策略及时入场捕捉上涨机会 +- **AND** 顺势交易提高胜率 +- **AND** EMA200确保不在下跌趋势中买入 + +#### Scenario: 参数配置 + +- **WHEN** 用户查看策略代码 +- **THEN** 策略参数清晰定义为类变量: + - `fast_period = 10`(MACD快线周期) + - `slow_period = 20`(MACD慢线周期) + - `signal_period = 9`(MACD信号线周期) +- **AND** 参数无需通过命令行传递 +- **AND** 参数可直接在代码中修改以适配不同市场环境 + +#### Scenario: 依赖管理 + +- **WHEN** 安装项目依赖 +- **THEN** ta-lib库已被正确安装(手动安装) +- **AND** `uv run python -c "import talib"`成功执行 +- **AND** 策略文件可正常运行 +- **AND** 如ta-lib未安装,给出明确错误提示 + +#### Scenario: 回测兼容性 + +- **WHEN** 使用现有backtest.py框架 +- **THEN** 框架通过`load_strategy()`函数成功加载macd_strategy.py +- **AND** 调用`calculate_indicators()`预处理数据 +- **AND** 初始化策略类并执行回测 +- **AND** 回测流程与SMA策略完全一致 + +#### Scenario: 指标数据完整性 + +- **WHEN** backtesting调用`calculate_indicators(data)` +- **THEN** 返回的DataFrame包含所有必需列: + - 原始列:[Open, High, Low, Close, Volume, factor] + - MACD指标列:[MACD_10_20_9, MACDh_10_20_9, MACDs_10_20_9] + - EMA趋势线列:[EMA_200] +- **AND** 无NaN值(除预热期外) +- **AND** 指标数据可用于策略决策和图表展示 + +#### Scenario: 预热期处理 + +- **WHEN** 数据长度不足以计算完整指标(前200天) +- **THEN** 指标值为NaN +- **AND** backtesting框架会自动跳过预热期 +- **AND** 策略逻辑在有足够数据后才执行 +- **AND** 避免因数据不足导致的错误信号 diff --git a/openspec/changes/archive/2026-01-28-add-macd-strategy/tasks.md b/openspec/changes/archive/2026-01-28-add-macd-strategy/tasks.md new file mode 100644 index 0000000..107f13c --- /dev/null +++ b/openspec/changes/archive/2026-01-28-add-macd-strategy/tasks.md @@ -0,0 +1,81 @@ +## 1. 环境准备 + +- [x] 1.1 安装ta-lib依赖包(已完成手动安装) +- [x] 1.2 验证ta-lib安装成功(`uv run python -c "import talib"`无报错) + +## 2. 目录结构 + +- [x] 2.1 确认strategies目录存在(如不存在则创建) +- [x] 2.2 移动现有strategy.py到strategies/sma_strategy.py +- [x] 2.3 验证文件移动成功且可正常导入 + +## 3. MACD策略文件创建 + +- [x] 3.1 创建strategies/macd_strategy.py文件 +- [x] 3.2 添加文件头部文档(策略说明、作者、日期) +- [x] 3.3 添加必要的导入语句(pandas、backtesting、talib、crossover) +- [x] 3.4 定义calculate_indicators()函数签名 +- [x] 3.5 定义get_strategy()函数 +- [x] 3.6 定义MacdTrendStrategy类框架 + +## 4. 指标计算实现 + +- [x] 4.1 在calculate_indicators()中使用ta-lib计算MACD指标 + - [x] 4.1.1 调用`talib.MACD(data['Close'], fastperiod=10, slowperiod=20, signalperiod=9)` + - [x] 4.1.2 验证MACD返回3列(MACD线、信号线、柱状图) + - [x] 4.1.3 计算EMA200趋势线(`talib.EMA(data['Close'], timeperiod=200)`) + - [x] 4.1.4 返回包含所有指标的完整DataFrame + +## 5. 策略类实现 + +- [x] 5.1 在MacdTrendStrategy类中定义可配置参数 + - [x] 5.1.1 fast_period = 10 + - [x] 5.1.2 slow_period = 20 + - [x] 5.1.3 signal_period = 9 +- [x] 5.2 实现init()方法 + - [x] 5.2.1 使用self.I()注册MACD线(self.data.MACD_10_20_9) + - [x] 5.2.2 使用self.I()注册MACD信号线(self.data.MACDs_10_20_9) + - [x] 5.2.3 使用self.I()注册EMA200(self.data.EMA_200) + - [x] 5.2.4 验证所有指标正确注册 +- [x] 5.3 实现next()方法交易逻辑 + - [x] 5.3.1 导入crossover函数用于检测金叉/死叉 + - [x] 5.3.2 实现买入条件:crossover(MACD, Signal) AND Close > EMA200 + - [x] 5.3.3 实现卖出条件:crossover(Signal, MACD) OR Close < EMA200 + - [x] 5.3.4 处理空仓状态(避免重复平仓) + - [x] 5.3.5 确保开仓前先平掉现有仓位 + +## 6. 代码验证 + +- [x] 6.1 检查Python语法正确性(无语法错误) +- [x] 6.2 验证导入语句正确(所有依赖正确导入) +- [x] 6.3 检查类继承自Strategy +- [x] 6.4 检查策略文件结构符合SMA策略模式 + +## 7. 回测兼容性验证 + +- [x] 7.1 使用backtest.py加载macd_strategy.py(`uv run python backtest.py --strategy-file strategies/macd_strategy.py`) +- [x] 7.2 验证策略文件成功加载无报错 +- [x] 7.3 执行简单回测(如测试股票、测试日期范围) +- [x] 7.4 验证回测结果输出正常 + +## 8. 文档和注释 + +- [x] 8.1 在文件头部添加清晰的策略说明文档 +- [x] 8.2 在关键逻辑处添加代码注释 +- [x] 8.3 说明MACD参数选择理由(10-20-9组合) +- [x] 8.4 说明EMA200趋势过滤原理 +- [x] 8.5 说明买入/卖出信号条件 + +## 9. 可选验证任务 + +- [ ] 9.1 对比MACD策略与SMA策略的回测结果 +- [ ] 9.2 测试不同参数组合的性能(如8-17-7、12-26-9) +- [ ] 9.3 验证EMA200过滤对回撤的影响 +- [ ] 9.4 测试不同市场环境(牛市、熊市、震荡市)下的表现 + +## 10. 完成 + +- [x] 10.1 所有核心功能实现完成 +- [x] 10.2 代码质量符合Python最佳实践 +- [x] 10.3 策略可被backtest.py正常加载和执行 +- [x] 10.4 回测结果符合预期(策略逻辑正确执行) diff --git a/openspec/specs/macd-trading/spec.md b/openspec/specs/macd-trading/spec.md new file mode 100644 index 0000000..e7612d7 --- /dev/null +++ b/openspec/specs/macd-trading/spec.md @@ -0,0 +1,134 @@ +## ADDED Requirements + +### Requirement: MACD趋势跟踪策略 + +系统应提供基于MACD指标的趋势跟踪交易策略,包括MACD计算、EMA200趋势过滤、以及基于金叉/死叉的交易信号生成。 + +#### Scenario: 策略文件加载 + +- **WHEN** 用户在命令行指定`--strategy-file strategies/macd_strategy.py` +- **THEN** backtest.py成功加载策略文件并执行回测 +- **AND** 策略类正确注册所有技术指标到backtesting框架 +- **AND** 策略逻辑根据MACD金叉/死叉和EMA200位置生成交易信号 + +#### Scenario: MACD指标计算 + +- **WHEN** 调用`calculate_indicators(data)`函数,传入包含[Open, High, Low, Close, Volume, factor]的DataFrame +- **THEN** 函数使用ta-lib计算以下指标并添加到DataFrame: + - MACD线(DIF): 10日EMA - 20日EMA + - MACD信号线(DEA): 9日EMA的MACD + - MACD柱状图(Histogram): MACD线 - 信号线 + - EMA200: 200日指数移动平均线 +- **AND** 返回包含原始数据和所有新增指标的DataFrame +- **AND** 指标名称使用ta-lib返回的默认列名(macd、macdsignal、macdhist) + +#### Scenario: 策略初始化 + +- **WHEN** backtesting框架初始化MacdTrendStrategy策略类 +- **THEN** 调用`init()`方法 +- **AND** 在`init()`中通过`self.I()`注册以下指标到backtesting框架: + - MACD线(`self.data.MACD_10_20_9`) + - MACD信号线(`self.data.MACDs_10_20_9`) + - EMA200(`self.data.EMA_200`) +- **AND** 所有参数(fast_period=10、slow_period=20、signal_period=9)在策略类中定义为类变量 +- **AND** 注册的指标可直接在`next()`方法中访问 + +#### Scenario: MACD金叉买入信号 + +- **WHEN** 策略检测到MACD线上穿信号线(金叉) +- **AND** 当前价格高于EMA200趋势线(确认上升趋势) +- **AND** 当前无持仓或持仓方向与买入信号相反 +- **THEN** 策略平掉现有仓位(如有) +- **AND** 策略开多仓(`self.buy()`) +- **AND** 在趋势市场下捕捉上涨机会 + +#### Scenario: EMA200跌破卖出信号 + +- **WHEN** 策略检测到当前价格跌破EMA200趋势线 +- **AND** 当前持有多仓 +- **THEN** 策略平掉多仓(`self.position.close()`) +- **AND** 不开空仓(仅平仓,避免逆势交易) +- **AND** 在趋势转向时及时止损保护利润 + +#### Scenario: MACD死叉卖出信号 + +- **WHEN** 策略检测到MACD线下穿信号线(死叉) +- **AND** 当前持有多仓 +- **THEN** 策略平掉多仓(`self.position.close()`) +- **AND** 不开空仓 +- **AND** 在动量减弱时退出持仓 + +#### Scenario: EMA200下方不开仓 + +- **WHEN** 当前价格低于EMA200趋势线 +- **AND** 检测到MACD金叉信号 +- **THEN** 策略不执行买入操作 +- **AND** 避免在下跌趋势中逆势交易 +- **AND** 等待价格回到EMA200上方再考虑入场 + +#### Scenario: 空仓状态处理 + +- **WHEN** 策略当前无持仓 +- **AND** 检测到卖出信号(MACD死叉或EMA200跌破) +- **THEN** 策略跳过卖出信号 +- **AND** 避免重复平仓导致错误 + +#### Scenario: 震荡市场过滤 + +- **WHEN** 市场处于震荡状态(价格围绕EMA200波动) +- **AND** MACD产生频繁的假金叉/死叉信号 +- **THEN** EMA200趋势过滤减少交易频率 +- **AND** 避免在无明确趋势时频繁交易 +- **AND** 等待趋势明确后再入场 + +#### Scenario: 趋势市场顺势交易 + +- **WHEN** 市场处于明确上升趋势(价格持续在EMA200上方) +- **AND** MACD金叉确认动量增强 +- **THEN** 策略及时入场捕捉上涨机会 +- **AND** 顺势交易提高胜率 +- **AND** EMA200确保不在下跌趋势中买入 + +#### Scenario: 参数配置 + +- **WHEN** 用户查看策略代码 +- **THEN** 策略参数清晰定义为类变量: + - `fast_period = 10`(MACD快线周期) + - `slow_period = 20`(MACD慢线周期) + - `signal_period = 9`(MACD信号线周期) +- **AND** 参数无需通过命令行传递 +- **AND** 参数可直接在代码中修改以适配不同市场环境 + +#### Scenario: 依赖管理 + +- **WHEN** 安装项目依赖 +- **THEN** ta-lib库已被正确安装(手动安装) +- **AND** `uv run python -c "import talib"`成功执行 +- **AND** 策略文件可正常运行 +- **AND** 如ta-lib未安装,给出明确错误提示 + +#### Scenario: 回测兼容性 + +- **WHEN** 使用现有backtest.py框架 +- **THEN** 框架通过`load_strategy()`函数成功加载macd_strategy.py +- **AND** 调用`calculate_indicators()`预处理数据 +- **AND** 初始化策略类并执行回测 +- **AND** 回测流程与SMA策略完全一致 + +#### Scenario: 指标数据完整性 + +- **WHEN** backtesting调用`calculate_indicators(data)` +- **THEN** 返回的DataFrame包含所有必需列: + - 原始列:[Open, High, Low, Close, Volume, factor] + - MACD指标列:[MACD_10_20_9, MACDh_10_20_9, MACDs_10_20_9] + - EMA趋势线列:[EMA_200] +- **AND** 无NaN值(除预热期外) +- **AND** 指标数据可用于策略决策和图表展示 + +#### Scenario: 预热期处理 + +- **WHEN** 数据长度不足以计算完整指标(前200天) +- **THEN** 指标值为NaN +- **AND** backtesting框架会自动跳过预热期 +- **AND** 策略逻辑在有足够数据后才执行 +- **AND** 避免因数据不足导致的错误信号 diff --git a/pyproject.toml b/pyproject.toml index 559fc43..a5b6809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,5 @@ dependencies = [ "peewee~=3.19.0", "psycopg2-binary~=2.9.11", "sqlalchemy>=2.0.46", + "ta-lib>=0.6.8", ] diff --git a/strategies/macd_strategy.py b/strategies/macd_strategy.py new file mode 100644 index 0000000..d570c8c --- /dev/null +++ b/strategies/macd_strategy.py @@ -0,0 +1,135 @@ +""" +MACD 趋势跟踪策略 + +策略逻辑: +- 当 MACD 线上穿信号线时 (金叉),且价格 > EMA200 时,买入 +- 当 MACD 线下穿信号线时 (死叉),或价格 < EMA200 时,卖出 + +指标计算: +- MACD(10, 20, 9): 快线 10 日,慢线 20 日,信号线 9 日 +- EMA200: 200 日指数移动平均线(趋势确认) + +参数选择理由: +- 快线 10: 比标准 12 更敏感,适应 A 股较高波动性 +- 慢线 20: 比标准 26 更快响应,同时保持趋势跟踪稳定性 +- 信号线 9: 保持标准,避免信号过于频繁 +- EMA200: 被广泛认可为牛熊分界线,避免逆势交易 + +趋势过滤: +- EMA200 上方: 确认为上升趋势,允许开多仓 +- EMA200 下方: 确认为下降趋势,不开多仓,强制平仓 + +Author: Sisyphus +Date: 2025-01-27 +""" + +import pandas as pd +from backtesting import Strategy +from backtesting.lib import crossover + + +def calculate_indicators(data): + """ + 计算策略所需的技术指标 + + 使用 ta-lib 库计算 MACD 和 EMA200 指标 + + 参数: + data: DataFrame, 包含 [Open, High, Low, Close, Volume, factor] + + 返回: + DataFrame, 添加了指标列: + - MACD_10_20_9: MACD 线 (DIF) + - MACDs_10_20_9: MACD 信号线 (DEA) + - MACDh_10_20_9: MACD 柱状图 (Histogram) + - EMA_200: 200 日指数移动平均线 + """ + data = data.copy() + + # 计算 MACD 指标 (10, 20, 9) + # talib.MACD 返回三个值: (macd, macdsignal, macdhist) + macd, macdsignal, macdhist = talib.MACD( + data["Close"], fastperiod=10, slowperiod=20, signalperiod=9 + ) + + data["MACD_10_20_9"] = macd + data["MACDs_10_20_9"] = macdsignal + data["MACDh_10_20_9"] = macdhist + + # 计算 EMA200 趋势线 + data["EMA_200"] = talib.EMA(data["Close"], timeperiod=200) + + return data + + +def get_strategy(): + """ + 返回策略类 + + 返回: + MacdTrendStrategy 类 + """ + return MacdTrendStrategy + + +class MacdTrendStrategy(Strategy): + """ + MACD 趋势跟踪策略 + + 结合 MACD 金叉/死叉信号和 EMA200 趋势过滤 + + 参数: + fast_period: MACD 快线周期 (默认: 10) + slow_period: MACD 慢线周期 (默认: 20) + signal_period: MACD 信号线周期 (默认: 9) + """ + + # 可配置参数 + fast_period = 10 + slow_period = 20 + signal_period = 9 + + def init(self): + """ + 初始化策略 + 注册指标到 backtesting 框架 + """ + # 注册 MACD 线 + self.macd = self.I(lambda x: x, self.data.MACD_10_20_9) + + # 注册 MACD 信号线 + self.macd_signal = self.I(lambda x: x, self.data.MACDs_10_20_9) + + # 注册 EMA200 趋势线 + self.ema200 = self.I(lambda x: x, self.data.EMA_200) + + def next(self): + """ + 每个时间步的决策逻辑 + + 买入条件: + - MACD 金叉 (MACD 线上穿信号线) + - 价格 > EMA200 (确认上升趋势) + + 卖出条件: + - MACD 死叉 (MACD 线下穿信号线) + - 或价格 < EMA200 (趋势转向,强制平仓) + """ + # 买入条件: MACD 金叉 AND 价格 > EMA200 + if ( + crossover(self.macd, self.macd_signal) + and self.data.Close[-1] > self.ema200[-1] + ): + self.position.close() # 先平掉现有仓位 + self.buy() # 开多仓 + + # 卖出条件: MACD 死叉 OR 价格 < EMA200 + elif ( + crossover(self.macd_signal, self.macd) + or self.data.Close[-1] < self.ema200[-1] + ): + self.position.close() # 平掉多仓 + + +# 导入 talib (必须在文件末尾,因为 calculate_indicators 函数中使用了 talib) +import talib diff --git a/strategy.py b/strategies/sma_strategy.py similarity index 100% rename from strategy.py rename to strategies/sma_strategy.py diff --git a/uv.lock b/uv.lock index 5e5e34a..478ea43 100644 --- a/uv.lock +++ b/uv.lock @@ -180,6 +180,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" }, ] +[[package]] +name = "build" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -897,6 +911,7 @@ dependencies = [ { name = "peewee" }, { name = "psycopg2-binary" }, { name = "sqlalchemy" }, + { name = "ta-lib" }, ] [package.metadata] @@ -911,6 +926,7 @@ requires-dist = [ { name = "peewee", specifier = "~=3.19.0" }, { name = "psycopg2-binary", specifier = "~=2.9.11" }, { name = "sqlalchemy", specifier = ">=2.0.46" }, + { name = "ta-lib", specifier = ">=0.6.8" }, ] [[package]] @@ -1377,6 +1393,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, ] +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1646,6 +1671,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "ta-lib" +version = "0.6.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/ec/27114f6255e6723783d4c4366810620a4347375ebf66f8aea86d9dd58ffd/ta_lib-0.6.8.tar.gz", hash = "sha256:3a9195299df9d7d2a6e9d16bebd6b706b0ea99e4b871864c4b034c2577e21a77", size = 380772, upload-time = "2025-10-20T20:49:56.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/61/c47098dfb28c468d29fccfbb2ba35a10001d37dd51c4200a4e50c788ede6/ta_lib-0.6.8-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:36b2a516fce57309840f5ef3fa2fd0c4449293fc72536a0400d2e1e26b414da8", size = 1075848, upload-time = "2025-10-20T20:49:29.517Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e9/a30e770902c1df915a94a43e652f432e7647b710c0e1120751c05805d4bc/ta_lib-0.6.8-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7993164e8e9f78ec31d38c47850ca6ba5451788b5b49a8a2dbb3322b36b5693b", size = 986649, upload-time = "2025-10-20T20:49:30.702Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2f/8961a9e7434a2d10b8f625bb4d5c049484a898e76e9c5e40398da410aec0/ta_lib-0.6.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:613cf06313331f49dd7b85a5a24fbddb1156c9723b6921a231906241726e5aee", size = 3971825, upload-time = "2025-10-20T20:49:32.185Z" }, + { url = "https://files.pythonhosted.org/packages/75/c1/352bc32394549ac9886829a24070a507a30abf45265135b60ee77354f7da/ta_lib-0.6.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce2bc1ea01200b6d8130ab917296d05d77a1a571ec6c1ee25cfca6d55cd5db4a", size = 3991433, upload-time = "2025-10-20T20:49:34.182Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/7bde1867df3bf015f48d510d2ba7491359ce13c79ecf5127acae3d308272/ta_lib-0.6.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a63a52221f8c73f82f4e00493351d987f594931198589287aee96f8da673cfd5", size = 3585925, upload-time = "2025-10-20T20:49:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/8d389f60bb085b6991764d7535f066dd6009fc4f5a45dbd26dc9eaaa3c0a/ta_lib-0.6.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559326d8f3d904cd4aa61f6a392d5626f35eec6a9f6cc83bcddb0abf88c40516", size = 3629696, upload-time = "2025-10-20T20:49:37.299Z" }, + { url = "https://files.pythonhosted.org/packages/82/bc/d2e4c2b752baaee592095feb69514764b004fe53af7cc893ba9c3854cc30/ta_lib-0.6.8-cp314-cp314-win32.whl", hash = "sha256:f5b6174bf4bf9152e368561dff410203c6921e4dd2afbcda3283a95957158112", size = 766352, upload-time = "2025-10-20T20:49:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/40/98/0f2755b5bde81d7b1eaf96b4204f18fabea38b0efc869cb0ea05d57e0afc/ta_lib-0.6.8-cp314-cp314-win_amd64.whl", hash = "sha256:1fb4028437201e19014e4e374272b739867c8a3eb655da46675ef4c2ff14b616", size = 886955, upload-time = "2025-10-20T20:49:38.513Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4c/d341020377f8b183405bdf3c5717fc2ca04a8d33b5c59b2348377ee459d9/ta_lib-0.6.8-cp314-cp314-win_arm64.whl", hash = "sha256:bfad1202fb1f9140e3810cc607058395f59032d9128cc0d716900c78bea5f337", size = 755896, upload-time = "2025-10-20T20:49:39.9Z" }, +] + [[package]] name = "terminado" version = "0.18.1"