甲醇制烯烃(MTO)产业链套利策略
一、交易策略解释
核心思想
甲醇制烯烃(MTO)套利的核心在于监控和交易“MTO利润价差”。MTO工艺是指以甲醇为原料生产乙烯和丙烯(烯烃),进而可以生产聚乙烯(PE,期货代码L)和聚丙烯(PP)。理论上,生产1吨烯烃大约需要消耗3吨甲醇(具体消耗系数因工艺、催化剂效率、产品结构等略有不同,且烯烃中乙烯和丙烯的产出比例也非固定,但通常丙烯产出比例较高或可调)。
MTO利润价差 (MTO Margin Spread):旨在量化这一转化过程的潜在盈利空间。由于MTO装置通常会同时产出聚乙烯和聚丙烯,计算利润价差时,需要将这两种主要产品的市场价值按照它们在生产中的相对份额或对总价值的贡献度进行加权合计,从而得到一个“混合烯烃产品”的综合市场价值。 随后,从这个综合产品价值中扣除生产这些产品所消耗的甲醇的成本,得出的差额即为MTO利润价差。简而言之,这个价差反映了:用一定量的甲醇生产出相应配比的聚乙烯和聚丙烯后,这些烯烃产品的总市场价值与所投入甲醇原料的总市场成本之间的净值。
核心思想是:
- 这个MTO利润价差会受到甲醇、聚乙烯、聚丙烯各自供需基本面以及煤化工/石化行业整体状况的影响,但通常会围绕一个由生产成本、合理利润和市场预期构成的“均衡水平”波动。
- 做多MTO利润 (Long the MTO Spread / "Buying the Margin"): 当交易者认为当前市场计算出的MTO利润过低,未来有望扩大时,他们会买入聚乙烯期货和聚丙烯期货,同时卖出甲醇期货。这在金融市场上模拟了MTO工厂锁定未来更高加工利润的行为。
- 做空MTO利润 (Short the MTO Spread / "Selling the Margin"): 当交易者认为当前市场计算出的MTO利润过高,未来可能收窄时,他们会卖出聚乙烯期货和聚丙烯期货,同时买入甲醇期货。这模拟了MTO工厂锁定当前较高加工利润的行为。
该策略预期当此价差显著偏离其历史均值或行业盈亏平衡点时,会向其“正常”水平回归。
理论基础
- 产业链的经济联系: MTO/MTP(甲醇制丙烯)是连接煤化工(甲醇主要来自煤或天然气)和石油化工下游(聚烯烃)的重要桥梁。甲醇价格与聚乙烯、聚丙烯价格之间存在成本和利润的传导关系。
- 供需基本面的驱动:
- 甲醇端 (MA): 主要原料(煤、天然气)价格、甲醇装置开工率、传统下游(如甲醛、醋酸、二甲醚)需求、新兴下游(MTO/MTP)需求、港口库存、进口情况等。
- 聚乙烯端 (L): 原油/石脑油价格(传统石脑油裂解制乙烯路线的成本)、PE装置(包括MTO和石脑油路线)开工率、下游需求(农膜、包装膜、注塑、管材等)、库存、进出口等。
- 聚丙烯端 (PP): 原油/石脑油/丙烷价格(石脑油裂解、PDH丙烷脱氢路线的成本)、PP装置(MTO/MTP、石脑油裂解、PDH路线)开工率、下游需求(注塑、纤维、薄膜等)、库存、进出口等。
- 这些因素的变化导致三者价格波动,进而影响MTO的生产利润价差。
- 均值回归特性 (Mean Reversion): 历史数据显示,MTO利润价差具有均值回归的特性。当利润过高时,可能刺激MTO装置提高开工率或新建产能,未来烯烃供应增加将压低L和PP价格,同时增加对MA的需求可能推高其价格,从而压缩利润。当利润过低甚至亏损时,MTO装置可能减产检修,未来烯烃供应减少将推升L和PP价格,同时减少对MA的需求可能压低其价格,从而修复利润。
- 企业套期保值行为: MTO生产企业会利用期货市场进行套期保值,例如在远期MTO利润可观时卖出L和PP期货、买入MA期货(锁定利润)。
策略适用场景
- MTO利润价差显著偏离: 当计算出的MTO生产利润(经过适当的比例和标准化处理后)显著高于或低于其历史均值或关键的盈亏平衡点时。
- 预期均值回归: 交易者基于对市场周期或短期供需失衡的判断,预期价差将向更可持续的水平回归。
- 基本面分析支持: 结合对MA、L、PP各自基本面的分析,如果基本面也支持价差向预期方向回归,则策略的可靠性更高。
- 流动性充足: 策略涉及三个品种的期货合约(郑州商品交易所的MA,大连商品交易所的L和PP),需要保证各合约均有良好的流动性。
二、天勤介绍
天勤平台概述
天勤(TqSdk)是一个由信易科技开发的开源量化交易系统,为期货、期权等衍生品交易提供专业的量化交易解决方案。平台具有以下特 点:
- 丰富的行情数据: 提供所有可交易合约的全部Tick和K线数据,基于内存数据库实现零延迟访问。
- 一站式的解决方案: 从历史数据分析到实盘交易的完整工具链,打通开发、回测、模拟到实盘的全流程。
- 专业的技术支持: 近百个技术指标源码,深度集成pandas和numpy,采用单线程异步模型保证性能。
策略开发流程
- 环境准备
- 安装Python环境(推荐Python 3.6或以上版本)
- 安装tqsdk包:pip install tqsdk
- 注册天勤账户获取访问密钥
- 数据准备
- 订阅近月与远月合约行情
- 获取历史K线或者Tick数据(用于分析与行情推进)
- 策略编写
- 设计信号生成逻辑(基于价差、均值和标准差)
- 编写交易执行模块(开仓、平仓逻辑)
- 实现风险控制措施(止损、资金管理)
- 回测验证
- 设置回测时间区间和初始资金
- 运行策略获取回测结果
- 分析绩效指标(胜率、收益率、夏普率等)
- 策略优化
- 调整参数(标准差倍数、窗口大小等)
- 添加过滤条件(成交量、波动率等)
- 完善风险控制机制
三、天勤实现策略
策略原理
核心价差定义与计算(MTO利润价差):
该策略的核心是追踪一个根据甲醇(MA)制取聚乙烯(L)和聚丙烯(PP)的理论化学计量关系,并结合各期货合约价值构建的“MTO利润价差”。这个价差的计算方式如下:
- 首先,为甲醇、聚乙烯和聚丙烯期货分别设定一个代表其在MTO工艺中相对投入产出量的比例因子(例如,生产1单位聚乙烯和1单位聚丙烯理论上需要消耗3单位甲醇)。
- 然后,将各期货品种的最新收盘价乘以其对应的合约乘数(代表每手合约的价值规模),再乘以各自的工艺比例因子,得到它们在价差组合中的调整后价值。
- MTO利润价差被定义为:聚乙烯期货和聚丙烯期货的调整后价值之和,减去甲醇期货的调整后价值。 这个计算出的价差,反映了按照预设工艺配比模拟的“原料成本”与“主要烯烃产品价值”之间的理论利润空间。
指标构建:
策略采用标准化得分(Z-score) 来衡量当前计算出的“MTO利润价差”与其近期历史水平的偏离程度。
- 历史价差序列: 收集特定甲醇、聚乙烯、聚丙烯期货合约在过去一段固定交易日(例如30天)内的每日收盘价。基于这些价格和上述价差计算方法,得到一个每日“MTO利润价差”的历史序列。
- 计算历史均值: 对这个历史价差序列计算其算术平均值。
- 计算历史标准差: 对同一个历史价差序列计算其标准差。
- 计算标准化得分: 用最新的每日收盘价计算出当前的“MTO利润价差”,然后通过以下公式得到其标准化得分: 标准化得分 = (当前MTO利润价差 - 历史均值) / 历史标准差 (若历史标准差为零,则此得分无意义或需特殊处理)。
交易逻辑
开仓信号 (入场):
-
预期利润缩小(做空MTO利润价差):
- 信号: 当计算出的“标准化得分”显著高于一个预设的正阈值(例如,大于2个标准差)。这表明当前的MTO利润远高于其历史平均水平,策略预期其会向均值回落。
- 操作: 卖出指定数量的聚乙烯期货和聚丙烯期货,同时买入相应比例数量的甲醇期货。
-
预期利润扩大(做多MTO利润价差):
- 信号: 当计算出的“标准化得分”显著低于一个预设的负阈值(例如,小于负2个标准差)。这表明当前的MTO利润远低于其历史平均水平,策略预期其会向均值回升。
- 操作: 买入指定数量的聚乙烯期货和聚丙烯期货,同时卖出相应比例数量的甲醇期货。
平仓信号 (止盈离场):
- 信号: 当“标准化得分”的绝对值回落到一个较小的预设阈值以内(例如,小于0.5个标准差)。这表示MTO利润价差已经回归到接近其历史平均的水平。
- 操作: 将所有持有的期货头寸全部平掉(即目标持仓设为零)。
特定条件下的平仓信号 (类似止损或极端行情退出): 当策略当前已持有头寸时,若出现以下情况也会触发平仓:
-
信号:
- 若最初是因利润过低而做多(聚乙烯和聚丙烯多头,甲醇空头),之后利润不仅未按预期回升,反而大幅反向运动至显著高于历史均值(例如,标准化得分超过一个更高的正阈值,如3个标准差)。此处的判断依据是聚乙烯的持仓方向。
- 若最初是因利润过高而做空(聚乙烯和聚丙烯空头,甲醇多头),之后利润不仅未按预期回落,反而大幅反向运动至显著低于历史均值(例如,标准化得分低于一个更低的负阈值,如负3个标准差)。此处的判断依据是聚乙烯的持仓方向。
-
操作: 与止盈操作类似,立即平掉所有持有的期货头寸。
回测
回测初始设置
- 测试周期: 2023 年 11 月 1 日 - 2024 年 4 月 9 日
- 交易品种: CZCE.MA409, DCE.l2409, DCE.pp2409
- 初始资金: 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
# === 用户参数 ===
# 合约参数
MA = "CZCE.MA409" # 甲醇期货合约
L = "DCE.l2409" # 聚乙烯期货合约
PP = "DCE.pp2409" # 聚丙烯期货合约
START_DATE = date(2023, 11, 1) # 回测开始日期
END_DATE = date(2024, 4, 9) # 回测结束日期
# 套利参数
LOOKBACK_DAYS = 30 # 计算历史价差的回溯天数
STD_THRESHOLD = 2.0 # 标准差阈值,超过此阈值视为套利机会
ORDER_VOLUME = 100 # 聚乙烯的下单手数
CLOSE_THRESHOLD = 0.5 # 平仓阈值(标准差)
# 生产比例(可根据实际工艺调整)
MA_RATIO = 3 # 生产1吨烯烃消耗3吨甲醇
L_RATIO = 1 # 产出1吨聚乙烯
PP_RATIO = 1 # 产出1吨聚丙烯
# === 初始化API ===
api = TqApi(backtest=TqBacktest(start_dt=START_DATE, end_dt=END_DATE),
auth=TqAuth("快期账号", "快期密码"))
# 获取合约行情和K线
ma_quote = api.get_quote(MA)
l_quote = api.get_quote(L)
pp_quote = api.get_quote(PP)
ma_klines = api.get_kline_serial(MA, 60*60*24, LOOKBACK_DAYS)
l_klines = api.get_kline_serial(L, 60*60*24, LOOKBACK_DAYS)
pp_klines = api.get_kline_serial(PP, 60*60*24, LOOKBACK_DAYS)
# 创建目标持仓任务
ma_pos = TargetPosTask(api, MA)
l_pos = TargetPosTask(api, L)
pp_pos = TargetPosTask(api, PP)
# 获取合约乘数
ma_volume_multiple = ma_quote.volume_multiple
l_volume_multiple = l_quote.volume_multiple
pp_volume_multiple = pp_quote.volume_multiple
# 初始化状态变量
position_time = 0 # 建仓时间
in_position = False # 是否有持仓
mean_spread = 0 # 历史价差均值
std_spread = 0 # 历史价差标准差
print(f"策略启动,监控合约: {MA}, {L}, {PP}")
# === 主循环 ===
try:
# 初始计算历史统计值
spreads = []
for i in range(len(ma_klines) - 1):
ma_price = ma_klines.close.iloc[i] * ma_volume_multiple * MA_RATIO
l_price = l_klines.close.iloc[i] * l_volume_multiple * L_RATIO
pp_price = pp_klines.close.iloc[i] * pp_volume_multiple * PP_RATIO
# MTO利润 = (L价值 + PP价值) - MA成本
spread = (l_price + pp_price) - ma_price
spreads.append(spread)
mean_spread = np.mean(spreads)
std_spread = np.std(spreads)
print(f"历史MTO利润均值: {mean_spread:.2f}, 标准差: {std_spread:.2f}")
# 主循环
while True:
api.wait_update()
# 当K线数据有变化时进行计算
if api.is_changing(ma_klines) or api.is_changing(l_klines) or api.is_changing(pp_klines):
# 重新计算历史价差统计
spreads = []
for i in range(len(ma_klines) - 1):
ma_price = ma_klines.close.iloc[i] * ma_volume_multiple * MA_RATIO
l_price = l_klines.close.iloc[i] * l_volume_multiple * L_RATIO
pp_price = pp_klines.close.iloc[i] * pp_volume_multiple * PP_RATIO
spread = (l_price + pp_price) - ma_price
spreads.append(spread)
mean_spread = np.mean(spreads)
std_spread = np.std(spreads)
# 计算当前利润价差
ma_price = ma_klines.close.iloc[-1] * ma_volume_multiple * MA_RATIO
l_price = l_klines.close.iloc[-1] * l_volume_multiple * L_RATIO
pp_price = pp_klines.close.iloc[-1] * pp_volume_multiple * PP_RATIO
current_spread = (l_price + pp_price) - ma_price
# 计算z-score (标准化的价差)
z_score = (current_spread - mean_spread) / std_spread
print(f"当前MTO利润: {current_spread:.2f}, Z-score: {z_score:.2f}")
# 获取当前持仓
ma_position = api.get_position(MA)
l_position = api.get_position(L)
pp_position = api.get_position(PP)
current_ma_pos = ma_position.pos_long - ma_position.pos_short
current_l_pos = l_position.pos_long - l_position.pos_short
current_pp_pos = pp_position.pos_long - pp_position.pos_short
# 计算实际下单手数(依据比例)
ma_volume = int(ORDER_VOLUME * MA_RATIO / L_RATIO)
# L和PP按同等手数下单
# === 交易信号判断 ===
if not in_position: # 如果没有持仓
if z_score > STD_THRESHOLD: # 利润显著高于均值
# 做空利润:卖出L和PP,买入MA
print(f"做空利润:卖出L{ORDER_VOLUME}手和PP{ORDER_VOLUME}手,买入MA{ma_volume}手")
l_pos.set_target_volume(-ORDER_VOLUME)
pp_pos.set_target_volume(-ORDER_VOLUME)
ma_pos.set_target_volume(ma_volume)
position_time = time.time()
in_position = True
elif z_score < -STD_THRESHOLD: # 利润显著低于均值
# 做多利润:买入L和PP,卖出MA
print(f"做多利润:买入L{ORDER_VOLUME}手和PP{ORDER_VOLUME}手,卖出MA{ma_volume}手")
l_pos.set_target_volume(ORDER_VOLUME)
pp_pos.set_target_volume(ORDER_VOLUME)
ma_pos.set_target_volume(-ma_volume)
position_time = time.time()
in_position = True
else: # 如果已有持仓
# 检查是否应当平仓
if abs(z_score) < CLOSE_THRESHOLD: # 利润回归正常
print("利润回归正常,平仓所有头寸")
l_pos.set_target_volume(0)
pp_pos.set_target_volume(0)
ma_pos.set_target_volume(0)
in_position = False
# 止损逻辑
if (z_score > STD_THRESHOLD * 1.5 and current_l_pos < 0) or \
(z_score < -STD_THRESHOLD * 1.5 and current_l_pos > 0):
print("止损:利润向不利方向进一步偏离")
l_pos.set_target_volume(0)
pp_pos.set_target_volume(0)
ma_pos.set_target_volume(0)
in_position = False
except BacktestFinished as e:
print("回测结束")
api.close()