A Z-Score Mean-Reversion Strategy for the Soybean Crush Spread
Summary
This futures strategy tracks the crush spread formed from soybean, soybean meal, and soybean oil contracts. It weights the three contract prices using an approximate processing ratio, computes the spread as the combined meal and oil value less the soybean value, and standardizes the current spread against its recent historical mean and standard deviation. When the z-score crosses a threshold, the script opens opposing positions across the three legs; it closes when the score returns near its mean or when a further adverse move triggers a stop condition.
The example specifies a 30-day lookback, entry at two standard deviations, and an exit threshold of half a standard deviation. It gives implementation details but no backtest results. The fixed contract ratios, use of a short history, and lack of explicit treatment of fees, slippage, contract rolls, or position risk limit what can be inferred about profitability or execution suitability.
Key ideas
- The strategy constructs a crush spread from soybean, meal, and oil futures using an approximate 10:8:2 ratio.
- It compares the current spread with its recent mean and standard deviation to generate a z-score.
- Extreme positive and negative scores trigger opposite three-leg positions.
- The strategy exits as the spread approaches its mean or after a further adverse deviation.
- The code supplies no performance results and omits several practical trading costs and risks.
Tags
Full text
# crush_spread.py
```py
#!/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
# === 用户参数 ===
# 合约参数
SOYBEAN = "DCE.a2409" # 大豆期货合约
SOYMEAL = "DCE.m2409" # 豆粕期货合约
SOYOIL = "DCE.y2409" # 豆油期货合约
START_DATE = date(2023, 11, 1) # 回测开始日期
END_DATE = date(2024, 4, 30) # 回测结束日期
# 套利参数
LOOKBACK_DAYS = 30 # 计算历史价差的回溯天数
STD_THRESHOLD = 2.0 # 标准差阈值,超过此阈值视为套利机会
ORDER_VOLUME = 500 # 大豆的下单手数
CLOSE_THRESHOLD = 0.5 # 平仓阈值(标准差)
# 压榨价差比例 - 1吨大豆压榨可得约0.785吨豆粕和0.18吨豆油
# 为了简化,使用10:8:2的整数比例
BEAN_RATIO = 10
MEAL_RATIO = 8
OIL_RATIO = 2
# === 初始化API ===
api = TqApi(backtest=TqBacktest(start_dt=START_DATE, end_dt=END_DATE),
auth=TqAuth("快期账户", "快期密码"))
# 获取合约行情和K线
bean_quote = api.get_quote(SOYBEAN)
meal_quote = api.get_quote(SOYMEAL)
oil_quote = api.get_quote(SOYOIL)
bean_klines = api.get_kline_serial(SOYBEAN, 60*60*24, LOOKBACK_DAYS)
meal_klines = api.get_kline_serial(SOYMEAL, 60*60*24, LOOKBACK_DAYS)
oil_klines = api.get_kline_serial(SOYOIL, 60*60*24, LOOKBACK_DAYS)
# 创建目标持仓任务
bean_pos = TargetPosTask(api, SOYBEAN)
meal_pos = TargetPosTask(api, SOYMEAL)
oil_pos = TargetPosTask(api, SOYOIL)
# 获取合约乘数
bean_volume_multiple = bean_quote.volume_multiple
meal_volume_multiple = meal_quote.volume_multiple
oil_volume_multiple = oil_quote.volume_multiple
# 初始化状态变量
position_time = 0 # 建仓时间
in_position = False # 是否有持仓
mean_spread = 0 # 历史价差均值
std_spread = 0 # 历史价差标准差
print(f"策略启动,监控合约: {SOYBEAN}, {SOYMEAL}, {SOYOIL}")
# === 主循环 ===
try:
# 初始计算历史统计值
spreads = []
for i in range(len(bean_klines) - 1):
bean_price = bean_klines.close.iloc[i] * bean_volume_multiple * BEAN_RATIO
meal_price = meal_klines.close.iloc[i] * meal_volume_multiple * MEAL_RATIO
oil_price = oil_klines.close.iloc[i] * oil_volume_multiple * OIL_RATIO
# 压榨价差 = (豆粕价值 + 豆油价值) - 大豆价值
spread = (meal_price + oil_price) - bean_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(bean_klines) or api.is_changing(meal_klines) or api.is_changing(oil_klines):
# 重新计算历史价差统计
spreads = []
for i in range(len(bean_klines) - 1):
bean_price = bean_klines.close.iloc[i] * bean_volume_multiple * BEAN_RATIO
meal_price = meal_klines.close.iloc[i] * meal_volume_multiple * MEAL_RATIO
oil_price = oil_klines.close.iloc[i] * oil_volume_multiple * OIL_RATIO
spread = (meal_price + oil_price) - bean_price
spreads.append(spread)
mean_spread = np.mean(spreads)
std_spread = np.std(spreads)
# 计算当前压榨价差
bean_price = bean_klines.close.iloc[-1] * bean_volume_multiple * BEAN_RATIO
meal_price = meal_klines.close.iloc[-1] * meal_volume_multiple * MEAL_RATIO
oil_price = oil_klines.close.iloc[-1] * oil_volume_multiple * OIL_RATIO
current_spread = (meal_price + oil_price) - bean_price
# 计算z-score (标准化的价差)
z_score = (current_spread - mean_spread) / std_spread
print(f"当前压榨价差: {current_spread:.2f}, Z-score: {z_score:.2f}")
# 获取当前持仓
bean_position = api.get_position(SOYBEAN)
meal_position = api.get_position(SOYMEAL)
oil_position = api.get_position(SOYOIL)
current_bean_pos = bean_position.pos_long - bean_position.pos_short
current_meal_pos = meal_position.pos_long - meal_position.pos_short
current_oil_pos = oil_position.pos_long - oil_position.pos_short
# 计算实际下单手数(依据比例)
meal_volume = int(ORDER_VOLUME * MEAL_RATIO / BEAN_RATIO)
oil_volume = int(ORDER_VOLUME * OIL_RATIO / BEAN_RATIO)
# === 交易信号判断 ===
if not in_position: # 如果没有持仓
if z_score > STD_THRESHOLD: # 价差显著高于均值,压榨利润偏高
# 卖出压榨价差:买入大豆,卖出豆粕和豆油
print(f"卖出压榨价差:买入大豆{ORDER_VOLUME}手,卖出豆粕{meal_volume}手和豆油{oil_volume}手")
bean_pos.set_target_volume(ORDER_VOLUME)
meal_pos.set_target_volume(-meal_volume)
oil_pos.set_target_volume(-oil_volume)
position_time = time.time()
in_position = True
elif z_score < -STD_THRESHOLD: # 价差显著低于均值,压榨利润偏低
# 买入压榨价差:卖出大豆,买入豆粕和豆油
print(f"买入压榨价差:卖出大豆{ORDER_VOLUME}手,买入豆粕{meal_volume}手和豆油{oil_volume}手")
bean_pos.set_target_volume(-ORDER_VOLUME)
meal_pos.set_target_volume(meal_volume)
oil_pos.set_target_volume(oil_volume)
position_time = time.time()
in_position = True
else: # 如果已有持仓
# 检查是否应当平仓
if abs(z_score) < CLOSE_THRESHOLD: # 价差恢复正常
print("价差恢复正常,平仓所有头寸")
bean_pos.set_target_volume(0)
meal_pos.set_target_volume(0)
oil_pos.set_target_volume(0)
in_position = False
# 也可以添加止损逻辑
if (z_score > STD_THRESHOLD * 1.5 and current_bean_pos > 0) or \
(z_score < -STD_THRESHOLD * 1.5 and current_bean_pos < 0):
print("止损:价差向不利方向进一步偏离")
bean_pos.set_target_volume(0)
meal_pos.set_target_volume(0)
oil_pos.set_target_volume(0)
in_position = False
except BacktestFinished as e:
print("回测结束")
api.close()
```Shown in full with attribution under the source's licence. Licence: Apache-2.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.