资源加载中... loading...

钢厂利润套利策略

Author: 雨幕(youquant), Created: 2021-09-22 15:52:55, Updated: 2023-11-22 20:32:39

img

钢厂利润套利策略

对大宗商品黑色产业链期货的研究,进行钢厂利润套利涉及螺纹钢(代码rb)、铁矿石(代码i)、焦炭(代码j)。在钢厂炼钢生产过程中影响产品成本的最主要因素就是原料。

通常对于基本面的研究我们认为,螺纹钢期货价格 = 1.6 * 铁矿石期货价格 + 0.5 * 焦炭期货价格 + 其它

以上是理想的情况,而期货价格是变化的,有溢价的。所以这个等式在实际的情况中是不相等的。从成品、原料组合的价格差变动来看,等式左右两边的价差可以理解为钢厂炼钢的利润,那么价差的波动就是钢厂利润的波动,因此追随钢厂利润波动的模式就是钢厂利润套利的模式。

公式变动为:

钢厂1吨螺纹钢的利润 = 1吨螺纹钢合约价格 – 1.6吨铁矿石合约价格 – 0.5吨焦炭合约价格 – 其它成本

钢厂炼钢利润波动的逻辑:如果炼钢利润高过一定水平,铁矿石和焦炭价格会跟涨,挤压炼钢利润;炼钢利润低过一定水平,钢材价格回升。我们可以看到钢厂利润波动的逻辑性较强。当钢厂利润达到高位时可以做空利润,即做空螺纹钢做多铁矿石焦炭。当钢厂利润处于底位时,可以做多利润,即做多螺纹钢做空铁矿石焦炭

对于钢厂利润套利策略,该示例策略仅用于示范,实盘时请根据自己的策略/经验进行评估。

头寸计算

需要注意的是铁矿石合约和焦炭合约一手是100吨,螺纹钢合约一手是10吨。那么就需要计算出一个合理的对冲头寸。让一次对冲的比例为:螺纹钢吨数 : 铁矿石吨数 : 焦炭吨数 = 1 : 1.6 : 0.5

策略计算出最小的下单对冲组合手数分别是:

img

rb手数:100 ,换算吨数:1000吨
i手数:16,换算吨数:1600吨
j手数:5,换算吨数:500吨
达成比例1000 :1600 : 500 = 螺纹钢吨数 : 铁矿石吨数 : 焦炭吨数 = 1 : 1.6 : 0.5

可见,100手rb合约,16手i合约,5手j合约是最小的整数组合了。如果允许偏差一点也可以100:16:5比例下减小5倍即:20:3:1。即使这样,这个保证金量也是不小的,看来这个玩法不太适合小散。

回测

使用布林指标捕获较大的偏差,做空钢厂利润。捕捉较小的偏差,做多钢厂利润。

img

img

img

策略源码

/*backtest
start: 2021-03-01 09:00:00
end: 2021-07-28 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/

// 参数
var symbol_rb = "rb2201" 
var symbol_j = "j2201"
var symbol_i = "i2201"
var bollPeriod = 50


// 全局变量
var q = $.NewTaskQueue()        
var p = $.NewPositionManager()

function calcCommonMultiple(a, b, c) {
    var divisor = -1
    var commonMultiple = a * b * c
    for (var i = 1 ; i < (commonMultiple / Math.min(a, b, c)) ; i++) {
        if (commonMultiple / a % i == 0 && commonMultiple / b % i == 0 && commonMultiple / c % i == 0) {
            divisor = i
        }
    }
    if (divisor == -1) {
        throw "没有找到"
    } else {
        return commonMultiple / divisor
    }
}

function main() {
    var preTS = 0
    var isFirst = true 
    var initAccount = null 
    while (true) {
        Sleep(1000)
        if (exchange.IO("status")) {
            if (!$.IsTrading(symbol_rb) || !$.IsTrading(symbol_i) || !$.IsTrading(symbol_j)) {
                continue 
            }

            if (isFirst) {
                initAccount = _C(exchange.GetAccount)
                isFirst = false 
            }
            
            // 订阅螺纹钢合约
            var info_rb = exchange.SetContractType(symbol_rb)
            if (!info_rb) {
                continue
            }
            var records_rb = exchange.GetRecords()

            // 订阅铁矿石合约
            var info_i = exchange.SetContractType(symbol_i)
            if (!info_i) {
                continue
            }
            var records_i = exchange.GetRecords()

            // 订阅焦炭合约
            var info_j = exchange.SetContractType(symbol_j)
            if (!info_j) {
                continue 
            }
            var records_j = exchange.GetRecords()

            // 校验records数据
            if (!records_rb || !records_i || !records_j || Math.min(records_rb.length, records_i.length, records_j.length) < bollPeriod) {
                continue
            }

            // 更新持仓
            var holds = {rb : 0, i : 0, j : 0}
            var pos = _C(exchange.GetPosition)
            for (var i = 0 ; i < pos.length ; i++) {
                if (pos[i].ContractType == symbol_rb) {
                    if (pos[i].Type == PD_LONG || pos[i].Type == PD_LONG_YD) {
                        holds.rb += pos[i].Amount 
                    } else {
                        holds.rb -= pos[i].Amount 
                    }
                } else if (pos[i].ContractType == symbol_i) {
                    if (pos[i].Type == PD_LONG || pos[i].Type == PD_LONG_YD) {
                        holds.i += pos[i].Amount
                    } else {
                        holds.i -= pos[i].Amount
                    }                    
                } else if (pos[i].ContractType == symbol_j) {
                    if (pos[i].Type == PD_LONG || pos[i].Type == PD_LONG_YD) {
                        holds.j += pos[i].Amount
                    } else {
                        holds.j -= pos[i].Amount
                    }                    
                }
            }

            // minCommonMultiple
            var minCommonMultiple = calcCommonMultiple(info_rb.VolumeMultiple, info_i.VolumeMultiple, info_j.VolumeMultiple)
            var amount_rb = minCommonMultiple / info_rb.VolumeMultiple * 10
            var amount_i = minCommonMultiple / info_i.VolumeMultiple * 16
            var amount_j = minCommonMultiple / info_j.VolumeMultiple * 5

            // 一般情况是1.5-1.6(矿石)+0.4-0.5(焦炭)=1吨粗钢 , 1 : 1.6 : 0.5 = 10 : 16 : 5
            var r = []
            for (var i = Math.min(records_rb.length, records_i.length, records_j.length) ; i > 0 ; i--) {
                var bar_rb = records_rb[records_rb.length - i]
                var bar_i = records_i[records_i.length - i]
                var bar_j = records_j[records_j.length - i]
                r.push(bar_rb.Close - 1.6 * bar_i.Close - 0.5 * bar_j.Close)
            }
            if (r.length < bollPeriod) {
                continue
            }
            
            var boll = TA.BOLL(r, bollPeriod, 2)
            var up = boll[0]
            var down = boll[2]
            if (records_rb[records_rb.length - 1].Time == records_i[records_i.length - 1].Time && records_i[records_i.length - 1].Time == records_j[records_j.length - 1].Time && preTS != records_j[records_j.length - 1].Time) {
                preTS = records_j[records_j.length - 1].Time
                $.PlotLine("close", r[r.length - 2], preTS)
                $.PlotLine("up", up[up.length - 2], preTS)
                $.PlotLine("down", down[down.length - 2], preTS)                
            }            

            // 判断触发条件
            if (r[r.length - 1] > up[up.length - 1] && holds.rb == 0 && holds.i == 0 && holds.j == 0) {   
                // 空rb多i,j
                q.pushTask(exchange, symbol_rb, "sell", amount_rb, function(task, ret) {
                    Log(task.desc, ret)
                    if (ret) {
                        q.pushTask(exchange, symbol_i, "buy", amount_i, function(task, ret) {
                            Log(task.desc, ret)
                            if (ret) {
                                q.pushTask(exchange, symbol_j, "buy", amount_j, function(task, ret) {
                                    Log(task.desc, ret)
                                    $.PlotFlag(new Date().getTime(), '空rb多i,j', 'up') 
                                })
                            }
                        })
                    }
                })
            } else if (r[r.length - 1] < down[down.length - 1] && holds.rb == 0 && holds.i == 0 && holds.j == 0) {   
                // 多rb空i,j
                q.pushTask(exchange, symbol_rb, "buy", amount_rb, function(task, ret) {
                    Log(task.desc, ret)
                    if (ret) {
                        q.pushTask(exchange, symbol_i, "sell", amount_i, function(task, ret) {
                            Log(task.desc, ret)
                            if (ret) {
                                q.pushTask(exchange, symbol_j, "sell", amount_j, function(task, ret) {
                                    Log(task.desc, ret)
                                    $.PlotFlag(new Date().getTime(), '多rb空i,j', 'down') 
                                })
                            }
                        })
                    }
                })
            } else if (r[r.length - 1] > up[up.length - 1] && holds.rb > 0 && holds.i < 0 && holds.j < 0) {
                // 平rb多,平i,j空
                q.pushTask(exchange, symbol_rb, "coverall", -1, function(task, ret) {
                    Log(task.desc, ret)
                    if (ret) {
                        q.pushTask(exchange, symbol_i, "coverall", -1, function(task, ret) {
                            Log(task.desc, ret)
                            if (ret) {
                                q.pushTask(exchange, symbol_j, "coverall", -1, function(task, ret) {
                                    Log(task.desc, ret)
                                    LogProfit(_C(exchange.GetAccount).Balance - initAccount.Balance)
                                    $.PlotFlag(new Date().getTime(), '平多rb平空i,j', 'up') 
                                })
                            }
                        })
                    }
                })
            } else if (r[r.length - 1] < down[down.length - 1] && holds.rb < 0 && holds.i > 0 && holds.j > 0) {
                // 平rb空,平i,j多
                q.pushTask(exchange, symbol_rb, "coverall", -1, function(task, ret) {
                    Log(task.desc, ret)
                    if (ret) {
                        q.pushTask(exchange, symbol_i, "coverall", -1, function(task, ret) {
                            Log(task.desc, ret)
                            if (ret) {
                                q.pushTask(exchange, symbol_j, "coverall", -1, function(task, ret) {
                                    Log(task.desc, ret)
                                    LogProfit(_C(exchange.GetAccount).Balance - initAccount.Balance)
                                    $.PlotFlag(new Date().getTime(), '平空rb平多i,j', 'down') 
                                })
                            }
                        })
                    }
                })
            }

            q.poll()
        } else {
            LogStatus(_D())
        }        
    }
}

策略需要勾选「画线类库」、「商品期货交易类库」

img

策略仅用于研究学习,实盘慎用。


更多内容

zhaosunday 问下画线类库在哪里没找到

雨幕(youquant) 可以在策略广场里搜索:https://www.fmz.cn/square