🚀 AI 一键生成 joinquant 策略代码

JoinQuant 平台如何编写 Dual Thrust 期货策略?(附源码)

joinquant | 发布时间: 2025/12/22
以下内容由 EasyQuant 生成。

问题描述

如何编写 Dual Thrust 期货交易策略?

解决方案

Dual Thrust 是一种经典的日内趋势跟踪策略,由 Michael Chalek 开发。它通过计算过去 N 天的最高价(High)、最低价(Low)和收盘价(Close)来构建一个震荡区间(Range),并结合当天的开盘价(Open)来确定上轨(Buy Line)和下轨(Sell Line)。

以下是在 JoinQuant 聚宽平台上实现的 Dual Thrust 期货策略代码。

策略逻辑

  1. 计算 Range
    • $HH = \text{过去 } N \text{ 日的最高价的最大值}$
    • $HC = \text{过去 } N \text{ 日的收盘价的最大值}$
    • $LC = \text{过去 } N \text{ 日的收盘价的最小值}$
    • $LL = \text{过去 } N \text{ 日的最低价的最小值}$
    • $Range = \max(HH - LC, HC - LL)$
  2. 确定轨道
    • $Upper = Open_{today} + K_1 \times Range$
    • $Lower = Open_{today} - K_2 \times Range$
  3. 交易信号
    • 当价格突破 $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

代码关键点解析

  1. 账户类型设置
    initialize 中必须使用 set_subportfolios 将账户类型设置为 index_futures(金融期货)或 futures(商品期货),否则默认是股票账户,无法进行期货开平仓操作。

  2. Range 的计算
    使用了 attribute_history 获取过去 g.N 天的日线数据。这个计算放在 run_daily(calculate_range, time='before_open') 中,确保每天开盘前更新一次震荡幅度。

  3. 上下轨的动态计算
    Dual Thrust 的核心在于轨道是基于当天开盘价浮动的。在 handle_data 中,我们通过 get_current_data()[security].day_open 获取当天的开盘价。
    注意:在回测中,分钟级别的 handle_data 能够获取到当天的 day_open

  4. 交易执行

    • 使用 order(security, amount, side='long'/'short') 进行下单。
    • side='long' 表示多头方向(买入开多或卖出平多)。
    • side='short' 表示空头方向(卖出开空或买入平空)。
    • 在开新仓前,先检查反向持仓并平仓(翻单逻辑)。
  5. 标的选择
    代码中使用 IF9999.CCFX(沪深300主力连续合约)。在实际回测中,主力连续合约是由聚宽根据持仓量拼接而成的虚拟合约,适合策略研究。

Q&A

Q: 如何修改策略以交易商品期货(如螺纹钢)?
A: 将 g.security 修改为对应的商品期货主力合约代码,例如 'RB9999.XSGE'。同时,建议在 initialize 中将 set_subportfoliostype 改为 '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)的偏差,超过一定比例则强制平仓。