PTA产业链套利策略

一、交易策略解释

核心思想

PTA产业链套利的核心在于监控和交易“涤纶短纤生产利润价差”。涤纶短纤(Polyester Staple Fiber, PF)的主要原料是精对苯二甲酸(PTA)和乙二醇(EG)。理论上,生产1吨涤纶短纤大约需要0.855-0.87吨的PTA和0.335-0.34吨的EG(具体比例因工艺和产品规格略有差异)。

  • 涤纶短纤生产利润价差 (PF Production Margin Spread / Polyester Spread): 利润价差 = 涤纶短纤价值 - (PTA成本价值 + 乙二醇成本价值)

核心思想是:

  • 这个生产利润价差会受到涤纶短纤、PTA、乙二醇各自供需基本面以及聚酯行业整体状况的影响,但通常会围绕一个由生产成本、合理利润和市场预期构成的“均衡水平”波动。
  • 做多生产利润 (Long the Polyester Spread / "Buying the Margin"): 当交易者认为当前市场计算出的生产利润过低,未来有望扩大时,他们会买入涤纶短纤期货,同时卖出PTA期货和乙二醇期货。这在金融市场上模拟了聚酯工厂锁定未来更高加工利润的行为(或对冲当前低利润风险)。
  • 做空生产利润 (Short the Polyester Spread / "Selling the Margin"): 当交易者认为当前市场计算出的生产利润过高,未来可能收窄时,他们会卖出涤纶短纤期货,同时买入PTA期货和乙二醇期货。这模拟了聚酯工厂锁定当前较高加工利润的行为。

该策略预期当此价差显著偏离其历史均值或行业盈亏平衡点时,会向其“正常”水平回归。

理论基础

  • 产业链的经济联系: 涤纶短纤是PTA和乙二醇的主要下游产品之一。它们之间存在紧密的上下游价格传导和成本利润关系。PTA自身的主要原料是PX(对二甲苯),PX价格的波动会影响PTA成本,进而传导至涤纶短纤。

  • 供需基本面的驱动:

    • 涤纶短纤端 (PF): 纺织服装行业景气度、出口订单、自身产能利用率、库存水平、替代品(如棉花)价格等。
    • PTA端 (TA): PX价格(主要成本)、PTA装置开工率、新增产能、下游聚酯(包括涤纶长丝、瓶片等)需求、库存等。
    • 乙二醇端 (EG): 原料路线(石脑油、乙烯、煤制等)的成本、国内外装置开工率、港口库存、进口量、下游聚酯需求等。
    • 这些因素的变化导致三者价格波动,进而影响涤纶短纤的生产利润价差。
  • 均值回归特性 (Mean Reversion): 历史数据显示,聚酯产品的生产利润价差虽然波动,但往往具有均值回归的特性。当利润过高时,可能刺激聚酯工厂提高开工率或新建产能,未来PF供应增加将压低PF价格,同时增加对TA和EG的需求可能推高其价格,从而压缩利润。当利润过低甚至亏损时,工厂可能减产检修,未来PF供应减少将推升PF价格,同时减少对TA和EG的需求可能压低其价格,从而修复利润。

  • 企业套期保值行为: 聚酯生产企业、PTA和EG生产及贸易企业会利用期货市场进行套期保值。聚酯厂可能在远期生产利润可观时卖出PF期货、买入TA和EG期货(锁定利润)。这些行为本身也会影响价差的动态。

策略适用场景

  • 生产利润价差显著偏离: 当计算出的涤纶短纤生产利润(经过适当的比例和标准化处理后)显著高于或低于其历史均值或关键的盈亏平衡点时。
  • 预期均值回归: 交易者基于对市场周期或短期供需失衡的判断,预期价差将向更可持续的水平回归。
  • 基本面分析支持: 结合对PF、TA、EG各自基本面的分析,如果基本面也支持价差向预期方向回归,则策略的可靠性更高。
  • 流动性充足: 策略涉及三个品种的期货合约(郑州商品交易所的TA和PF,大连商品交易所的EG),需要保证各合约均有良好的流动性。

二、天勤介绍

天勤平台概述

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

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

策略开发流程

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

三、天勤实现策略

策略原理

核心价差定义与计算(涤纶短纤生产利润价差):

该策略的核心是追踪一个根据涤纶短纤(PF)生产中原料(PTA和乙二醇EG)的理论消耗比例,并结合各期货合约价值构建的“生产利润价差”。这个价差的计算方式如下:

  • 首先,为涤纶短纤、PTA和乙二醇期货分别设定一个代表其在生产关系中相对量的比例因子(例如,生产1单位涤纶短纤需要0.86单位PTA和0.34单位乙二醇)。

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

  • 涤纶短纤生产利润价差被定义为:涤纶短纤期货的调整后价值,减去PTA期货和乙二醇期货调整后价值的总和。 这个计算出的价差,反映了按照预设生产配比模拟的“原料成本”与“成品价值”之间的理论利润空间。

  • 指标构建:

策略采用标准化得分(Z-score) 来衡量当前计算出的“涤纶短纤生产利润价差”与其近期历史水平的偏离程度。

  • 历史价差序列: 收集特定涤纶短纤、PTA、乙二醇期货合约在过去一段固定交易日(例如30天)内的每日收盘价。基于这些价格和上述价差计算方法,得到一个每日“涤纶短纤生产利润价差”的历史序列。
  • 计算历史均值: 对这个历史价差序列计算其算术平均值。
  • 计算历史标准差: 对同一个历史价差序列计算其标准差。
  • 计算标准化得分: 用最新的每日收盘价计算出当前的“涤纶短纤生产利润价差”,然后通过以下公式得到其标准化得分: 标准化得分 = (当前涤纶短纤生产利润价差 - 历史均值) / 历史标准差 (若历史标准差为零,则此得分无意义或需特殊处理)。

交易逻辑

开仓信号 (入场):

  • 预期利润缩小(做空生产利润价差):

    • 信号: 当计算出的“标准化得分”显著高于一个预设的正阈值(例如,大于2个标准差)。这表明当前的生产利润远高于其历史平均水平,策略预期其会向均值回落。
    • 操作: 卖出指定数量的涤纶短纤期货,同时买入相应比例数量的PTA期货和乙二醇期货。
  • 预期利润扩大(做多生产利润价差):

    • 信号: 当计算出的“标准化得分”显著低于一个预设的负阈值(例如,小于负2个标准差)。这表明当前的生产利润远低于其历史平均水平,策略预期其会向均值回升。
    • 操作: 买入指定数量的涤纶短纤期货,同时卖出相应比例数量的PTA期货和乙二醇期货。

平仓信号 (止盈离场):

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

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

  • 信号:

    • 若最初是因利润过低而做多(涤纶短纤多头,原料空头),之后利润不仅未按预期回升,反而大幅反向运动至显著高于历史均值(例如,标准化得分超过一个更高的正阈值,如3个标准差)。
    • 若最初是因利润过高而做空(涤纶短纤空头,原料多头),之后利润不仅未按预期回落,反而大幅反向运动至显著低于历史均值(例如,标准化得分低于一个更低的负阈值,如负3个标准差)。
  • 操作: 与止盈操作类似,立即平掉所有持有的期货头寸。

回测

回测初始设置

  • 测试周期: 2024 年 2 月 1 日 - 2024 年 3 月 13 日
  • 交易品种: CZCE.PF409/CZCE.TA409/DCE.eg2409
  • 初始资金: 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

# === 用户参数 ===
# 合约参数
PF = "CZCE.PF409"      # 涤纶短纤期货合约
PTA = "CZCE.TA409"     # PTA期货合约
EG = "DCE.eg2409"      # 乙二醇期货合约
START_DATE = date(2024, 2, 1)   # 回测开始日期
END_DATE = date(2024, 4, 30)     # 回测结束日期

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

# 生产比例(可根据实际工艺调整)
PF_RATIO = 1
PTA_RATIO = 0.86
EG_RATIO = 0.34

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

# 获取合约行情和K线
pf_quote = api.get_quote(PF)
pta_quote = api.get_quote(PTA)
eg_quote = api.get_quote(EG)

pf_klines = api.get_kline_serial(PF, 60*60*24, LOOKBACK_DAYS)
pta_klines = api.get_kline_serial(PTA, 60*60*24, LOOKBACK_DAYS)
eg_klines = api.get_kline_serial(EG, 60*60*24, LOOKBACK_DAYS)

# 创建目标持仓任务
pf_pos = TargetPosTask(api, PF)
pta_pos = TargetPosTask(api, PTA)
eg_pos = TargetPosTask(api, EG)

# 获取合约乘数
pf_volume_multiple = pf_quote.volume_multiple
pta_volume_multiple = pta_quote.volume_multiple
eg_volume_multiple = eg_quote.volume_multiple

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

print(f"策略启动,监控合约: {PF}, {PTA}, {EG}")

# === 主循环 ===
try:
    # 初始计算历史统计值
    spreads = []
    for i in range(len(pf_klines) - 1):
        pf_price = pf_klines.close.iloc[i] * pf_volume_multiple * PF_RATIO
        pta_price = pta_klines.close.iloc[i] * pta_volume_multiple * PTA_RATIO
        eg_price = eg_klines.close.iloc[i] * eg_volume_multiple * EG_RATIO
        
        # 涤纶短纤生产利润 = 涤纶短纤价值 - (PTA成本 + EG成本)
        spread = pf_price - (pta_price + eg_price)
        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(pf_klines) or api.is_changing(pta_klines) or api.is_changing(eg_klines):
            # 重新计算历史价差统计
            spreads = []
            for i in range(len(pf_klines) - 1):
                pf_price = pf_klines.close.iloc[i] * pf_volume_multiple * PF_RATIO
                pta_price = pta_klines.close.iloc[i] * pta_volume_multiple * PTA_RATIO
                eg_price = eg_klines.close.iloc[i] * eg_volume_multiple * EG_RATIO
                
                spread = pf_price - (pta_price + eg_price)
                spreads.append(spread)
            
            mean_spread = np.mean(spreads)
            std_spread = np.std(spreads)
            
            # 计算当前利润价差
            pf_price = pf_klines.close.iloc[-1] * pf_volume_multiple * PF_RATIO
            pta_price = pta_klines.close.iloc[-1] * pta_volume_multiple * PTA_RATIO
            eg_price = eg_klines.close.iloc[-1] * eg_volume_multiple * EG_RATIO
            
            current_spread = pf_price - (pta_price + eg_price)
            
            # 计算z-score (标准化的价差)
            z_score = (current_spread - mean_spread) / std_spread
            
            print(f"当前涤纶短纤利润: {current_spread:.2f}, Z-score: {z_score:.2f}")
            
            # 获取当前持仓
            pf_position = api.get_position(PF)
            pta_position = api.get_position(PTA)
            eg_position = api.get_position(EG)
            
            current_pf_pos = pf_position.pos_long - pf_position.pos_short
            current_pta_pos = pta_position.pos_long - pta_position.pos_short
            current_eg_pos = eg_position.pos_long - eg_position.pos_short
            
            # 计算实际下单手数(依据比例)
            pta_volume = int(ORDER_VOLUME * PTA_RATIO / PF_RATIO)
            eg_volume = int(ORDER_VOLUME * EG_RATIO / PF_RATIO)
            
            # === 交易信号判断 ===
            if not in_position:  # 如果没有持仓
                if z_score > STD_THRESHOLD:  # 利润显著高于均值
                    # 做空利润:卖出PF,买入PTA和EG
                    print(f"做空利润:卖出PF{ORDER_VOLUME}手,买入PTA{pta_volume}手和EG{eg_volume}手")
                    pf_pos.set_target_volume(-ORDER_VOLUME)
                    pta_pos.set_target_volume(pta_volume)
                    eg_pos.set_target_volume(eg_volume)
                    position_time = time.time()
                    in_position = True
                    
                elif z_score < -STD_THRESHOLD:  # 利润显著低于均值
                    # 做多利润:买入PF,卖出PTA和EG
                    print(f"做多利润:买入PF{ORDER_VOLUME}手,卖出PTA{pta_volume}手和EG{eg_volume}手")
                    pf_pos.set_target_volume(ORDER_VOLUME)
                    pta_pos.set_target_volume(-pta_volume)
                    eg_pos.set_target_volume(-eg_volume)
                    position_time = time.time()
                    in_position = True
            
            else:  # 如果已有持仓
                # 检查是否应当平仓
                if abs(z_score) < CLOSE_THRESHOLD:  # 利润回归正常
                    print("利润回归正常,平仓所有头寸")
                    pf_pos.set_target_volume(0)
                    pta_pos.set_target_volume(0)
                    eg_pos.set_target_volume(0)
                    in_position = False

                
                # 止损逻辑
                if (z_score > STD_THRESHOLD * 1.5 and current_pf_pos > 0) or \
                   (z_score < -STD_THRESHOLD * 1.5 and current_pf_pos < 0):
                    print("止损:利润向不利方向进一步偏离")
                    pf_pos.set_target_volume(0)
                    pta_pos.set_target_volume(0)
                    eg_pos.set_target_volume(0)
                    in_position = False

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