双焦(焦煤-焦炭)套利策略

一、交易策略解释

核心思想

双焦套利的核心在于监控和交易“炼焦利润价差”。焦煤(JM)是生产焦炭(J)的主要原料,通常约1.3吨左右的焦煤(经过洗选和配煤)可以生产1吨焦炭(具体配比因煤种、工艺、焦炭质量要求等因素而异)。

  • 炼焦利润价差 (Coking Margin Spread): 该价差代表了在期货市场上,基于特定手数配比的焦炭期货价值与相应生产所需的焦煤期货价值之间的差额。

核心思想是:

  • 这个炼焦利润价差会受到焦煤、焦炭各自供需基本面以及钢铁行业整体状况的影响。策略通过计算该价差的标准化得分(Z-score)来衡量其偏离历史均值的程度。
  • 预期价差扩大(做多炼焦利润): 当炼焦利润价差的Z-score显著为正(例如,高于一个设定的正阈值),表明当前(期货市场反映的)炼焦利润远高于其历史平均水平。策略预期此偏离趋势短期内可能持续或进一步扩大,因此会买入焦炭期货,同时卖出相应配比的焦煤期货。
  • 预期价差缩小或转负(做空炼焦利润): 当炼焦利润价差的Z-score显著为负(例如,低于一个设定的负阈值),表明当前炼焦利润远低于其历史平均水平。策略预期此偏离趋势短期内可能持续或进一步扩大(即利润更低或亏损更大),因此会卖出焦炭期货,同时买入相应配比的焦煤期货。
  • 该策略的平仓逻辑则基于价差(或其Z-score)向其历史均值区域回归。即,入场后,当价差的偏离程度减小,Z-score的绝对值回落到某个较小的阈值内时,头寸会被平掉。

理论基础

  • 产业链的直接联系: 焦煤是焦炭的直接上游原料,焦炭主要用于高炉炼铁,是钢铁产业链的重要组成部分。焦煤成本是影响焦炭价格和焦化厂利润的核心因素。
  • 供需基本面的驱动:
    • 焦煤端 (JM): 国内主焦煤产量(受矿山安全、环保政策影响大)、进口煤(如蒙古煤、澳洲煤的政策和通关情况)、下游焦化厂开工率及焦煤库存、钢厂焦化厂的补库需求等。
    • 焦炭端 (J): 焦化企业开工率(受利润、环保限产政策影响)、下游钢厂高炉开工率及对焦炭的需求、钢厂及港口焦炭库存、焦炭进出口情况等。
    • 这些因素的变化导致两者价格波动,进而影响炼焦利润价差。
  • 均值回归特性 (Mean Reversion of Coking Profit): 历史数据显示,炼焦利润(无论是现货还是期货价差)在一定周期内具有围绕某个均值波动的特性。当利润过高时,可能刺激焦化厂提高开工率,增加焦炭供应,压低焦炭价格,同时增加对焦煤的需求,推高焦煤价格,从而压缩利润。当利润过低甚至亏损时,焦化厂可能减产,减少焦炭供应,支撑焦炭价格,同时减少对焦煤的需求,压低焦煤价格,从而修复利润。策略的平仓和部分止损逻辑依赖于此特性。
  • 企业套期保值行为: 焦化厂和钢厂会利用期货市场进行套期保值,例如在远期炼焦利润可观时,焦化厂可能卖出焦炭期货、买入焦煤期货(锁定利润)。

策略适用场景

  • 炼焦利润价差显著偏离: 当计算出的炼焦利润价差的Z-score突破预设的阈值时,视为潜在的交易信号。
  • 预期均值回归(主要针对平仓和整体策略有效性): 虽然开仓可能基于短期偏离趋势的持续,但策略的盈利平仓通常依赖于价差最终向其历史均值区域回归的预期。
  • 基本面分析辅助: 结合对焦煤和焦炭各自基本面的分析。如果Z-score发出信号,但基本面强烈指向相反方向的结构性变化(例如,重大的行业政策调整),则可能需要谨慎。
  • 流动性充足: 策略涉及大连商品交易所(DCE)的焦煤期货(DCE.jm)和焦炭期货(DCE.j),需要保证各合约均有良好的流动性。

二、天勤介绍

天勤平台概述

天勤(TqSdk)是一个由信易科技开发的开源量化交易系统,为期货、期权等衍生品交易提供专业的量化交易解决方案。平台具有以下特 点:

  • 丰富的行情数据 提供所有可交易合约的全部Tick和K线数据,基于内存数据库实现零延迟访问。
  • 一站式的解决方案 从历史数据分析到实盘交易的完整工具链,打通开发、回测、模拟到实盘的全流程。
  • 专业的技术支持 近百个技术指标源码,深度集成pandas和numpy,采用单线程异步模型保证性能。

策略开发流程

  • 环境准备
    • 安装Python环境(推荐Python 3.6或以上版本)
    • 安装tqsdk包:pip install tqsdk
    • 注册天勤账户获取访问密钥
  • 数据准备
    • 订阅近月与远月合约行情
    • 获取历史K线或者Tick数据(用于分析与行情推进)
  • 策略编写
    • 设计信号生成逻辑(基于价差、均值和标准差)
    • 编写交易执行模块(开仓、平仓逻辑)
    • 实现风险控制措施(止损、资金管理)
  • 回测验证
    • 设置回测时间区间和初始资金
    • 运行策略获取回测结果
    • 分析绩效指标(胜率、收益率、夏普率等)
  • 策略优化
    • 调整参数(标准差倍数、窗口大小等)
    • 添加过滤条件(成交量、波动率等)
    • 完善风险控制机制

三、天勤实现策略

策略原理

核心价差定义与计算(炼焦利润价差组合价值):

该策略的核心是追踪一个基于特定手数比例的焦炭和焦煤期货合约组合的价值差。这个“炼焦利润价差组合价值”通过以下方式计算得出:

  • 首先,为焦炭(J)和焦煤(JM)期货分别设定一个代表其在价差组合中相对权重的“手数比例因子”(例如,代码中焦炭的J_RATIO为10,焦煤的JM_RATIO为22)。

  • 然后,将各期货品种的最新收盘价乘以其对应的合约乘数(代表每手合约的价值规模),再乘以各自的“手数比例因子”,得到它们在该价差组合中的调整后价值。

  • 炼焦利润价差组合价值被定义为:焦炭期货的调整后价值减去焦煤期货的调整后价值。 价差 = (焦炭收盘价 × 焦炭合约乘数 × J_RATIO) - (焦煤收盘价 × 焦煤合约乘数 × JM_RATIO)。这个计算出的价差,反映了按照预设手数比例因子模拟的“原料组合成本”与“成品组合价值”之间的理论利润空间。

  • 指标构建:

策略采用标准化得分(Z-score) 来衡量当前计算出的“炼焦利润价差组合价值”与其近期历史水平的偏离程度。

  • 历史价差序列: 收集特定焦炭和焦煤期货合约在过去一段固定交易日内的每日收盘价。基于这些价格和上述价差计算方法,得到一个每日“炼焦利润价差组合价值”的历史序列。
  • 计算历史均值: 对这个历史价差序列计算其算术平均值。
  • 计算历史标准差: 对同一个历史价差序列计算其标准差。
  • 计算标准化得分: 用最新的每日收盘价计算出当前的“炼焦利润价差组合价值”,然后通过以下公式得到其标准化得分: 标准化得分 = (当前价差组合价值 - 历史均值) / 历史标准差 (若历史标准差为零或极小,则此得分可能无意义或需特殊处理,但代码中未显式处理此情况)。

交易逻辑

开仓信号 (入场):

  • 做空炼焦利润价差 (预期价差回落):

    • 信号: 当计算出的“标准化得分”显著高于一个预设的正阈值(例如,大于STD_THRESHOLD,如2.0)。这表明当前的炼焦利润价差远高于其历史平均水平,策略预期其会向均值回落。
    • 操作: 卖出计算数量的焦炭期货,同时买入计算数量的焦煤期货。
  • 做多炼焦利润价差 (预期价差回升):

    • 信号: 当计算出的“标准化得分”显著低于一个预设的负阈值(例如,小于负STD_THRESHOLD,如-2.0)。这表明当前的炼焦利润价差远低于其历史平均水平,策略预期其会向均值回升。
    • 操作: 买入计算数量的焦炭期货,同时卖出计算数量的焦煤期货。

平仓信号 (止盈离场):

  • 信号: 当“标准化得分”的绝对值回落到一个较小的预设阈值以内(例如,小于CLOSE_THRESHOLD,如0.5)。这表示炼焦利润价差已经回归到接近其历史平均的水平。
  • 操作: 将所有持有的期货头寸全部平掉(即目标持仓设为零)。

特定条件下的平仓信号 (类似止损或极端行情退出): 当策略当前已持有头寸时,若出现以下情况也会触发平仓:

  • 信号:
    • 若最初是因预期价差回落而做空价差(即焦炭空头,焦煤多头,此时焦炭持仓为负),之后价差不仅未按预期运行,反而大幅反向运动至“标准化得分”显著高于一个更极端的正阈值(例如,大于STD_THRESHOLD的1.5倍)。
    • 若最初是因预期价差回升而做多价差(即焦炭多头,焦煤空头,此时焦炭持仓为正),之后价差不仅未按预期运行,反而大幅反向运动至“标准化得分”显著低于一个更极端的负阈值(例如,小于负STD_THRESHOLD的1.5倍)。
  • 操作: 与获利离场操作类似,立即平掉所有持有的期货头寸。

回测

回测初始设置

  • 测试周期: 2023 年 11 月 1 日 - 2024 年 4 月 30 日
  • 交易品种: DCE.j2409/DCE.jm2409
  • 初始资金: 1000万元

回测结果

上述回测累计收益走势图

完整代码示例

#!/usr/bin/env python
# coding=utf-8
__author__ = "Chaos"

from datetime import date
from tqsdk import TqApi, TqAuth, TargetPosTask, TqBacktest, BacktestFinished
import numpy as np
import time

# === 用户参数 ===
# 合约参数
J = "DCE.j2409"  # 焦炭期货合约
JM = "DCE.jm2409"  # 焦煤期货合约
START_DATE = date(2023, 11, 1)  # 回测开始日期
END_DATE = date(2024, 4, 30)  # 回测结束日期

# 套利参数
LOOKBACK_DAYS = 30  # 计算历史价差的回溯天数
STD_THRESHOLD = 2.0  # 标准差阈值,超过此阈值视为套利机会
ORDER_VOLUME = 50  # 焦炭的下单手数
CLOSE_THRESHOLD = 0.5  # 平仓阈值(标准差)

# 配比参数(可根据实际工艺调整)
J_RATIO = 10  # 10手焦炭
JM_RATIO = 22  # 22手焦煤(约1.32配比)

# === 初始化API ===
api = TqApi(backtest=TqBacktest(start_dt=START_DATE, end_dt=END_DATE),
            auth=TqAuth("快期账号", "快期密码"))

# 获取合约行情和K线
j_quote = api.get_quote(J)
jm_quote = api.get_quote(JM)

j_klines = api.get_kline_serial(J, 60 * 60 * 24, LOOKBACK_DAYS)
jm_klines = api.get_kline_serial(JM, 60 * 60 * 24, LOOKBACK_DAYS)

# 创建目标持仓任务
j_pos = TargetPosTask(api, J)
jm_pos = TargetPosTask(api, JM)

# 获取合约乘数
j_volume_multiple = j_quote.volume_multiple
jm_volume_multiple = jm_quote.volume_multiple

# 初始化状态变量
position_time = 0  # 建仓时间
in_position = False  # 是否有持仓
mean_spread = 0  # 历史价差均值
std_spread = 0  # 历史价差标准差

print(f"策略启动,监控合约: {J}, {JM}")

# === 主循环 ===
try:
    # 初始计算历史统计值
    spreads = []
    for i in range(len(j_klines) - 1):
        j_value = j_klines.close.iloc[i] * j_volume_multiple * J_RATIO
        jm_value = jm_klines.close.iloc[i] * jm_volume_multiple * JM_RATIO
        spread = j_value - jm_value
        spreads.append(spread)

    mean_spread = np.mean(spreads)
    std_spread = np.std(spreads)
    print(f"历史炼焦利润均值: {mean_spread:.2f}, 标准差: {std_spread:.2f}")

    # 主循环
    while True:
        api.wait_update()

        # 当K线数据有变化时进行计算
        if api.is_changing(j_klines) or api.is_changing(jm_klines):
            # 重新计算历史价差统计
            spreads = []
            for i in range(len(j_klines) - 1):
                j_value = j_klines.close.iloc[i] * j_volume_multiple * J_RATIO
                jm_value = jm_klines.close.iloc[i] * jm_volume_multiple * JM_RATIO
                spread = j_value - jm_value
                spreads.append(spread)

            mean_spread = np.mean(spreads)
            std_spread = np.std(spreads)

            # 计算当前炼焦利润价差
            j_value = j_klines.close.iloc[-1] * j_volume_multiple * J_RATIO
            jm_value = jm_klines.close.iloc[-1] * jm_volume_multiple * JM_RATIO
            current_spread = j_value - jm_value

            # 计算z-score (标准化的价差)
            z_score = (current_spread - mean_spread) / std_spread

            print(f"当前炼焦利润: {current_spread:.2f}, Z-score: {z_score:.2f}")

            # 获取当前持仓
            j_position = api.get_position(J)
            jm_position = api.get_position(JM)

            current_j_pos = j_position.pos_long - j_position.pos_short
            current_jm_pos = jm_position.pos_long - jm_position.pos_short

            # === 交易信号判断 ===
            if not in_position:
                if z_score > STD_THRESHOLD:
                    # 做空炼焦利润:卖出焦炭,买入焦煤
                    print(f"做空炼焦利润:卖出焦炭{ORDER_VOLUME}手,买入焦煤{int(ORDER_VOLUME * JM_RATIO / J_RATIO)}手")
                    j_pos.set_target_volume(-ORDER_VOLUME)
                    jm_pos.set_target_volume(int(ORDER_VOLUME * JM_RATIO / J_RATIO))
                    position_time = time.time()
                    in_position = True
                elif z_score < -STD_THRESHOLD:
                    # 做多炼焦利润:买入焦炭,卖出焦煤
                    print(f"做多炼焦利润:买入焦炭{ORDER_VOLUME}手,卖出焦煤{int(ORDER_VOLUME * JM_RATIO / J_RATIO)}手")
                    j_pos.set_target_volume(ORDER_VOLUME)
                    jm_pos.set_target_volume(-int(ORDER_VOLUME * JM_RATIO / J_RATIO))
                    position_time = time.time()
                    in_position = True

            else:  # 如果已有持仓
                # 检查是否应当平仓
                if abs(z_score) < CLOSE_THRESHOLD:  # 利润回归正常
                    print("利润回归正常,平仓所有头寸")
                    j_pos.set_target_volume(0)
                    jm_pos.set_target_volume(0)
                    in_position = False
                # 止损逻辑
                if (z_score > STD_THRESHOLD * 1.5 and current_j_pos < 0) or \
                        (z_score < -STD_THRESHOLD * 1.5 and current_j_pos > 0):
                    print("止损:利润向不利方向进一步偏离")
                    j_pos.set_target_volume(0)
                    jm_pos.set_target_volume(0)
                    in_position = False

except BacktestFinished as e:
    print("回测结束")
    api.close()