上一篇我们在回测系统里研究了「A股港股对冲套利策略」,本篇我们从实战角度出发。在富途的模拟盘中对策略进行测试,升级。
策略真正创建实盘开始测试时,参数就不能再写死在策略代码中了,参数就需要配置在策略界面参数上。
var symbolH = "02333.HK" // 港股代码
var symbolA = "601633.SH" // A股代码
var isGetBaseStocks = true // 是否需要建底仓
...
设计在策略界面参数上,这些变量在策略代码中以全局变量使用(其实就是全局变量,可以参看API文档上策略参数的说明)。
回测时是必须要创建底仓的,否则没法对冲。在实际模拟盘测试就不一定了,因为可能上一次运行时已经创建好底仓了。所以对于「创建底仓股数」这个参数可以让它基于「是否需要创建底仓」这个参数来显示或者隐藏。写法如下:
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_a2h
、level_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)文章中的回测结果一样。回测是策略在历史数据中的快速运行,但是使用富途模拟盘测试的时候等待差价触发开仓对冲、平仓对冲一个周期可能要等很久。所以给策略加上交互按钮让策略可以手动对冲。
策略代码中就需要对交互控件响应处理:
// 处理交互命令
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)
}
}
}
那么这就在富途模拟盘上跑起来!
卖出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)
}
图中的持仓盈亏就不是我们的考察对象了,我们只考察对冲带来的盈亏。算上底仓建仓净值是亏的。
把策略持仓、价格等数据实时显示在状态栏,通过以下代码把数据写入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) + "`")
我们用回测再测试下,这样策略运行时就和上一篇文章中状态栏的显示不同了。
升级后:
升级前:
策略用于学习、教学。 股市有风险,入市需谨慎。