
이전 기사에서 우리는 다양한 종류의 계약 스프레드 모니터링 전략을 설계했습니다. 이 기사에서는 이 아이디어를 계속 개선할 것입니다. 이 아이디어가 실현 가능한지 살펴보고 OKEX V5 시뮬레이션 플랫폼에서 실행하여 전략 설계를 검증해 보겠습니다. 이러한 과정은 디지털 통화 프로그래밍 거래 및 양적 거래 과정에서도 거쳐야 할 과정입니다. 초보자들이 귀중한 경험을 쌓을 수 있기를 바랍니다.
먼저 스포일러를 하나 알려드리겠습니다. 이 전략은 효과가 있고, 저는 약간 기대가 됩니다!



전반적인 전략 설계는 가장 간단한 방식으로 구현됩니다. 세부 사항에 대한 과도한 요구 사항은 없지만 코드에서 몇 가지 팁을 배울 수 있습니다. 전체 전략 코드는 400줄도 안 되어서 읽고 이해하기 지루하지 않습니다. 물론, 이것은 단지 테스트용 데모일 뿐이며, 테스트하려면 잠시 실행해 보아야 합니다. 그래서 제가 말하고 싶은 것은 현재의 전략은 포지션을 성공적으로 여는 데에만 국한되며, 포지션을 닫는 것과 같은 다양한 상황은 실제로 테스트하고 검증해야 한다는 것입니다. 프로그램 설계에서는 버그가 불가피하므로 테스트와 디버깅이 매우 중요합니다!
전략 설계로 돌아가서, 이전 문서의 코드를 기반으로 전략에 다음 내용을 추가합니다.
위에 추가된 기능들이 있습니다. 설계를 단순화하기 위해, 이 전략은 긍정적 헤지(단기 선물 계약, 장기 단기 계약)에만 설계되었습니다. 현재, 영구 선물 계약의 금리는 단기적으로 마이너스이므로, 영구 선물 계약에 대한 매수 포지션을 취해 금리 수입이 늘어날 수 있는지 살펴보는 좋은 시점입니다.
잠시 전략을 실행해보자~
약 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 시뮬레이션 디스크를 설정하여 테스트해 보실 수 있습니다. 오! 그런데 이 전략은 백테스트가 불가능합니다.