问题描述
解决方案
麦克金利动态指标 (McGinley Dynamic) 策略
麦克金利动态指标(McGinley Dynamic Indicator)是由 John R. McGinley 发明的一种平滑机制。它看起来像移动平均线(MA),但它是一个能够根据市场速度自动调整的平滑指标。相比于传统的 SMA 或 EMA,它能更紧密地追踪价格,最大限度地减少价格分离(Lag)并避免鞭锯效应(Whipsaw)。
1. 指标公式与逻辑
公式:
$$ MD_i = MD_{i-1} + \frac{Close_i - MD_{i-1}}{N \times (\frac{Close_i}{MD_{i-1}})^4} $$
其中:
- $MD_i$: 当前周期的麦克金利值
- $MD_{i-1}$: 上一周期的麦克金利值
- $Close_i$: 当前收盘价
- $N$: 周期参数(类似于移动平均线的周期,如 14, 20 等)
策略逻辑:
本策略采用最基础的趋势跟随逻辑:
- 买入信号:当收盘价 上穿 麦克金利动态指标线时,视为上升趋势,全仓买入。
- 卖出信号:当收盘价 下穿 麦克金利动态指标线时,视为下降趋势,清仓卖出。
2. JoinQuant 策略代码实现
以下是基于聚宽 API 实现的完整策略代码。代码中包含了一个计算函数,用于在策略初始化时“预热”指标,以及在每日交易中动态更新指标。
# -*- coding: utf-8 -*-
import numpy as np
def initialize(context):
"""
初始化函数,设定基准、股票池、参数等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# --- 策略参数设置 ---
# 要操作的标的:这里以平安银行为例
g.security = '000001.XSHE'
# McGinley Dynamic 的周期 N
g.N = 14
# 全局变量,用于存储上一日的 McGinley Dynamic 值
g.prev_md = None
def calculate_initial_md(security, n, end_date):
"""
辅助函数:用于在策略开始前计算初始的 MD 值(预热)
"""
# 获取过去 3*N 天的数据来预热指标,确保初始值趋于稳定
# 注意:这里获取的是策略开始前的数据
count = n * 3
h = attribute_history(security, count, '1d', ['close'], df=False)
close_prices = h['close']
if len(close_prices) == 0:
return None
# 初始 MD 值通常设为第一个收盘价
md = close_prices[0]
# 迭代计算直到最后一个数据点
for i in range(1, len(close_prices)):
close = close_prices[i]
# 防止除以0错误
if md == 0:
md = close
else:
# McGinley Dynamic 公式
md = md + (close - md) / (n * (close / md)**4)
return md
def handle_data(context, data):
"""
按天回测时,每天执行一次
"""
security = g.security
# 获取当前标的的数据
current_data = data[security]
current_price = current_data.close
# 如果数据中有 NaN,则跳过
if np.isnan(current_price):
return
# --- 1. 计算/更新 McGinley Dynamic 指标 ---
# 如果是第一天运行,或者 g.prev_md 尚未初始化,先进行预热计算
if g.prev_md is None:
# 获取截止到昨天的历史数据进行预热
g.prev_md = calculate_initial_md(security, g.N, context.previous_date)
# 如果历史数据不足,直接用当前价格作为初始值,并跳过今日交易
if g.prev_md is None:
g.prev_md = current_price
return
# 根据今日收盘价,计算今日的 MD 值
# 公式: MD = MD_prev + (Price - MD_prev) / (N * (Price / MD_prev)^4)
try:
current_md = g.prev_md + (current_price - g.prev_md) / (g.N * (current_price / g.prev_md)**4)
except ZeroDivisionError:
current_md = current_price
# --- 2. 交易逻辑 ---
# 获取当前现金
cash = context.portfolio.available_cash
# 获取当前持仓
position = context.portfolio.positions[security]
# 信号判断
# 价格上穿 MD 线 -> 买入
if current_price > current_md and position.closeable_amount == 0:
log.info("价格(%.2f) 上穿 MD(%.2f),买入 %s" % (current_price, current_md, security))
order_value(security, cash)
# 价格下穿 MD 线 -> 卖出
elif current_price < current_md and position.closeable_amount > 0:
log.info("价格(%.2f) 下穿 MD(%.2f),卖出 %s" % (current_price, current_md, security))
order_target(security, 0)
# --- 3. 更新状态与绘图 ---
# 更新 prev_md 供明天使用
g.prev_md = current_md
# 在回测图中绘制指标
record(Price=current_price, McGinley_Dynamic=current_md)
3. 代码关键点解析
-
指标预热 (
calculate_initial_md):- McGinley Dynamic 是一个递归指标,今天的计算依赖昨天的结果。
- 在回测的第一天,我们没有“昨天”的指标值。因此,代码中获取了过去
3 * N天的历史数据来迭代计算,使指标值收敛,避免策略启动初期的信号失真。
-
公式实现:
- 核心代码行:
current_md = g.prev_md + (current_price - g.prev_md) / (g.N * (current_price / g.prev_md)**4) - 这里体现了该指标的特性:分母中的
(Price / MD_prev)^4是调节因子。当价格大幅偏离指标时,调节因子变大或变小,从而加速指标对价格的追随速度。
- 核心代码行:
-
交易执行:
- 使用
order_value进行全仓买入。 - 使用
order_target(security, 0)进行清仓卖出。 - 加入了
record()函数,方便在回测界面的图表中直观对比股价与指标的走势。
- 使用
Q&A
Q1: 麦克金利动态指标相比普通均线(MA)有什么优势?
A1: 普通均线存在固定的滞后性。麦克金利动态指标通过引入价格与指标比率的4次方作为调节因子,在市场剧烈波动(价格快速远离指标)时,指标会加速移动以追赶价格;在市场盘整时,指标移动变慢以减少错误信号。这使得它比 EMA 更平滑,同时比 SMA 反应更快。
Q2: 代码中的 g.N 参数应该如何设置?
A2: g.N 类似于移动平均线的周期。常见的设置是 10 到 20 之间(如 14)。数值越小,指标越敏感,交易越频繁;数值越大,指标越平滑,更适合捕捉长期趋势。您可以根据回测结果进行调整。
Q3: 这个策略可以直接用于实盘吗?
A3: 上述代码是一个基础的单因子择时策略,主要用于演示指标计算逻辑。在实盘前,建议您:
- 增加风控模块(如止损、止盈)。
- 考虑多只股票的轮动或资产配置,而非单吊一只股票。
- 注意滑点和手续费对高频震荡市的影响。