Moving-Average Signals for Vegetable Oil Futures Spread Trading
Summary
This example strategy trades relative price relationships among soybean oil, rapeseed oil, and palm oil futures. It calculates a normalized spread index from the three daily closing prices, then compares five-period and fifteen-period moving averages. A bullish crossover combined with the index exceeding a threshold triggers a long position in rapeseed oil and a short position in soybean oil. A bearish crossover with a lower threshold instead buys soybean oil and sells palm oil. When the index returns near the moving-average range, the strategy targets flat positions.
The code shows how signals are translated into target contract volumes, but it offers no backtest, transaction-cost model, hedge-ratio rationale, or performance evidence. The instrument contracts and fixed position sizes are specific examples, and the source labels the strategy as a functional demonstration requiring adaptation before live use. It therefore illustrates a rule structure for spread trading rather than establishing that the relationships are stable or profitable.
Key ideas
- The strategy derives a normalized spread index from the daily closes of three vegetable oil futures.
- It uses short- and long-period moving averages to identify spread crossovers.
- A bullish signal buys rapeseed oil and sells soybean oil, while a bearish signal buys soybean oil and sells palm oil.
- Positions are targeted to zero when the spread returns near the moving-average range.
- The example provides no performance testing or transaction-cost analysis.
Tags
Full text
# oi-y-p-spreads.py
```py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Ringo"
"""
豆油、棕榈油、菜油套利策略
注: 该示例策略仅用于功能示范, 实盘时请根据自己的策略/经验进行修改
"""
from tqsdk import TqApi, TargetPosTask
from tqsdk.tafunc import ma
# 设定豆油,菜油,棕榈油指定合约
SYMBOL_Y = "DCE.y2001"
SYMBOL_OI = "CZCE.OI001"
SYMBOL_P = "DCE.p2001"
api = TqApi()
klines_y = api.get_kline_serial(SYMBOL_Y, 24 * 60 * 60)
klines_oi = api.get_kline_serial(SYMBOL_OI, 24 * 60 * 60)
klines_p = api.get_kline_serial(SYMBOL_P, 24 * 60 * 60)
target_pos_oi = TargetPosTask(api, SYMBOL_OI)
target_pos_y = TargetPosTask(api, SYMBOL_Y)
target_pos_p = TargetPosTask(api, SYMBOL_P)
# 设置指标计算函数,计算三种合约品种的相对位置,并将指标画在副图
def cal_spread(klines_y, klines_p, klines_oi):
index_spread = ((klines_y.close - klines_p.close) - (klines_oi.close - klines_y.close)) / (
klines_oi.close - klines_p.close)
klines_y["index_spread"] = index_spread
ma_short = ma(index_spread, 5)
ma_long = ma(index_spread, 15)
return index_spread, ma_short, ma_long
index_spread, ma_short, ma_long = cal_spread(klines_y, klines_p, klines_oi)
klines_y["index_spread.board"] = "index_spread"
print("ma_short是%.2f,ma_long是%.2f,index_spread是%.2f" % (ma_short.iloc[-2], ma_long.iloc[-2], index_spread.iloc[-2]))
while True:
api.wait_update()
if api.is_changing(klines_y.iloc[-1], "datetime"):
index_spread, ma_short, ma_long = cal_spread(klines_y, klines_p, klines_oi)
print("日线更新,ma_short是%.2f,ma_long是%.2f,index_spread是%.2f" % (
ma_short.iloc[-2], ma_long.iloc[-2], index_spread.iloc[-2]))
# 指数上涨,短期上穿长期,则认为相对于y,oi被低估,做多oi,做空y
if (ma_short.iloc[-2] > ma_long.iloc[-2]) and (ma_short.iloc[-3] < ma_long.iloc[-3]) and (
index_spread.iloc[-2] > 1.02 * ma_short.iloc[-2]):
target_pos_y.set_target_volume(-100)
target_pos_oi.set_target_volume(100)
# 指数下跌,短期下穿长期,则认为相对于y,p被高估,做多y,做空p
elif (ma_short.iloc[-2] < ma_long.iloc[-2]) and (ma_short.iloc[-3] > ma_long.iloc[-3]) and (
index_spread.iloc[-2] < 0.98 * ma_short.iloc[-2]):
target_pos_y.set_target_volume(100)
target_pos_p.set_target_volume(-100)
# 现在策略表现平稳,则平仓,赚取之前策略收益
elif ma_short.iloc[-2] * 0.98 < index_spread.iloc[-2] < ma_long.iloc[-2] * 1.02:
target_pos_oi.set_target_volume(0)
target_pos_p.set_target_volume(0)
target_pos_y.set_target_volume(0)
```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.