In the last article, we designed a multi-symbol contract spread monitoring strategy together. In this article, we will continue to improve this idea. Let's see if the idea is feasible, and run it with OKX V5 simulated bot to verify the strategy design. These processes are also required to be experienced in the process of cryptocurrency programmed trading and quantitative trading. I hope that you can accumulate valuable experience from that.
Spoiler: the strategy has been running, which is a little bit exciting.
The overall design of the strategy is implemented by the simplest idea. Although there is no strict requirement for the detail processing, you can still learn some tricks from the code. The entire strategy is less than 400 lines, so it is not too boring to read it. Of course, this is only a DEMO for test, and we need to run it for a while to see the result. What I want to say is: the present strategy is only successful in opening positions, and there are various situations, such as closing positions, to be actually tested and detected. Bugs in the program design are unavoidable, so testing and debugging are very important!
Back to the strategy design, based on the code in last article, I have added:
- Data persistence design (by using the function _G to store data, and restore the data after restart);
- Adding the grid data structure to each monitored contract spread pair (to control the hedge to close positions);
- Implementing a simple hedging function to open or close positions by hedge;
- Adding a function of obtaining the total equity to calculate the floating profit and loss;
- Adding some displays of exporting status bar data.
Those above are the functions added. To be simple, the strategy only designed positive hedge (make short for long-term contract; make long for short-term contract). At present, the perpetual contract (short-term) has a negative funding rate; just make in the perpetual contract, to see if the return of the funding rate can be increased.
Let the strategy run for a while.
It has been tested for about 3 days, and the spread fluctuations are actually fine.
Part of the return from the funding rate can be seen in the following picture.
The strategy source code is shared as follows:
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) { // Price cannot be less than or equal to 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) // Switch to simulation environment Log("Only OKEX V5 API is supported, switch to OKEX V5 simulation bot:") } else { exchange.IO("simulate", false) // Switch to real bot Log("Only OKEX V5 API is supported, switch to OKEX V5 simulation bot:") } if (exchange.GetName() != "Futures_OKCoin") { throw "Support OKEX futures" } // Initialization if (isReset) { _G(null) LogReset(1) LogProfitReset() LogVacuum() Log("Reset all data", "#FF0000") } // Initialization marker var isFirst = true // Profit print period var preProfitPrintTS = 0 // Total equity var totalEquity = 0 var posTbls = [] // Position table array // Declare arrCfg var arrCfg = [] _.each(arrNearContractType, function(ct) { arrCfg.push(createCfg(formatSymbol(ct)[0])) }) var objCharts = Chart(arrCfg) objCharts.reset() // Create object var exName = exchange.GetName() + "_V5" var nearConfigureFunc = $.getConfigureFunc()[exName] var farConfigureFunc = $.getConfigureFunc()[exName] var nearEx = $.createBaseEx(exchange, nearConfigureFunc) var farEx = $.createBaseEx(exchange, farConfigureFunc) // Pre-write the contract that require subscriptions _.each(arrNearContractType, function(ct) { nearEx.pushSubscribeSymbol(ct) }) _.each(arrFarContractType, function(ct) { farEx.pushSubscribeSymbol(ct) }) while (true) { var ts = new Date().getTime() // Obtain market data nearEx.goGetTickers() farEx.goGetTickers() var nearTickers = nearEx.getTickers() var farTickers = farEx.getTickers() if (!farTickers || !nearTickers) { Sleep(2000) continue } var tbl = { type : "table", title : "Long term-near term spread", cols : ["Trading pair", "long term", "near term", "positive hedging", "negative hedging"], 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]]) } } } }) }) // Initialization if (isFirst) { isFirst = false var recoveryNets = _G("nets") var recoveryInitTotalEquity = _G("initTotalEquity") if (!recoveryNets) { // Check positions _.each(subscribeFarTickers, function(farTicker) { var pos = farEx.getFuPos(farTicker.originalSymbol, ts) if (pos.length != 0) { Log(farTicker.originalSymbol, pos) throw "Initialized with a position" } }) _.each(subscribeNearTickers, function(nearTicker) { var pos = nearEx.getFuPos(nearTicker.originalSymbol, ts) if (pos.length != 0) { Log(nearTicker.originalSymbol, pos) throw "Initialized with a position" } }) // Construct 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 "Initialization to obtain total equity failed!" } } else { // Recovery nets = recoveryNets initTotalEquity = recoveryInitTotalEquity } } // Retrieve the grid and check if the trading is triggered _.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("Not found", obj.symbol, " 's spread") return } // Check grid, add dynamically 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, }) } // Search grid 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)) { // Positive hedging opening position 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)) { // Positive hedging closing position upP.sell = false } } } obj.prePlus = currPlus // Record the current spread as a cache, and use it to judge whether it's above the SMA or below the SMA next time // Add other chart outputs }) if (ts - preProfitPrintTS > 1000 * 60 * 5) { // Print every 5 minutes var currTotalEquity = getTotalEquity() if (currTotalEquity) { totalEquity = currTotalEquity LogProfit(totalEquity - initTotalEquity, "&") // Print dynamic equity profits } // Check positions posTbls = [] // Reset, update _.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" : ["contract code", "amount", "price"], "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 } // Show grid var netTbls = [] _.each(nets, function(obj) { var netTbl = { "type" : "table", "title" : obj.symbol, "cols" : ["grid"], "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(), "total equity:", totalEquity, "initial total equity:", initTotalEquity, "floating profit and loss:", 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("Failed to obtain the total equity of the account!") 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, "Order amount calculation error:", 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("Execute the tail function", "#FF0000") _G("nets", nets) _G("initTotalEquity", initTotalEquity) Log("save the data:", _G("nets"), _G("initTotalEquity")) }
Strategy public address: https://www.fmz.com/strategy/288559
The strategy uses a template class library written by myself, which is not public because it is not too good. The above strategy source code can be modified without using this template.
are interested, you can use an OKX V5 simulation bot to test.
Oh! By the way, this strategy cannot be backtested~