Recently, there are many Martingale strategies discussed in the FMZ official group, and there are not many Martingale strategies of cryptocurrency contracts on the platform. Therefore, I took this opportunity to design a simple Martingale strategy for cryptocurrency futures. Why is it called a Martingale strategy? Because the potential risks of the Martin strategy are indeed not small, it is not designed exactly according to the Martin strategy. However, this type of strategy still has a lot of risks, and the parameter settings of the Martin-type strategy are closely related to the risk, and the risk should not be ignored.

This article mainly explains and learns from the design of Martin-type strategies. Since the strategy idea is very clear, as a user of FMZ, we consider strategy design more.

Obtain total equity

Total equity is often used when designing cryptocurrency futures strategies. This is because returns have to be calculated, especially when you need to calculate floating returns. Since the position is occupied with margin, the pending order is also occupied. At this time, the API interface exchange.GetAccount() of the FMZ platform is called to obtain the available assets and pending order frozen assets. In fact, most cryptocurrency futures exchanges provide the data of total equity, but this attribute is not uniformly packaged on FMZ.

So we design functions to obtain this data according to different exchanges:

// OKEX V5 obtain total equity
function getTotalEquity_OKEX_V5() {
    var totalEquity = null 
    var ret = exchange.IO("api", "GET", "/api/v5/account/balance", "ccy=USDT")
    if (ret) {
        try {
            totalEquity = parseFloat(ret.data[0].details[0].eq)
        } catch(e) {
            Log("failed to obtain the total equity of the account!")
            return null
        }
    }
    return totalEquity
}

// Binance futures
function getTotalEquity_Binance() {
    var totalEquity = null 
    var ret = exchange.GetAccount()
    if (ret) {
        try {
            totalEquity = parseFloat(ret.Info.totalWalletBalance)
        } catch(e) {
            Log("failed to obtain the total equity of the account!")
            return null
        }
    }
    return totalEquity
}

The totalEquity in the code is the total equity we need. Then we write a function as the call entry, and call the corresponding function according to the name of the exchange.

function getTotalEquity() {
    var exName = exchange.GetName()
    if (exName == "Futures_OKCoin") {
        return getTotalEquity_OKEX_V5()
    } else if (exName == "Futures_Binance") {
        return getTotalEquity_Binance()
    } else {
        throw "This exchange is not supported"
    }
}

Design some auxiliary functions

Before designing the main function and main logic, we need to do some preparations and design some auxiliary functions.

  • Cancel all current pending orders
  function cancelAll() {
      while (1) {
          var orders = _C(exchange.GetOrders)
          if (orders.length == 0) {
              break
          }
          for (var i = 0 ; i < orders.length ; i++) {
              exchange.CancelOrder(orders[i].Id, orders[i])
              Sleep(500)
          }
          Sleep(500)
      }
  }

This function is familiar to those who often read the strategy example code on the FMZ strategy square, and many strategies have used similar designs. The function is to get the current pending order list, and then cancel them one by one.

  • Placement operations for futures
  function trade(distance, price, amount) {
      var tradeFunc = null 
      if (distance == "buy") {
          tradeFunc = exchange.Buy
      } else if (distance == "sell") {
          tradeFunc = exchange.Sell
      } else if (distance == "closebuy") {
          tradeFunc = exchange.Sell
      } else {
          tradeFunc = exchange.Buy
      }
      exchange.SetDirection(distance)
      return tradeFunc(price, amount)
  }

  function openLong(price, amount) {
      return trade("buy", price, amount)
  }

  function openShort(price, amount) {
      return trade("sell", price, amount)
  }

  function coverLong(price, amount) {
      return trade("closebuy", price, amount)
  }

  function coverShort(price, amount) {
      return trade("closesell", price, amount)
  }

There are four directions for futures trading: openLong, openShort, coverLong andcoverShort. So we designed four order functions corresponding to these operations. If you consider only the order, then there are several necessary factors: direction, order price and order volume.
So we also designed a function named: trade to handle the operation when distance, price, amount are specified.
The function calls to openLong, openShort, coverLong and coverShort are ultimately completed by the trade function, that is, placing an order on a futures exchange based on the established distance, price, and quantity.

Main function

The strategy idea is very simple, take the current price as the baseline, and place sell (short) and buy orders (long) at a certain distance up or down. Once the transaction is completed, all remaining orders will be cancelled, and then a new closing order will be placed at a certain distance according to the price of the position, and an increase order will be placed at the updated current price, but the order volume will not be doubled for additional positions.

  • Initial work
    Because of the pending order, we need two global variables to record the order ID.
  var buyOrderId = null
  var sellOrderId = null

Then the strategy interface parameters are designed to use the OKEX_V5 simulated bot option, so some processing needs to be done in the code:

  var exName = exchange.GetName()    
  // Switch OKEX V5 simulated bot
  if (isSimulate && exName == "Futures_OKCoin") {
      exchange.IO("simulate", true)
  }

There is also an option to reset all information in the interface parameters, so there should be corresponding processing in the code:

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

We only run perpetual contracts, so the writing is fixed here and set to perpetual only.

  exchange.SetContractType("swap")

Then we also need to consider the accuracy of the order price and the order amount. If the accuracy is not set properly, the accuracy will be lost during the strategy calculation process. If the data has a large number of decimal places, it is easy to cause the order to be rejected by the exchange interface.

  exchange.SetPrecision(pricePrecision, amountPrecision)
  Log("set precision", pricePrecision, amountPrecision)

Simple data recovery by design

  if (totalEq == -1 && !IsVirtual()) {
      var recoverTotalEq = _G("totalEq")
      if (!recoverTotalEq) {
          var currTotalEq = getTotalEquity()
          if (currTotalEq) {
              totalEq = currTotalEq
              _G("totalEq", currTotalEq)
          } else {
              throw "failed to obtain initial equity"
          }
      } else {
          totalEq = recoverTotalEq
      }
  }

If you want to specify the initial total equity of the account when the strategy is running, you can set the parameter totalEq. If this parameter is set to -1, the strategy will read the stored total equity data. If there is no stored total equity data, the current read total equity is used as the initial total equity of the strategy running progress. After that, an increase in total equity indicates a profit, and a decrease in total equity indicates a loss. If the total equity data is read, the strategy will continue to run with this data.

  • main logic
    After the initial work is done, finally we came to the main logic part of the strategy. For the convenience of explanation, I wrote the instructions directly on the code comments.
    while (1) {                                  // The main logic of the strategy is designed as an infinite loop
        var ticker = _C(exchange.GetTicker)      // Read the current market information first, mainly using the latest transaction price
        var pos = _C(exchange.GetPosition)       // Read current position data
        if (pos.length > 1) {                    // Judging the position data, because of the logic of this strategy, it is unlikely that long and short positions will appear at the same time, so if there are long and short positions at the same time, an error will be thrown
            Log(pos)
            throw "Simultaneous long and short positions"                  // Throw an error to stop the strategy
        }
        //Depends on status
        if (pos.length == 0) {                    // Make different operations according to the position status, when there is no position, pos.length == 0 
            // If you have not held a position, count the profit once
            if (!IsVirtual()) {
                var currTotalEq = getTotalEquity()
                if (currTotalEq) {
                    LogProfit(currTotalEq - totalEq, "current total equity:", currTotalEq)
                }
            }

            buyOrderId = openLong(ticker.Last - targetProfit, amount)       // Open a buy order for a long position
            sellOrderId = openShort(ticker.Last + targetProfit, amount)     // Open a short sell order
        } else if (pos[0].Type == PD_LONG) {   // For long positions, the position and quantity of pending orders are different
            var n = 1
            var price = ticker.Last
            buyOrderId = openLong(price - targetProfit * n, amount)
            sellOrderId = coverLong(pos[0].Price + targetProfit, pos[0].Amount)
        } else if (pos[0].Type == PD_SHORT) {   // For short positions, the position and quantity of pending orders are different
            var n = 1
            var price = ticker.Last
            buyOrderId = coverShort(pos[0].Price - targetProfit, pos[0].Amount)
            sellOrderId = openShort(price + targetProfit * n, amount)
        }

        if (!sellOrderId || !buyOrderId) {   // If one side of the pending order fails, cancel all pending orders and start over
            cancelAll()
            buyOrderId = null 
            sellOrderId = null
            continue
        } 

        while (1) {  // The pending order is completed, start monitoring the order
            var isFindBuyId = false 
            var isFindSellId = false
            var orders = _C(exchange.GetOrders)
            for (var i = 0 ; i < orders.length ; i++) {
                if (buyOrderId == orders[i].Id) {
                    isFindBuyId = true 
                }
                if (sellOrderId == orders[i].Id) {
                    isFindSellId = true 
                }               
            }
            if (!isFindSellId && !isFindBuyId) {    // Detected that both buy and sell orders have been filled
                cancelAll()
                break
            } else if (!isFindBuyId) {   // Detected buy order closing
                Log("buy order closing")
                cancelAll()
                break
            } else if (!isFindSellId) {  // Detected sell order closing
                Log("sell order closing")
                cancelAll()
                break
            }
            LogStatus(_D())
            Sleep(3000)
        }
        Sleep(500)
    }

The whole logic and design are explained.

Backtesting

Let the strategy go through a May 19 market.

It can be seen that the Martingale strategy still has certain risks.

The real bot can be run with the OKEX V5 simulation bot

Strategy address: https://www.fmz.com/strategy/294957

Strategies are mainly used for learning, and real money should be used with caution~!

Leave a Reply

Your email address will not be published. Required fields are marked *