Hedging strategy of cryptocurrency manual futures and spots

Hedging strategy of cryptocurrency manual futures and spots

·

13 min read

In view of the fact that the hedging frequency of the futures and spots hedging strategy is not high, it is actually possible to operate manually. However, if you do it manually, it is very inconvenient to switch pages of various exchanges, observe prices, and calculate the difference, and sometimes you may want to see more varieties, and it is not necessary to set up several monitors to display the market. Is it possible to achieve this goal of manual operation with a semi-automatic strategy? It is better to have multi-species, oh! Yes, it is best to open and close positions with one-click. Oh! Yes, there is also a position display...

When there is a need, do it now!

Design a hedging strategy of cryptocurrency manual futures and spots

The writing is rather long-winded, with less than 600 lines of code.

function createManager(fuEx, spEx, symbolPairs, cmdHedgeAmount, fuMarginLevel, fuMarginReservedRatio) {
    var self = {}
    self.fuEx = fuEx
    self.spEx = spEx
    self.symbolPairs = symbolPairs
    self.pairs = []                        
    self.fuExTickers = null                
    self.spExTickers = null                
    self.tickerUpdateTS = 0                
    self.fuMarginLevel = fuMarginLevel     
    self.fuMarginReservedRatio = fuMarginReservedRatio 
    self.cmdHedgeAmount = cmdHedgeAmount   
    self.preUpdateAccTS = 0                
    self.accAndPosUpdateCount = 0          
    self.profit = []                       
    self.allPairs = []                     

    self.PLUS = 0          
    self.MINUS = 1         
    self.COVER_PLUS = 2    
    self.COVER_MINUS = 3   
    self.arrTradeTypeDesc = ["positive arbitrage", "reverse arbitrage", "close positive arbitrage", "close reverse arbitrage"]

    self.updateTickers = function() {
        self.fuEx.goGetTickers()
        self.spEx.goGetTickers()
        var fuExTickers = self.fuEx.getTickers()
        var spExTickers = self.spEx.getTickers()

        if (!fuExTickers || !spExTickers) {
            return null
        }
        self.fuExTickers = fuExTickers
        self.spExTickers = spExTickers
        self.tickerUpdateTS = new Date().getTime()
        return true 
    }

    self.hedge = function(index, fuSymbol, spSymbol, tradeType, amount) {
        var fe = self.fuEx
        var se = self.spEx
        var pair = self.pairs[index]
        var timeStamp = new Date().getTime()

        var fuDirection = null 
        var spDirection = null     
        var fuPrice = null 
        var spPrice = null 

        if (tradeType == self.PLUS) {
            fuDirection = fe.OPEN_SHORT
            spDirection = se.OPEN_LONG
            fuPrice = pair.fuTicker.bid1
            spPrice = pair.spTicker.ask1
        } else if (tradeType == self.MINUS) {
            fuDirection = fe.OPEN_LONG
            spDirection = se.OPEN_SHORT
            fuPrice = pair.fuTicker.ask1
            spPrice = pair.spTicker.bid1
        } else if (tradeType == self.COVER_PLUS) {
            fuDirection = fe.COVER_SHORT
            spDirection = se.COVER_LONG
            fuPrice = pair.fuTicker.ask1
            spPrice = pair.spTicker.bid1            
        } else if (tradeType == self.COVER_MINUS) {
            fuDirection = fe.COVER_LONG
            spDirection = se.COVER_SHORT
            fuPrice = pair.fuTicker.bid1
            spPrice = pair.spTicker.ask1
        } else {
            throw "unknow tradeType!"
        }

        fe.goGetAcc(fuSymbol, timeStamp)              
        se.goGetAcc(spSymbol, timeStamp)
        var nowFuAcc = fe.getAcc(fuSymbol, timeStamp)
        var nowSpAcc = se.getAcc(spSymbol, timeStamp)
        if (!nowFuAcc || !nowSpAcc) {
            Log(fuSymbol, spSymbol, ", failed to get account data")
            return 
        }
        pair.nowFuAcc = nowFuAcc           
        pair.nowSpAcc = nowSpAcc

        var nowFuPos = fe.getFuPos(fuSymbol, timeStamp)
        var nowSpPos = se.getSpPos(spSymbol, spPrice, pair.initSpAcc, pair.nowSpAcc)
        if (!nowFuPos || !nowSpPos) {
            Log(fuSymbol, spSymbol, ", failed to get position data")
            return 
        }
        pair.nowFuPos = nowFuPos
        pair.nowSpPos = nowSpPos

        var fuAmount = amount 
        var spAmount = amount
        if (tradeType == self.PLUS || tradeType == self.MINUS) {
            if (nowFuAcc.Balance < (pair.initFuAcc.Balance + pair.initFuAcc.FrozenBalance) * self.fuMarginReservedRatio + (fuAmount * fuPrice / self.fuMarginLevel)) {
                Log(pair.fuSymbol, "insufficient deposit!", "this plan uses", (fuAmount * fuPrice / self.fuMarginLevel), "currently available:", nowFuAcc.Balance, 
                    "Plan to reserve:", (pair.initFuAcc.Balance + pair.initFuAcc.FrozenBalance) * self.fuMarginReservedRatio)
                return 
            }
            if ((tradeType == self.PLUS && nowSpAcc.Balance < spAmount * spPrice)) {  
                Log(pair.spSymbol, "insufficient funds!", "this purchase plans to use", spAmount * spPrice, "currently available:", nowSpAcc.Balance)
                return 
            } else if (tradeType == self.MINUS && nowSpAcc.Stocks < spAmount) {       
                Log(pair.spSymbol, "insufficient funds!", "this selling plans to use", spAmount, "currently available:", nowSpAcc.Stocks)
                return 
            }
        } else {
            var fuLongPos = self.getLongPos(nowFuPos)
            var fuShortPos = self.getShortPos(nowFuPos)
            var spLongPos = self.getLongPos(nowSpPos)
            var spShortPos = self.getShortPos(nowSpPos)
            if ((tradeType == self.COVER_PLUS && !fuShortPos) || (tradeType == self.COVER_MINUS && !fuLongPos)) {  
                Log(fuSymbol, spSymbol, ", there is no corresponding position in futures!")
                return 
            } else if (tradeType == self.COVER_PLUS && Math.abs(fuShortPos.amount) < fuAmount) {
                fuAmount = Math.abs(fuShortPos.amount)
            } else if (tradeType == self.COVER_MINUS && Math.abs(fuLongPos.amount) < fuAmount) {
                fuAmount = Math.abs(fuLongPos.amount)
            }
            if ((tradeType == self.COVER_PLUS && !spLongPos) || (tradeType == self.COVER_MINUS && !spShortPos)) {  
                Log(fuSymbol, spSymbol, ", there is no corresponding position in the spot!")
                return 
            } else if (tradeType == self.COVER_PLUS && Math.min(Math.abs(spLongPos.amount), nowSpAcc.Stocks) < spAmount) {               
                spAmount = Math.min(Math.abs(spLongPos.amount), nowSpAcc.Stocks)
            } else if (tradeType == self.COVER_MINUS && Math.min(Math.abs(spShortPos.amount), nowSpAcc.Balance / spPrice) < spAmount) {  
                spAmount = Math.min(Math.abs(spShortPos.amount), nowSpAcc.Balance / spPrice)
            }
        }

        fuAmount = fe.calcAmount(fuSymbol, fuDirection, fuPrice, fuAmount)  
        spAmount = se.calcAmount(spSymbol, spDirection, spPrice, spAmount)
        if (!fuAmount || !spAmount) {
            Log(fuSymbol, spSymbol, "order quantity calculation error:", fuAmount, spAmount)
            return 
        } else {
            fuAmount = fe.calcAmount(fuSymbol, fuDirection, fuPrice, fuAmount[1])
            spAmount = se.calcAmount(spSymbol, spDirection, spPrice, Math.min(fuAmount[1], spAmount[1]))
            if (!fuAmount || !spAmount) {
                Log(fuSymbol, spSymbol, "order quantity calculation error:", fuAmount, spAmount)
                return 
            }
        }
        Log("contract code:", fuSymbol + "/" + spSymbol, "direction:", self.arrTradeTypeDesc[tradeType], "difference:", fuPrice - spPrice, "quantity of futures:", fuAmount, "quantity of spots:", spAmount, "@")  

        fe.goGetTrade(fuSymbol, fuDirection, fuPrice, fuAmount[0])
        se.goGetTrade(spSymbol, spDirection, spPrice, spAmount[0])

        var feIdMsg = fe.getTrade()
        var seIdMsg = se.getTrade()
        return [feIdMsg, seIdMsg]
    }

    self.process = function() {
        var nowTS = new Date().getTime()
        if(!self.updateTickers()) {
            return 
        }

        _.each(self.pairs, function(pair, index) {
            var fuTicker = null 
            var spTicker = null
            _.each(self.fuExTickers, function(ticker) {
                if (ticker.originalSymbol == pair.fuSymbol) {
                    fuTicker = ticker
                }
            })
            _.each(self.spExTickers, function(ticker) {
                if (ticker.originalSymbol == pair.spSymbol) {
                    spTicker = ticker
                }
            })
            if (fuTicker && spTicker) {
                pair.canTrade = true 
            } else {
                pair.canTrade = false
            }
            fuTicker = fuTicker ? fuTicker : {}
            spTicker = spTicker ? spTicker : {}
            pair.fuTicker = fuTicker
            pair.spTicker = spTicker
            pair.plusDiff = fuTicker.bid1 - spTicker.ask1
            pair.minusDiff = fuTicker.ask1 - spTicker.bid1
            if (pair.plusDiff && pair.minusDiff) {
                pair.plusDiff = _N(pair.plusDiff, Math.max(self.fuEx.judgePrecision(fuTicker.bid1), self.spEx.judgePrecision(spTicker.ask1)))
                pair.minusDiff = _N(pair.minusDiff, Math.max(self.fuEx.judgePrecision(fuTicker.ask1), self.spEx.judgePrecision(spTicker.bid1)))
            }

            if (nowTS - self.preUpdateAccTS > 1000 * 60 * 5) {    
                self.fuEx.goGetAcc(pair.fuSymbol, nowTS)
                self.spEx.goGetAcc(pair.spSymbol, nowTS)
                var fuAcc = self.fuEx.getAcc(pair.fuSymbol, nowTS)   
                var spAcc = self.spEx.getAcc(pair.spSymbol, nowTS)
                if (fuAcc) {
                    pair.nowFuAcc = fuAcc
                }
                if (spAcc) {
                    pair.nowSpAcc = spAcc
                }
                var nowFuPos = self.fuEx.getFuPos(pair.fuSymbol, nowTS)
                var nowSpPos = self.spEx.getSpPos(pair.spSymbol, (pair.spTicker.ask1 + pair.spTicker.bid1) / 2, pair.initSpAcc, pair.nowSpAcc)

                if (nowFuPos && nowSpPos) {
                    pair.nowFuPos = nowFuPos
                    pair.nowSpPos = nowSpPos                    
                    self.keepBalance(pair)
                } else {
                    Log(pair.fuSymbol, pair.spSymbol, "portfolio position update failed, nowFuPos:", nowFuPos, " nowSpPos:", nowSpPos)
                }
                self.accAndPosUpdateCount++    
            }
        })

        if (nowTS - self.preUpdateAccTS > 1000 * 60 * 5) {       
            self.preUpdateAccTS = nowTS
            self.profit = self.calcProfit()
            LogProfit(self.profit[0], "futures:", self.profit[1], "spots:", self.profit[2], "&")    // Print the total profit curve, use the & character not to print the profit log
        }

        var cmd = GetCommand()
        if(cmd) {
            Log("interactive commands:", cmd)
            var arr = cmd.split(":") 
            if(arr[0] == "plus") {
                var pair = self.pairs[parseFloat(arr[1])]
                self.hedge(parseFloat(arr[1]), pair.fuSymbol, pair.spSymbol, self.PLUS, self.cmdHedgeAmount)
            } else if (arr[0] == "cover_plus") {
                var pair = self.pairs[parseFloat(arr[1])]
                self.hedge(parseFloat(arr[1]), pair.fuSymbol, pair.spSymbol, self.COVER_PLUS, self.cmdHedgeAmount)
            }
        }

        LogStatus("current time:", _D(), "data update time:", _D(self.tickerUpdateTS), "position account update count:", self.accAndPosUpdateCount, "\n", "Profit and loss:", self.profit[0], "futures profit and loss:", self.profit[1],
            "spot profit and loss:", self.profit[2], "\n`" + JSON.stringify(self.returnTbl()) + "`", "\n`" + JSON.stringify(self.returnPosTbl()) + "`")
    }

    self.keepBalance = function (pair) {
        var nowFuPos = pair.nowFuPos
        var nowSpPos = pair.nowSpPos
        var fuLongPos = self.getLongPos(nowFuPos)
        var fuShortPos = self.getShortPos(nowFuPos)
        var spLongPos = self.getLongPos(nowSpPos)
        var spShortPos = self.getShortPos(nowSpPos)

        if (fuLongPos || spShortPos) {    
            Log("reverse arbitrage is not supported") 
        }
        if (fuShortPos || spLongPos) {    
            var fuHoldAmount = fuShortPos ? fuShortPos.amount : 0
            var spHoldAmount = spLongPos ? spLongPos.amount : 0
            var sum = fuHoldAmount + spHoldAmount
            if (sum > 0) {            
                var spAmount = self.spEx.calcAmount(pair.spSymbol, self.spEx.COVER_LONG, pair.spTicker.bid1, Math.abs(sum), true)
                if (spAmount) {
                    Log(pair.fuSymbol, pair.spSymbol, "excess spot positions", Math.abs(sum), "fuShortPos:", fuShortPos, "spLongPos:", spLongPos)
                    self.spEx.goGetTrade(pair.spSymbol, self.spEx.COVER_LONG, pair.spTicker.bid1, spAmount[0])
                    var seIdMsg = self.spEx.getTrade()                    
                }
            } else if (sum < 0) {     
                var fuAmount = self.fuEx.calcAmount(pair.fuSymbol, self.fuEx.COVER_SHORT, pair.fuTicker.ask1, Math.abs(sum), true)
                if (fuAmount) {
                    Log(pair.fuSymbol, pair.spSymbol, "long futures positions", Math.abs(sum), "fuShortPos:", fuShortPos, "spLongPos:", spLongPos)
                    self.fuEx.goGetTrade(pair.fuSymbol, self.fuEx.COVER_SHORT, pair.fuTicker.ask1, fuAmount[0])
                    var feIdMsg = self.fuEx.getTrade()
                }
            }
        }
    }

    self.getLongPos = function (positions) {
        return self.getPosByDirection(positions, PD_LONG)
    }

    self.getShortPos = function (positions) {
        return self.getPosByDirection(positions, PD_SHORT)
    }

    self.getPosByDirection = function (positions, direction) {
        var ret = null
        if (positions.length > 2) {
            Log("position error, three positions detected:", JSON.stringify(positions))
            return ret 
        }
        _.each(positions, function(pos) {
            if ((direction == PD_LONG && pos.amount > 0) || (direction == PD_SHORT && pos.amount < 0)) {
                ret = pos
            }
        })
        return ret 
    }

    self.calcProfit = function() {   
        var arrInitFuAcc = []
        var arrNowFuAcc = []
        _.each(self.pairs, function(pair) {
            arrInitFuAcc.push(pair.initFuAcc)
            arrNowFuAcc.push(pair.nowFuAcc)
        })
        var fuProfit = self.fuEx.calcProfit(arrInitFuAcc, arrNowFuAcc)
        var spProfit = 0
        var deltaBalance = 0
        _.each(self.pairs, function(pair) {
            var nowSpAcc = pair.nowSpAcc
            var initSpAcc = pair.initSpAcc
            var stocksDiff = nowSpAcc.Stocks + nowSpAcc.FrozenStocks - (initSpAcc.Stocks + initSpAcc.FrozenStocks)
            var price = stocksDiff > 0 ? pair.spTicker.bid1 : pair.spTicker.ask1
            spProfit += stocksDiff * price
            deltaBalance = nowSpAcc.Balance + nowSpAcc.FrozenBalance - (initSpAcc.Balance + initSpAcc.FrozenBalance)
        })
        spProfit += deltaBalance
        return [fuProfit + spProfit, fuProfit, spProfit]    
    }

    self.returnPosTbl = function() {
        var posTbl = {
            type : "table", 
            title : "positions", 
            cols : ["index", "future", "future leverage", "qunatity", "spot", "qunatity"], 
            rows : []
        }
        _.each(self.pairs, function(pair, index) {
            var nowFuPos = pair.nowFuPos
            var nowSpPos = pair.nowSpPos
            for (var i = 0 ; i < nowFuPos.length ; i++) {
                if (nowSpPos.length > 0) {
                    posTbl.rows.push([index, nowFuPos[i].symbol, nowFuPos[i].marginLevel, nowFuPos[i].amount, nowSpPos[0].symbol, nowSpPos[0].amount])
                } else {
                    posTbl.rows.push([index, nowFuPos[i].symbol, nowFuPos[i].marginLevel, nowFuPos[i].amount, "--", "--"])
                }               
            }
        })

        return posTbl
    }

    self.returnTbl = function() {
        var fuExName = "[" + self.fuEx.getExName() + "]"
        var spExName = "[" + self.spEx.getExName() + "]"
        var combiTickersTbl = {
            type : "table", 
            title : "combiTickersTbl", 
            cols : ["future", "code" + fuExName, "entrusted selling", "entrusted purchase", "spot", "code" + spExName, "entrusted selling", "entrusted purchase", "positive hedging spreads", "reverse hedging spreads", "positive hedge", "positive hedge closeout"], 
            rows : []
        }
        _.each(self.pairs, function(pair, index) {
            var spSymbolInfo = self.spEx.getSymbolInfo(pair.spTicker.originalSymbol)  
            combiTickersTbl.rows.push([
                pair.fuTicker.symbol, 
                pair.fuTicker.originalSymbol, 
                pair.fuTicker.ask1, 
                pair.fuTicker.bid1, 
                pair.spTicker.symbol, 
                pair.spTicker.originalSymbol, 
                pair.spTicker.ask1, 
                pair.spTicker.bid1,
                pair.plusDiff,
                pair.minusDiff,
                {'type':'button', 'cmd': 'plus:' + String(index), 'name': 'positive arbitrage'},
                {'type':'button', 'cmd': 'cover_plus:' + String(index), 'name': 'close positive arbitrage'}
            ])
        })

        var accsTbl = {
            type : "table", 
            title : "accs",
            cols : ["code" + fuExName, "initial coin", "initial frozen coin", "initial money", "initial frozen money", "coin", "frozen coin", "money", "frozen money",
                "code" + spExName, "initial coin", "initial frozen coin", "initial money", "initial frozen money", "coin", "frozen coin", "money", "frozen money"], 
            rows : []
        }
        _.each(self.pairs, function(pair) {
            var arr = [pair.fuTicker.originalSymbol, pair.initFuAcc.Stocks, pair.initFuAcc.FrozenStocks, pair.initFuAcc.Balance, pair.initFuAcc.FrozenBalance, pair.nowFuAcc.Stocks, pair.nowFuAcc.FrozenStocks, pair.nowFuAcc.Balance, pair.nowFuAcc.FrozenBalance,
                pair.spTicker.originalSymbol, pair.initSpAcc.Stocks, pair.initSpAcc.FrozenStocks, pair.initSpAcc.Balance, pair.initSpAcc.FrozenBalance, pair.nowSpAcc.Stocks, pair.nowSpAcc.FrozenStocks, pair.nowSpAcc.Balance, pair.nowSpAcc.FrozenBalance]
            for (var i = 0 ; i < arr.length ; i++) {
                if (typeof(arr[i]) == "number") {
                    arr[i] = _N(arr[i], 6)  
                }
            }
            accsTbl.rows.push(arr)
        })

        var symbolInfoTbl = {
            type : "table", 
            title : "symbolInfos", 
            cols : ["contract code" + fuExName, "quantity accuracy", "price accuracy", "multiplier", "minimum order quantity", "spot code" + spExName, "quantity accuracy", "price accuracy", "multiplier", "minimum order quantity"], 
            rows : []
        }
        _.each(self.pairs, function(pair) {
            var fuSymbolInfo = self.fuEx.getSymbolInfo(pair.fuTicker.originalSymbol)
            var spSymbolInfo = self.spEx.getSymbolInfo(pair.spTicker.originalSymbol)
            symbolInfoTbl.rows.push([fuSymbolInfo.symbol, fuSymbolInfo.amountPrecision, fuSymbolInfo.pricePrecision, fuSymbolInfo.multiplier, fuSymbolInfo.min, 
                spSymbolInfo.symbol, spSymbolInfo.amountPrecision, spSymbolInfo.pricePrecision, spSymbolInfo.multiplier, spSymbolInfo.min])
        })

        var allPairs = []
        _.each(self.fuExTickers, function(fuTicker) {
            _.each(self.spExTickers, function(spTicker) {
                if (fuTicker.symbol == spTicker.symbol) {
                    allPairs.push({symbol: fuTicker.symbol, fuSymbol: fuTicker.originalSymbol, spSymbol: spTicker.originalSymbol, plus: fuTicker.bid1 - spTicker.ask1})
                }
            })
        })
        _.each(allPairs, function(pair) {
            var findPair = null 
            _.each(self.allPairs, function(selfPair) {
                if (pair.fuSymbol == selfPair.fuSymbol && pair.spSymbol == selfPair.spSymbol) {
                    findPair = selfPair
                }
            })
            if (findPair) {  
                findPair.minPlus = pair.plus < findPair.minPlus ? pair.plus : findPair.minPlus
                findPair.maxPlus = pair.plus > findPair.maxPlus ? pair.plus : findPair.maxPlus
                pair.minPlus = findPair.minPlus
                pair.maxPlus = findPair.maxPlus
            } else {        
                self.allPairs.push({symbol: pair.symbol, fuSymbol: pair.fuSymbol, spSymbol: pair.spSymbol, plus: pair.plus, minPlus: pair.plus, maxPlus: pair.plus})
                pair.minPlus = pair.plus
                pair.maxPlus = pair.plus
            }
        })
        return [combiTickersTbl, accsTbl, symbolInfoTbl]
    }

    self.onexit = function() {        
        _G("pairs", self.pairs)
        _G("allPairs", self.allPairs)
        Log("perform tailing processing and save data", "#FF0000")
    }

    self.init = function() {
        var fuExName = self.fuEx.getExName()
        var spExName = self.spEx.getExName()
        var gFuExName = _G("fuExName")
        var gSpExName = _G("spExName")
        if ((gFuExName && gFuExName != fuExName) || (gSpExName && gSpExName != spExName)) {
            throw "the exchange object has changed and the data needs to be reset"
        }
        if (!gFuExName) {
            _G("fuExName", fuExName)
        }
        if (!gSpExName) {
            _G("spExName", spExName)
        }

        self.allPairs = _G("allPairs")
        if (!self.allPairs) {
            self.allPairs = []
        }

        var arrPair = _G("pairs")
        if (!arrPair) {
            arrPair = []
        }
        var arrStrPair = self.symbolPairs.split(",")
        var timeStamp = new Date().getTime()
        _.each(arrStrPair, function(strPair) {
            var arrSymbol = strPair.split("|")
            var recoveryPair = null 
            _.each(arrPair, function(pair) {
                if (pair.fuSymbol == arrSymbol[0] && pair.spSymbol == arrSymbol[1]) {
                    recoveryPair = pair
                }
            })

            if (!recoveryPair) {
                var pair = {
                    fuSymbol : arrSymbol[0],
                    spSymbol : arrSymbol[1],
                    fuTicker : {}, 
                    spTicker : {},
                    plusDiff : null,
                    minusDiff : null,
                    canTrade : false,        
                    initFuAcc : null,        
                    initSpAcc : null,        
                    nowFuAcc : null,         
                    nowSpAcc : null,         
                    nowFuPos : null,         
                    nowSpPos : null,         
                    fuMarginLevel : null     
                }
                self.pairs.push(pair)
                Log("initialization:", pair)
            } else {
                self.pairs.push(recoveryPair)
                Log("Restore:", recoveryPair)
            }
            self.fuEx.pushSubscribeSymbol(arrSymbol[0])
            self.spEx.pushSubscribeSymbol(arrSymbol[1])
            if (!self.pairs[self.pairs.length - 1].initFuAcc) {
                self.fuEx.goGetAcc(arrSymbol[0], timeStamp)
                var nowFuAcc = self.fuEx.getAcc(arrSymbol[0], timeStamp)
                self.pairs[self.pairs.length - 1].initFuAcc = nowFuAcc
                self.pairs[self.pairs.length - 1].nowFuAcc = nowFuAcc
            }
            if (!self.pairs[self.pairs.length - 1].initSpAcc) {
                self.spEx.goGetAcc(arrSymbol[1], timeStamp)
                var nowSpAcc = self.spEx.getAcc(arrSymbol[1], timeStamp)
                self.pairs[self.pairs.length - 1].initSpAcc = nowSpAcc
                self.pairs[self.pairs.length - 1].nowSpAcc = nowSpAcc
            }
            Sleep(300)
        })
        Log("self.pairs:", self.pairs)
        _.each(self.pairs, function(pair) {
            var fuSymbolInfo = self.fuEx.getSymbolInfo(pair.fuSymbol)
            if (!fuSymbolInfo) {
                throw pair.fuSymbol + ", species information acquisition failure!"
            } else {
                Log(pair.fuSymbol, fuSymbolInfo)
            }
            var spSymbolInfo = self.spEx.getSymbolInfo(pair.spSymbol)
            if (!spSymbolInfo) {
                throw pair.spSymbol + ", species information acquisition failure!"
            } else {
                Log(pair.spSymbol, spSymbolInfo)
            }
        })

        _.each(self.pairs, function(pair) {
            pair.fuMarginLevel = self.fuMarginLevel
            var ret = self.fuEx.setMarginLevel(pair.fuSymbol, self.fuMarginLevel)
            Log(pair.fuSymbol, "leverage settings:", ret)
            if (!ret) {
                throw "initial setting of leverage failed!"
            }
        })
    }

    self.init()
    return self
}

var manager = null 
function main() {
    if(isReset) {        
        _G(null)
        LogReset(1)
        LogProfitReset()
        LogVacuum()
        Log("reset all data", "#FF0000")
    }

    if (isOKEX_V5_Simulate) {
        for (var i = 0 ; i < exchanges.length ; i++) {
            if (exchanges[i].GetName() == "Futures_OKCoin" || exchanges[i].GetName() == "OKEX") {
                var ret = exchanges[i].IO("simulate", true)
                Log(exchanges[i].GetName(), "switch analog disk")
            }
        }
    }

    var fuConfigureFunc = null 
    var spConfigureFunc = null 
    if (exchanges.length != 2) {
        throw "two exchange objects need to be added!"
    } else {
        var fuName = exchanges[0].GetName()
        if (fuName == "Futures_OKCoin" && isOkexV5) {
            fuName += "_V5"
            Log("Use OKEX V5 interface")
        }
        var spName = exchanges[1].GetName()
        fuConfigureFunc = $.getConfigureFunc()[fuName]
        spConfigureFunc = $.getConfigureFunc()[spName]
        if (!fuConfigureFunc || !spConfigureFunc) {
            throw (fuConfigureFunc ? "" : fuName) + " " +  (spConfigureFunc ? "" : spName) + " not support!"
        }
    }
    var fuEx = $.createBaseEx(exchanges[0], fuConfigureFunc)
    var spEx = $.createBaseEx(exchanges[1], spConfigureFunc)
    manager = createManager(fuEx, spEx, symbolPairs, cmdHedgeAmount, fuMarginLevel, fuMarginReservedRatio)

    while(true) {
        manager.process()
        Sleep(interval)
    }
}

function onerror() {
    if (manager) {
        manager.onexit()
    }    
}

function onexit() {
    if (manager) {
        manager.onexit()
    }
}

Since the multi-species strategy is more suitable for IO design, a template class library named MultiSymbolCtrlLib is used in the code (encapsulation, calling the exchange interface through IO). Therefore, the strategy cannot be backtested, but it can be tested with the simulated bot (although the real bot has been run for 2 months, the test and familiarization stage is still run with the simulated bot).

Parameters

Before starting the test, let's talk about the parameter design first.

There are not many strategy parameters, the more important ones are:

  • Hedging control table

      LTC-USDT-211231|LTC_USDT,BTC-USDT-211231|BTC_USDT
    

    Here is the setup strategy to monitor those combinations. For example, the above setup is to monitor the Litecoin contract (LTC-USDT-211231) of the future exchange and the Litecoin (LTC_USDT) of the spot exchange. A combination of future contract and spot trading pair are separated by | symbols to form a combination. Different combinations are separated by , symbols. Note that the symbols here are all in the state of the English input method! Then you may ask me how to find the contract code. These contract codes and spot trading pairs are all defined by the exchange, not defined by the FMZ platform. For example, LTC-USDT-211231 is a second-quarter contract currently, called next_quarter on FMZ, and OKEX's interface system is called LTC-USDT-211231. For the Litecoin/USDT trading pair, the WexApp simulation bot is written as LTC_USDT. So how to fill in here depends on the name defined in the exchange.

  • Hedging amount for interactive control hedging Click the status bar control button to hedge the amount. The unit is the number of coins, and the strategy will be automatically converted into the number of contracts to place an order.

Other functions are to set the analog disk, reset the data, use the OKEX V5 interface (because it is also compatible with V3) and so on, which are not particularly important.

Testing

The first exchange object adds the futures exchange, and the second adds the spot exchange object.

Futures exchanges use OKX's V5 interface simulation bots, and spot exchanges use WexApp simulation bots.

Click the positive set button of the BTC combination and open the position.

Click to close the positive arbitrage then.

Lose!!! It seems that closing the position cannot cover the handling fee when the profit spread is small, it is necessary to calculate the handling fee, the approximate slippage, and plan the spread reasonably, and then close the position.

Strategy source code: https://www.fmz.com/strategy/314352

Those who are interested can use and modify it.

From: https://blog.mathquant.com/2022/09/01/hedging-strategy-of-cryptocurrency-manual-futures-and-spots.html