Pesquisa e exemplo sobre a concepção da estratégia de cobertura de Maker Spots e Futures

Autora:Lydia., Criado: 2022-11-09 14:49:29, Atualizado: 2023-09-14 20:38:22

img

Por um longo tempo, os futuros e hedging de pontos é geralmente projetado para detectar a diferença de preço. Quando a diferença de preço é atendida, vamos tomar a ordem para hedge. Pode ser projetado como a cobertura do fabricante? A resposta é absolutamente sim. Hoje, eu vou trazer-lhe uma ideia de design e protótipo de código para a cobertura do fabricante.

Ideia de cobertura do fabricante

Em mercados diferentes do mesmo ou do mesmo tipo de assunto, as oportunidades de hedging surgem quando há uma grande diferença entre as ordens de venda e as ordens de compra de dois mercados. Geralmente, faremos fabricantes que atendam à diferença de preço e mantenham posições de hedging. Portanto, há dois propósitos para a hedging. O primeiro é hedgear a posição e o segundo é garantir que a diferença entre a ordem de compra e venda atenda às nossas expectativas na maior extensão possível. A vantagem da negociação de fabricantes a esse respeito é que a comissão é menor. A desvantagem é que não é fácil fazer um negócio e é fácil fazer um negócio em uma única posição.

A ideia de negociação que projetamos é colocar uma ordem de compra na carteira de ordens de mercado A e uma ordem de venda na carteira de ordens de mercado B. Em seguida, verificamos nossas ordens pendentes na conta e fazemos o próximo passo para as transações de ordem pendente verificadas. Por exemplo, se encontrarmos uma mudança na ordem pendente, equilibramos a posição de hedge de pontos e futuros imediatamente e cobrimos ou fechamos a posição de excesso nas posições de pontos e futuros. De acordo com o aumento da posição de hedge, ajustamos a distância da primeira ordem pendente na ordem para a próxima posição na ordem e hedge para obter um spread maior gradualmente.

Projeto de código

Os comentários são escritos no código diretamente. O exemplo é usado apenas para design de referência e foi testado na demonstração do OKEX V5. O exemplo não é uma estratégia perfeita, por favor use-o apenas para referência.

// 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)
    }
}

Análise de teste de retrocesso

img img img img

Podemos ver que as ordens pendentes e as ordens de retirada são mais. A partir das estatísticas do sistema de backtesting, a conta de câmbio de futuros perdeu -0.01666 ETH e a câmbio spot fez um lucro de 842.23758 USDT. O preço spot do ETH foi de 4252 USDT no final do backtest, -0.01666 * 4252 = -70.83832000000001.

Mas é apenas um backtest, e há definitivamente mais detalhes para serem trabalhados no bot real.


Relacionados

Mais.