上一篇文章,我们一起设计了一个多品种的合约差价监控策略,本篇文章我们继续完善这个思路。来看一下这个思路是否可行,并且用OKEX V5的模拟盘跑一跑,验证一下策略设计。这些过程也是做数字货币程序化交易、量化交易过程中需要经历的,希望萌新们能积累下宝贵的经验。
先剧透一下,策略跑起来了,有些小激动!
策略整体设计按照最简思路去实现,虽然对于细节处理没有过于苛求,但是还是可以从代码中学习到一些小技巧。策略代码整体不到400行,阅读理解起来不会很枯燥。当然这个只是一个测试DEMO,需要跑一段时间测试看看。所以我要说的是:目前策略仅仅是成功的开仓,还有平仓等等各种情况要实际测试检验,程序设计中BUG不可避免,所以测试,DEBUG(排错)很重要!
回到策略设计来说,在上篇文章的代码基础上,给策略加上了:
以上就是增加的功能,为了简化设计,策略只设计了做正对冲(空远期合约,多近期合约)。目前永续合约(近期)为负费率,正好做多永续合约,看能不能在增加点费率收益。
让策略跑一会儿~
测试了大概3天时间,差价波动其实还是可以的。
可以看到有部分资金费率的收益。
下面分享一下策略源码:
var arrNearContractType = strNearContractType.split(\",\")
var arrFarContractType = strFarContractType.split(\",\")
var nets = null
var initTotalEquity = null
var OPEN_PLUS = 1
var COVER_PLUS = 2
function createNet(begin, diff, initAvgPrice, diffUsagePercentage) {
if (diffUsagePercentage) {
diff = diff * initAvgPrice
}
var oneSideNums = 3
var up = []
var down = []
for (var i = 0 ; i < oneSideNums ; i++) {
var upObj = {
sell : false,
price : begin + diff / 2 + i * diff
}
up.push(upObj)
var j = (oneSideNums - 1) - i
var downObj = {
sell : false,
price : begin - diff / 2 - j * diff
}
if (downObj.price <= 0) { // 价格不能小于等于0
continue
}
down.push(downObj)
}
return down.concat(up)
}
function createCfg(symbol) {
var cfg = {
extension: {
layout: \'single\',
height: 300,
col: 6
},
title: {
text: symbol
},
xAxis: {
type: \'datetime\'
},
series: [{
name: \'plus\',
data: []
}]
}
return cfg
}
function formatSymbol(originalSymbol) {
var arr = originalSymbol.split(\"-\")
return [arr[0] + \"_\" + arr[1], arr[0], arr[1]]
}
function main() {
if (isSimulate) {
exchange.IO(\"simulate\", true) // 切换为模拟环境
Log(\"仅支持OKEX V5 API,切换为OKEX V5 模拟盘:\")
} else {
exchange.IO(\"simulate\", false) // 切换为实盘
Log(\"仅支持OKEX V5 API,切换为OKEX V5 实盘:\")
}
if (exchange.GetName() != \"Futures_OKCoin\") {
throw \"支持OKEX期货\"
}
// 初始化
if (isReset) {
_G(null)
LogReset(1)
LogProfitReset()
LogVacuum()
Log(\"重置所有数据\", \"#FF0000\")
}
// 初始化标记
var isFirst = true
// 收益打印周期
var preProfitPrintTS = 0
// 总权益
var totalEquity = 0
var posTbls = [] // 持仓表格数组
// 声明arrCfg
var arrCfg = []
_.each(arrNearContractType, function(ct) {
arrCfg.push(createCfg(formatSymbol(ct)[0]))
})
var objCharts = Chart(arrCfg)
objCharts.reset()
// 创建对象
var exName = exchange.GetName() + \"_V5\"
var nearConfigureFunc = $.getConfigureFunc()[exName]
var farConfigureFunc = $.getConfigureFunc()[exName]
var nearEx = $.createBaseEx(exchange, nearConfigureFunc)
var farEx = $.createBaseEx(exchange, farConfigureFunc)
// 预先写入需要订阅的合约
_.each(arrNearContractType, function(ct) {
nearEx.pushSubscribeSymbol(ct)
})
_.each(arrFarContractType, function(ct) {
farEx.pushSubscribeSymbol(ct)
})
while (true) {
var ts = new Date().getTime()
// 获取行情数据
nearEx.goGetTickers()
farEx.goGetTickers()
var nearTickers = nearEx.getTickers()
var farTickers = farEx.getTickers()
if (!farTickers || !nearTickers) {
Sleep(2000)
continue
}
var tbl = {
type : \"table\",
title : \"远期-近期差价\",
cols : [\"交易对\", \"远期\", \"近期\", \"正对冲\", \"反对冲\"],
rows : []
}
var subscribeFarTickers = []
var subscribeNearTickers = []
_.each(farTickers, function(farTicker) {
_.each(arrFarContractType, function(symbol) {
if (farTicker.originalSymbol == symbol) {
subscribeFarTickers.push(farTicker)
}
})
})
_.each(nearTickers, function(nearTicker) {
_.each(arrNearContractType, function(symbol) {
if (nearTicker.originalSymbol == symbol) {
subscribeNearTickers.push(nearTicker)
}
})
})
var pairs = []
_.each(subscribeFarTickers, function(farTicker) {
_.each(subscribeNearTickers, function(nearTicker) {
if (farTicker.symbol == nearTicker.symbol) {
var pair = {symbol: nearTicker.symbol, nearTicker: nearTicker, farTicker: farTicker, plusDiff: farTicker.bid1 - nearTicker.ask1, minusDiff: farTicker.ask1 - nearTicker.bid1}
pairs.push(pair)
tbl.rows.push([pair.symbol, farTicker.originalSymbol, nearTicker.originalSymbol, pair.plusDiff, pair.minusDiff])
for (var i = 0 ; i < arrCfg.length ; i++) {
if (arrCfg[i].title.text == pair.symbol) {
objCharts.add([i, [ts, pair.plusDiff]])
}
}
}
})
})
// 初始化
if (isFirst) {
isFirst = false
var recoveryNets = _G(\"nets\")
var recoveryInitTotalEquity = _G(\"initTotalEquity\")
if (!recoveryNets) {
// 检查持仓
_.each(subscribeFarTickers, function(farTicker) {
var pos = farEx.getFuPos(farTicker.originalSymbol, ts)
if (pos.length != 0) {
Log(farTicker.originalSymbol, pos)
throw \"初始化时有持仓\"
}
})
_.each(subscribeNearTickers, function(nearTicker) {
var pos = nearEx.getFuPos(nearTicker.originalSymbol, ts)
if (pos.length != 0) {
Log(nearTicker.originalSymbol, pos)
throw \"初始化时有持仓\"
}
})
// 构造nets
nets = []
_.each(pairs, function (pair) {
farEx.goGetAcc(pair.farTicker.originalSymbol, ts)
nearEx.goGetAcc(pair.nearTicker.originalSymbol, ts)
var obj = {
\"symbol\" : pair.symbol,
\"farSymbol\" : pair.farTicker.originalSymbol,
\"nearSymbol\" : pair.nearTicker.originalSymbol,
\"initPrice\" : (pair.nearTicker.ask1 + pair.farTicker.bid1) / 2,
\"prePlus\" : pair.farTicker.bid1 - pair.nearTicker.ask1,
\"net\" : createNet((pair.farTicker.bid1 - pair.nearTicker.ask1), diff, (pair.nearTicker.ask1 + pair.farTicker.bid1) / 2, true),
\"initFarAcc\" : farEx.getAcc(pair.farTicker.originalSymbol, ts),
\"initNearAcc\" : nearEx.getAcc(pair.nearTicker.originalSymbol, ts),
\"farTicker\" : pair.farTicker,
\"nearTicker\" : pair.nearTicker,
\"farPos\" : null,
\"nearPos\" : null,
}
nets.push(obj)
})
var currTotalEquity = getTotalEquity()
if (currTotalEquity) {
initTotalEquity = currTotalEquity
} else {
throw \"初始化获取总权益失败!\"
}
} else {
// 恢复
nets = recoveryNets
initTotalEquity = recoveryInitTotalEquity
}
}
// 检索网格,检查是否触发交易
_.each(nets, function(obj) {
var currPlus = null
_.each(pairs, function(pair) {
if (pair.symbol == obj.symbol) {
currPlus = pair.plusDiff
obj.farTicker = pair.farTicker
obj.nearTicker = pair.nearTicker
}
})
if (!currPlus) {
Log(\"没有查询到\", obj.symbol, \"的差价\")
return
}
// 检查网格,动态添加
while (currPlus >= obj.net[obj.net.length - 1].price) {
obj.net.push({
sell : false,
price : obj.net[obj.net.length - 1].price + diff * obj.initPrice,
})
}
while (currPlus <= obj.net[0].price) {
var price = obj.net[0].price - diff * obj.initPrice
if (price <= 0) {
break
}
obj.net.unshift({
sell : false,
price : price,
})
}
// 检索网格
for (var i = 0 ; i < obj.net.length - 1 ; i++) {
var p = obj.net[i]
var upP = obj.net[i + 1]
if (obj.prePlus <= p.price && currPlus > p.price && !p.sell) {
if (hedge(nearEx, farEx, obj.nearSymbol, obj.farSymbol, obj.nearTicker, obj.farTicker, hedgeAmount, OPEN_PLUS)) { // 正对冲开仓
p.sell = true
}
} else if (obj.prePlus >= p.price && currPlus < p.price && upP.sell) {
if (hedge(nearEx, farEx, obj.nearSymbol, obj.farSymbol, obj.nearTicker, obj.farTicker, hedgeAmount, COVER_PLUS)) { // 正对冲平仓
upP.sell = false
}
}
}
obj.prePlus = currPlus // 记录本次差价,作为缓存,下次用于判断上穿下穿
// 增加其它表格输出
})
if (ts - preProfitPrintTS > 1000 * 60 * 5) { // 5分钟打印一次
var currTotalEquity = getTotalEquity()
if (currTotalEquity) {
totalEquity = currTotalEquity
LogProfit(totalEquity - initTotalEquity, \"&\") // 打印动态权益收益
}
// 检查持仓
posTbls = [] // 重置,更新
_.each(nets, function(obj) {
var currFarPos = farEx.getFuPos(obj.farSymbol)
var currNearPos = nearEx.getFuPos(obj.nearSymbol)
if (currFarPos && currNearPos) {
obj.farPos = currFarPos
obj.nearPos = currNearPos
}
var posTbl = {
\"type\" : \"table\",
\"title\" : obj.symbol,
\"cols\" : [\"合约代码\", \"数量\", \"价格\"],
\"rows\" : []
}
_.each(obj.farPos, function(pos) {
posTbl.rows.push([pos.symbol, pos.amount, pos.price])
})
_.each(obj.nearPos, function(pos) {
posTbl.rows.push([pos.symbol, pos.amount, pos.price])
})
posTbls.push(posTbl)
})
preProfitPrintTS = ts
}
// 显示网格
var netTbls = []
_.each(nets, function(obj) {
var netTbl = {
\"type\" : \"table\",
\"title\" : obj.symbol,
\"cols\" : [\"网格\"],
\"rows\" : []
}
_.each(obj.net, function(p) {
var color = \"\"
if (p.sell) {
color = \"#00FF00\"
}
netTbl.rows.push([JSON.stringify(p) + color])
})
netTbl.rows.reverse()
netTbls.push(netTbl)
})
LogStatus(_D(), \"总权益:\", totalEquity, \"初始总权益:\", initTotalEquity, \" 浮动盈亏:\", totalEquity - initTotalEquity,
\"\\n`\" + JSON.stringify(tbl) + \"`\" + \"\\n`\" + JSON.stringify(netTbls) + \"`\" + \"\\n`\" + JSON.stringify(posTbls) + \"`\")
Sleep(interval)
}
}
function getTotalEquity() {
var totalEquity = null
var ret = exchange.IO(\"api\", \"GET\", \"/api/v5/account/balance\", \"ccy=USDT\")
if (ret) {
try {
totalEquity = parseFloat(ret.data[0].details[0].eq)
} catch(e) {
Log(\"获取账户总权益失败!\")
return null
}
}
return totalEquity
}
function hedge(nearEx, farEx, nearSymbol, farSymbol, nearTicker, farTicker, amount, tradeType) {
var farDirection = null
var nearDirection = null
if (tradeType == OPEN_PLUS) {
farDirection = farEx.OPEN_SHORT
nearDirection = nearEx.OPEN_LONG
} else {
farDirection = farEx.COVER_SHORT
nearDirection = nearEx.COVER_LONG
}
var nearSymbolInfo = nearEx.getSymbolInfo(nearSymbol)
var farSymbolInfo = farEx.getSymbolInfo(farSymbol)
nearAmount = nearEx.calcAmount(nearSymbol, nearDirection, nearTicker.ask1, amount * nearSymbolInfo.multiplier)
farAmount = farEx.calcAmount(farSymbol, farDirection, farTicker.bid1, amount * farSymbolInfo.multiplier)
if (!nearAmount || !farAmount) {
Log(nearSymbol, farSymbol, \"下单量计算错误:\", nearAmount, farAmount)
return
}
nearEx.goGetTrade(nearSymbol, nearDirection, nearTicker.ask1, nearAmount[0])
farEx.goGetTrade(farSymbol, farDirection, farTicker.bid1, farAmount[0])
var nearIdMsg = nearEx.getTrade()
var farIdMsg = farEx.getTrade()
return [nearIdMsg, farIdMsg]
}
function onexit() {
Log(\"执行扫尾函数\", \"#FF0000\")
_G(\"nets\", nets)
_G(\"initTotalEquity\", initTotalEquity)
Log(\"保存数据:\", _G(\"nets\"), _G(\"initTotalEquity\"))
}
策略公开地址:https://www.fmz.com/strategy/288559
策略用到了一个我自己写的模板类库,由于写的太菜就不公开了,以上策略源码可以修改一下不使用这个模板。
有兴趣的可以挂个OKEX V5模拟盘测试。
哦!对了,这策略不能回测~
买好币上币库:https://www.kucoin.com/r/1f7w3