Cryptocurrency spot hedging strategy design(1)

Cryptocurrency spot hedging strategy design(1)

·

12 min read

Hedging strategies are very good practice strategies for beginners in strategy design. This article implements a simple but live cryptocurrency spot hedging strategy, hoping that beginners can learn some design experience.

Design some functions and strategy interface parameters according to the strategy requirements

First of all, it is clear that the strategy to be designed is a cryptocurrency spot hedging strategy. We design the simplest hedging strategy. We sell on the exchange with the higher price only between the two spot exchanges, and buy on the exchange with the lower price to take the difference. When the exchanges with higher prices are all denominated coins (because the coins with higher prices are sold), and the exchanges with lower prices are all coins (the coins with lower prices are bought), it cannot be hedged. At this time, we can only wait for the price reversal to hedge.

When hedging, the price and quantity of the order are limited by the exchange, and there is also a limit on the minimum order quantity. In addition to the minimum limit, strategy in hedging also needs to consider the maximum order volume at one time. If the order volume is too large, there will not be enough order volume. It is also necessary to consider how to convert the exchange rate if the two exchange-denominated coins are different. When hedging, the handling fee and slippage of the order taker are all transaction costs, not as long as there is a price difference can be hedged. Therefore, the hedging price difference also has a trigger value. If it is lower than a certain price difference, the hedging will lose.

Based on these considerations, the strategy needs to be designed with several parameters:

  • Hedge difference: hedgeDiffPrice, when the difference exceeds this value, the hedging operation is triggered.

  • Minimum hedge amount:minHedgeAmount, the minimum order amount (coins) that can be hedged.

  • Maximum hedge amount:maxHedgeAmount, the maximum order amount (coins) for one hedging.

  • Price precision of A: pricePrecisionA, the order price precision (number of decimal places) placed by Exchange A.

  • Amount precision of A: amountPrecisionA, the amount precision of the order placed by Exchange A (number of decimal places).

  • Price precision of B: pricePrecisionB, the order price precision (number of decimal places) placed by Exchange B.

  • Amount precision of B: amountPrecisionB, the amount precision of the order placed by Exchange B (number of decimal places).

  • Rate of Exchange A: rateA, the exchange rate conversion of the first added exchange object, the default is 1, not converted.

  • Rate of Exchange B: rateB, the exchange rate conversion of the second added exchange object, the default is 1, not converted.

The hedging strategy needs to keep the number of coins in the two accounts unchanged (that is, not holding positions in any direction, and maintaining neutrality), so there needs to be a balance logic in the strategy to always detect the balance. When checking balance, it is unavoidable to obtain asset data from two exchanges. We need to write a function to use.

  • updateAccs

      function updateAccs(arrEx) {
          var ret = []
          for (var i = 0 ; i < arrEx.length ; i++) {
              var acc = arrEx[i].GetAccount()
              if (!acc) {
                  return null
              }
              ret.push(acc)
          }
          return ret 
      }
    

After placing the order, if there is no completed order, we need to cancel it in time, and the order cannot be kept pending. This operation needs to be processed in both the balance module and the hedging logic, so it is also necessary to design an order full withdrawal function.

  • cancelAll

      function cancelAll() {
          _.each(exchanges, function(ex) {
              while (true) {
                  var orders = _C(ex.GetOrders)
                  if (orders.length == 0) {
                      break
                  }
                  for (var i = 0 ; i < orders.length ; i++) {
                      ex.CancelOrder(orders[i].Id, orders[i])
                      Sleep(500)
                  }
              }
          })
      }
    

When balancing the number of coins, we need to find the price accumulated to a certain number of coins in a certain depth data, so we need such a function to handle it.

  • getDepthPrice

      function getDepthPrice(depth, side, amount) {
          var arr = depth[side]
          var sum = 0
          var price = null
          for (var i = 0 ; i < arr.length ; i++) {
              var ele = arr[i]
              sum += ele.Amount
              if (sum >= amount) {
                  price = ele.Price
                  break
              }
          }
          return price
      }
    

Then we need to design and write the specific hedging order operation, which needs to be designed to place concurrent orders:

  • hedge

      function hedge(buyEx, sellEx, price, amount) {
          var buyRoutine = buyEx.Go("Buy", price, amount)
          var sellRoutine = sellEx.Go("Sell", price, amount)
          Sleep(500)
          buyRoutine.wait()
          sellRoutine.wait()
      }
    

Finally, let's complete the design of the balance function, which is slightly complicated.

  • keepBalance

      function keepBalance(initAccs, nowAccs, depths) {
          var initSumStocks = 0
          var nowSumStocks = 0 
          _.each(initAccs, function(acc) {
              initSumStocks += acc.Stocks + acc.FrozenStocks
          })
          _.each(nowAccs, function(acc) {
              nowSumStocks += acc.Stocks + acc.FrozenStocks
          })
    
          var diff = nowSumStocks - initSumStocks
          // Calculate the currency difference
          if (Math.abs(diff) > minHedgeAmount && initAccs.length == nowAccs.length && nowAccs.length == depths.length) {
              var index = -1
              var available = []
              var side = diff > 0 ? "Bids" : "Asks"
              for (var i = 0 ; i < nowAccs.length ; i++) {
                  var price = getDepthPrice(depths[i], side, Math.abs(diff))
                  if (side == "Bids" && nowAccs[i].Stocks > Math.abs(diff)) {
                      available.push(i)
                  } else if (price && nowAccs[i].Balance / price > Math.abs(diff)) {
                      available.push(i)
                  }
              }
              for (var i = 0 ; i < available.length ; i++) {
                  if (index == -1) {
                      index = available[i]
                  } else {
                      var priceIndex = getDepthPrice(depths[index], side, Math.abs(diff))
                      var priceI = getDepthPrice(depths[available[i]], side, Math.abs(diff))
                      if (side == "Bids" && priceIndex && priceI && priceI > priceIndex) {
                          index = available[i]
                      } else if (priceIndex && priceI && priceI < priceIndex) {
                          index = available[i]
                      }
                  }
              }
              if (index == -1) {
                  Log("unable to balance")            
              } else {
                  // balance order
                  var price = getDepthPrice(depths[index], side, Math.abs(diff))
                  if (price) {
                      var tradeFunc = side == "Bids" ? exchanges[index].Sell : exchanges[index].Buy
                      tradeFunc(price, Math.abs(diff))
                  } else {
                      Log("invalid price", price)
                  }
              }        
              return false
          } else if (!(initAccs.length == nowAccs.length && nowAccs.length == depths.length)) {
              Log("errors:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
              return true 
          } else {
              return true 
          }
      }
    

After designing these functions according to the strategy requirements, then start to design the main function of the strategy.

Main function design of the strategy

On FMZ platform, the strategy is executed from the main function. At the beginning of the main function, we have to do some initialization work of the strategy.

  • Exchange object name Because many operations in the strategy have to use the exchange objects, such as getting market quotations, placing orders and so on. So it would be cumbersome to use a long name every time, the tip is to use a simple name instead, for example:

      var exA = exchanges[0]
      var exB = exchanges[1]
    

    This makes it easier to write code later.

  • Exchange rate, precision related design

        // precision, exchange rate settings
        if (rateA != 1) {
            // set exchange rate A
            exA.SetRate(rateA)
            Log("Exchange A sets the exchange rate:", rateA, "#FF0000")
        }
        if (rateB != 1) {
            // set exchange rate B
            exB.SetRate(rateB)
            Log("Exchange B sets the exchange rate:", rateB, "#FF0000")
        }
        exA.SetPrecision(pricePrecisionA, amountPrecisionA)
        exB.SetPrecision(pricePrecisionB, amountPrecisionB)
    

    If the exchange rate parameters rateA, rateB are set to 1 (the default is 1), that is, rateA != 1 or rateB != 1 will not trigger, so the exchange rate conversion will not be set.

  • Reset all data

    Sometimes it is necessary to delete all logs and clear the recorded data when the strategy starts. The you can design a strategy interface parameter isReset, and design the reset code in the initialization part of the strategy, for example:

        if (isReset) {   // When isReset is true, reset the data
            _G(null)
            LogReset(1)
            LogProfitReset()
            LogVacuum()
            Log("reset all data", "#FF0000")
        }
    
  • Restore initial account data, update current account data In order to judge the balance, the strategy needs to continuously record the initial account assets for comparison with the current one. The variable nowAccs is used to record the current account data, using the function we just designed updateAccs to get the account data of the current exchange. initAccs is used to record the initial account status (the number of coins, the number of denominated coins, etc. on exchanges A and B). For initAccs, use the _G() function to restore first (the _G function will record data persistently, and can return the recorded data again, see the API documentation for details: [link](https:// www.fmz.com/api#_gk-v)), if the query does not work, use the current account information to assign value and use the _G function to record.

    Such as the following code:

        var nowAccs = _C(updateAccs, exchanges)
        var initAccs = _G("initAccs")
        if (!initAccs) {
            initAccs = nowAccs
            _G("initAccs", initAccs)
        }
    

Trading logic, main loop in main function

The code in the main loop is the process of each round of execution of the strategy logic, which is executed over and over again to form the main loop of the strategy. Let's take a look at the process of each execution of the program in the main loop.

  • Obtain market data and judge the validity of market data

            var ts = new Date().getTime()
            var depthARoutine = exA.Go("GetDepth")
            var depthBRoutine = exB.Go("GetDepth")
            var depthA = depthARoutine.wait()
            var depthB = depthBRoutine.wait()
            if (!depthA || !depthB || depthA.Asks.length == 0 || depthA.Bids.length == 0 || depthB.Asks.length == 0 || depthB.Bids.length == 0) {
                Sleep(500)
                continue 
            }
    

    Here we can see that the concurrent function exchange.Go of the FMZ platform is used to create concurrent objects depthARoutine, depthBRoutine that call the GetDepth() interface. When these two concurrent objects are created, the GetDepth() interface is called immediately, and both requests for depth data are sent to the exchange. Then call the wait() method of the depthARoutine, depthBRoutine objects to obtain the depth data.
    After obtaining the depth data, it is necessary to check the depth data to determine its validity. In the case of abnormal data, the execution of the continue statement is triggered to re-execute the main loop.

  • Use the spread value parameter or the spread ratio parameter?

            var targetDiffPrice = hedgeDiffPrice
            if (diffAsPercentage) {
                targetDiffPrice = (depthA.Bids[0].Price + depthB.Asks[0].Price + depthB.Bids[0].Price + depthA.Asks[0].Price) / 4 * hedgeDiffPercentage
            }
    

    In terms of parameters, we have made such a design: the parameters of FMZ can be show or hide based on a parameter, so we can make a parameter to decide whether to use price spread, or spread ratio.

    A parameter diffAsPercentage has been added to the parameters of the strategy interface. The other two parameter settings to show or hide based on this parameter are: hedgeDiffPrice@!diffAsPercentage, which is displayed when diffAsPercentage is false. hedgeDiffPercentage@diffAsPercentage, which is displayed when diffAsPercentage is true. After this design, we checked the diffAsPercentage parameter, which is the hedge trigger condition based on the price difference ratio. Without the diffAsPercentage parameter checked, the hedge is triggered by the price difference.

  • Determine the hedging trigger conditions

            if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) {          // A -> B market conditions are met            
                var price = (depthA.Bids[0].Price + depthB.Asks[0].Price) / 2
                var amount = Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount)
                if (nowAccs[0].Stocks > minHedgeAmount && nowAccs[1].Balance / price > minHedgeAmount) {
                    amount = Math.min(amount, nowAccs[0].Stocks, nowAccs[1].Balance / price, maxHedgeAmount)
                    Log("trigger A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks)  // Tips
                    hedge(exB, exA, price, amount)
                    cancelAll()
                    lastKeepBalanceTS = 0
                    isTrade = true 
                }            
            } else if (depthB.Bids[0].Price - depthA.Asks[0].Price > targetDiffPrice && Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount) >= minHedgeAmount) {   // B -> A market conditions are met
                var price = (depthB.Bids[0].Price + depthA.Asks[0].Price) / 2
                var amount = Math.min(depthB.Bids[0].Amount, depthA.Asks[0].Amount)
                if (nowAccs[1].Stocks > minHedgeAmount && nowAccs[0].Balance / price > minHedgeAmount) {
                    amount = Math.min(amount, nowAccs[1].Stocks, nowAccs[0].Balance / price, maxHedgeAmount)
                    Log("trigger B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks)  // Tips
                    hedge(exA, exB, price, amount)
                    cancelAll()
                    lastKeepBalanceTS = 0
                    isTrade = true 
                }            
            }
    

    Hedging trigger conditions are as follows:

    1. Meet the hedging spread first, and only when the spread of the order meets the set spread parameters can it be hedged.

    2. The amount that can be hedged in the market must meet the minimum hedging amount set in the parameters. Because the minimum order amount that may be limited by different exchanges is different, the smallest of the two should be taken.

    3. The assets in the exchange of the sell operation are enough to sell, and the assets in the exchange of the buy operation are enough to buy. When these conditions are met, execute the hedging function to place a hedging order. Before the main function, we declare a variable isTrade in advance to mark whether hedging occurs. Here, if the hedging is triggered, the variable is set to true. And reset the global variable lastKeepBalanceTS to 0 (lastKeepBalanceTS is used to mark the timestamp of the last balancing operation, setting it to 0 will trigger the balancing operation immediately), and then cancel all pending orders.

  • Balancing operation

            if (ts - lastKeepBalanceTS > keepBalanceCyc * 1000) {
                nowAccs = _C(updateAccs, exchanges)
                var isBalance = keepBalance(initAccs, nowAccs, [depthA, depthB])
                cancelAll()
                if (isBalance) {
                    lastKeepBalanceTS = ts
                    if (isTrade) {
                        var nowBalance = _.reduce(nowAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
                        var initBalance = _.reduce(initAccs, function(sumBalance, acc) {return sumBalance + acc.Balance}, 0)
                        LogProfit(nowBalance - initBalance, nowBalance, initBalance, nowAccs)
                        isTrade = false 
                    }                
                }            
            }
    

    It can be seen that the balancing function is executed periodically, but if the lastKeepBalanceTS is reset to 0 after the hedging operation is triggered, the balancing operation will be triggered immediately. The profit will be calculated after a successful balancing.

  • Status bar information

            LogStatus(_D(), "A->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, " B->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, " targetDiffPrice:", targetDiffPrice, "\n", 
                "current A, Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n", 
                "current B, Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n", 
                "initial A, Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n", 
                "initial B, Stocks:", initAccs[1].Stocks, "FrozenStocks:", initAccs[1].FrozenStocks, "Balance:", initAccs[1].Balance, "FrozenBalance", initAccs[1].FrozenBalance)
    

    The status bar is not particularly complex in design. It displays the current time, the price difference from Exchange A to Exchange B and the price difference from Exchange B to Exchange A. And it displays the current hedge target spread, the asset data of the exchange A account and the exchange B account.

Handling of trading pairs of different denominated currencies

In terms of parameters, we designed the conversion rate value parameter, and we also designed the exchange rate conversion in the initial operation of the main function at the beginning of the strategy. It should be noted that the SetRate exchange rate conversion function needs to be executed first. Because this function affects two aspects:

  • Price conversion in all market data, order data, and position data.

  • The conversion of the denominated currency in the account assets. For example, the current trading pair is BTC_USDT, the price unit is USDT, and the available denomination currency in the account assets is also USDT. If I want to convert the value into CNY, set exchange.SetRate(6.8) in the code to convert the data obtained by all functions under the exchange exchange object to CNY. To convert to what denominated currency, pass in the exchange rate from the current denominated currency to the target denominated currency to the SetRate function.

Complete Strategy: Spot Hedging Strategy of Different Denominated Currencis (Tutorial)

From: https://blog.mathquant.com/2022/07/18/cryptocurrency-spot-hedging-strategy-design-1.html