🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 中实现期货跨期套利(价差交易)策略?

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

问题描述

请帮我编写一个期货的跨期套利策略。

解决方案

这是一个基于 JoinQuant (聚宽) 平台的期货跨期套利策略示例。

策略逻辑说明

本策略采用 统计套利(Statistical Arbitrage) 的思路,利用布林带(Bollinger Bands)模型对两个合约的价差进行均值回归交易。

  1. 标的选择:选取某品种(如螺纹钢 RB)持仓量最大的两个合约,分别作为“近月合约”和“远月合约”。
  2. 价差计算价差 = 近月合约价格 - 远月合约价格
  3. 信号生成
    • 计算价差的移动平均线(Mean)和标准差(Std)。
    • 上轨 = Mean + K * Std。
    • 下轨 = Mean - K * Std。
  4. 交易逻辑
    • 做空价差:当价差 > 上轨,认为价差过大,卖出近月,买入远月。
    • 做多价差:当价差 < 下轨,认为价差过小,买入近月,卖出远月。
    • 平仓:当价差回归到均值附近(穿越中轨),平掉所有仓位。

策略代码

# -*- coding: utf-8 -*-
from jqdata import *
import numpy as np

def initialize(context):
    """
    初始化函数
    """
    # 1. 设定基准(这里随意设定,期货策略通常看绝对收益)
    set_benchmark('000300.XSHG')
    
    # 2. 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    
    # 3. 设置日志级别
    log.set_level('order', 'error')
    
    # 4. 初始化期货账户 (重要:必须设置为 futures 类型)
    init_cash = context.portfolio.starting_cash
    set_subportfolios([SubPortfolioConfig(cash=init_cash, type='futures')])
    
    # 5. 设置期货保证金比例 (根据实际情况调整,这里设为15%)
    set_option('futures_margin_rate', 0.15)
    
    # 6. 定义全局变量
    g.underlying_symbol = 'RB'  # 套利品种:螺纹钢
    g.near_contract = None      # 近月合约
    g.far_contract = None       # 远月合约
    
    # 7. 策略参数
    g.window = 60    # 计算均值和标准差的窗口长度(分钟或天,取决于运行频率)
    g.k = 2          # 布林带宽度(标准差倍数)
    g.lot_size = 2   # 每次交易的手数
    
    # 8. 运行频率设置
    # 每天开盘前更新主力合约
    run_daily(update_contracts, time='before_open')
    # 每分钟运行策略逻辑
    run_daily(trade_logic, time='every_bar')

def update_contracts(context):
    """
    每天开盘前更新主力合约和次主力合约
    逻辑:获取该品种所有合约,按持仓量排序,取前两名
    """
    # 获取该品种当前可交易的所有合约
    all_contracts = get_future_contracts(g.underlying_symbol)
    
    if not all_contracts:
        log.info("未找到合约")
        return

    # 获取所有合约的持仓量
    # 注意:get_current_data 在 before_open 可能拿不到当天的,取前一天的
    current_data = get_current_data()
    contract_oi = []
    
    for code in all_contracts:
        # 过滤掉即将交割的合约(可选逻辑)
        info = get_security_info(code)
        days_to_end = (info.end_date - context.current_dt.date()).days
        if days_to_end < 5:
            continue
            
        oi = current_data[code].open_interest
        contract_oi.append((code, oi))
    
    # 按持仓量降序排列
    contract_oi.sort(key=lambda x: x[1], reverse=True)
    
    if len(contract_oi) < 2:
        log.info("合约数量不足,无法套利")
        return
    
    # 选取持仓量最大的两个合约
    # 通常交割日期较近的为近月,较远的为远月
    c1 = contract_oi[0][0]
    c2 = contract_oi[1][0]
    
    info1 = get_security_info(c1)
    info2 = get_security_info(c2)
    
    if info1.start_date < info2.start_date:
        new_near = c1
        new_far = c2
    else:
        new_near = c2
        new_far = c1
        
    # 如果合约发生变化,需要处理旧仓位(这里简化处理:如果有旧合约且不在新组合中,建议平仓)
    # 本示例为简化逻辑,假设换月时手动或通过平仓逻辑处理,这里仅更新引用
    if g.near_contract != new_near or g.far_contract != new_far:
        log.info("合约更替: 近月 %s -> %s, 远月 %s -> %s" % (g.near_contract, new_near, g.far_contract, new_far))
        # 实际实盘中,这里可能需要强制平掉旧合约的仓位
        # close_all_positions(context) 
        
    g.near_contract = new_near
    g.far_contract = new_far

def trade_logic(context):
    """
    盘中交易逻辑
    """
    if g.near_contract is None or g.far_contract is None:
        return
        
    # 获取历史价格数据
    # 获取过去 g.window 个单位的数据
    near_hist = attribute_history(g.near_contract, g.window, '1m', ['close'])
    far_hist = attribute_history(g.far_contract, g.window, '1m', ['close'])
    
    if len(near_hist) < g.window or len(far_hist) < g.window:
        return
        
    # 计算价差序列 (近月 - 远月)
    spread_series = near_hist['close'] - far_hist['close']
    
    # 计算布林带
    spread_mean = spread_series.mean()
    spread_std = spread_series.std()
    
    up_rail = spread_mean + g.k * spread_std
    down_rail = spread_mean - g.k * spread_std
    
    # 获取当前最新价差
    curr_near_price = near_hist['close'][-1]
    curr_far_price = far_hist['close'][-1]
    curr_spread = curr_near_price - curr_far_price
    
    # 获取当前仓位
    # long_positions 字典 key 是合约代码
    positions = context.portfolio.long_positions
    short_positions = context.portfolio.short_positions
    
    # 定义持仓状态
    # 0: 空仓, 1: 正套(买近卖远), -1: 反套(卖近买远)
    holding_status = 0
    
    # 检查近月合约持仓
    if g.near_contract in positions and positions[g.near_contract].total_amount > 0:
        # 近月有多单,说明是买近卖远(正套)
        holding_status = 1
    elif g.near_contract in short_positions and short_positions[g.near_contract].total_amount > 0:
        # 近月有空单,说明是卖近买远(反套)
        holding_status = -1
        
    # --- 交易信号判断 ---
    
    # 1. 开仓逻辑:做空价差 (卖近买远)
    # 价差高于上轨,预期价差缩小
    if holding_status == 0 and curr_spread > up_rail:
        log.info("价差 %.2f > 上轨 %.2f,触发做空价差(卖近买远)" % (curr_spread, up_rail))
        # 卖出近月
        order_target(g.near_contract, 0, side='long') # 确保无多单
        order_target(g.near_contract, g.lot_size, side='short')
        # 买入远月
        order_target(g.far_contract, 0, side='short') # 确保无空单
        order_target(g.far_contract, g.lot_size, side='long')
        
    # 2. 开仓逻辑:做多价差 (买近卖远)
    # 价差低于下轨,预期价差扩大
    elif holding_status == 0 and curr_spread < down_rail:
        log.info("价差 %.2f < 下轨 %.2f,触发做多价差(买近卖远)" % (curr_spread, down_rail))
        # 买入近月
        order_target(g.near_contract, 0, side='short')
        order_target(g.near_contract, g.lot_size, side='long')
        # 卖出远月
        order_target(g.far_contract, 0, side='long')
        order_target(g.far_contract, g.lot_size, side='short')
        
    # 3. 平仓逻辑:回归均值
    # 如果持有反套(卖近买远),且价差回归到均值以下
    elif holding_status == -1 and curr_spread <= spread_mean:
        log.info("价差 %.2f 回归均值 %.2f,平反套仓位" % (curr_spread, spread_mean))
        close_all_positions(context)
        
    # 如果持有正套(买近卖远),且价差回归到均值以上
    elif holding_status == 1 and curr_spread >= spread_mean:
        log.info("价差 %.2f 回归均值 %.2f,平正套仓位" % (curr_spread, spread_mean))
        close_all_positions(context)

def close_all_positions(context):
    """
    平掉所有仓位
    """
    # 平多单
    for code, pos in context.portfolio.long_positions.items():
        if pos.total_amount > 0:
            order_target(code, 0, side='long')
            
    # 平空单
    for code, pos in context.portfolio.short_positions.items():
        if pos.total_amount > 0:
            order_target(code, 0, side='short')

关键点解析

  1. 账户初始化 (set_subportfolios)

    • 期货策略必须在 initialize 中将账户类型设置为 futures,否则无法进行开空仓操作。
  2. 合约选择 (update_contracts)

    • 策略没有写死合约代码(如 RB2310),而是通过 get_future_contracts 获取该品种所有合约,并根据 open_interest(持仓量)动态选择最活跃的两个合约。这保证了策略可以长期运行,自动换月。
  3. 价差计算

    • 使用 attribute_history 获取分钟级别的收盘价。
    • 利用 numpypandas 的向量化操作直接计算价差序列。
  4. 交易指令 (order_target)

    • 期货交易需要指定 side 参数。side='long' 表示操作多头仓位,side='short' 表示操作空头仓位。
    • order_target(code, 0, side='...') 用于平仓。

风险提示与改进建议

  1. 移仓换月风险:代码中简单的合约更新逻辑在实盘中可能导致旧合约未平仓就开了新合约。在实盘中,建议在 update_contracts 中加入逻辑:如果主力合约发生变化,先强制平掉旧合约的仓位。
  2. 单腿风险:使用 order_target 依次下单时,如果第一腿成交了,第二腿因为涨跌停或流动性没成交,会暴露在单边风险中。高级策略通常使用 batch_submit_orders 或自行监控成交回报来保证双边成交。
  3. 数据频率:本策略使用分钟线 (1m)。对于高频套利,可能需要使用 Tick 数据;对于低频套利,可以使用日线 (1d)。

Q&A

Q: 如何将此策略改为其他品种,例如豆粕?
A: 只需在 initialize 函数中,将 g.underlying_symbol = 'RB' 修改为 g.underlying_symbol = 'M' 即可。

Q: 为什么使用 order_target 而不是 order
A: order_target 会自动计算达到目标仓位需要买卖的数量,这在处理平仓和反手(例如从多单转为空单)时比 order 更方便,减少了计算当前持仓的逻辑错误。

Q: 策略中的 g.window = 60 是什么意思?
A: 这代表布林带的计算周期。如果策略按分钟运行,这就是过去60分钟的均值;如果按天运行,就是过去60天的均值。周期越短,交易越频繁,但也更容易受噪音干扰。