问题描述
解决方案
这是一个基于 QMT 平台的 MACD 和 KDJ 双指标共振(同时金叉) 策略。
策略逻辑说明
- 核心思想:
- MACD (趋势指标):代表中长期趋势。当 DIF 上穿 DEA 时(金叉),视为趋势转强信号。
- KDJ (摆动指标):代表短期超买超卖。当 K 线 上穿 D 线时(金叉),视为短期买入信号。
- 共振买入:当两者在同一根 K 线(例如同一天)同时发生金叉时,意味着短期爆发力与中长期趋势形成共振,可靠性理论上高于单一指标。
- 买入条件:
- MACD 发生金叉(当前 DIF > DEA 且 上一周期 DIF <= 上一周期 DEA)。
- KDJ 发生金叉(当前 K > D 且 上一周期 K <= 上一周期 D)。
- 卖出条件:
- 为防止风险,当任一指标出现死叉(MACD 死叉 或 KDJ 死叉)时,执行平仓。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import talib
def init(ContextInfo):
# 1. 设置资金账号 (请替换为您自己的资金账号)
ContextInfo.accID = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.accID)
# 2. 设置股票池 (示例:平安银行)
ContextInfo.target_list = ['000001.SZ', '600000.SH']
ContextInfo.set_universe(ContextInfo.target_list)
# 3. 设置策略运行周期,这里以日线为例
ContextInfo.period = '1d'
# 4. 设置一些参数
ContextInfo.macd_fast = 12
ContextInfo.macd_slow = 26
ContextInfo.macd_signal = 9
ContextInfo.kdj_n = 9
ContextInfo.kdj_m1 = 3
ContextInfo.kdj_m2 = 3
def handlebar(ContextInfo):
# 获取当前设置的股票池
stock_list = ContextInfo.get_universe()
# 获取当前 K 线位置,如果是回测,barpos 会随时间递增
index = ContextInfo.barpos
# 获取当前时间,用于日志输出
realtime = ContextInfo.get_bar_timetag(index)
date_str = timetag_to_datetime(realtime, '%Y-%m-%d')
# 遍历每只股票进行计算
for stock in stock_list:
# 1. 获取历史行情数据
# 为了计算 MACD 和 KDJ,我们需要足够多的历史数据,这里取 100 根
# 注意:get_market_data_ex 返回的是 dict {code: dataframe}
data_map = ContextInfo.get_market_data_ex(
['open', 'high', 'low', 'close'],
[stock],
period=ContextInfo.period,
count=100,
dividend_type='front' # 前复权
)
if stock not in data_map:
continue
df = data_map[stock]
# 数据长度不足,无法计算指标,跳过
if len(df) < 30:
continue
# 2. 计算 MACD 指标
close_prices = df['close'].values
# 使用 talib 计算 MACD
# diff 对应 DIF, dea 对应 DEA, macd 对应 MACD柱
diff, dea, macd = talib.MACD(
close_prices,
fastperiod=ContextInfo.macd_fast,
slowperiod=ContextInfo.macd_slow,
signalperiod=ContextInfo.macd_signal
)
# 3. 计算 KDJ 指标
# 由于 talib 的 STOCH 计算方式与国内软件(如通达信)略有差异,
# 这里使用自定义函数计算符合国内习惯的 KDJ
k_vals, d_vals, j_vals = calc_kdj_china(df, ContextInfo.kdj_n, ContextInfo.kdj_m1, ContextInfo.kdj_m2)
# 4. 获取当前和上一周期的指标值
# -1 代表当前最新一根 K 线,-2 代表上一根
curr_diff, prev_diff = diff[-1], diff[-2]
curr_dea, prev_dea = dea[-1], dea[-2]
curr_k, prev_k = k_vals[-1], k_vals[-2]
curr_d, prev_d = d_vals[-1], d_vals[-2]
# 5. 判断金叉与死叉
# MACD 金叉: 上一刻 DIF <= DEA, 当前 DIF > DEA
macd_golden_cross = (prev_diff <= prev_dea) and (curr_diff > curr_dea)
# MACD 死叉
macd_dead_cross = (prev_diff >= prev_dea) and (curr_diff < curr_dea)
# KDJ 金叉: 上一刻 K <= D, 当前 K > D
kdj_golden_cross = (prev_k <= prev_d) and (curr_k > curr_d)
# KDJ 死叉
kdj_dead_cross = (prev_k >= prev_d) and (curr_k < curr_d)
# 6. 获取当前持仓
position = get_position(ContextInfo, stock)
# 7. 交易逻辑
# 买入逻辑:MACD 和 KDJ 同时金叉 (双金叉共振)
if macd_golden_cross and kdj_golden_cross:
if position == 0:
# 全仓买入 (示例逻辑,实际请做好资金管理)
# 这里简单演示,每次买入 1000 股
order_shares(stock, 1000, 'fix', df['close'].iloc[-1], ContextInfo, ContextInfo.accID)
print(f"{date_str} [买入] {stock}: MACD与KDJ同时金叉")
# 卖出逻辑:任一指标死叉则平仓 (止盈/止损)
elif (macd_dead_cross or kdj_dead_cross):
if position > 0:
order_shares(stock, -position, 'fix', df['close'].iloc[-1], ContextInfo, ContextInfo.accID)
print(f"{date_str} [卖出] {stock}: 出现死叉信号 (MACD:{macd_dead_cross}, KDJ:{kdj_dead_cross})")
def get_position(ContextInfo, stock_code):
"""
获取某只股票的持仓数量
"""
# 获取持仓对象列表
positions = get_trade_detail_data(ContextInfo.accID, 'stock', 'position')
for pos in positions:
if pos.m_strInstrumentID + '.' + pos.m_strExchangeID == stock_code:
return pos.m_nVolume
return 0
def calc_kdj_china(df, n=9, m1=3, m2=3):
"""
计算符合国内行情的 KDJ 指标
RSV = (CLOSE - LOW_N) / (HIGH_N - LOW_N) * 100
K = (M1-1)/M1 * PREV_K + 1/M1 * RSV
D = (M2-1)/M2 * PREV_D + 1/M2 * K
J = 3*K - 2*D
"""
high = df['high']
low = df['low']
close = df['close']
# 计算 RSV
low_list = low.rolling(window=n, min_periods=1).min()
high_list = high.rolling(window=n, min_periods=1).max()
rsv = (close - low_list) / (high_list - low_list) * 100
# 处理除以0的情况
rsv = rsv.fillna(0)
# 计算 K, D, J
k_vals = []
d_vals = []
# 初始值通常设为 50
k = 50
d = 50
for i in range(len(rsv)):
rsv_val = rsv.iloc[i]
# 迭代计算
k = (m1 - 1) / m1 * k + 1 / m1 * rsv_val
d = (m2 - 1) / m2 * d + 1 / m2 * k
k_vals.append(k)
d_vals.append(d)
k_series = pd.Series(k_vals, index=df.index)
d_series = pd.Series(d_vals, index=df.index)
j_series = 3 * k_series - 2 * d_series
return k_series.values, d_series.values, j_series.values
关键点解析
- 数据获取 (
get_market_data_ex):- 使用了
get_market_data_ex接口,这是 QMT 推荐的高效数据接口。 count=100:为了保证 MACD(需要较长周期平滑)和 KDJ 计算准确,至少需要获取 100 根 K 线的数据。
- 使用了
- 指标计算:
- MACD:直接使用了 QMT 内置的
talib库,计算速度快且标准。 - KDJ:为了匹配国内行情软件(如通达信、同花顺)的算法,我手写了一个
calc_kdj_china函数。talib自带的STOCH函数计算逻辑与国内略有不同,直接使用可能会导致信号对不上。
- MACD:直接使用了 QMT 内置的
- 共振逻辑:
- 代码中使用了
if macd_golden_cross and kdj_golden_cross:来判断。 - 注意:严格要求“同一天”双金叉在实际行情中出现的频率较低。如果您发现买入信号太少,可以放宽条件,例如:MACD 处于金叉状态(DIF>DEA)且 KDJ 刚刚金叉。
- 代码中使用了
- 交易执行:
- 使用了
order_shares进行下单。 - 使用了
get_trade_detail_data来查询当前持仓,确保不会重复买入或卖空。
- 使用了
如何使用
- 打开 QMT 客户端,进入【模型研究】或【策略编辑器】。
- 新建一个 Python 策略。
- 将上述代码完整复制进去。
- 修改
ContextInfo.accID为您的实际资金账号。 - 点击【编译】检查无误后,可以进行【回测】或【运行】。
Q&A
Q: 为什么 KDJ 指标不用 talib.STOCH?
A: talib.STOCH 是国际标准算法,而国内 A 股软件普遍采用的是一种平滑移动平均算法(迭代法)。直接用 talib 算出来的数值会和您在看盘软件上看到的不一样,导致信号偏差。代码中提供的 calc_kdj_china 函数模拟了国内软件的算法。
Q: 策略没有信号怎么办?
A: "同时金叉"是一个非常严格的条件。如果回测没有交易,建议放宽条件。例如:只要 MACD 趋势向上(DIF > DEA),此时 KDJ 发生金叉即可买入。您可以将买入判断修改为:
# 宽松版买入逻辑
if (curr_diff > curr_dea) and kdj_golden_cross:
# 买入...
Q: 如何在回测中查看买卖点?
A: 代码中包含了 print 语句,回测时请查看底部的“日志”窗口,或者在图表上右键选择“叠加模型”来查看信号。