问题描述
解决方案
这是一个基于 一目均衡表(Ichimoku Cloud) 的 QMT 量化交易策略。
策略原理
一目均衡表由五个指标组成,本策略主要利用以下逻辑进行趋势跟踪:
-
指标计算:
- 转换线 (Tenkan-sen): (过去9期最高价 + 过去9期最低价) / 2
- 基准线 (Kijun-sen): (过去26期最高价 + 过去26期最低价) / 2
- 先行带 A (Senkou Span A): (转换线 + 基准线) / 2,向未来平移26期(即当前K线对比的是26期前计算出的云带值)。
- 先行带 B (Senkou Span B): (过去52期最高价 + 过去52期最低价) / 2,向未来平移26期。
-
交易信号:
- 买入开仓 (Long Entry):
- 收盘价 高于 云带(即 Close > Span A 且 Close > Span B)。
- 转换线 上穿 基准线(金叉),表示短期动能强于长期。
- 卖出平仓 (Long Exit):
- 收盘价 跌破 云带(Close < Span A 或 Close < Span B)。
- 或者 转换线 下穿 基准线(死叉)。
- 买入开仓 (Long Entry):
QMT 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import time
def init(ContextInfo):
# ================= 策略参数设置 =================
# 转换线周期 (默认9)
ContextInfo.tenkan_window = 9
# 基准线周期 (默认26)
ContextInfo.kijun_window = 26
# 先行带B计算周期 (默认52)
ContextInfo.senkou_b_window = 52
# 云带位移周期 (默认26)
ContextInfo.displacement = 26
# 交易标的 (示例:平安银行)
ContextInfo.stock_code = '000001.SZ'
# 设置股票池
ContextInfo.set_universe([ContextInfo.stock_code])
# 资金账号 (请修改为您的实际账号)
ContextInfo.account_id = '6000000000'
# 账号类型:'STOCK'股票, 'FUTURE'期货
ContextInfo.account_type = 'STOCK'
# 设置交易账号
ContextInfo.set_account(ContextInfo.account_id)
# 每次交易数量 (股)
ContextInfo.trade_vol = 1000
def get_ichimoku_data(ContextInfo, stock_code):
"""
计算一目均衡表数据
"""
# 获取足够的历史数据,长度需要覆盖最长周期 + 位移 + 缓冲
# 52 (Senkou B) + 26 (Displacement) + buffer
count = 150
# 获取行情数据
# 注意:get_market_data_ex 返回的是 {code: dataframe}
data_map = ContextInfo.get_market_data_ex(
['high', 'low', 'close'],
[stock_code],
period=ContextInfo.period,
count=count,
dividend_type='front' # 前复权
)
if stock_code not in data_map:
return None
df = data_map[stock_code]
if len(df) < ContextInfo.senkou_b_window + ContextInfo.displacement:
return None
# 1. 计算转换线 (Tenkan-sen): (9日最高 + 9日最低) / 2
high_9 = df['high'].rolling(window=ContextInfo.tenkan_window).max()
low_9 = df['low'].rolling(window=ContextInfo.tenkan_window).min()
df['tenkan_sen'] = (high_9 + low_9) / 2
# 2. 计算基准线 (Kijun-sen): (26日最高 + 26日最低) / 2
high_26 = df['high'].rolling(window=ContextInfo.kijun_window).max()
low_26 = df['low'].rolling(window=ContextInfo.kijun_window).min()
df['kijun_sen'] = (high_26 + low_26) / 2
# 3. 计算先行带 A (Senkou Span A): (转换线 + 基准线) / 2
# 注意:在图表中是向未来平移26期。
# 在代码逻辑中,我们要比较的是"当前价格"与"当前时刻对应的云带"。
# 当前时刻的云带A,实际上是26期之前计算出来的 (Tenkan + Kijun)/2
df['senkou_span_a'] = ((df['tenkan_sen'] + df['kijun_sen']) / 2).shift(ContextInfo.displacement)
# 4. 计算先行带 B (Senkou Span B): (52日最高 + 52日最低) / 2
# 同样向未来平移26期
high_52 = df['high'].rolling(window=ContextInfo.senkou_b_window).max()
low_52 = df['low'].rolling(window=ContextInfo.senkou_b_window).min()
df['senkou_span_b'] = ((high_52 + low_52) / 2).shift(ContextInfo.displacement)
# 删除包含NaN的行 (主要是由于shift和rolling造成的)
df.dropna(inplace=True)
return df
def handlebar(ContextInfo):
# 获取当前K线索引
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
stock_code = ContextInfo.stock_code
# 计算指标
df = get_ichimoku_data(ContextInfo, stock_code)
if df is None or len(df) < 2:
return
# 获取最新一根K线的数据 (当前时刻)
curr_bar = df.iloc[-1]
# 获取前一根K线的数据 (用于判断交叉)
prev_bar = df.iloc[-2]
# 提取变量
close = curr_bar['close']
tenkan = curr_bar['tenkan_sen']
kijun = curr_bar['kijun_sen']
span_a = curr_bar['senkou_span_a']
span_b = curr_bar['senkou_span_b']
prev_tenkan = prev_bar['tenkan_sen']
prev_kijun = prev_bar['kijun_sen']
# 获取当前持仓
position = 0
positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
for pos in positions:
if pos.m_strInstrumentID == stock_code:
position = pos.m_nVolume
break
# ================= 交易逻辑 =================
# 1. 买入条件
# 条件A: 收盘价在云带上方 (Close > Span A AND Close > Span B)
price_above_cloud = (close > span_a) and (close > span_b)
# 条件B: 转换线金叉基准线 (Tenkan 上穿 Kijun)
golden_cross = (prev_tenkan <= prev_kijun) and (tenkan > kijun)
# 也可以放宽条件:只要转换线大于基准线,且价格在云上
trend_is_up = tenkan > kijun
# 最终买入信号:价格在云上 且 (发生金叉 或 已经是多头排列)
# 这里演示严格策略:价格在云上 且 发生金叉
buy_signal = price_above_cloud and golden_cross
# 2. 卖出条件
# 条件A: 收盘价跌入云带或跌破云带
price_enter_cloud = (close < span_a) or (close < span_b)
# 条件B: 转换线死叉基准线
death_cross = (prev_tenkan >= prev_kijun) and (tenkan < kijun)
sell_signal = price_enter_cloud or death_cross
# ================= 执行交易 =================
# 只有在最后一根K线(实时行情)或回测模式下才下单
if not ContextInfo.is_last_bar():
# 如果是回测,每一根K线都要跑,不需要return
# 如果是实盘,通常只在最新K线跑,但QMT机制是历史K线也会跑一遍handlebar
# 这里为了兼容回测,不做return拦截,但请注意实盘时的信号闪烁问题
pass
# 买入操作
if buy_signal and position == 0:
print(f"[{ContextInfo.stock_code}] 触发买入信号: 价格={close}, 云带A={span_a}, 云带B={span_b}")
# 23: 买入, 1101: 单股单账号普通下单
passorder(23, 1101, ContextInfo.account_id, stock_code, 5, -1, ContextInfo.trade_vol, ContextInfo)
# 卖出操作
elif sell_signal and position > 0:
print(f"[{ContextInfo.stock_code}] 触发卖出信号: 价格={close}, 转换线={tenkan}, 基准线={kijun}")
# 24: 卖出
passorder(24, 1101, ContextInfo.account_id, stock_code, 5, -1, position, ContextInfo)
# 绘图 (可选,用于回测界面观察)
ContextInfo.paint('Tenkan', tenkan, -1, 0, 'yellow')
ContextInfo.paint('Kijun', kijun, -1, 0, 'blue')
ContextInfo.paint('SpanA', span_a, -1, 0, 'green')
ContextInfo.paint('SpanB', span_b, -1, 0, 'red')
代码要点解析
-
数据获取 (
get_market_data_ex):- 一目均衡表需要较长的历史数据(至少52期用于计算 Senkou Span B,再加上26期的位移
shift,以及计算 Rolling 时的损耗),因此count设置为 150 以确保数据充足。 - 使用
dividend_type='front'进行前复权,保证技术指标计算不受分红配股影响。
- 一目均衡表需要较长的历史数据(至少52期用于计算 Senkou Span B,再加上26期的位移
-
云带的位移 (
shift):- 在经典理论中,先行带 A 和 B 是画在“未来”的。
- 在代码逻辑中,我们需要判断当前价格与当前时刻对应的云带的关系。
- 因此,代码中使用
.shift(26)将计算出的云带值向后移动,使得 DataFrame 中当前索引 (iloc[-1]) 对应的senkou_span_a正是 26 天前基于当时数据预测出来的、用于当前时刻支撑/阻力的数值。
-
交易逻辑:
- 买入: 采用较严格的趋势确认,要求价格完全处于云带上方(强趋势),并且短期均线(转换线)上穿中期均线(基准线)。
- 卖出: 采用保护性止损逻辑,一旦价格跌入云层内部(趋势减弱)或发生死叉,即平仓。
-
下单函数 (
passorder):- 使用
23(买入) 和24(卖出) 操作码。 1101代表普通股票交易。- 价格类型
5代表最新价(市价)。
- 使用
Q&A
Q: 为什么代码中要用 shift(26)?
A: 一目均衡表的先行带(Senkou Span)定义是将计算结果向未来平移26天。在 Pandas DataFrame 中,如果不平移,当前行的数据是基于当前收盘价计算的。为了让“今天”的行显示“26天前预测的云带值”,我们需要将数据向下(向未来)移动26行。
Q: 如何在实盘中避免信号闪烁?
A: handlebar 函数在实盘中会随每一个 Tick 更新。如果只希望在 K 线走完时确认信号,可以在逻辑中加入 if ContextInfo.is_new_bar(): 的判断,或者使用 do_order 函数配合,或者仅在收盘前几分钟运行策略。本代码使用的是实时触发逻辑。
Q: 这个策略适用于什么周期?
A: 一目均衡表传统上是为日线 (1d) 设计的。如果用于分钟线(如 15m, 60m),建议适当调整参数(9, 26, 52)以适应更高频的波动,或者保持参数不变但接受更多的噪音。