1
0

Compare commits

...

4 Commits

Author SHA1 Message Date
9a46bd7e4c 完成macd策略 2026-01-28 00:10:43 +08:00
407b70bd0e 优化命令行输出效果 2026-01-27 22:25:22 +08:00
d8159af1d2 优化图表颜色为红涨绿跌 2026-01-27 22:22:13 +08:00
4e4bb1ab6e 修复回测图像生成 2026-01-27 21:51:48 +08:00
12 changed files with 833 additions and 46 deletions

1
.gitignore vendored
View File

@@ -139,3 +139,4 @@ dmypy.json
.pyre/
.pytype/
cython_debug/
output

View File

@@ -119,6 +119,17 @@ def load_strategy(strategy_file):
return calculate_indicators, strategy_class
def apply_color_scheme():
"""
应用颜色方案:红涨绿跌(中国股市风格)
"""
import backtesting._plotting as plotting
from bokeh.colors.named import tomato, lime
plotting.BULL_COLOR = tomato
plotting.BEAR_COLOR = lime
def parse_arguments():
"""
解析命令行参数
@@ -199,51 +210,54 @@ def print_stats(stats):
print("回测结果")
print("=" * 60)
# 基本指标
metrics = [
("Return (%)", "总收益率", "Return [%]"),
("Return", "总收益", "Return"),
("Sharpe Ratio", "夏普比率", "Sharpe Ratio"),
("Sortino Ratio", "索提诺比率", "Sortino Ratio"),
("Calmar Ratio", "卡尔玛比率", "Calmar Ratio"),
("Max Drawdown (%)", "最大回撤 (%)", "Max. Drawdown [%]"),
("Avg Drawdown (%)", "平均回撤 (%)", "Avg. Drawdown [%]"),
("Max Drawdown Duration", "最大回撤持续天数", "Max. Drawdown Duration"),
("Avg Drawdown Duration", "平均回撤持续天数", "Avg. Drawdown Duration"),
]
for key, cn_name, en_name in metrics:
try:
value = getattr(stats, key, None)
if value is not None:
formatted = format_value(value, cn_name, key)
print(f"{cn_name:20s}: {formatted}")
except Exception:
pass
print()
# 交易统计
trade_metrics = [
("# Trades", "总交易次数", "# Trades"),
("Win Rate [%]", "胜率 (%)", "Win Rate [%]"),
("Best Trade", "最佳交易", "Best Trade"),
("Worst Trade", "最差交易", "Worst Trade"),
("Avg Trade", "平均交易", "Avg. Trade"),
("Avg Win Trade", "平均盈利交易", "Avg. Win Trade"),
("Avg Loss Trade", "平均亏损交易", "Avg. Loss Trade"),
("Profit Factor", "盈利因子", "Profit Factor"),
("Expectancy", "期望值", "Expectancy"),
]
for key, cn_name, en_name in trade_metrics:
try:
value = getattr(stats, key, None)
if value is not None:
formatted = format_value(value, cn_name, key)
print(f"{cn_name:20s}: {formatted}")
except Exception:
pass
indicator_name_mapping = {
# 'Start': '回测开始时间',
# 'End': '回测结束时间',
# 'Duration': '回测持续时长',
# 'Exposure Time [%]': '持仓时间占比(%',
'Equity Final [$]': '最终收益',
'Equity Peak [$]': '峰值收益',
'Return [%]': '总收益率(%',
'Buy & Hold Return [%]': '买入并持有收益率(%',
'Return (Ann.) [%]': '年化收益率(%',
'Volatility (Ann.) [%]': '年化波动率(%',
# 'CAGR [%]': '复合年均增长率(%',
# 'Sharpe Ratio': '夏普比率',
'Sortino Ratio': '索提诺比率',
'Calmar Ratio': '卡尔玛比率',
# 'Alpha [%]': '阿尔法系数(%',
# 'Beta': '贝塔系数',
'Max. Drawdown [%]': '最大回撤(%',
'Avg. Drawdown [%]': '平均回撤(%',
'Max. Drawdown Duration': '最大回撤持续时长',
'Avg. Drawdown Duration': '平均回撤持续时长',
'# Trades': '总交易次数',
'Win Rate [%]': '胜率(%',
# 'Best Trade [%]': '最佳单笔交易收益率(%',
# 'Worst Trade [%]': '最差单笔交易收益率(%',
# 'Avg. Trade [%]': '平均单笔交易收益率(%',
# 'Max. Trade Duration': '单笔交易最长持有时长',
# 'Avg. Trade Duration': '单笔交易平均持有时长',
# 'Profit Factor': '盈利因子',
# 'Expectancy [%]': '期望收益(%',
'SQN': '系统质量数',
# 'Kelly Criterion': '凯利准则',
}
for k, v in stats.items():
if k in indicator_name_mapping:
cn_name = indicator_name_mapping.get(k, k)
if isinstance(v, (int, float)):
if "%" in cn_name or k in ['Sharpe Ratio', 'Sortino Ratio', 'Calmar Ratio', 'Profit Factor']:
formatted_value = f"{v:.2f}"
elif "$" in cn_name:
formatted_value = f"{v:.2f}"
elif "次数" in cn_name:
formatted_value = f"{v:.0f}"
else:
formatted_value = f"{v:.4f}"
else:
formatted_value = str(v)
print(f'{cn_name}: {formatted_value}')
print("=" * 60 + "\n")
@@ -256,6 +270,8 @@ def main():
# 解析参数
args = parse_arguments()
apply_color_scheme()
# 加载数据
print(f"加载股票数据: {args.code} ({args.start_date} ~ {args.end_date})")
data = load_data_from_db(args.code, args.start_date, args.end_date)
@@ -293,7 +309,7 @@ def main():
# 生成图表
if args.output:
print(f"\n生成图表: {args.output}")
bt.plot(filename=args.output, show=False)
bt.plot(filename=args.output, open_browser=False)
print(f"图表已保存到: {args.output}")
print("\n回测完成!")

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-27

View File

@@ -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
无 - 所有设计决策已明确。

View File

@@ -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策略

View File

@@ -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** 避免因数据不足导致的错误信号

View File

@@ -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()注册EMA200self.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 回测结果符合预期(策略逻辑正确执行)

View File

@@ -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** 避免因数据不足导致的错误信号

View File

@@ -14,4 +14,5 @@ dependencies = [
"peewee~=3.19.0",
"psycopg2-binary~=2.9.11",
"sqlalchemy>=2.0.46",
"ta-lib>=0.6.8",
]

135
strategies/macd_strategy.py Normal file
View File

@@ -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

46
uv.lock generated
View File

@@ -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"