多品种商品期货对冲网格交易策略 分析 与 实现
先睹为快,对于自动化、量化交易策略的研究分类大致为,趋势型、 对冲型、 网格策略、 高频策略、 人工智能、 算法交易、 深度学习。其中 网格策略 和 对冲类型策略 可以算是比较经典的策略类型。 如果网格和对冲策略组合起来可以产生什么样的化学反应呢?
-
1、对冲策略
对冲是一个很大的概念,对冲是针对单边交易来讲的,单边持多仓或者持空仓的风险比较大,应对方法就是在风险出现前,选择买入相关性为负的品种进行对冲 或者 选择卖出相关性为正的品种进行对冲。
金融学上,对冲(hedge)指特意减低另一项投资的风险的投资。它是一种在减低商业风险的同时仍然能在投资中获利的手法。一般对冲是同时进行两笔行情相关、方向相反、数量相当、盈亏相抵的交易。行情相关是指影响两种商品价格行情的市场供求关系存在同一性,供求关系若发生变化,同时会影响两种商品的价格,且价格变化的方向大体一致。方向相反指两笔交易的买卖方向相反,这样无论价格向什么方向变化,总是一盈一亏。当然要做到盈亏相抵,两笔交易的数量大小须根据各自价格变动的幅度来确定,大体做到数量相当。-
举例: (跨品种套利,两个标的物如:豆油、棕榈油 用途、性质 等商品属性类似,价格相关性满足套利条件)
例如,现在大连商品交易所在2010年12月17日,豆油1109合约(A)价格报价10052元每吨,棕榈油1109合约(B)报价9412元每吨,如果采取对冲套利交易,有两种策略:1、建多仓A 同时 建空仓B,2、建空仓A 同时 建多仓B 。
建多仓A 同时 建空仓B,就是买入豆油1109合约,卖出棕榈油1109合约,价差走强,取得盈利,反之亏损;建空仓A 同时 建多仓B,就是卖出豆油1109合约,买入棕榈油1109合约,价差走弱,取得盈利,反之亏损。
我们通过买一个合约,卖出另一个合约的交易策略,规避单向交易风险,赚钱价差利润,这种交易方式,就是对冲套利交易。
-
建多仓A 同时 建空仓B 图例解释:
-
建空仓A 同时 建多仓B 图例解释:
可见这种对冲组合,买卖的不是一种标的物,而是买卖两种标的物组合的差价,也是具有一定方向性的。这样对冲的优点在于,降低交易风险,根据两个品种的相关性,差价一般情况是在一定范围内波动。出现单边行情的几率不是很大,大大降低了交易亏损风险。
对冲策略大致有: 跨市场对冲套利、同品种跨期对冲套利、跨品种对冲套利、期货期权对冲 等等。
相关内容有: 统计套利、 无风险套利、 基本面套利(现货市场期货市场)这里就不再赘述。
风险在于: 临近交割期,或者到交割期差价没有按照预期回归到平仓值,只能亏损平仓了结。
-
-
-
2、网格策略
这是一种仓位策略,用于动态调仓。网格策略的分类比较多,各种变形版本也很多。举例几种:基本网格、马丁格尔网格、反马丁格尔网格、动态网格等等。
所谓网格交易法(grid trading method),也称鱼网交易法,指以某点为基点,每上涨或下跌一定点数挂一定数量空单或多单,设定盈利目标,但不设止损,当价格朝期望方向进展时获利平仓,并在原点位挂同样的买单或卖单。
缺点:当出现大的单边行情时,对资金和仓位要求很高,因为留下了一串的浮亏单,浮亏会很快远远超过盈利直至暴仓。
-
3、对冲网格
为什么要组合?
目的就是要尽量解决 对冲策略 、网格策略的自身缺点。在对冲策略中如果不能很好的控制仓位,在一个差价上对冲了较大的量。如果差价没有按照预期触发对冲平仓,对冲仓位将会一直处于浮亏状态,资金利用效率低。并且如果临近交割时间,必须要止损。在网格策略中最怕标的物出现单边行情,策略会陷入浮亏状态,只能不断加仓、被深套或者爆仓。
如果把对冲差价作为标的物的价格,对冲差价上面讲过,是大概率在一定区间内反复波动。这一点可以在一定程度上降低网格的风险,并且按照网格的仓位控制方式,在每个差价上持有小量的仓位。这样根据网格的参数设置在行情波动的一定范围内会反复对冲平仓,产生盈利。
但是风险是依然存在的,就是在交割前差价出现大幅变化,并且在交割前没有回归到可平仓位置(浮亏较大)。导致前期网格盈利的部分难以抵消当前亏损。
-
4、对冲网格模型的仓位控制
对冲网格策略的仓位控制其实就是 网格参数的设置,网格可以设置成等距等量网格,也可以设置成一定规律递增递减,或者根据对于历史差价的分析得出的不均匀的网格参数设置。
-
5、源码分析(超详细,手把手)
基于以上的分析,可以写出一个模型来实践一下思路。
BotVS 量化平台 上实现了 << 商品期货套利 - 多品种网格对冲模型 >>-
策略简介:
传统的跨期对冲一般指统计套利, 用线性回归或者其它办法生成一个套利区间, 这样套利机会比较少, 而且有预测性, 未来价差很可能不是预计的那样回归
为了解决这种办法, 进而更频繁的进行套利操作, 我们把两个关联品种或者跨期品种的套利价差定义成一个网格, 每满足一定的价差就开一次仓, 做一次对冲
这样价差来回在我们设置的网格里进行波动,我们就能不断的开仓平仓实现盈利.
策略用到了一个处理底层交易细节的模板 《商品期货交易类库》 地址 : https://www.botvs.com/strategy/12961
例如 $.NewTaskQueue 函数就是该模板的 导出接口函数 用于生成 交易任务队列控制对象,有兴趣研究交易细节逻辑的可以学习下。
-
-
源码详细分析:
// 商品期货套利 - 多品种网格对冲模型 注释版
function Hedge(q, e, positions, symbolA, symbolB, hedgeSpread) { // 对冲对象 生成函数, 参数q 为最开始调用 商品期货交易类库模板 的 导出函数 生成的 交易队列控制对象, 参数e 为交易所对象,positions 为初始仓位
// symbolA 为第一个合约类型, symbolB 为第二个合约类型, hedgeSpread 为对冲 交易量控制表 字符串。
var self = {} // 声明一个 空对象 用于给空对象 初始化 后返回 (即对冲对象)
self.q = q // 给对冲对象 添加 属性 q ,并用参数的q 初始赋值。
self.symbolA = symbolA // 给对冲对象self 添加属性 symbolA ,储存 第一个合约类型
self.symbolB = symbolB // ...
self.name = symbolA + " & " + symbolB // 对冲对象的 名称 即对冲组合
self.e = e // 对冲对象 中储存交易所对象 的引用
self.isBusy = false // 繁忙 标记 初始为 不繁忙。
self.diffA = 0 // 差价A , 即 symbolA买一 - symbolB卖一
self.diffB = 0 // 差价B , 即 symbolB卖一 - symbolA买一
self.update = _D() // 记录 更新时间
var arr = hedgeSpread.split(';') // 把传入的 交易量控制表 字符串 按照 ';' 字符 分割。
self.dic = [] // 网格节点 对象 数组
var n = 0 //
var coefficient = 1 // 系数 初始1
for (var i = 0; i < positions.length; i++) { // 遍历持仓信息。
if (positions[i].ContractType == symbolA) { // 如果遍历当前的 持仓信息 的合约类型 和 当前对冲对象的 第一个合约类型 相同
n += positions[i].Amount // 累计 类型为 symbolA 的合约的持仓量
if (positions[i].Type == PD_LONG || positions[i].Type == PD_LONG_YD) { // 如果 持仓类型是 多仓 , coefficient 赋值为 -1 ,即 代表 反对冲: 多 symbolA 空 symbolB
coefficient = -1
}
}
}
_.each(arr, function(pair) { // 把 控制表字符串 数组的 每一个单元 迭代传递到 匿名函数 作为参数 pair
var tmp = pair.split(':'); // 把每个 网格节点的 单元 按照':'字符 分割成 参数 数组 tmp
if (tmp.length != 3) { // 由于 格式 是 30:15:1 即 开仓差价: 平仓差价: 下单量 ,所以 用 ':' 分割后 tmp长度 不是3的 即 格式错误
throw "开仓表不正确"; // 抛出异常 格式错误
}
var st = { // 每次迭代的时候 构造一个对象
open: Number(tmp[0]), // 开仓 把 tmp[0] 即 30:15:1 按 ':' 分割后 生成的数组中的 第一个元素 30 , 通过 Number 函数 转换为数值
cover: Number(tmp[1]), // 平仓..
amount: Number(tmp[2]), // 量..
hold: 0 // 持仓 初始为0
}
if (n > 0) { // 如果 n 大于0 ,即 开始的时候 有持仓。
var m = Math.min(n, st.amount) // 取 当前合约组合中 symbolA的 持仓量 和 网格节点 中的开仓量 二者的 最小值 赋值给 m 变量
n -= m // 在持仓累计数量 n 中 减去 m
st.hold = m * coefficient // 正对冲 coefficient 这个系数 为1 , 如果 symbolA 为 合约的持仓 类型为 多仓, 则是反对冲 那么 coefficient 在之前 就会被赋值为 -1
// 在迭代过程中 n 被 分散到各个 网格节点 恢复网格 持仓数据。
Log("恢复", self.name, st) // 输出本次的恢复信息。
}
self.dic.push(st) // 把恢复好的节点 压入 dic数组 。
});
if (n > 0) { // 如果 迭代完成后 n 值 依然大于0 即 还有仓位没有分配恢复 , 抛出错误
throw "恢复失败, 有多余仓位 " + n;
}
self.poll = function() { // 给 self 对冲对象 添加 属性poll 并用一个 匿名函数 初始化 赋值
if (self.isBusy || (!$.IsTrading(self.symbolA))) { // 如果 self 对象的属性 isBusy 为true 即繁忙 ,或者 合约类型 symbolA 不在交易时间内 (通过调用$.IsTrading这个模板导出函数获取) ,则调用return返回
return
}
var insDetailA = exchange.SetContractType(self.symbolA) // 设置 合约类型 symbolA
if (!insDetailA) { // 返回 null 则 调用 return 返回
return
}
var tickerA = exchange.GetTicker() // 获取 symbolA 合约的行情信息
if (!tickerA) { // 获取失败 调用return
return
}
var insDetailB = exchange.SetContractType(self.symbolB) // 设置 合约类型 symbolB
if (!insDetailB) {
return
}
var tickerB = exchange.GetTicker() // 获取 symbolB 合约的行情信息
if (!tickerB) {
return
}
self.update = _D(tickerA.Time) // 更新时间
var action = null // 动作变量
var diffA = _N(tickerA.Buy - tickerB.Sell) // A合约的 买一 减去 B合约的 卖一
var diffB = _N(tickerA.Sell - tickerB.Buy) // A合约的 卖一 减去 B合约的 买一
self.diffA = diffA // 赋值 给 对象 的 成员diffA
self.diffB = diffB // 赋值 ..
for (var i = 0; i < self.dic.length && !action; i++) { // 遍历 网格的 节点 ,直到 遍历结束 或者 有action 执行。
if (self.dic[i].hold == 0) { // 如果 网格 节点的持仓量 为 0
if (self.dic[i].open <= diffA) { // 如果 网格 节点的开仓价 小于等于 差价A (即 A合约买一 减去 B合约的卖一 的差价 突破 网格节点的 开仓价)
action = [i, "sell", "buy", self.dic[i].amount] // action 记录下 网格节点 索引 、symbolA sell ,symbolB buy ,操作量 。
} else if (self.dic[i].open <= -diffB) { // -diffB 实际就是 tickerB.Buy - tickerA.Sell ,也就是 B合约买一 减去 A合约的卖一 的差价 突破 网格节点的 开仓价
action = [i, "buy", "sell", -self.dic[i].amount] // action 记录..
}
} else { // 网格节点的 持仓量 不为0
if (self.dic[i].hold > 0 && self.dic[i].cover >= diffB) { // 如果 节点的持仓量 大于0 即 正对冲 持仓 A合约持空,B合约持多。 并且 平仓差价(合约A sell ,合约B buy)小于平仓线
action = [i, "closesell", "closebuy", self.dic[i].hold] // action 记录下 网格节点 索引 , 合约A 平空仓 , 合约B 平多仓, 按照节点持仓量 平。
} else if (self.dic[i].hold < 0 && self.dic[i].cover >= -diffA) { // 如果持仓量 小于0 , 并且 -diffA 即 tickerB.Sell - tickerA.Buy ,小于平仓线
action = [i, "closebuy", "closesell", self.dic[i].hold] // action 记录下 ..
}
}
}
if (!action) { // 如果 action 为初始赋值的 null ,即没有 被 赋值操作。 调用 return 返回
return
}
Log("A卖B买: " + _N(diffA) + ", A买B卖: " + _N(diffB), ", Action: " + JSON.stringify(action)) // 如果 action 有值 ,输出信息 差价A 差价B 和 action 储存的数据。
self.isBusy = true // 有操作 即 锁定, 为繁忙状态, 处理完成前 在该函数开始处都会触发 return
self.q.pushTask(self.e, self.symbolA, action[1], self.dic[action[0]].amount, function(task, ret) { // 调用 交易队列对象q 的成员函数 把 具体操作 参数传入 , 压任务进队列 等待处理。
if (!ret) { // 回调函数, 如果完成的 返回值 ret 为false 即 操作失败,
self.isBusy = false // 重新把 isBusy 赋值为 false ,解除锁定
return // 返回
}
self.q.pushTask(self.e, self.symbolB, action[2], self.dic[action[0]].amount, function(task, ret) { // A 合约 操作 成功 则,压入B合约的 操作任务 进 任务队列 等待处理。
if (!ret) { // 如果 A合约操作完成 B 合约 操作失败 则 抛出异常
throw "开仓失败..."
}
self.isBusy = false // 解除锁定
if (task.action != "buy" && task.action != "sell") { // 如果 调用该回调函数的 任务 不是开仓 操作
self.dic[action[0]].hold = 0; // 当前 操作的网格 节点 的持仓量 重新赋值为 0
} else { // 如果是 开仓操作
self.dic[action[0]].hold = action[3]; // 把 当前任务 的下单量 参数 赋值给 当前网格节点的持仓量
}
})
})
}
return self // 该函数 返回一个 对冲组合 对象
}
// 注释版
function main() {
SetErrorFilter("ready|login|timeout") // 过滤常规错误
Log("正在与交易服务器连接...")
while (!exchange.IO("status")) Sleep(1000); // 一个循环 直到 IO函数返回 true 连接上服务器后 跳出
Log("与交易服务器连接成功") // 显示与服务器连接
var mode = exchange.IO("mode", 0); // 调整行情获取模式。立即返回模式。
if (typeof(mode) !== 'number') { // 调整行情模式函数 如果返回的不是数值类型
throw "切换模式失败, 请更新到最新托管者!"; // 抛出错误异常
} else {
Log("已切换到适合多品种价格查询的立即模式");
}
if (CoverAll) { // 如果 开启平掉初始仓位的功能 CoverAll 启动时平掉所有仓位(界面上的参数)
Log("开始平掉所有残余仓位..."); // 显示信息 平掉所有初始仓位
$.NewPositionManager().CoverAll(); // 调用商品期货模板的导出函数生成一个对象,并调用该对象的CoverAll方法,平掉所有初始仓位。
Log("操作完成"); // CoverAll函数执行完 打印 日志信息,显示操作完成。
}
LogStatus("尝试获取持仓状态") // 状态栏 显示尝试获取持仓状态的信息
var positions = _C(exchange.GetPosition) // 获取所有持仓信息。
LogStatus("Ready")
if (positions.length > 0 && !AutoRestore) { // 如果持仓信息 数组 positions 长度大于0 并且界面参数没有开启自动恢复
throw "发现持仓, 请勾选自动恢复" // 抛出错误异常
}
var pairs = [] // 声明一个数组, 用来储存 处理对冲组合的对象。
var q = $.NewTaskQueue(function(task, ret) { // 调用商品期货模板导出函数,生成一个 队列对象,用来处理并发的交易操作
Log(task.desc, ret ? "成功" : "失败") // 匿名函数(回调) 在队列中的任务完成后打印 显示任务的描述,返回状态。
})
var arr = HedgeTable.split('('); // 处理 界面参数 交易参考表, 按照 '('符号 分割字符串,初步 把多种合约控制表混合的字符串分割成 每一个组合为一个元素的字符串数组。
var tbl = { // 声明一个 tbl 对象 用来记录 显示在状态栏的数据
type: 'table', // 调用LogStatus 时 ,让状态栏显示为表格,详见API , 此处类型需要设置为 table
title: 'Runtime', // 状态栏表格的 标题
cols: ['Pair', 'Open', 'Cover', 'Hold', 'DiffA', 'DiffB', 'Time'], // 表格的每列的表头。
rows: [] // 表格的每行数据,初始时是一个空数组。
};
_.each(arr, function(item) { // 一个JS 库的迭代 函数, 把arr中的每个元素 作为 (第二个参数) 匿名函数的参数 ,迭代执行匿名函数。
if (item != '') { // 忽略 item 为空字符串的 情况。item 即为 每个合约组合 以及他们的 开仓控制表。
var tmp = item.split(')'); // 根据 ')' 字符 分割 合约组合 和 控制表 。
var pair = tmp[0].replace('(', '').split('&'); // 合约组合字符串中存在 '('字符替换为'' 空字符, 再进行按 '&' 字符分割为 字符串数组。分别储存 合约类型。
var symbolDetail = _C(exchange.SetContractType, pair[0]) // 使用 第一个 合约类型 设置合约类型,然后Log 显示 该合约的一些信息。
Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
symbolDetail = _C(exchange.SetContractType, pair[1]) // 使用 第二个 合约类型 设置合约类型,然后Log 显示 该合约的一些信息。
Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
pairs.push(Hedge(q, exchanges[0], positions, pair[0], pair[1], tmp[1])) // 用以上处理好的数据:q处理交易对象,exchanges[0]交易所对象,当前迭代的 合约组合中的第一个合约类型,第二个类型,交易控制表 这几个变量作为参数 传入 Hedge 函数 ,Hedge函数返回一个对象 ,
//把该对象压入pairs数组
}
});
var ts = 0
var lastUpdate = 0
while (true) { // 策略 主循环
if (!exchange.IO("status")) { // 如果 连接服务器状态 为false 则 程序进入睡眠1秒 跳过一下代码,重复主循环。
Sleep(1000)
continue
}
var now = new Date().getTime() // 获取当前时间戳
if (now - ts > (CalcPeriod * 60000)) { // 如果 当前时间 距离上次 账户权益统计 的时间 大于 账户权益统计周期(分) 参数设定的值,执行 if 内代码。
var account = exchange.GetAccount() // 获取账户信息
if (account) { // 如果正常获取 account 数据
var obj = JSON.parse(exchange.GetRawJSON()) // 调用 GetRawJSON API 获取account 的详细信息JSON 格式 用parse 函数解析为 JS 对象。返回一个 对象给 obj
$.PlotLine('账户权益', obj['Balance'] + obj['PositionProfit']); // 调用 画线模板的导出函数 $.PlotLine 画账户权益曲线, 数值为: obj的Balance属性 加 PositionProfit 属性
ts = now // 把本次更新 账户权益 的时间戳 记录在ts 变量 用于比较。
}
}
// IO("wait") 会一直等待收到任何一个品种的行情推送信息, 返回收到行情的真实时间
var n = exchange.IO("wait") // IO("wait") 函数 返回的也是一个纳秒级的 时间戳。
// 计算行情信息传到策略层花费的时间
var idle = UnixNano() - n // 最新托管者 增加的内置函数,获取纳秒 时间
if (now - lastUpdate > 5000) { // 当前的时间戳如果 比上次记录的更新时间(lastUpdate)值大 超过5000(5秒) 则执行 if 分支代码
tbl.rows = [] // tbl 对象 的行设置 为空数组,即清空。
_.each(pairs, function(t) { // 储存对冲对象的数组 内的元素迭代 传递到匿名函数 作为参数t。
for (var i = 0; i < t.dic.length; i++) { // 遍历当前对冲对象 的 交易量 控制数组 dic
tbl.rows.push([t.name, t.dic[i].open, t.dic[i].cover, t.dic[i].hold, t.diffA, t.diffB, t.update]) // 把每一个 差价的 开平仓 值 持仓、当前的差价值 等信息 压入到表格 行数组中
}
});
LogStatus('`' + JSON.stringify(tbl) + '`\nUpdate: ' + _D() + ', Idle: ' + (idle/1000000) + ' ms') // 把更新过的表格显示在 状态栏。显示上更新时间。
lastUpdate = now // 把当前的时间戳(毫秒) 更新给lastUpdate 变量 用于下一次比较。
}
_.each(pairs, function(t) { // 迭代 对冲对象 作为 匿名函数的参数 t ,迭代执行。
t.poll() // 执行当前的 对冲对象的 成员函数 poll()
});
q.poll() // 调用模板生成的 交易队列控制对象 q 的成员函数 poll() 模拟并发执行交易任务
}
}
BotVS 原创文章