Research and Example on the Maker Spots and Futures Hedging Strategy Design

Research and Example on the Maker Spots and Futures Hedging Strategy Design

·

10 min read

For a long time, futures and spots hedging is generally designed to detect the price difference. When the price difference is met, we will take the order to hedge. Can it be designed as maker hedging? The answer is absolutely yes. Today, I will bring you a design idea and code prototype for the maker hedging.

Idea of maker hedging

In different markets of the same or the same type of subject matter, hedging opportunities arise when there is a large difference between the selling orders and buying orders of two markets. Generally, we will do makers that meet the price difference and hold hedging positions. Therefore, there are two purposes for hedging. The first is to hedge the position and the second is to ensure that the difference between the buying and selling order meets our expectations to the largest extent. The advantage of maker trading in this respect is that the comission fee is lower. The disadvantage is that it is not easy to make a deal, and it is easy to make a deal on a single position.

The trading idea we design is to place a buy order in the A market order book and a sell order in the B market order book. Then we check our account pending orders, and do the next step for the checked pending order transactions. For example, if we find a change in the pending order, we balance the hedge position of spots and futures immediately, and cover or close the overflow position in the spots and futures position. According to the increase of the hedging position, we adjust the distance of the first pending order in the order to the next position in the order, and hedge to get a larger spread gradually.

Code design

The comments are written in the code directly. The example is used for reference design only and it has been tested on the OKX V5 demo. The example is not a perfect strategy, please use it for reference only.

javascript// Temporary parameters
var fuContractType = "quarter"    // Futures contracts
var fuSymbol = "ETH_USDT"         // Futures trading pairs
var spSymbol = "ETH_USDT"         // Spots trading pairs
var minAmount = 0.1               // Amount per transaction, minimum transaction amount, currency
var step = 40                     // Difference step length
var buff = 5                      // Buffer price difference
var balanceType = "open"          // When the single position transaction is balanced, open the covering position and close the closing position

var depthManager = function(fuEx, spEx, fuCt, fuSymbol, spSymbol) {
    var self = {}
    self.fuExDepth = null
    self.spExDepth = null 
    self.plusPrice = null
    self.minusPrice = null 

    self.update = function() {        
        spEx.SetCurrency(spSymbol)
        if (!IsVirtual()) {
            fuEx.SetCurrency(fuSymbol)
        }        
        fuEx.SetContractType(fuCt)

        var fuRoutine = fuEx.Go("GetDepth")
        var spRoutine = spEx.Go("GetDepth")
        var fuDepth = fuRoutine.wait()
        var spDepth = spRoutine.wait()
        if (!fuDepth || !spDepth) {
            return false 
        }
        self.fuExDepth = fuDepth
        self.spExDepth = spDepth

        if (fuDepth.Bids.length == 0 || fuDepth.Asks.length == 0 || spDepth.Bids.length == 0 || spDepth.Asks.length == 0) {
            return false 
        }
        self.plusPrice = fuDepth.Bids[0].Price - spDepth.Asks[0].Price   // futures Bid - spot Ask
        self.minusPrice = fuDepth.Asks[0].Price - spDepth.Bids[0].Price  // futures Ask - spot Bid
        return true 
    }

    self.getData = function() {       
        return {
            "fuExDepth" : self.fuExDepth,
            "spExDepth" : self.spExDepth,
            "plusPrice" : self.plusPrice,
            "minusPrice" : self.minusPrice
        }
    }
    return self 
}

var positionManager = function(fuEx, spEx, fuCt, fuSymbol, spSymbol, step, buffDiff, balanceType, initSpAcc) {
    var self = {}
    self.balanceType = balanceType
    self.depth = null 
    self.level = 1
    self.lastUpdateTs = 0
    self.fuPos = []
    self.spPos = []
    self.initSpAcc = initSpAcc
    self.spAcc = null
    self.hedgePos = null
    self.hedgePosPrice = 0
    self.minAmount = 0.01
    self.offset = ["", 0]

    self.update = function() {
        spEx.SetCurrency(spSymbol)
        if (!IsVirtual()) {
            fuEx.SetCurrency(fuSymbol)
        }        
        fuEx.SetContractType(fuCt)

        self.offset = ["", 0]
        var fuRoutine = fuEx.Go("GetPosition")
        var spRoutine = spEx.Go("GetAccount")
        var fuPos = fuRoutine.wait()
        var spAcc = spRoutine.wait()
        if (!fuPos || !spAcc) {
            return false 
        }
        self.fuPos = fuPos
        self.spAcc = spAcc
        if (!self.initSpAcc) {
            return false 
        }
        self.spPos = (spAcc.Stocks + spAcc.FrozenStocks) - (self.initSpAcc.Stocks + self.initSpAcc.FrozenStocks)   // Current one minus the initial one, positive number means going long
        // Check fuPos
        if (fuPos.length > 1) {
            return false 
        }
        fuPosAmount = fuPos.length == 0 ? 0 : (fuPos[0].Type == PD_LONG ? fuPos[0].Amount : -fuPos[0].Amount)
        if ((fuPosAmount > 0 && self.spPos > 0) || (fuPosAmount < 0 && self.spPos < 0)) {
            return false 
        }

        fuPosAmount = self.piece2Coin(fuPosAmount)

        self.hedgePos = (fuPosAmount == 0 || self.spPos == 0) ? 0 : (fuPosAmount < 0 && self.spPos > 0 ? Math.min(Math.abs(fuPosAmount), Math.abs(self.spPos)) : -Math.min(Math.abs(fuPosAmount), Math.abs(self.spPos)))
        var diffBalance = (spAcc.Balance + spAcc.FrozenBalance) - (self.initSpAcc.Balance + self.initSpAcc.FrozenBalance)
        if (self.hedgePos == 0) {
            self.hedgePosPrice = 0    
        } else {
            self.hedgePosPrice = fuPos[0].Price - (Math.abs(diffBalance) / Math.abs(self.spPos))
        }
        self.offset[1] = fuPosAmount + self.spPos  // If positive, long positions overflow, if negative, short positions overflow
        if (fuPosAmount > 0 && self.spPos < 0) {   // Reverse arbitrage
            self.offset[0] = "minus"
        } else if (fuPosAmount < 0 && self.spPos > 0) {
            self.offset[0] = "plus"
        } else if (fuPosAmount == 0 && self.spPos < 0) {
            self.offset[0] = "minus"
        } else if (fuPosAmount > 0 && self.spPos == 0) {
            self.offset[0] = "minus"
        } else if (fuPosAmount == 0 && self.spPos > 0) {
            self.offset[0] = "plus"
        } else if (fuPosAmount < 0 && self.spPos == 0) {
            self.offset[0] = "plus"
        }
        return true 
    }

    self.getData = function() {
        return {
            "fuPos" : self.fuPos,
            "spPos" : self.spPos,
            "initSpAcc" : self.initSpAcc,
            "spAcc" : self.spAcc,
            "hedgePos" : self.hedgePos,
            "hedgePosPrice" : self.hedgePosPrice,
        }
    }

    self.keepBalance = function(depth) {
        var fuDepth = depth.fuExDepth
        var spDepth = depth.spExDepth
        if (self.offset[0] == "plus") {
            if (self.offset[1] >= self.minAmount) {
                if (self.balanceType == "close") {
                    // If the spot long position is excessive, close the spot long position
                    spEx.Sell(-1, self.offset[1])
                } else if (self.balanceType == "open") {
                    // If the spot long position is excessive, open the future short position
                    fuEx.SetDirection("sell")
                    fuEx.Sell(-1, self.coin2Piece(Math.abs(self.offset[1])))
                }
            } else if (self.offset[1] <= -self.minAmount) {
                if (self.balanceType == "close") {
                    // If the future short position is excessive, close the future short position
                    fuEx.SetDirection("closesell")
                    fuEx.Buy(-1, self.coin2Piece(Math.abs(self.offset[1])))
                } else if (self.balanceType == "open") {
                    // If the future short position is excessive, open the spot long position
                    spEx.Buy(-1, spDepth.Asks[0].Price * Math.abs(self.offset[1]))
                }
            }
            return false 
        } else if (self.offset[0] == "minus") {
            if (self.offset[1] >= self.minAmount) {
                if (self.balanceType == "close") {
                    // If the future long position is excessive, close the future long position
                    fuEx.SetDirection("closebuy")
                    fuEx.Sell(-1, self.coin2Piece(self.offset[1]))
                } else if (self.balanceType == "open") {
                    // If the future long position is excessive, open the spot short position
                    spEx.Sell(-1, self.offset[1])
                }
            } else if (self.offset[1] <= -self.minAmount) {
                if (self.balanceType == "close") {
                    // If the spot short position is excessive, close the spot short position
                    spEx.Buy(-1, spDepth.Asks[0].Price * Math.abs(self.offset[1]))
                } else if (self.balanceType == "open") {
                    // If the spot short position is excessive, open the future long position
                    fuEx.SetDirection("buy")
                    fuEx.Buy(-1, self.coin2Piece(Math.abs(self.offset[1])))
                }
            }
            return false 
        }
        return true 
    }

    self.process = function(depthManager) {
        var ts = new Date().getTime()
        var depth = depthManager.getData()
        var orders = self.getOrders()
        if (!orders) {
            return 
        }
        self.depth = depth
        var fuOrders = orders[0]
        var spOrders = orders[1]

        if (fuOrders.length == 0 && spOrders.length == 0) {
            // Reset level
            if (self.hedgePos == 0) {
                self.level = 1
            } else {
                self.level = Math.max(1, _N(self.hedgePos / self.minAmount, 0))
            }

            // Limit the maximum position
            if (Math.abs(self.hedgePos) > 1) {
                return 
            }

            // Pending orders
            var fuDepth = depth.fuExDepth
            var spDepth = depth.spExDepth
            self.update()

            if (self.hedgePos >= 0 && fuDepth.Bids[0].Price - spDepth.Asks[0].Price > 0) {        // Positive arbitrage
                var distance = (step * self.level - (fuDepth.Asks[0].Price - spDepth.Bids[0].Price)) / 2          
                fuEx.SetDirection("sell")
                fuEx.Sell(fuDepth.Asks[0].Price + distance, self.coin2Piece(self.minAmount), fuDepth.Asks[0].Price, "Price difference of makers:", fuDepth.Asks[0].Price + distance - (spDepth.Bids[0].Price - distance))
                spEx.Buy(spDepth.Bids[0].Price - distance, self.minAmount, spDepth.Bids[0].Price)
            } else if (self.hedgePos <= 0 && spDepth.Bids[0].Price - fuDepth.Asks[0].Price > 0) { // Reverse arbitrage
                var distance = (step * self.level - (spDepth.Asks[0].Price - fuDepth.Bids[0].Price)) / 2          
                fuEx.SetDirection("buy")
                fuEx.Buy(fuDepth.Bids[0].Price - distance, self.coin2Piece(self.minAmount), fuDepth.Bids[0].Price, "Price difference of makers:", spDepth.Asks[0].Price + distance - (fuDepth.Bids[0].Price - distance))
                spEx.Sell(spDepth.Asks[0].Price + distance, self.minAmount, spDepth.Asks[0].Price)
            }
        } else if (fuOrders.length == 1 && spOrders.length == 1) {
            var fuDepth = depth.fuExDepth
            var spDepth = depth.spExDepth            
            // Judge the position
            var isCancelAll = false 
            if (self.hedgePos >= 0 && fuDepth.Bids[0].Price - spDepth.Asks[0].Price > 0) {        // Positive arbitrage
                var distance = (step * self.level - (fuDepth.Asks[0].Price - spDepth.Bids[0].Price)) / 2
                if (Math.abs(fuOrders[0].Price - (fuDepth.Asks[0].Price + distance)) > buffDiff || Math.abs(spOrders[0].Price - (spDepth.Bids[0].Price - distance)) > buffDiff) {
                    isCancelAll = true 
                }
            } else if (self.hedgePos <= 0 && spDepth.Bids[0].Price - fuDepth.Asks[0].Price > 0) { // Reverse arbitrage
                var distance = (step * self.level - (spDepth.Asks[0].Price - fuDepth.Bids[0].Price)) / 2
                if (Math.abs(spOrders[0].Price - (spDepth.Asks[0].Price + distance)) > buffDiff || Math.abs(fuOrders[0].Price - (fuDepth.Bids[0].Price - distance)) > buffDiff) {
                    isCancelAll = true 
                }
            } else {
                isCancelAll = true 
            }
            if (isCancelAll) {
                self.cancelAll(fuEx, fuOrders)
                self.cancelAll(spEx, spOrders)
                self.lastUpdateTs = 0
            }
        } else {            
            self.cancelAll(fuEx, fuOrders)
            self.cancelAll(spEx, spOrders)       
            self.lastUpdateTs = 0
        }

        if (ts - self.lastUpdateTs > 1000 * 60 * 2) {
            self.update()
            self.keepBalance(depth)
            self.update()
            self.lastUpdateTs = ts 
        }
        LogStatus(_D())   // The status bar can be designed to output the data and information to be observed
    }

    self.getOrders = function() {
        spEx.SetCurrency(spSymbol)
        if (!IsVirtual()) {
            fuEx.SetCurrency(fuSymbol)
        }        
        fuEx.SetContractType(fuCt)

        var fuRoutine = fuEx.Go("GetOrders")
        var spRoutine = spEx.Go("GetOrders")
        var fuOrders = fuRoutine.wait()
        var spOrders = spRoutine.wait()
        if (!fuOrders || !spOrders) {
            return false 
        }
        return [fuOrders, spOrders]
    }

    // Number of currency converted into contracts
    self.coin2Piece = function(amount) {
        if (IsVirtual()) {
            if (fuEx.GetName() == "Futures_Binance") {
                return amount
            } else if (fuEx.GetName() == "Futures_OKCoin") {
                var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
                return _N(amount / (100 / price), 0)
            } else {
                throw "not support"
            }            
        }
        if (fuEx.GetName() == "Futures_OKCoin") {
            if (fuEx.GetQuoteCurrency() == "USDT") {
                return _N(amount * 10, 0)
            } else if (fuEx.GetQuoteCurrency() == "USD") {
                var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
                return _N(amount / (100 / price), 0)
            } else {
                throw "not support"
            }
        } else {
            throw "not support"
        }
    }

    // Number of contracts converted into currency
    self.piece2Coin = function(amount) {
        if (IsVirtual()) {
            if (fuEx.GetName() == "Futures_Binance") {
                return amount
            } else if (fuEx.GetName() == "Futures_OKCoin") {
                var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
                return amount * 100 / price
            } else {
                throw "not support"
            }            
        }
        if (fuEx.GetName() == "Futures_OKCoin") {
            if (fuEx.GetQuoteCurrency() == "USDT") {
                return amount * 0.1
            } else if (fuEx.GetQuoteCurrency() == "USD") {
                var price = (self.depth.fuExDepth.Bids[0].Price + self.depth.fuExDepth.Asks[0].Price) / 2
                return amount * 100 / price
            } else {
                throw "not support"
            }
        } else {
            throw "not support"
        }
    }

    self.cancelAll = function(e, orders) {
        var isFirst = true 
        while (true) {
            Sleep(500)
            if (orders && isFirst) {
                isFirst = false 
            } else {
                orders = e.GetOrders()
            }
            if (!orders) {
                continue
            } else {
                for (var i = 0 ; i < orders.length ; i++) {
                    e.CancelOrder(orders[i].Id, orders[i])
                }
            }
            if (orders.length == 0) {
                break
            }
        }
    }

    self.CoverAll = function() {
        // Close all positions
        // Here we can realize one-click position closing
    }

    self.setMinAmount = function(minAmount) {
        self.minAmount = minAmount
    }

    self.init = function() {
        while(!self.spAcc) {
            self.update()
            Sleep(1000)
        }
        if (!self.initSpAcc) {  
            var positionManager_initSpAcc = _G("positionManager_initSpAcc")
            if (!positionManager_initSpAcc) {
                self.initSpAcc = self.spAcc
                _G("positionManager_initSpAcc", self.initSpAcc)
            } else {
                self.initSpAcc = positionManager_initSpAcc
            }
        } else {
            _G("positionManager_initSpAcc", self.initSpAcc)
        }
        // Print the initial information
        Log("self.initSpAcc:", self.initSpAcc.Balance, self.initSpAcc.FrozenBalance, self.initSpAcc.Stocks, self.initSpAcc.FrozenStocks)
    }
    self.init()
    return self
}

function main() {
    _G(null)       // Clear the persistent data
    LogReset(1)    // Reset logs

    // The following code can be switchedto the OKEX Demo
    // exchanges[0].IO("simulate", true)
    // exchanges[1].IO("simulate", true)

    var dm = depthManager(exchanges[0], exchanges[1], fuContractType, fuSymbol, spSymbol)
    var pm = positionManager(exchanges[0], exchanges[1], fuContractType, fuSymbol, spSymbol, step, buff, balanceType)
    pm.setMinAmount(minAmount)

    while (true) {
        if (!dm.update()) {
            Sleep(3000)
            continue
        }

        var cmd = GetCommand()
        if (cmd) {
            // Handle interactions
            Log("Interaction command:", cmd)
            var arr = cmd.split(":") 
            if (arr[0] == "") {
                pm.CoverAll()
            }            
        }

        pm.process(dm)
        Sleep(5000)
    }
}

Backtest analysis

We can see that pending orders and withdrawal orders are more. From the backtesting system's statistics, the futures exchange account lost -0.01666 ETH and the spot exchange made a profit of 842.23758 USDT. The ETH spot price was 4252 USDT at the end of the backtest, -0.01666 * 4252 = -70.83832000000001. It obtains profit overall after plus the spot profit.

But it is just on a backtest, and there are definitely more details to be worked out in the real bot.

From: https://blog.mathquant.com/2022/11/09/research-and-example-on-the-maker-spots-and-futures-hedging-strategy-design.html