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

Author: 雨幕(youquant), Created: 2021-09-09 16:04:24, Updated: 2023-11-22 20:37:11

img

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

上一篇我们在回测系统里研究了「A股港股对冲套利策略」,本篇我们从实战角度出发。在富途的模拟盘中对策略进行测试,升级。

策略参数设计

策略真正创建实盘开始测试时,参数就不能再写死在策略代码中了,参数就需要配置在策略界面参数上。

var symbolH = "02333.HK"    // 港股代码
var symbolA = "601633.SH"   // A股代码
var isGetBaseStocks = true  // 是否需要建底仓
...

设计在策略界面参数上,这些变量在策略代码中以全局变量使用(其实就是全局变量,可以参看API文档上策略参数的说明)。

img

回测时是必须要创建底仓的,否则没法对冲。在实际模拟盘测试就不一定了,因为可能上一次运行时已经创建好底仓了。所以对于「创建底仓股数」这个参数可以让它基于「是否需要创建底仓」这个参数来显示或者隐藏。写法如下:

baseStocks@isGetBaseStocks

这样在勾选了「是否需要创建底仓」参数时「创建底仓股数」才会显示,该功能在API文档上也有描述可以查看。

在实际测试运行时,有时候还需要清空之前的所有记录,所以还需要设计个重置机制。

所以设计了isReset参数,策略代码中有对应的处理代码。

    // 重置所有
    if(isReset) {         // 如果勾选了界面上isReset参数
        _G(null)          // 清空所有持久化记录的数据
        LogReset(1)       // 清空所有日志
        LogProfitReset()  // 清空所有收益图表数据
        LogVacuum()       // 释放数据库中日志空间
        Log("重置所有数据", "#FF0000")  // 输出信息
    }

对冲的开仓差价、平仓差价之前是写死在代码中的。同样这些最好做成参数可以在策略界面参数上设置。

所以策略参数上又多了:

minHedgeDiffPrice	最小对冲差价	
spacing	            每次对冲差价间距	
hedgeProfit	        对冲利润差价

对应代码中修改为:

        if (a2h > minHedgeDiffPrice + level_a2h * spacing) {            
            var ret = hedge(symbolH, symbolA, tickerH.Sell, tickerA.Buy, hedgeAmount)
            if (ret) {
                level_a2h++
                $.PlotFlag(ts, 'sell', 'S')
                Log("ret:", ret, "对冲差价:", tickerA.Buy - tickerH.Sell)
            }
        } else if (-h2a > minHedgeDiffPrice + level_h2a * spacing) {            
            var ret = hedge(symbolA, symbolH, tickerA.Sell, tickerH.Buy, hedgeAmount)
            if (ret) {
                level_h2a++        
                $.PlotFlag(ts, 'buy', 'B')
                Log("ret:", ret, "对冲差价:", tickerA.Sell - tickerH.Buy)
            }
        }
        
        if (a2h < minHedgeDiffPrice + (level_a2h - 1) * spacing - hedgeProfit && level_a2h > 0) {            
            var ret = hedge(symbolA, symbolH, tickerA.Sell, tickerH.Buy, hedgeAmount)
            if (ret) {
                level_a2h--
                $.PlotFlag(ts, 'buy', 'B')
                Log("ret:", ret, "对冲差价:", tickerA.Sell - tickerH.Buy)
                calcProfit(initAcc, tickerA.Last, tickerH.Last)
            }
        } else if (-h2a < minHedgeDiffPrice + (level_h2a - 1) * spacing - hedgeProfit && level_h2a > 0) {
            var ret = hedge(symbolH, symbolA, tickerH.Sell, tickerA.Buy, hedgeAmount)
            if (ret) {
                level_h2a--
                $.PlotFlag(ts, 'sell', 'S')
                Log("ret:", ret, "对冲差价:", tickerA.Buy - tickerH.Sell)
                calcProfit(initAcc, tickerA.Last, tickerH.Last)
            }
        }

策略中数据持久化记录

因为策略中对冲了需要计算盈亏、如果策略重启了需要恢复数据记录等。在策略设计中就必须有数据持久化、数据恢复机制。

例如:

  • 计算盈亏 计算盈亏需要用当前的资产数值(钱数) 减去 最初记录的资产数值(钱数)。当前的资产数值可以随时获取,但是最初的资产数据呢?就算开始获取了,如果策略重启了呢?所以需要对于初始资产持久化记录,并且在重启时还能恢复。

          if (!initAcc) {
              initAcc = _G("initAcc")
              if (!initAcc) {
                  initAcc = updateAcc()
                  _G("initAcc", initAcc)
              }
              Log("初始账户数据", initAcc)
              Log("A股资产:", initAcc.initAcc_A.Balance + initAcc.initAcc_A.FrozenBalance, "港股资产:", initAcc.initAcc_H.Balance + initAcc.initAcc_H.FrozenBalance)
          }
    

    代码中这段主要就是用于记录最初账户和持仓数据、恢复最初记录的账户和持仓数据。

  • 恢复对冲的进度 在代码中初始化的位置:

      var level = _G("level")
      if (!level) {
          level = {"level_a2h" : 0, "level_h2a" : 0}
          _G("level", level)
      } else {
          Log("载入level:", level)
      }
      level_a2h = level.level_a2h
      level_h2a = level.level_h2a  
    

    有这样的一段代码,用来恢复对冲进度。和账户最初持仓、资产数据不同的是对冲进度相关的数据level_a2hlevel_h2a需要每次策略停止时更新,所以需要给策略设计上扫尾函数。

    扫尾函数:

    // 策略正常退出时扫尾
    function onexit() {
        var level = {"level_a2h" : level_a2h, "level_h2a" : level_h2a}
        _G("level", level)
        Log("记录:", level)
    }
    // 策略异常退出时扫尾
    function onerror() {
        var level = {"level_a2h" : level_a2h, "level_h2a" : level_h2a}
        _G("level", level)
        Log("记录:", level)
    }
    

交互命令

回测了一下和A股港股对冲套利策略(1)文章中的回测结果一样。回测是策略在历史数据中的快速运行,但是使用富途模拟盘测试的时候等待差价触发开仓对冲、平仓对冲一个周期可能要等很久。所以给策略加上交互按钮让策略可以手动对冲。

img

策略代码中就需要对交互控件响应处理:

        // 处理交互命令
        if (cmd) {
            Log("接收到命令:", cmd)
            var arr = cmd.split(":")
            if (arr[0] == "buyH_sellA") {                
                var amount = parseFloat(arr[1])
                var ret = hedge(symbolH, symbolA, tickerH.Sell, tickerA.Buy, amount)
                if (ret) {
                    $.PlotFlag(ts, 'sell', 'S')
                    Log("ret:", ret, "对冲差价:", tickerA.Buy - tickerH.Sell)
                    calcProfit(initAcc, tickerA.Last, tickerH.Last)
                }
            } else if (arr[0] == "buyA_sellH") {
                var amount = parseFloat(arr[1])
                var ret = hedge(symbolA, symbolH, tickerA.Sell, tickerH.Buy, amount)
                if (ret) {
                    $.PlotFlag(ts, 'buy', 'B')
                    Log("ret:", ret, "对冲差价:", tickerA.Sell - tickerH.Buy)
                    calcProfit(initAcc, tickerA.Last, tickerH.Last)
                }
            }
        } 

那么这就在富途模拟盘上跑起来!

img

卖出A股,买入港股的开仓对冲差价:34.17,平仓对冲差价:33.51。34.17-33.51理论上是一股赚取了0.66的差价,那么1000股就是0.66*1000=660元,当然还要减去手续费之类的。

使用盈亏计算函数计算盈亏

function calcProfit(initAcc, priceA, priceH) {
    Sleep(5000)   // 需要等待,否则拿到的是旧数据,会引起计算错误
    var acc0 = _C(exchanges[0].GetAccount)
    var acc1 = _C(exchanges[1].GetAccount)
    var pos0 = GetPosition(exchanges[0], symbolA)
    var pos1 = GetPosition(exchanges[1], symbolH)
    var holdA = pos0 ? pos0.Amount : 0
    var holdH = pos1 ? pos1.Amount : 0
    var holdA_DiffBalance = (holdA - initAcc.holdA) * priceA
    var holdH_DiffBalance = (holdH - initAcc.holdH) * priceH
    LogProfit((acc0.Balance + acc1.Balance) - (initAcc.initAcc_A.Balance + initAcc.initAcc_H.Balance) + (holdA_DiffBalance + holdH_DiffBalance), acc0.Balance + acc0.FrozenBalance, acc1.Balance + acc1.FrozenBalance)
}

img

图中的持仓盈亏就不是我们的考察对象了,我们只考察对冲带来的盈亏。算上底仓建仓净值是亏的。

持仓、价格等信息实时显示

把策略持仓、价格等数据实时显示在状态栏,通过以下代码把数据写入tbl结构中。

        // tbl
        var acc0 = exchanges[0].GetAccount()
        var balance = frozenBalance = "--"
        if (acc0) {
            balance = acc0.Balance 
            frozenBalance = acc0.FrozenBalance
        }
        var pos0 = GetPosition(exchanges[0], symbolA)
        var holdPrice = holdAmount = holdProfit = holdCanCoverAmount = "--"
        if (pos0) {
            holdPrice = pos0.Price
            holdAmount = pos0.Amount
            holdProfit = pos0.Profit
            holdCanCoverAmount = pos0.CanCoverAmount
        }
        tbl.rows.push([symbolA, tickerA.Last, balance, frozenBalance, holdPrice, holdAmount, holdProfit, holdCanCoverAmount])

        var acc1 = exchanges[1].GetAccount()
        var balance = frozenBalance = "--"
        if (acc1) {
            balance = acc1.Balance 
            frozenBalance = acc1.FrozenBalance
        }
        var pos1 = GetPosition(exchanges[1], symbolH)
        var holdPrice = holdAmount = holdProfit = holdCanCoverAmount = "--"
        if (pos1) {
            holdPrice = pos1.Price
            holdAmount = pos1.Amount
            holdProfit = pos1.Profit
            holdCanCoverAmount = pos1.CanCoverAmount
        }
        tbl.rows.push([symbolH, tickerH.Last, balance, frozenBalance, holdPrice, holdAmount, holdProfit, holdCanCoverAmount])
        lastTbl = tbl
        if (acc0 && acc1) {        
            floatProfit += (acc0.Balance + acc1.Balance) - (initAcc.initAcc_A.Balance + initAcc.initAcc_H.Balance) + 
                (((pos0 ? pos0.Amount : 0) - initAcc.holdA) * tickerA.Last + ((pos1 ? pos1.Amount : 0) - initAcc.holdH) * tickerH.Last)
        }        

使用LogStatus函数把tbl结构输出在状态栏上显示成表格。

LogStatus(_D(), statusMsg, "对冲的浮动盈亏:", floatProfit, "\n", "`" + JSON.stringify(tbl) + "`")

我们用回测再测试下,这样策略运行时就和上一篇文章中状态栏的显示不同了。

升级后:

img

升级前:

img

策略用于学习、教学。 股市有风险,入市需谨慎。

策略源码:https://www.youquant.com/strategy/312721


更多内容