Novice, Check it Out —— Take You to Cryptocurrency Quantitative Trading (6)

Author: Ninabadass, Created: 2022-04-21 18:13:03, Updated: 2022-04-22 12:00:05

Novice, Check it Out —— Take You to Cryptocurrency Quantitative Trading (6)

In the last article, we made a simple grid strategy together. In this article, we upgraded and expanded this strategy into a multi-symbol spot grid strategy, and let this strategy be tested in practice. The purpose is not to find a “holy grail”, but to discuss various problems and solutions in the process of designing strategies. This article will explain some of my experience in designing the strategy. The content of this article is slightly complicated and requires a certain foundation in programming.

Thinking of Design Based on Strategy Requirement

In this article, same as the previous one, we discuss about the design based on FMZ Quant Trading Platform (FMZ.COM).

  • Multiple Symbol To be honest, I want the grid strategy to do not only BTC_USDT, but also LTC_USDT/EOS_USDT/DOGE_USDT/ETC_USDT/ETH_USDT. Anyway, for the spot trading pairs, operate grid trading of all symbols that you want to trade at the same time.

    yeah, it feels good to capture the vibrating market quotes of multiple symbols.
    Although the requirement sounds simple, it becomes difficult when you start to design.

      1. First, obtaining the market quotes of multiple symbols. It is the first problem to be solved. After reading the platform API documentation, I found that platforms usually provide the aggregated interfaces. Okay, we use the aggregated market data interface to obtain the data.
      1. The second problem is the account assets. For we want to perform multi-symbol strategies, we should consider to separately manage the assets of each trading pair, and obtain all asset data and record it once. Why should we obtain the account asset data? And why also record each trading pair separately?

    Because you need to judge the available assets when placing orders, isn’t necessary to obtain the data before judgement? Moreover, the return needs to be calculated. Should we record the initial acocunt asset data first? And then obtain the asset data of the current account and calculate the profit and loss by comparing with the initial one? Fortunately, the asset account interface of a platform usually returns all currency asset data, so we only need to obtain it once, and then process the data.

      1. Strategy parameter design. The parameter design of multi-symbol strategy is quite different from the parameter design of single-symbol one, because even the trading logic of each symbol in the multi-symbol strategy is the same, it is possible that the parameters of each symbol during trading are different. For example, in the grid strategy, you may want to trade 0.01 BTC each time when doing BTC_USDT trading pair, but it is obviously inappropriate to use this parameter (trading 0.01 currency) when doing DOGE_USDT. Of course, you can also process by the amount of USDT. But there will still be problems. What if you want to trade 1000U by BTC_USDT and 10U by DOGE_USDT? The demand can never be satisfied. Probably, some students might think of the question, and propose that, “I can set more groups of parameters, and separately control the parameters of different trading pairs to be operated.” That still cannot flexibly meet the need, for how many groups of parameters should be set? We set 3 groups; what if we want to operate 4 symbols? Do we need to modify the strategy and increase parameters? Therefore, fully think about the need for differentiation when designing multi-symbol strategy parameters. One solution is to design the parameters into common strings or JSON strings.
        For example:
      ETHUSDT:100:0.002|LTCUSDT:20:0.1
      

      “|” is used to split the data of each symbol, indicating that ETHUSDT:100:0.002 controls the trading pair ETH_USDT, and LTCUSDT:20:0.1 controls the trading pair LTC_USDT. The “|” in the middle plays a role of segmentation. In ETHUSDT:100:0.002, “ETHUSDT” represents the trading pair you want to operate; “100” is the grid spacing; “0.002” is the traded ETH amount of each grid; “:” is used to split the data mentioned above (surely, the rules of parameters are made by the strategy designer; you can design whatever you want based on your need).
      These strings already contain the parameter information of each symbol you need to operate. You can parse the strings, and assign values to the variables in the strategy, to control the trading logic of each symbol. How to parse? Let’s use the example mentioned above.

      function main() {
          var net = []  // the recorded grid parameters; when specifically running the grid trading logic, use the data from here 
          var params = "ETHUSDT:100:0.002|LTCUSDT:20:0.1"
          var arrPair = params.split("|")
          _.each(arrPair, function(pair) {
              var arr = pair.split(":")
              var symbol = arr[0]              // trading pair name 
              var diff = parseFloat(arr[1])    // grid spacing 
              var amount = parseFloat(arr[2])  // grid order amount 
              net.push({symbol : symbol, diff : diff, amount : amount})
          })
          Log("Grid parameter data:", net)
      }
      

      img

      See, we have parsed the parameters. Sure, you can directly use JSON strings, which is easier.

      function main() {        
          var params = '[{"symbol":"ETHUSDT","diff":100,"amount":0.002},{"symbol":"LTCUSDT","diff":20,"amount":0.1}]'
          var net = JSON.parse(params)  // the recorded grid parameters; when specifically running the grid trading logic, use the data from here         
          _.each(net, function(pair) {
              Log("Trading pair:", pair.symbol, pair)
          })
      }
      

      img

      1. Data Persistence There is a big difference between a practical strategy and a teaching strategy. The teaching strategy in last article is just for the initial test of the strategy logic and deign. There are more problems to concern about when actually running a strategy in a bot. When running a bot, the bot can be started and stopped. At that time, all the data during the running of the bot will be lost. So, how to continue the previous status when restarting the bot, after the bot is stopped? Here, it is necessary to persistently save the key data when the bot is running, so that the data can be read and the bot continues to run when the bot is restarted. You can use the _G() function on FMZ Quant, or use the operation function DBExec() in the database, and you can query the FMZ API documentation for details.

      For example, we want to design a clean-up function by using the function _G(), to save the grid data.

      var net = null 
      function main() {  // strategy main function 
          // first read the stored net 
          net = _G("net")
          
          // ...
      }
      
      function onExit() {
          _G("net", net)
          Log("Execute the clean-up processing, and save the data", "#FF0000")
      }
      
      function onexit() {    // the onexit function defined by the platform system, which will be triggered when clicking the bot to stop 
          onExit()
      }
      
      function onerror() {   // the onerror function defined by the platform system, which will be triggered when the program exception occurs 
          onExit()
      }
      
      1. Limits on Order Amount Precision, Order Price Precision, Minimum Order Volume, Minimum Order Amount, etc.

      The backtest system does not have such strict limits on the order volume and order precision; but in a bot, each platform has strict standards for the order price and order volume, and different trading pairs have different limits. Therefore, beginners often test OKEX in the backtest system. Once the strategy is run on a bot, there are various problems when a trade is triggered, and then the content of the error message is not read, and various crazy phenomena appear.

      For multi-symbol cases, the requirement is more complicated. For a single-symbol strategy, you can design a parameter to specify information such as precision. However, when you design a multi-symbol strategy, it is obvious that writing the information into a parameter will make the parameter very tedious.

      At this time, you need to check the API documentation of the platform to see if there are interfaces for trading pair related information in the documentation. If there are these interfaces, you can design an automatic access interface in the strategy to obtain information such as precision, and configure it into the trading pair information in the trade (in short, the precision is automatically obtained from the platform, and then adapted to the variable related to the strategy parameter).

      1. Adaptation of Different Platform Why the problem is mentioned in the end? The processing of all the problems we mentioned above will lead to the last problem. For we plan to use the aggregated market interface in the strategy, the adaptation of access the adaptation of the Because our strategy plans to use the aggregated market interface, solutions like accessing the platform trading pair precision and other data adaptation,as well as accessing the account information to process each trading pair separately, etc., will bring great differences due to different platforms. There are differences in interface call and differences in mechanism. For spot platforms, the difference is comparatively small if the grid strategy is extended to the futures version. The differences in the mechanism of each platform are even greater. One solution is to design an FMZ template library; write the design of implementing the differentiation in the library to reduce the coupling between the strategy itself and the platform. The disadvantage of that is that you need to write a template library, and in this template, specifically implement the differentiation based on each platform.

Design a Template Library

Based on the above analysis, we design a template library to reduce the coupling between strategy, platform mechanism and interface.
We can design the template library like this (part of the code is omitted):

function createBaseEx(e, funcConfigure) {
    var self = {}
    self.e = e 
    
    self.funcConfigure = funcConfigure
    self.name = e.GetName()
    self.type = self.name.includes("Futures_") ? "Futures" : "Spot"
    self.label = e.GetLabel()
    
    // the interfaces that need to be implemented 
    self.interfaceGetTickers = null   // create a function that asynchronously obtains the aggregated market quote threads
    self.interfaceGetAcc = null       // create a function that asynchronously obtains the account data threads 
    self.interfaceGetPos = null       // obtain positions 
    self.interfaceTrade = null        // create concurrent orders 
    self.waitTickers = null           // wait for the concurrent market quote data  
    self.waitAcc = null               // wait for the account concurrent data 
    self.waitTrade = null             // wait for order concurrent data
    self.calcAmount = null            // calculate the order amount according to the trading pair precision and other data 
    self.init = null                  // initialization; obtain the precision and other data 
    
    // execute the configuration function, to configure objects 
    funcConfigure(self)

    // detect whether all the interfaces arranged by configList can be implemented 
    _.each(configList, function(funcName) {
        if (!self[funcName]) {
            throw "interface" + funcName + "not implemented"
        }
    })
    
    return self
}

$.createBaseEx = createBaseEx
$.getConfigureFunc = function(exName) {
    dicRegister = {
        "Futures_OKCoin" : funcConfigure_Futures_OKCoin,    //  the implementation of OKEX Futures 
        "Huobi" : funcConfigure_Huobi,
        "Futures_Binance" : funcConfigure_Futures_Binance,
        "Binance" : funcConfigure_Binance,
        "WexApp" : funcConfigure_WexApp,                    // the implementation of wexApp
    }
    return dicRegister
}

In the template, implement the code writing aimed at a specific playform; take the FMZ simulated bot WexApp as an example:

function funcConfigure_WexApp(self) {
    var formatSymbol = function(originalSymbol) {
        // BTC_USDT
        var arr = originalSymbol.split("_")
        var baseCurrency = arr[0]
        var quoteCurrency = arr[1]
        return [originalSymbol, baseCurrency, quoteCurrency]
    }

    self.interfaceGetTickers = function interfaceGetTickers() {
        self.routineGetTicker = HttpQuery_Go("https://api.wex.app/api/v1/public/tickers")
    }

    self.waitTickers = function waitTickers() {
        var ret = []
        var arr = JSON.parse(self.routineGetTicker.wait()).data
        _.each(arr, function(ele) {
            ret.push({
                bid1: parseFloat(ele.buy), 
                bid1Vol: parseFloat(-1),
                ask1: parseFloat(ele.sell), 
                ask1Vol: parseFloat(-1),
                symbol: formatSymbol(ele.market)[0],
                type: "Spot", 
                originalSymbol: ele.market
            })
        })
        return ret 
    }

    self.interfaceGetAcc = function interfaceGetAcc(symbol, updateTS) {
        if (self.updateAccsTS != updateTS) {
            self.routineGetAcc = self.e.Go("GetAccount")
        }
    }

    self.waitAcc = function waitAcc(symbol, updateTS) {
        var arr = formatSymbol(symbol)
        var ret = null 
        if (self.updateAccsTS != updateTS) {
            ret = self.routineGetAcc.wait().Info
            self.bufferGetAccRet = ret 
        } else {
            ret = self.bufferGetAccRet
        }
        if (!ret) {
            return null 
        }        
        var acc = {symbol: symbol, Stocks: 0, FrozenStocks: 0, Balance: 0, FrozenBalance: 0, originalInfo: ret}
        _.each(ret.exchange, function(ele) {
            if (ele.currency == arr[1]) {
                // baseCurrency
                acc.Stocks = parseFloat(ele.free)
                acc.FrozenStocks = parseFloat(ele.frozen)
            } else if (ele.currency == arr[2]) {
                // quoteCurrency
                acc.Balance = parseFloat(ele.free)
                acc.FrozenBalance = parseFloat(ele.frozen)
            }
        })
        return acc
    }

    self.interfaceGetPos = function interfaceGetPos(symbol, price, initSpAcc, nowSpAcc) {
        var symbolInfo = self.getSymbolInfo(symbol)
        var sumInitStocks = initSpAcc.Stocks + initSpAcc.FrozenStocks
        var sumNowStocks = nowSpAcc.Stocks + nowSpAcc.FrozenStocks
        var diffStocks = _N(sumNowStocks - sumInitStocks, symbolInfo.amountPrecision)
        if (Math.abs(diffStocks) < symbolInfo.min / price) {
            return []
        }
        return [{symbol: symbol, amount: diffStocks, price: null, originalInfo: {}}]
    }

    self.interfaceTrade = function interfaceTrade(symbol, type, price, amount) {
        var tradeType = ""
        if (type == self.OPEN_LONG || type == self.COVER_SHORT) {
            tradeType = "bid"
        } else {
            tradeType = "ask"
        }
        var params = {
            "market": symbol,
            "side": tradeType,
            "amount": String(amount),
            "price" : String(-1),
            "type" : "market"
        }
        self.routineTrade = self.e.Go("IO", "api", "POST", "/api/v1/private/order", self.encodeParams(params))
    }

    self.waitTrade = function waitTrade() {
        return self.routineTrade.wait()
    }

    self.calcAmount = function calcAmount(symbol, type, price, amount) {
        // obtain the trading pair information 
        var symbolInfo = self.getSymbolInfo(symbol)
        if (!symbol) {
            throw symbol + ",trading pair information not found"
        }
        var tradeAmount = null 
        var equalAmount = null  // record the symbol amount  
        if (type == self.OPEN_LONG || type == self.COVER_SHORT) {
            tradeAmount = _N(amount * price, parseFloat(symbolInfo.pricePrecision))
            // detect the minimum trading amount 
            if (tradeAmount < symbolInfo.min) {
                Log(self.name, " tradeAmount:", tradeAmount, "less than", symbolInfo.min)
                return false 
            }
            equalAmount = tradeAmount / price
        } else {
            tradeAmount = _N(amount, parseFloat(symbolInfo.amountPrecision))
            // detect the minimum trading amount 
            if (tradeAmount < symbolInfo.min / price) {
                Log(self.name, " tradeAmount:", tradeAmount, "less than", symbolInfo.min / price)
                return false 
            }
            equalAmount = tradeAmount
        }
        return [tradeAmount, equalAmount]
    }

    self.init = function init() {   // the function that automatically processes conditions like precision, etc.  
        var ret = JSON.parse(HttpQuery("https://api.wex.app/api/v1/public/markets"))
        _.each(ret.data, function(symbolInfo) {
            self.symbolsInfo.push({
                symbol: symbolInfo.pair,
                amountPrecision: parseFloat(symbolInfo.basePrecision),
                pricePrecision: parseFloat(symbolInfo.quotePrecision),
                multiplier: 1,
                min: parseFloat(symbolInfo.minQty),
                originalInfo: symbolInfo
            })
        })        
    }
}

It will be very easy to use the template in the strategy:

function main() {
    var fuExName = exchange.GetName()
    var fuConfigureFunc = $.getConfigureFunc()[fuExName]
    var ex = $.createBaseEx(exchange, fuConfigureFunc)

    var arrTestSymbol = ["LTC_USDT", "ETH_USDT", "EOS_USDT"]
    var ts = new Date().getTime()
    
    // test to obtain the market quotes 
    ex.goGetTickers()
    var tickers = ex.getTickers()
    Log("tickers:", tickers)
    
    // test to obtain the account information 
    ex.goGetAcc(symbol, ts)
    
    _.each(arrTestSymbol, function(symbol) {        
        _.each(tickers, function(ticker) {
            if (symbol == ticker.originalSymbol) {
                // print the market quote data 
                Log(symbol, ticker)
            }
        })

        // print asset data 
        var acc = ex.getAcc(symbol, ts)
        Log("acc:", acc.symbol, acc)
    })
}

Strategy Bot

It is very simple to design and write the strategy based on the above template. The entire strategy has about over 300 lines of code. That implements a cryptocurrency spot multi-symbol grid strategy.

img

img

Right now, it has losses T_T, so the source code will not be provided.

There are several registration codes; if you are interested, you can try them in wexApp:

Purchase Address: https://www.fmz.com/m/s/284507
Registration Code:
adc7a2e0a2cfde542e3ace405d216731
f5db29d05f57266165ce92dc18fd0a30
1735dca92794943ddaf277828ee04c27
0281ea107935015491cda2b372a0997d
1d0d8ef1ea0ea1415eeee40404ed09cc

There was only over 200 USD, and when the bot was just started, it came across a great single sided market. It needs time to cover the loss. The biggest advantage of the spot grid strategy is: “feel safe to sleep!” The stability of the strategy is fine, and I have not modified it since 27th, May. I do not dare to try the futures grid strategy temporarily.


More