Cryptocurrency Spot Hedge Strategy (1)

Author: Ninabadass, Created: 2022-04-14 11:57:46, Updated: 2022-04-14 16:32:56

Cryptocurrency Spot Hedge Strategy (1)

For beginners in strategy designing, the hedge strategy is a very good one for practice. This article implements a simple but solid cryptocurrency spot hedge strategy, hoping to allow beginners to learn some design experience.

Design Functions & Parameters by Strategy Requirement

First of all, we need to make sure that the strategy to be designed is a cryptocurrency spot hedge strategy. We design the simplest hedge. We only sell on the platform with the higher price between the two spot platforms, and buy on the platform with the lower price to earn the price spread. When the platform with the higher price is full of quote currency symbols (because the price is high, all currency symbols are sold), or when the platform with the lower price is full of currency symbols (because the price is low, currency symbols are bought by all assets), it cannot be hedged. At this time, you can only wait for the price to reverse to hedge.

For the order price and amount during hedging, there are precision limits in every platform, and there is also a limit on the minimum order amount. In addition to the minimum limit, the strategy also needs to consider the maximum order amount for a hedge. If the order amount is too large, the market will not have an enough order volume for that. It is also necessary to consider how to convert the exchange rate if the two platforms have different quote currencies. The handling fee during hedging and the slippage of the order taker are all trading costs. Hedge does not always happen as long as there is a price difference. Therefore, the hedging price spread also has a trigger value. If it is lower than a certain price spread, the hedge will make a loss.

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

  • Hedge spread: hedgeDiffPrice; when the spread exceeds the value, a hedge will be triggered.
  • Minimum hedge amount: minHedgeAmount, the minimum order amount (symbol amount) available for a hedge.
  • Maximum hedge amount: maxHedgeAmount, the maximum order amount (symbol amount) available for a hedge.
  • Price precision A: pricePrecisionA, the order price precision (decimal digits) of platform A.
  • Order amount precision A: amountPrecisionA, the order amount precision (decimal digits) of platform A.
  • Price precision B: pricePrecisionB, the order price precision (decimal digits) of platform B.
  • Order amount precision B: amountPrecisionB, the order amount precision (decimal digits) of platform B.
  • Exchange rate A: rateA, the exchange rate converting of the first added exchange object; the default is 1,indicating not to convert.
  • Exchange rate B: rateB, the exchange rate converting of the second added exchange object; the default is 1,indicating not to convert.

The hedge strategy needs to keep the currency symbol amount of the two accounts unchanged (that is, not holding any directional positions, and maintaining neutral), so there needs to be a balance logic in the strategy to always detect the balance. When checking the balance, it is unavoidable to obtain the asset data from the two platforms. Therefore, We need to write a function for 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 an order, if there is no executed 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 hedge logic, so it is also necessary to design a function of canceling all orders.

  • 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 amount of currency symbols, we need to find the price with a certain amount in a certain depth data, so we need a function like this 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 concurrently place 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 more 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 currency spread 
        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("cannot balance")            
            } else {
                // balanced ordering 
                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("error:", "initAccs.length:", initAccs.length, "nowAccs.length:", nowAccs.length, "depths.length:", depths.length)
            return true 
        } else {
            return true 
        }
    }
    

These functions have been designed according to the strategy requirements, and we can start to design the main function of the strategy.

Strategy Main Function Design

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

  • Exchange Object Name For many operations in the strategy use exchange objects, such as getting market quotes, placing orders, and so on, So it would be inconvenient to use a longer name every time, my little trick is to use a simple short name instead, for example:

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

    Then, it will be more comfortable to write the code later.

  • Exchange Rate & Precision

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

    If one of the exchange rate parameters, namely rateA and rateB, is set to 1 (the default is 1), that is, rateA != 1 or rateB != 1 means not triggered, and the exchange rate cannot be converted.

  • Reset All Date

    img

    Sometimes, it is necessary to delete all logs and vacuum the data records when the strategy is started. You can design a strategy interface parameter isReset, and then 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")
      }
    
  • Recover the Initial Account Data and Update the Current Account Data In order to judge the balance, the strategy needs to continuously record the initial account asset condition for comparison with the current one. The variable nowAccs is used to record the current account data. Use the updateAccs function we just designed to get the account data of the current platform. initAccs is used to record the initial account status (data like currency symbol amount of both A and B, quote currency amount, etc.). For initAccs, first use the _G() function to restore (the _G function will record data persistently, and can return the recorded data again; read the API documentation for details: link).
    If you cannot query the data, use the current account information to assign and use _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 strategy logic execution, and the non-stop repeating execution constructs the strategy main loop. Let’s take a look at each execution flow of the program in the main loop.

  • Obtain the Market Quotes and Judge the Validity

          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 you can see that the concurrent function exchange.Go of FMZ platform is used to create concurrent objects depthARoutine and depthBRoutine that call the GetDepth() interface. When these two concurrent objects are created, the GetDepth() interface is called immediately, and both requests for the depth data are sent to the platform. Then, call the wait() method of objectdepthARoutine and object depthBRoutine to obtain the depth data. After obtaining the depth data, it is necessary to check the depth data to judge its validity. In the case of data exception, the continue statement is triggered to re-execute the main loop.

  • Use price spread or spread ratio?

          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.

    img

    The parameter diffAsPercentage has been added to the parameters of the strategy interface. The other two parameters, which will show or hide based on the parameter, are set as: hedgeDiffPrice@!diffAsPercentage; when diffAsPercentage is false, it will be shown. hedgeDiffPercentage@diffAsPercentage; when diffAsPercentage is true, it will be displayed.
    After the design, we have checked the diffAsPercentage parameter, which is to use the spread ratio as the hedge trigger condition. If the diffAsPercentage parameter is not checked, the price spread is used as the hedge trigger condition.

  • Judge Hedge Trigger

          if (depthA.Bids[0].Price - depthB.Asks[0].Price > targetDiffPrice && Math.min(depthA.Bids[0].Amount, depthB.Asks[0].Amount) >= minHedgeAmount) {          // A -> B market condition satisfied             
              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("triggerA->B:", depthA.Bids[0].Price - depthB.Asks[0].Price, price, amount, nowAccs[1].Balance / price, nowAccs[0].Stocks)  // prompt message 
                  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 condition satisfied 
              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("triggerB->A:", depthB.Bids[0].Price - depthA.Asks[0].Price, price, amount, nowAccs[0].Balance / price, nowAccs[1].Stocks)  // prompt message
                  hedge(exA, exB, price, amount)
                  cancelAll()
                  lastKeepBalanceTS = 0
                  isTrade = true 
              }            
          }
    

    There are several trigger conditions for hedge: 1.First, meet the hedge spread; only when the market spread meets the set spread parameter, can the hedge be possible.

    2.The hedge amount of the market should meet the minimum hedge amount set in the parameters. Because the minimum order amount of different platforms are different, the smallest of the two should be taken.

    3.The assets in the platform with the selling operation are enough to sell, and the assets in the platform with the buying operation are enough to buy. When these conditions are met, execute the hedge function to place orders by hedge. Before the main function, we declare a variable isTrade in advance to mark whether the hedge occurs. Here, if the hedge 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 latest balance operation, and setting it to 0 will trigger the balance operation immediately), and then cancel all pending orders.

  • Balance 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 balance function is executed periodically, but if the lastKeepBalanceTS is reset to 0 after the hedge operation is triggered, the balance operation will be triggered immediately. After the balance is successful, the return will be calculated.

  • 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", 
              "currentA,Stocks:", nowAccs[0].Stocks, "FrozenStocks:", nowAccs[0].FrozenStocks, "Balance:", nowAccs[0].Balance, "FrozenBalance", nowAccs[0].FrozenBalance, "\n", 
              "currentB,Stocks:", nowAccs[1].Stocks, "FrozenStocks:", nowAccs[1].FrozenStocks, "Balance:", nowAccs[1].Balance, "FrozenBalance", nowAccs[1].FrozenBalance, "\n", 
              "initialA,Stocks:", initAccs[0].Stocks, "FrozenStocks:", initAccs[0].FrozenStocks, "Balance:", initAccs[0].Balance, "FrozenBalance", initAccs[0].FrozenBalance, "\n", 
              "initialB,Stocks:", initAccs[1].Stocks, "FrozenStocks:", initAccs[1].FrozenStocks, "Balance:", initAccs[1].Balance, "FrozenBalance", initAccs[1].FrozenBalance)
    

    The status bar is not designed to be particularly complicated. It displays the current time, the price spread from platform A to platform B as well as the price spread from B to A; it also displays the current hedge target spread, the account asset data of platform A, and the account asset data of platform B.

Trading Pair Processing for Different Quote Currency

In terms of parameters, we designed the parameter of converting exchange rate value, and we have 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.

For the function will affect two aspects:

  • Price conversion in all market quote data, order data, and position data.
  • Conversion of quote currencies in account assets.

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

Complete strategy: Spot Hedge Strategy of Different Quote Currency (Teaching)


More