问题描述
解决方案
Dual Thrust 是一种经典的日内趋势跟踪策略,由 Michael Chalek 开发。它通过计算过去 N 天的最高价(High)、最低价(Low)和收盘价(Close)来构建一个震荡区间(Range),并结合当天的开盘价(Open)来确定上轨(Buy Line)和下轨(Sell Line)。
以下是在 JoinQuant 聚宽平台上实现的 Dual Thrust 期货策略代码。
策略逻辑
- 计算 Range:
- $HH = \text{过去 } N \text{ 日的最高价的最大值}$
- $HC = \text{过去 } N \text{ 日的收盘价的最大值}$
- $LC = \text{过去 } N \text{ 日的收盘价的最小值}$
- $LL = \text{过去 } N \text{ 日的最低价的最小值}$
- $Range = \max(HH - LC, HC - LL)$
- 确定轨道:
- $Upper = Open_{today} + K_1 \times Range$
- $Lower = Open_{today} - K_2 \times Range$
- 交易信号:
- 当价格突破 $Upper$ 时,做多(如果持有空单则平空开多)。
- 当价格跌破 $Lower$ 时,做空(如果持有多单则平多开空)。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
"""
初始化函数,设定基准、手续费、全局变量等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定操作的标的:沪深300股指期货主力连续合约
g.security = 'IF9999.CCFX'
# 设定期货账户(必须设定,否则默认是股票账户无法交易期货)
set_subportfolios([SubPortfolioConfig(cash=context.portfolio.starting_cash, type='index_futures')])
# 设定期货交易手续费(以中金所为例,平今仓手续费较高,这里简化设置)
set_order_cost(OrderCost(open_commission=0.000023, close_commission=0.000023, close_today_commission=0.0023, min_commission=0), type='index_futures')
# 策略参数
g.N = 5 # 回溯天数
g.k1 = 0.5 # 上轨系数
g.k2 = 0.5 # 下轨系数
# 全局变量记录轨道
g.range = 0
g.buy_line = 0
g.sell_line = 0
# 每天开盘前运行,计算Range
run_daily(calculate_range, time='before_open')
def calculate_range(context):
"""
每天开盘前计算过去N天的Range
"""
# 获取过去N天的历史数据(不包含今天)
# fields: high, low, close
hist = attribute_history(g.security, g.N, '1d', ['high', 'low', 'close'], skip_paused=True)
if len(hist) < g.N:
return
# 计算四个关键价格
HH = hist['high'].max()
HC = hist['close'].max()
LC = hist['close'].min()
LL = hist['low'].min()
# 计算Range
# Range = Max(HH-LC, HC-LL)
g.range = max(HH - LC, HC - LL)
# 打印日志方便调试
# log.info("今日 Range: %.2f" % g.range)
def handle_data(context, data):
"""
盘中运行函数,每分钟执行一次(分钟回测模式下)
"""
security = g.security
# 获取当前时间,避免在非交易时间段产生信号(如集合竞价期间)
current_dt = context.current_dt
# 获取当天的开盘价
# 注意:get_current_data() 获取的是当天的快照数据
current_data = get_current_data()
day_open = current_data[security].day_open
# 如果开盘价为nan(尚未开盘),则直接返回
if np.isnan(day_open):
return
# 计算上下轨
# 上轨 = TodayOpen + K1 * Range
# 下轨 = TodayOpen - K2 * Range
g.buy_line = day_open + g.k1 * g.range
g.sell_line = day_open - g.k2 * g.range
# 获取当前最新价格
current_price = data[security].close
# 获取当前持仓情况
# long_positions 和 short_positions 是字典,key是标的代码
long_pos = context.portfolio.long_positions[security]
short_pos = context.portfolio.short_positions[security]
# 交易逻辑
# 1. 突破上轨,做多
if current_price > g.buy_line:
# 如果持有空单,先平空
if short_pos.total_amount > 0:
order_target(security, 0, side='short')
log.info("价格突破上轨,平空单")
# 如果没有多单,则开多
if long_pos.total_amount == 0:
# 全仓开多(这里简单演示,实际需考虑资金管理)
# 使用 order_value 开仓,注意期货保证金
# 这里简单开1手演示
order(security, 1, side='long')
log.info("价格突破上轨,开多单,价格: %.2f, 上轨: %.2f" % (current_price, g.buy_line))
# 2. 跌破下轨,做空
elif current_price < g.sell_line:
# 如果持有多单,先平多
if long_pos.total_amount > 0:
order_target(security, 0, side='long')
log.info("价格跌破下轨,平多单")
# 如果没有空单,则开空
if short_pos.total_amount == 0:
# 开1手空单
order(security, 1, side='short')
log.info("价格跌破下轨,开空单,价格: %.2f, 下轨: %.2f" % (current_price, g.sell_line))
def after_trading_end(context):
"""
收盘后运行
"""
pass
代码关键点解析
-
账户类型设置:
在initialize中必须使用set_subportfolios将账户类型设置为index_futures(金融期货)或futures(商品期货),否则默认是股票账户,无法进行期货开平仓操作。 -
Range 的计算:
使用了attribute_history获取过去g.N天的日线数据。这个计算放在run_daily(calculate_range, time='before_open')中,确保每天开盘前更新一次震荡幅度。 -
上下轨的动态计算:
Dual Thrust 的核心在于轨道是基于当天开盘价浮动的。在handle_data中,我们通过get_current_data()[security].day_open获取当天的开盘价。
注意:在回测中,分钟级别的handle_data能够获取到当天的day_open。 -
交易执行:
- 使用
order(security, amount, side='long'/'short')进行下单。 side='long'表示多头方向(买入开多或卖出平多)。side='short'表示空头方向(卖出开空或买入平空)。- 在开新仓前,先检查反向持仓并平仓(翻单逻辑)。
- 使用
-
标的选择:
代码中使用IF9999.CCFX(沪深300主力连续合约)。在实际回测中,主力连续合约是由聚宽根据持仓量拼接而成的虚拟合约,适合策略研究。
Q&A
Q: 如何修改策略以交易商品期货(如螺纹钢)?
A: 将 g.security 修改为对应的商品期货主力合约代码,例如 'RB9999.XSGE'。同时,建议在 initialize 中将 set_subportfolios 的 type 改为 'futures',并根据该品种调整 set_order_cost 中的手续费率。
Q: 为什么使用 get_current_data() 获取开盘价而不是 data 对象?
A: handle_data 中的 data 对象主要用于获取上一分钟(或单位时间)的收盘价、高低价等历史切片数据。而当天的开盘价(Open)属于当天的属性,通过 get_current_data()[security].day_open 获取最为准确和方便。
Q: K1 和 K2 参数如何设置?
A: K1 和 K2 是策略的敏感度参数。
- 如果 K1 < K2,则多头触发相对容易,适合多头趋势明显的市场。
- 如果 K1 > K2,则空头触发相对容易,适合空头趋势明显的市场。
- 通常情况下,可以设为 K1 = K2(如 0.5 或 0.7),这需要通过参数优化(Optimization)来找到适合特定品种的最佳值。
Q: 策略是否包含止损?
A: 上述基础代码是纯粹的翻单策略(Reverse Strategy),即持有直到反向信号出现才平仓反手。如果需要止损,可以在 handle_data 中加入逻辑:判断当前价格与开仓均价(context.portfolio.long_positions[security].avg_cost)的偏差,超过一定比例则强制平仓。