问题描述
解决方案
这是一个基于 QMT 平台 Python API 编写的 KDJ 指标量化策略。
策略逻辑
- 指标计算:计算 KDJ 指标(N=9, M1=3, M2=3)。
- $RSV = (收盘价 - N日内最低价) / (N日内最高价 - N日内最低价) \times 100$
- $K = 2/3 \times 前一日K + 1/3 \times 当日RSV$
- $D = 2/3 \times 前一日D + 1/3 \times 当日K$
- $J = 3K - 2D$
- 买入条件:当 $J$ 值小于 20 时,认为超卖,执行买入(满仓)。
- 卖出条件:当 $J$ 值大于 80 时,认为超买,执行卖出(空仓)。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
初始化函数,策略启动时调用一次
"""
# 设置资金账号,请修改为您的实际账号
# 格式如:'6000000248'
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.account_id)
# 策略参数设置
ContextInfo.N = 9 # 计算RSV的周期
ContextInfo.M1 = 3 # K值的平滑周期
ContextInfo.M2 = 3 # D值的平滑周期
# 设置交易标的,这里默认为当前主图显示的品种
# 如果需要固定品种,可以设置为 ContextInfo.stock_code = '600000.SH'
ContextInfo.stock_code = ContextInfo.stockcode + '.' + ContextInfo.market
print("策略初始化完成,交易标的:", ContextInfo.stock_code)
def get_kdj(df, N, M1, M2):
"""
计算KDJ指标的辅助函数
"""
# 计算 RSV
low_list = df['low'].rolling(window=N).min()
high_list = df['high'].rolling(window=N).max()
rsv = (df['close'] - low_list) / (high_list - low_list) * 100
# 填充NaN值,防止计算中断
rsv = rsv.fillna(50)
# 计算 K, D, J
# 注意:国内主流软件(如通达信、同花顺)的KDJ算法中,平滑因子通常为 1/M
# pandas的ewm中,alpha = 1/com + 1,这里使用 com = M-1 来近似 SMA 迭代逻辑
# K = (M1-1)/M1 * PrevK + 1/M1 * RSV => alpha = 1/M1
k = rsv.ewm(alpha=1/M1, adjust=False).mean()
d = k.ewm(alpha=1/M2, adjust=False).mean()
j = 3 * k - 2 * d
return k, d, j
def handlebar(ContextInfo):
"""
K线周期回调函数
"""
# 获取当前正在处理的K线索引
index = ContextInfo.barpos
# 获取历史行情数据,多取一些数据以保证指标计算准确
# 获取 Open, High, Low, Close
data_len = 100
df_dict = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
[ContextInfo.stock_code],
period=ContextInfo.period,
count=data_len,
dividend_type='follow' # 跟随主图复权
)
if ContextInfo.stock_code not in df_dict:
return
df = df_dict[ContextInfo.stock_code]
# 数据长度不足以计算指标时直接返回
if len(df) < ContextInfo.N + 2:
return
# 计算 KDJ
k, d, j = get_kdj(df, ContextInfo.N, ContextInfo.M1, ContextInfo.M2)
# 获取当前K线的 J 值
# 注意:在回测中,iloc[-1]是当前bar;在实盘中,iloc[-1]是最新的一根K线(可能未走完)
current_j = j.iloc[-1]
current_k = k.iloc[-1]
current_d = d.iloc[-1]
# 获取当前价格
current_price = df['close'].iloc[-1]
# 打印日志方便调试
# print(f"时间: {df.index[-1]}, K: {current_k:.2f}, D: {current_d:.2f}, J: {current_j:.2f}")
# --- 交易逻辑 ---
# 1. 买入信号:J < 20
if current_j < 20:
# 使用 order_target_percent 调整仓位到 100% (满仓买入)
# 如果已经是满仓,该函数不会重复下单
order_target_percent(ContextInfo.stock_code, 1.0, ContextInfo, ContextInfo.account_id)
# 只有在最后一根K线(实盘最新时刻)才打印信号,避免回测刷屏
if ContextInfo.is_last_bar():
print(f"触发买入信号 (J={current_j:.2f} < 20),执行买入。")
# 2. 卖出信号:J > 80
elif current_j > 80:
# 使用 order_target_percent 调整仓位到 0% (清仓卖出)
order_target_percent(ContextInfo.stock_code, 0.0, ContextInfo, ContextInfo.account_id)
if ContextInfo.is_last_bar():
print(f"触发卖出信号 (J={current_j:.2f} > 80),执行卖出。")
代码使用说明
- 编码格式:代码第一行
# -*- coding: gbk -*-是必须的,因为 QMT 的 Python 编辑器默认使用 GBK 编码,防止中文注释乱码。 - 账号设置:请务必将
init函数中的YOUR_ACCOUNT_ID替换为您实际的资金账号(字符串格式)。 - 数据获取:使用了
get_market_data_ex接口,这是 QMT 推荐的高效数据接口。 - 指标计算:代码中手动实现了 KDJ 算法,使用了 Pandas 的
ewm(指数加权移动平均) 来模拟国内通用的SMA迭代算法,确保计算结果与行情软件一致。 - 交易函数:使用了
order_target_percent。- 当 J < 20 时,目标仓位设为
1.0(100%),即全仓买入。 - 当 J > 80 时,目标仓位设为
0.0(0%),即清仓卖出。 - 这种方式比单纯的
buy/sell更容易管理仓位,避免重复下单。
- 当 J < 20 时,目标仓位设为
- 运行模式:
- 回测:在策略编辑器中点击“回测”,设置好时间区间和基准即可。
- 实盘:在策略编辑器中点击“运行”,或者在“模型交易”中加载此策略。实盘时,
handlebar会随行情 Tick 更新,但交易信号通常基于 K 线数据。
常见问题 Q&A
Q: 为什么 J 值计算出来和看盘软件上不一样?
A: KDJ 的初始值(第一天的数据)处理方式不同会导致前期数据有差异,但随着数据量增加,差异会迅速收敛。此外,国内软件通常使用 SMA(权重为1/3的移动平均),代码中已通过 ewm(alpha=1/3) 进行了对齐。
Q: 如何只在 K 线走完那一刻下单(避免盘中信号闪烁)?
A: 可以在 handlebar 中添加判断。QMT 的机制是实盘中最后一根 K 线每个 Tick 都会触发 handlebar。如果希望 K 线收盘确认后再发单,可以使用 ContextInfo.is_new_bar() 判断是否是新的一根 K 线开始,然后基于 iloc[-2] (上一根K线) 的 J 值进行交易;或者使用 passorder 配合 quickTrade=0 参数。上述代码使用的是实时信号(盘中 J 值触及即触发)。
Q: 如何交易多只股票?
A: 可以在 init 中设置 ContextInfo.set_universe(['600000.SH', '000001.SZ']),然后在 handlebar 中遍历 ContextInfo.get_universe(),对每只股票分别获取数据和计算指标。