问题描述
解决方案
这是一个基于 Z-Score(标准分数) 的均值回归策略。
策略逻辑
Z-Score 用于衡量当前价格偏离移动平均线多少个标准差。
- 计算公式:$Z = \frac{\text{当前收盘价} - \text{N日移动平均价}}{\text{N日标准差}}$
- 做多信号:当 $Z < -Threshold$(例如 -2.0),说明价格统计上过低,买入/做多。
- 做空信号:当 $Z > Threshold$(例如 2.0),说明价格统计上过高,卖出/做空。
- 平仓信号:当 $|Z| < ExitThreshold$(例如 0.5),说明价格回归均值附近,平仓。
策略代码
以下代码适用于 QMT 的 Python 策略编辑器。代码包含了详细注释,并使用了 get_market_data_ex 获取数据,以及 order_target_percent 进行仓位管理。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 设置策略参数
ContextInfo.period = 20 # 均线和标准差的计算周期
ContextInfo.threshold = 2.0 # 开仓阈值(2倍标准差)
ContextInfo.exit_threshold = 0.5 # 平仓阈值(回归到0.5倍标准差以内)
# 设置交易账号(请替换为您自己的资金账号)
# ContextInfo.set_account('您的资金账号')
# 设定股票池(这里以沪深300ETF为例,也可以在界面上选择运行品种)
ContextInfo.target_stock = '510300.SH'
ContextInfo.set_universe([ContextInfo.target_stock])
# 策略运行变量
ContextInfo.holding_side = 0 # 0:空仓, 1:多头, -1:空头
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前主图或设置的标的代码
stock_code = ContextInfo.target_stock
# 获取当前K线索引
index = ContextInfo.barpos
# 获取历史行情数据
# 我们需要 period 个数据来计算均值,为了稳健多取一点
count = ContextInfo.period + 5
# 使用 get_market_data_ex 获取数据 (推荐接口)
# 注意:subscribe=True 确保实盘时数据自动更新
data_map = ContextInfo.get_market_data_ex(
['close'],
[stock_code],
period=ContextInfo.period,
count=count,
subscribe=True
)
if stock_code not in data_map:
return
# 获取收盘价序列 (pandas DataFrame)
df = data_map[stock_code]
# 确保数据量足够计算
if len(df) < ContextInfo.period:
return
# 提取收盘价 Series
close_series = df['close']
# --- 计算 Z-Score ---
# 1. 计算 N 日移动平均线 (MA)
ma = close_series.rolling(window=ContextInfo.period).mean()
# 2. 计算 N 日标准差 (Std)
std = close_series.rolling(window=ContextInfo.period).std()
# 3. 计算 Z-Score
# Z = (当前价格 - 均值) / 标准差
z_score_series = (close_series - ma) / std
# 获取当前最新的 Z-Score 值
current_z = z_score_series.iloc[-1]
# 如果计算结果无效(如NaN),则跳过
if np.isnan(current_z):
return
# --- 绘图 (可选) ---
# 在副图画出 Z-Score,方便观察
ContextInfo.paint('Z_Score', current_z, -1, 0)
ContextInfo.paint('Upper', ContextInfo.threshold, -1, 0, 'red')
ContextInfo.paint('Lower', -ContextInfo.threshold, -1, 0, 'green')
# --- 交易逻辑 ---
# 1. 开空/卖出信号:价格偏离均值过高 (Z > 2.0)
if current_z > ContextInfo.threshold:
# 如果当前不是空头,则做空
# 注意:股票账户通常只能卖出持仓(Target=0),期货/融券账户可以做空(Target<0)
# 这里演示设为 0% (清仓/卖出),如果是期货可设为 -0.5
print(f"Z-Score: {current_z:.2f} > {ContextInfo.threshold}, 触发卖出/做空信号")
order_target_percent(stock_code, 0.0, ContextInfo, ContextInfo.get_account(0))
ContextInfo.holding_side = 0 # 标记状态
# 2. 开多/买入信号:价格偏离均值过低 (Z < -2.0)
elif current_z < -ContextInfo.threshold:
# 如果当前不是多头,则做多
# 目标仓位设为 50%
print(f"Z-Score: {current_z:.2f} < -{ContextInfo.threshold}, 触发买入/做多信号")
order_target_percent(stock_code, 0.5, ContextInfo, ContextInfo.get_account(0))
ContextInfo.holding_side = 1
# 3. 平仓/回归信号:价格回归均值附近 (|Z| < 0.5)
elif abs(current_z) < ContextInfo.exit_threshold:
# 如果当前持有头寸,则平仓
if ContextInfo.holding_side != 0:
print(f"Z-Score: {current_z:.2f} 回归均值, 平仓")
order_target_percent(stock_code, 0.0, ContextInfo, ContextInfo.get_account(0))
ContextInfo.holding_side = 0
代码关键点说明
-
数据获取 (
get_market_data_ex):- 这是 QMT 推荐的新版数据接口。
- 它返回一个字典,Key 是股票代码,Value 是 Pandas DataFrame。
subscribe=True非常重要,它保证了在实盘运行时,策略能自动订阅并接收最新的行情推送。
-
指标计算 (Pandas Rolling):
- 使用了 Pandas 的
rolling(window=N).mean()和rolling(window=N).std()来高效计算移动平均和标准差。 - 这种向量化计算比手动循环快得多。
- 使用了 Pandas 的
-
交易执行 (
order_target_percent):order_target_percent(stock, target, ...)是一个非常方便的函数。- 它会自动计算当前持仓和目标持仓的差额,并自动下单买入或卖出。
- 注意:
- 如果是普通股票账户,目标仓位只能是
0.0到1.0(即只能做多或平仓)。代码中Z > Threshold时设置为0.0即代表卖出平仓。 - 如果是信用账户(融券)或期货账户,目标仓位可以设置为负数(例如
-0.5)来实现真正的反向开空。
- 如果是普通股票账户,目标仓位只能是
-
绘图 (
paint):- 代码中加入了
ContextInfo.paint,运行策略时,你可以在副图看到 Z-Score 的曲线变化,便于验证策略逻辑。
- 代码中加入了
如何运行
- 打开 QMT 客户端,进入【模型研究】或【策略交易】。
- 新建一个 Python 策略,将上述代码粘贴进去。
- 在
init函数中,将ContextInfo.set_account的注释取消,并填入你的真实资金账号(回测模式下不需要)。 - 点击【编译】,确保无误。
- 选择一个品种(如 510300.SH),点击【运行】或【回测】。
常见问题 Q&A
Q: 为什么 get_market_data_ex 返回的数据有时候是空的?
A: 请检查 ContextInfo.set_universe 是否设置了对应的股票,或者在界面上是否选择了正确的品种。另外,如果是回测,请确保本地已经下载了对应的历史数据(在数据管理中补充数据)。
Q: 如何实现真正的“做空”?
A: 在代码的 Z > Threshold 分支中,将 order_target_percent 的第二个参数设置为负数(例如 -0.5)。前提是你绑定的账号支持做空(如期货账号或两融账号)。
Q: 这里的 Z-Score 是基于收盘价计算的吗?
A: 是的,代码中使用 df['close'],即基于收盘价计算。如果需要基于盘中实时价格,逻辑是一样的,因为 handlebar 在实盘中会随每个 Tick 更新,iloc[-1] 取到的就是最新价。