输入/搜索内容
1
关注
279
关注者
A股港股对冲套利策略(1)
创建于 2021-09-01 14:42:07  更新于 2024-11-28 09:11:25
 2
 1440

img

A股港股对冲套利策略(1)

回测系统中研究

在优宽量化交易平台(优宽)的回测系统中研究策略设计是非常方便的,因为实盘、模拟盘是有开盘时间的,时间有限。所以最初的设计放在回测系统中是最合适不过了。策略语言使用JavaScript,因为在优宽上JavaScript语言是最方便上手的。

我们选择股票:长城汽车作为研究对象。 港股代码:601633.SH,A股代码:02333.HK

首先我们可以先测试订阅股票代码,观察股票的基本信息。

javascript
function main() { var infoA = exchange.SetContractType("601633.SH") var infoH = exchange.SetContractType("02333.HK") Log(infoA) Log(infoH) }

打印出来的信息,首先看港股:

javascript
{ "ExchangeID": "HK", "ExchangeInstID": "02333.HK", "InstrumentID": "02333.HK", "InstrumentName": "长城汽车", "LongMarginRatio": 1, "MaxLimitOrderVolume": 10000, "MinBuyVolume": 1, "OpenDate": "", "PriceTick": 0.05, "ShortMarginRatio": 1, "VolumeMultiple": 500 }

再来看A股:

javascript
{ "ExchangeID": "SSE", "ExchangeInstID": "601633.SH", "InstrumentID": "601633.SH", "InstrumentName": "长城汽车", "LongMarginRatio": 1, "MaxLimitOrderVolume": 10000, "MinBuyVolume": 1, "OpenDate": "20110928", "PriceTick": 0.01, "ProductClass": "stock", "ShortMarginRatio": 1, "VolumeMultiple": 100 }

回测系统也输出了长城汽车A股与港股的图表。

img

通过打印出的数据,我们需要关注两个字段:PriceTickVolumeMultiplePriceTick是价格一跳,到策略设计下单时具体要参考这个数据。VolumeMultiple是一手的股数。从以上数据中可以看出,港股和A股在这些规则上是略有差异的。

接下来我们来研究这两只股票的价格以及差价情况,这里我们就需要思考:“对于A股、港股不同计价单位的股票要怎么处理。A股市场上股票价格是用CNY的,港股市场是港元。是不能直接相减计算差价的”。好在优宽量化交易平台有非常方便的汇率转换函数SetRate可以换算港元为CNY。

这里比较方便的设计是:
使用两个交易所对象,一个用来处理A股的操作(即:exchanges[0]),一个用来处理港股的操作(即:exchanges[1])。

在优宽量化交易平台上依然很方便的可以编写出测试策略代码:

pine
/*backtest start: 2020-09-01 09:00:00 end: 2021-08-31 15:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_XTP","currency":"STOCK","minFee":0},{"eid":"Futures_XTP","currency":"STOCK","minFee":0}] */ var symbolH = "02333.HK" // exchanges[1] var symbolA = "601633.SH" // exchanges[0] var H2A_Rate = 0.8302 // 港币对CNY汇率 function newDate() { var timezone = 8 //目标时区时间,东八区 var offset_GMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟 var nowDate = new Date().getTime() // 本地时间距 1970 年 1 月 1 日午夜(GMT 时间)之间的毫秒数 var targetDate = new Date(nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000) return targetDate } function IsTrading() { var now = newDate() // 使用 newDate() 代替 new Date() 因为服务器时区问题 var day = now.getDay() var hour = now.getHours() var minute = now.getMinutes() StatusMsg = "非交易时段" if (day === 0 || day === 6) { return false } if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) { // 9:30-11:30 StatusMsg = "交易时段" return true } else if (hour >= 13 && hour < 15) { // 13:00-15:00 StatusMsg = "交易时段" return true } return false } function main() { SetErrorFilter("market not ready") for (var i in exchanges) { if((exchanges[i].GetCurrency() != "STOCK" && exchanges[i].GetCurrency() != "STOCK_CNY" ) || (exchanges[i].GetName() != "Futures_Futu" && exchanges[i].GetName() != "Futures_XTP")) { throw "不支持" } exchanges[i].SetPrecision(2, 0) } // 设置港币汇率 if (exchanges.length != 2) { throw "需要添加2个交易所对象,可以使用同一个账号配置2个交易所对象。" } else { if (!symbolA.includes(".SH") && !symbolA.includes(".SZ")) { throw "参数symbolA需要设置A股代码。" } if (!symbolH.includes(".HK")) { throw "参数symbolH需要设置港股代码。" } exchanges[1].SetRate(H2A_Rate) Log("设置港元->CNY的汇率:", H2A_Rate) } while (true) { Sleep(1000 *2) if (!IsTrading()) { continue } var infoA = exchanges[0].SetContractType(symbolA) if (!infoA) { continue } var tickerA = exchanges[0].GetTicker() var infoH = exchanges[1].SetContractType(symbolH) if (!infoH) { continue } var tickerH = exchanges[1].GetTicker() if (!tickerA || !tickerH) { continue } var a2h = tickerA.Buy - tickerH.Sell var h2a = tickerA.Sell - tickerH.Buy var ts = new Date().getTime() $.PlotLine("a2h", a2h, ts) $.PlotLine("h2a", h2a, ts) } }

因为股票数据量实在太大,优宽平台回测系统回测只能使用日线级别,并且是模拟级别回测。差价变动只能非常粗略的宏观表示出来,所以差价变动仅供参考。

img

融券卖空

img

img

因为股票是类似现货交易,如果要做对冲套利需要做空股指。不过今天我们不选择股指之类的金融工具,而是选择类似融券的思路。对于策略测试来说首先就需要先有“融券”了(因为回测系统肯定没有融券这个机制,所以只能预先买入一些作为“融券”),策略开始运行时首先买入一定数量的股票作为“融券”,买入后再记录资金数量作为资产初始数值。

继续在回测中改造策略,让策略能跑起来。

pine
/*backtest start: 2020-09-01 09:00:00 end: 2021-08-31 15:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_XTP","currency":"STOCK","minFee":0},{"eid":"Futures_XTP","currency":"STOCK","minFee":0}] */ var symbolH = "02333.HK" // exchanges[1] var symbolA = "601633.SH" // exchanges[0] var isGetBaseStocks = true // 是否需要建底仓 var hedgeAmount = 1000 // 每次对冲 var baseStocks = 10000 // 底仓股数,不是手数 var slidePoint = 5 // 滑价点数 var H2A_Rate = 0.8302 // 港币对CNY汇率 function newDate() { var timezone = 8 //目标时区时间,东八区 var offset_GMT = new Date().getTimezoneOffset() // 本地时间和格林威治的时间差,单位为分钟 var nowDate = new Date().getTime() // 本地时间距 1970 年 1 月 1 日午夜(GMT 时间)之间的毫秒数 var targetDate = new Date(nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000) return targetDate } function IsTrading() { var now = newDate() // 使用 newDate() 代替 new Date() 因为服务器时区问题 var day = now.getDay() var hour = now.getHours() var minute = now.getMinutes() StatusMsg = "非交易时段" if (day === 0 || day === 6) { return false } if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) { // 9:30-11:30 StatusMsg = "交易时段" return true } else if (hour >= 13 && hour < 15) { // 13:00-15:00 StatusMsg = "交易时段" return true } return false } function GetPosition(e, contractTypeName) { var allAmount = 0 var allProfit = 0 var allFrozen = 0 var posMargin = 0 var price = 0 var direction = null positions = _C(e.GetPosition) for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractTypeName) { continue } if (positions[i].Type == PD_LONG) { posMargin = positions[i].MarginLevel allAmount += positions[i].Amount allProfit += positions[i].Profit allFrozen += positions[i].FrozenAmount price = positions[i].Price direction = positions[i].Type } } if (allAmount === 0) { return null } return { MarginLevel: posMargin, FrozenAmount: allFrozen, Price: price, Amount: allAmount, Profit: allProfit, Type: direction, ContractType: contractTypeName, CanCoverAmount: allAmount - allFrozen } } function getBaseStocks(priceA, priceH) { var infoA = _C(exchanges[0].SetContractType, symbolA) exchanges[0].SetDirection("buy") exchanges[0].Buy(priceA + infoA.PriceTick * slidePoint, (baseStocks - baseStocks % infoA.VolumeMultiple), "一手股数:" + infoA.VolumeMultiple) Log(symbolA, exchanges[0].GetPosition(symbolA)) var infoH = _C(exchanges[1].SetContractType, symbolH) exchanges[1].SetDirection("buy") exchanges[1].Buy(priceH + infoH.PriceTick * slidePoint, (baseStocks - baseStocks % infoH.VolumeMultiple), "一手股数:" + infoH.VolumeMultiple) Log(symbolH, exchanges[1].GetPosition(symbolH)) Log("底仓建仓完毕", "#FF0000") var acc0 = _C(exchanges[0].GetAccount) var acc1 = _C(exchanges[1].GetAccount) var initAcc = {"initAcc_A" : acc0, "initAcc_H" : acc1} _G("initAcc", initAcc) } function hedge(buySymbol, sellSymbol, buyPrice, sellPrice, amount) { var buyEx = buySymbol == symbolA ? exchanges[0] : exchanges[1] var sellEx = sellSymbol == symbolH ? exchanges[1] : exchanges[0] // 卖出的检查持仓 var infoSell = sellEx.SetContractType(sellSymbol) var sellExPos = GetPosition(sellEx, sellSymbol) if (!sellExPos) { return } // 检查资产数值 var infoBuy = buyEx.SetContractType(buySymbol) var buyExAcc = buyEx.GetAccount() if (!buyExAcc) { return } // 检查资金、仓位 amount = Math.min(buyExAcc.Balance / buyPrice, sellExPos.CanCoverAmount, amount) var minAmount = Math.max(infoBuy.VolumeMultiple, infoSell.VolumeMultiple) if (amount < minAmount) { return } amount = amount - amount % minAmount buyEx.SetDirection("buy") var buyId = buyEx.Buy(buyPrice + infoBuy.PriceTick * slidePoint, amount, buySymbol, "一手股数:" + infoBuy.VolumeMultiple) sellEx.SetDirection("closebuy") var sellId = sellEx.Sell(sellPrice - infoSell.PriceTick * slidePoint, amount, sellSymbol, "一手股数:" + infoSell.VolumeMultiple) return {"buyId" : buyId, "sellId" : sellId} } function main() { SetErrorFilter("market not ready") for (var i in exchanges) { if((exchanges[i].GetCurrency() != "STOCK" && exchanges[i].GetCurrency() != "STOCK_CNY" ) || (exchanges[i].GetName() != "Futures_Futu" && exchanges[i].GetName() != "Futures_XTP")) { throw "不支持" } exchanges[i].SetPrecision(2, 0) } var initAcc = null var level_a2h = 0 var level_h2a = 0 // 设置港币汇率 if (exchanges.length != 2) { throw "需要添加2个交易所对象,可以使用同一个账号配置2个交易所对象。" } else { if (!symbolA.includes(".SH") && !symbolA.includes(".SZ")) { throw "参数symbolA需要设置A股代码。" } if (!symbolH.includes(".HK")) { throw "参数symbolH需要设置港股代码。" } exchanges[1].SetRate(H2A_Rate) Log("设置港元->CNY的汇率:", H2A_Rate) } while (true) { Sleep(1000 *2) if (!IsTrading()) { continue } var infoA = exchanges[0].SetContractType(symbolA) if (!infoA) { continue } var tickerA = exchanges[0].GetTicker() var infoH = exchanges[1].SetContractType(symbolH) if (!infoH) { continue } var tickerH = exchanges[1].GetTicker() if (!tickerA || !tickerH) { continue } // 需要判断涨跌停 if (isGetBaseStocks) { getBaseStocks(tickerA.Sell, tickerH.Sell) isGetBaseStocks = false } if (!initAcc) { initAcc = _G("initAcc") Log("初始账户数据", initAcc) } var a2h = tickerA.Buy - tickerH.Sell var h2a = tickerA.Sell - tickerH.Buy var ts = new Date().getTime() $.PlotLine("a2h", a2h, ts) $.PlotLine("h2a", h2a, ts) if (a2h > 20 + level_a2h * 10) { var ret = hedge(symbolH, symbolA, tickerH.Sell, tickerA.Buy, hedgeAmount) if (ret) { level_a2h++ $.PlotFlag(ts, 'sell', 'S') } } else if (-h2a > 20 + level_h2a * 10) { var ret = hedge(symbolA, symbolH, tickerA.Sell, tickerH.Buy, hedgeAmount) if (ret) { level_h2a++ $.PlotFlag(ts, 'buy', 'B') } } if (a2h < 15 && level_a2h > 0) { var ret = hedge(symbolA, symbolH, tickerA.Sell, tickerH.Buy, hedgeAmount) if (ret) { level_a2h-- $.PlotFlag(ts, 'buy', 'B') var acc0 = _C(exchanges[0].GetAccount) var acc1 = _C(exchanges[1].GetAccount) LogProfit((acc0.Balance + acc1.Balance) - (initAcc.initAcc_A.Balance + initAcc.initAcc_H.Balance), {"initAcc_A" : acc0, "initAcc_H" : acc1}) } } else if (-h2a < 15 && level_h2a > 0) { var ret = hedge(symbolH, symbolA, tickerH.Sell, tickerA.Buy, hedgeAmount) if (ret) { level_h2a-- $.PlotFlag(ts, 'sell', 'S') var acc0 = _C(exchanges[0].GetAccount) var acc1 = _C(exchanges[1].GetAccount) LogProfit((acc0.Balance + acc1.Balance) - (initAcc.initAcc_A.Balance + initAcc.initAcc_H.Balance), {"initAcc_A" : acc0, "initAcc_H" : acc1}) } } LogStatus(_D(), "\n", "账户信息:", exchanges[0].GetAccount(), exchanges[1].GetAccount(), "\n level_a2h:", level_a2h, "level_h2a:", level_h2a, "\n", exchanges[0].GetPosition(), exchanges[1].GetPosition()) } }

这次增加了:创建底仓的函数getBaseStocks,对冲交易函数hedgemain函数中增加了交易触发条件相关的代码。
因为港股和A股的每手股数不同,对冲时要同时买入卖出相同股数的股票。

所以代码中会有:

javascript
Math.max(infoBuy.VolumeMultiple, infoSell.VolumeMultiple)

这样的计算,目的就是取两只股票最小交易单位(一手)中最大的股数,用于下单量的控制。回测系统和实盘时,下单量均为股数,并非手数。并且股数必须严格按照一手的股数下单(必须为一手股数的整倍数)。

这里为了在回测环境里研究,对于对冲的触发差价刻意设置为20元,每当开仓对冲一次level_a2hlevel_h2a标记变动一次记录(递增1),平仓对冲一次也变动记录(递减1)。并且在对冲开仓、平仓时在图表上标记(通过画线类库$.PlotFlag函数)

当然这个策略代码目前只是DEMO中的DEMO,不具备创建实盘、模拟盘测试的条件。目前仅仅能在回测系统中测试研究。

img

可以看到对冲了四次,依次:开仓对冲、平仓对冲、开仓对冲、开仓对冲。

img

img

回测系统自动生成的盈亏就不再考量了,因为有开始创建底仓的干扰。可以只看LogProfit函数输出的收益,这个是对冲收益。

股票的好处就是可以一直持有,通过长期对冲来降低初始股票的建仓成本。

下一篇我们继续扩展这个策略代码,目标是可以在富途的模拟账号下运行。

相关推荐
评论
全部评论 (0)
暂无数据
暂无数据
  • 1
iPhone 下载
社区
回测系统
© 2015 - ∞ YouQuant 豫ICP备19046564号