在商品期货市场中,如果你问一个交易老手首先要考虑的因素是什么?答案可能是唯一的,风险!因为无论我们的基本面判断是多么精确,还是技术分析使用的多么熟练,然而这个世界是动态变化的,谁也不能完全避免风险。因此怎样可以在无风险或者风险较小的情况下,获得稳定的利润,并形成利润的复利效应,才是我们应该追寻的目标。
如何避免风险呢?那么我们就需要在不确定的市场中寻找到一个相对的确定性。这种相对的不确定性会存在于上下游品种之间的价格相关性(铁矿石和螺纹钢,玉米和玉米淀粉),还有相同品种不同月份合约之间的差价等。依靠我们使用肉眼的判断和手工的计算确实比较麻烦,幸好我们可以借助量化工具的帮忙,在纷繁变化的市场中找到不合理的偏差,偏差总是要修正的,我们的目标就是及时找到这个偏差进行入场,然后在它修正之前平仓获利了解。今天我们介绍的策略蝶式套利策略,依据的原理就是如此。
首先我们介绍一下蝶式套利策略。蝶式套利是跨期套利中的一种常见形式。它是由共享居中交割月份的一个牛市套利和一个熊市套利组合而成。由于较近月份和较远月份的期货合约分别处于居中月份的两侧,形同蝴蝶的两个翅膀,故称之为蝶式套利。
其中,居中月份合约的数量等于较近月份和远期月份数量之和,近端、远端合约的方向一致,中间合约的方向则和它们相反。这相当于在较近月份与居中月份之间的牛市(或熊市)套利和在居中月份与远期月份之间的熊市(或牛市)套利的一种组合。即一组是:买近月、卖中间月、买远月(多头蝶式套利);另一组是:卖近月、买中间月、卖远月(空头蝶式套利)。两组交易所跨的是三种不同的交割期,这三种不同交割期的期货合约,不仅品种相同,而且数量也相等,差别仅仅是价格。
正是由于不同交割月份的期货合约在客观上存在着价格水平的差异,而且随着市场供求关系的变动,中间交割月份的合约与两旁交割月份的合约价格还有可能会出现更大的价差。这就造成了套利者对蝶式套利的高度兴趣,即通过操作蝶式套利,利用不同交割月份期货合约价差的变动对冲了结,平仓获利。
蝶式套利在净头寸上没有开口,它在头寸的策略就是买入(或卖出)较近月份合约,同时卖出(或买入)居中月份合约,并买入(或卖出)远期月份合约。因为没有净头寸,所以在一定程度上可以大大的降低风险。
因此,我们要首先确定的事情是,价差是否会出现偏移,并且偏移过后是否会稳定回归?我们可以在优宽平台,获取完整一日的SA403,SA405,SA409分钟级别价格走势图,也就是目前正在交易的纯碱的403代表近期合约,纯碱405主力代表居中合约,纯碱409代表远期合约,查看它们之间的差价走势图。
第一个我们展示的价差是SA403和SA405的价差,可以看到除了尾盘最后十分钟,其他时间段基本都处于均值55附近,呈现不规则的波动。
下面我们来看SA405和SA409之间的价差,可以看到这个变化的更加平均,以均值为40,上下4个单位频繁的跳动。
因此,在一定程度上可以认为,在一个交易日内,价差是比较稳定的,如果价差出现偏离,那么在很大程度上可以得到回归。有些朋友可能还有疑问,这只是一个交易日的结果,在所有的交易日内,价差都会稳定回归吗?并且可以维持在相同的均值范围吗?我们画图展示一下。
第一个图像是近期和中期的价差十日走势图,第二个是中期和远期的。根据图像中展示,价差并不是稳定不变的,并且在1月19日到1月25日出现了巨量的偏移(这期间纯碱多个合约出现了涨停的情况),因此如果我们编写这个蝶式套利策略,我们需要考虑这些特殊情况。相对于使用固定的价差,我们可以设置在每一个不同的交易日内,通过不同品种之间的ticker差价,计算价差的波动区间。当价差超过稳定的波动区间,但是也不能超过一定的阈值(证明价格是在合理范围内波动),我们入场进行交易,等到获得固定的利润(价差回归),或者损失达到一定范围(价差单边趋势),我们离场,然后等待下一次的入场机会。
所以我们可以整理一下策略的逻辑:
并且我们也做了一些策略的优化,在晚上开盘和早上开盘前20分钟之内,还有尾盘的10分钟,都是是波动比较剧烈的行情,我们选择跳过这些时间段进行交易;而且为了避免持仓横跨两个交易日的风险,在尾盘10分钟的时候,如果持仓,我们平掉所有的仓位。
这就是策略的基本编写思路,我们在优宽平台编写代码试一下:
// 判断当前是否在交易时间范围内的函数
function timeSel() {
var t = new Date();
var hour = t.getHours();
var minute = t.getMinutes();
var day = t.getDay();
var isYes = false;
// 判断条件包括周一至周五的不同时间段(常规品种交易时间)
if ((day >= 1 && day <= 5) &&
((hour === 9 && minute >= 0) || (hour > 9 && hour < 11) || (hour === 11 && minute < 30)) ||
((hour === 13 && minute >= 30) || (hour > 13 && hour < 15)) ||
((hour >= 21 && hour < 23))) {
isYes = true;
}
return isYes;
}
// 计算给定数据的均值、方差和标准差的函数
function calculateStats(data) {
var mean = data.reduce((acc, val) => acc + val, 0) / data.length;
var squaredDiffs = data.map(val => Math.pow(val - mean, 2));
var variance = squaredDiffs.reduce((acc, val) => acc + val, 0) / squaredDiffs.length;
var stdDev = Math.sqrt(variance);
return { mean, variance, stdDev };
}
// 主要的交易策略函数
function main() {
// 初始化
SetErrorFilter("502:|503:|tcp|character|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|EOF|reused|(CTP_T@10010)")
var initAccount = _C(exchange.GetAccount)
var p = $.NewPositionManager()
var pretime = -1
var islock = false //防止重复开仓
var posContract = []
var posType = []
var posPrice = []
var posProfit = []
var curprofit = '--'
var wincount = 0
var losscount = 0
var nearTickList = []
var middleTickList = []
var farTickList = []
while(1) {
if(exchange.IO("status") && timeSel()) {
// 获取当前时间
var t = new Date();
var day = t.getDate();
var hour = t.getHours();
var minute = t.getMinutes();
// 处理新的交易日
if(day != pretime) {
nearTickList = []
middleTickList = []
farTickList = []
pretime = day
}
// 获取近、中、远三个合约的最新行情
_C(exchange.SetContractType, nearContract)
var neartick = _C(exchange.GetTicker)
if(neartick) nearTickList.push(neartick.Last)
_C(exchange.SetContractType, middleContract)
var middletick = _C(exchange.GetTicker)
if(middletick) middleTickList.push(middletick.Last)
_C(exchange.SetContractType, farContract)
var fartick = _C(exchange.GetTicker)
if(fartick) farTickList.push(fartick.Last)
// 非特定时间段执行策略
if(!((hour == 21 && minute < 20) || (hour == 9 && minute < 20) || (hour == 14 && minute > 50))) {
// 处理蝴蝶左翼的差价及统计信息
var valueA = neartick.Last - middletick.Last
var diffArrA = nearTickList.map((val, index) => val - middleTickList[index])
var statsA = calculateStats(diffArrA)
var diffnearmiddleUpperThre = statsA.mean + stdThre * statsA.stdDev
var diffnearmiddleUpperBound = statsA.mean + stdBound * statsA.stdDev
var diffnearmiddleLowerThre = statsA.mean - stdThre * statsA.stdDev
var diffnearmiddleLowerBound = statsA.mean - stdBound * statsA.stdDev
var posAContidion = valueA > diffnearmiddleUpperThre && valueA < diffnearmiddleUpperBound
var negAContidion = valueA < diffnearmiddleLowerThre && valueA > diffnearmiddleLowerBound
// 处理蝴蝶右翼的差价及统计信息
var valueB = middletick.Last - fartick.Last
var diffArrB = middleTickList.map((val, index) => val - farTickList[index])
var statsB = calculateStats(diffArrB)
var difffarmiddleUpperThre = statsB.mean + stdThre * statsB.stdDev
var difffarmiddleUpperBound = statsB.mean + stdBound * statsB.stdDev
var difffarmiddleLowerThre = statsB.mean - stdThre * statsB.stdDev
var difffarmiddleLowerBound = statsB.mean - stdBound * statsB.stdDev
// 获取交易信号
var posBContidion = valueB > difffarmiddleUpperThre && valueB < difffarmiddleUpperBound
var negBContidion = valueB < difffarmiddleLowerThre && valueB > difffarmiddleLowerBound
// 根据条件进行交易
if(islock == false && posAContidion && negBContidion){
p.OpenLong(middleContract, 2)
p.OpenShort(nearContract, 1)
p.OpenShort(farContract, 1)
islock = true
}
if(islock == false && negAContidion && posBContidion){
p.OpenShort(middleContract, 2)
p.OpenLong(nearContract, 1)
p.OpenLong(farContract, 1)
islock = true
}
// 处理持仓盈亏
var pos = _C(exchange.GetPosition)
if(pos && pos.length > 0){
profit = pos.reduce((sum, item) => sum + item.Profit, 0)
curprofit = profit
}
// 止盈止损
if(pos.length > 0 && profit >= stopWin){
Log(profit, '胜利#FF0000')
p.CoverAll()
wincount += 1
islock = false
curprofit = '--'
}
if(pos.length > 0 && profit <= stopLoss){
Log(profit, '失败#00FF00')
p.CoverAll()
losscount += 1
islock = false
curprofit = '--'
}
}
// 处理交易日结束
if(hour == 14 && minute > 50){
var pos = _C(exchange.GetPosition)
if(pos.length > 0){
Log(profit, '时间到#0000FF')
p.CoverAll()
curprofit = '--'
islock = false
}
}
// 绘制差价曲线图
$.PlotMultLine("近月中期差价", "差价", valueA, new Date().getTime(),{layout: 'single', height: '600px'})
$.PlotMultLine("中期远月差价", "差价", valueB, new Date().getTime(),{layout: 'single', height: '600px'})
// 更新策略运行统计表
tblStatusA = {
"type" : "table",
"title" : "策略运行统计表",
"cols" : ["实时盈利", "止盈次数", "止损次数"],
"rows" : []
}
tblStatusA.rows = [];
tblStatusA.rows.push([curprofit, wincount, losscount])
// 更新策略实时状态表
tblAStatusB = {
"type" : "table",
"title" : "策略实时状态表",
"cols" : ["持仓品种", "持仓类型", "持仓价格", "持仓盈亏"],
"rows" : []
}
var statusPos = _C(exchange.GetPosition)
var posContract = []
var posType = []
var posPrice = []
var posProfit = []
if(statusPos && statusPos.length > 0){
for (var i = 0; i < statusPos.length; i++) {
posType.push(statusPos[i]['Type'] % 2 === 0 ? '多头' : '空头')
posContract.push(statusPos[i]['ContractType'])
posPrice.push(statusPos[i]['Price'])
posProfit.push(statusPos[i]['Profit'])
}
}
tblAStatusB.rows = []
for (var j = 0; j < posContract.length; j++) {
tblAStatusB.rows.push([posContract[j], posType[j], posPrice[j], posProfit[j]]);
}
// 合并表格信息
lastStatus = '`' + JSON.stringify(tblStatusA) + '`\n' + '`' + JSON.stringify(tblAStatusB) + '`'
// 打印策略信息
LogStatus(lastStatus)
// 打印收益信息
var accountInfo = _C(exchange.GetAccount)
var curprofit = accountInfo.Info.Balance - initAccount.Info.Balance//该收益计算只能在实盘中进行使用
LogProfit(curprofit, "权益", '&')
Sleep(100)
} else {
// 处理未连接到交易服务器的情况
LogStatus("正在等待与交易服务器连接, " + _D())
Sleep(3000)
}
}
}
下面我们介绍一下各部分的函数代码:
时间选择逻辑 (timeSel函数): TimeSel 函数用于确定当前是否处于交易时间范围内。交易时间范围包括周一至周五的目标品种的交易时间段,这是设置的是包含夜盘的一般品种,如果大家钟爱只有日盘或者其他特殊时间段的品种,可以自行设置。如果在交易时间范围内,返回 true,否则返回 false。
统计函数 (calculateStats 函数): CalculateStats 函数接收一个数据数组,计算其均值、方差和标准差。通过使用这些统计值可以量化数据分布的特征。
交易逻辑主体 (main 函数):
对于策略的外部参数,我们可以设置目标品种,包括短期,中期和长期的合约,价差标准差阈值和上下限,以及止盈和止损的系数。大家可以根据自己喜欢的品种进行不同的尝试。
总体来说,这个策略的核心思路是在两个品种的差价上设置阈值,当差价超过阈值时执行相应的交易动作。整个逻辑结构还是比较清晰的,通过不断获取行情数据和根据条件执行交易来实现策略的自动化交易。
大家可能最关心的是这个策略的收益,比较让人兴奋的一点是,这个策略可以做到极高的胜率,但是出现开仓的信号和平仓的信号等待的时间是比较长的,如果使用了走势特别平稳的品种,比如纸浆玉米等,开仓和平仓的等待都是比较耗时的(可能一天也等不到);因此,它更加适合于存在差价回归,但是差价也并不是特别严格的品种,大家可以在对价差分析的基础上挑选合适的品种。当然,本策略实际上算是高频策略的一种,当价差出现偏差需要立即开仓,当达到盈利点位,需要立即平仓,因此代码部分可以优化的地方还有很多。本篇文章更多的是一个抛砖引玉的作用,希望更多朋友可以提出宝贵的意见,我们共同完善好这个策略。