LeeksReaper Strategy Analysis(1)

LeeksReaper Strategy Analysis(1)

·

12 min read

Recently, there is a hot discussion about the print money robot in FMZ Quant WeChat group. A very old strategy has entered the eyes of the Quants again: LeeksReaper.
The robot trading principle of print money is borrowed from the leeksreaper strategy, and I blame myself for not reading too much into the leeksreaper strategy at that time and not understanding it very well. So, I re-read the original strategy carefully again and looked at the transplanted version of the transplanted OKCoin leeksreaper on the FMZ Quant.
The strategy of transplanted leeksreaper based on FMZ Quant platform is analyzed to explore the idea of the strategy. So that platform users can learn the idea of this strategy.
In this article, we will analyze more from the aspects of strategy idea and intention to minimize the boring content related to programming.

Source code of [Transplanting OKCoin LeeksReaper] strategy:

pinefunction LeeksReaper() {
    var self = {}
    self.numTick = 0
    self.lastTradeId = 0
    self.vol = 0
    self.askPrice = 0
    self.bidPrice = 0
    self.orderBook = {Asks:[], Bids:[]}
    self.prices = []
    self.tradeOrderId = 0
    self.p = 0.5
    self.account = null
    self.preCalc = 0
    self.preNet = 0

    self.updateTrades = function() {
        var trades = _C(exchange.GetTrades)
        if (self.prices.length == 0) {
            while (trades.length == 0) {
                trades = trades.concat(_C(exchange.GetTrades))
            }
            for (var i = 0; i < 15; i++) {
                self.prices[i] = trades[trades.length - 1].Price
            }
        }
        self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) {
            // Huobi not support trade.Id
            if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) {
                self.lastTradeId = Math.max(trade.Id == 0 ? trade.Time : trade.Id, self.lastTradeId)
                mem += trade.Amount
            }
            return mem
        }, 0)

    }
    self.updateOrderBook = function() {
        var orderBook = _C(exchange.GetDepth)
        self.orderBook = orderBook
        if (orderBook.Bids.length < 3 || orderBook.Asks.length < 3) {
            return
        }
        self.bidPrice = orderBook.Bids[0].Price * 0.618 + orderBook.Asks[0].Price * 0.382 + 0.01
        self.askPrice = orderBook.Bids[0].Price * 0.382 + orderBook.Asks[0].Price * 0.618 - 0.01
        self.prices.shift()
        self.prices.push(_N((orderBook.Bids[0].Price + orderBook.Asks[0].Price) * 0.35 +
            (orderBook.Bids[1].Price + orderBook.Asks[1].Price) * 0.1 +
            (orderBook.Bids[2].Price + orderBook.Asks[2].Price) * 0.05))
    }
    self.balanceAccount = function() {
        var account = exchange.GetAccount()
        if (!account) {
            return
        }
        self.account = account
        var now = new Date().getTime()
        if (self.orderBook.Bids.length > 0 && now - self.preCalc > (CalcNetInterval * 1000)) {
            self.preCalc = now
            var net = _N(account.Balance + account.FrozenBalance + self.orderBook.Bids[0].Price * (account.Stocks + account.FrozenStocks))
            if (net != self.preNet) {
                self.preNet = net
                LogProfit(net)
            }
        }
        self.btc = account.Stocks
        self.cny = account.Balance
        self.p = self.btc * self.prices[self.prices.length-1] / (self.btc * self.prices[self.prices.length-1] + self.cny)
        var balanced = false

        if (self.p < 0.48) {
            Log("Start balance", self.p)
            self.cny -= 300
            if (self.orderBook.Bids.length >0) {
                exchange.Buy(self.orderBook.Bids[0].Price + 0.00, 0.01)
                exchange.Buy(self.orderBook.Bids[0].Price + 0.01, 0.01)
                exchange.Buy(self.orderBook.Bids[0].Price + 0.02, 0.01)
            }
        } else if (self.p > 0.52) {
            Log("Start balance", self.p)
            self.btc -= 0.03
            if (self.orderBook.Asks.length >0) {
                exchange.Sell(self.orderBook.Asks[0].Price - 0.00, 0.01)
                exchange.Sell(self.orderBook.Asks[0].Price - 0.01, 0.01)
                exchange.Sell(self.orderBook.Asks[0].Price - 0.02, 0.01)
            }
        }
        Sleep(BalanceTimeout)
        var orders = exchange.GetOrders()
        if (orders) {
            for (var i = 0; i < orders.length; i++) {
                if (orders[i].Id != self.tradeOrderId) {
                    exchange.CancelOrder(orders[i].Id)
                }
            }
        }
    }

    self.poll = function() {
        self.numTick++
        self.updateTrades()
        self.updateOrderBook()
        self.balanceAccount()

        var burstPrice = self.prices[self.prices.length-1] * BurstThresholdPct
        var bull = false
        var bear = false
        var tradeAmount = 0
        if (self.account) {
            LogStatus(self.account, 'Tick:', self.numTick, ', lastPrice:', self.prices[self.prices.length-1], ', burstPrice: ', burstPrice)
        }

        if (self.numTick > 2 && (
            self.prices[self.prices.length-1] - _.max(self.prices.slice(-6, -1)) > burstPrice ||
            self.prices[self.prices.length-1] - _.max(self.prices.slice(-6, -2)) > burstPrice && self.prices[self.prices.length-1] > self.prices[self.prices.length-2]
            )) {
            bull = true
            tradeAmount = self.cny / self.bidPrice * 0.99
        } else if (self.numTick > 2 && (
            self.prices[self.prices.length-1] - _.min(self.prices.slice(-6, -1)) < -burstPrice ||
            self.prices[self.prices.length-1] - _.min(self.prices.slice(-6, -2)) < -burstPrice && self.prices[self.prices.length-1] < self.prices[self.prices.length-2]
            )) {
            bear = true
            tradeAmount = self.btc
        }
        if (self.vol < BurstThresholdVol) {
            tradeAmount *= self.vol / BurstThresholdVol
        }

        if (self.numTick < 5) {
            tradeAmount *= 0.8
        }

        if (self.numTick < 10) {
            tradeAmount *= 0.8
        }

        if ((!bull && !bear) || tradeAmount < MinStock) {
            return
        }
        var tradePrice = bull ? self.bidPrice : self.askPrice
        while (tradeAmount >= MinStock) {
            var orderId = bull ? exchange.Buy(self.bidPrice, tradeAmount) : exchange.Sell(self.askPrice, tradeAmount)
            Sleep(200)
            if (orderId) {
                self.tradeOrderId = orderId
                var order = null
                while (true) {
                    order = exchange.GetOrder(orderId)
                    if (order) {
                        if (order.Status == ORDER_STATE_PENDING) {
                            exchange.CancelOrder(orderId)
                            Sleep(200)
                        } else {
                            break
                        }
                    }
                }
                self.tradeOrderId = 0
                tradeAmount -= order.DealAmount
                tradeAmount *= 0.9
                if (order.Status == ORDER_STATE_CANCELED) {
                    self.updateOrderBook()
                    while (bull && self.bidPrice - tradePrice > 0.1) {
                        tradeAmount *= 0.99
                        tradePrice += 0.1
                    }
                    while (bear && self.askPrice - tradePrice < -0.1) {
                        tradeAmount *= 0.99
                        tradePrice -= 0.1
                    }
                }
            }
        }
        self.numTick = 0
    }
    return self
}

function main() {
    var reaper = LeeksReaper()
    while (true) {
        reaper.poll()
        Sleep(TickInterval)
    }
}

Strategy overview

Generally, when you get a strategy to study, you shall take a look at the overall program structure first. The strategy code is not very long, with less than 200 lines of code, it's very concise, and the original strategy is highly restored, almost the same. The strategy code runs from the main() function. The entire strategy code, except main(), is a function named LeeksReaper(). The LeeksReaper() function is very easy to understood, it can be understood as the constructor of the leeksreaper strategy logic module (an object). In short, LeeksReaper() is responsible for constructing a leeksreaper trading logic.

Keywords:

· The first line of strategy main function:
var reaper = LeeksReaper(), the code declares a local variable reaper and then calls the LeeksReaper() function to construct a strategy logic object that assigns a value to reaper.

The next step of strategy main function:

javascriptwhile (true) {
    reaper.poll()
    Sleep(TickInterval)
}

Enter a while endless loop and keep executing the processing function poll() of the reaper object, the poll() function is exactly where the main logic of the trading strategy lies and the whole strategy program starts executing the trading logic over and over again.
As for the line Sleep(TickInterval), it is easy to understood, it is to control the pause time after each execution of the overall trading logic, with the purpose of controlling the rotation frequency of the trading logic.

Analyse LeeksReaper() constructor

Look at how the LeeksReaper() function constructs a strategy logic object.

The LeeksReaper() function starts by declaring an empty object, var self = {}, and during the execution of the LeeksReaper() function will gradually add some methods and attributes to this empty object, finally completing the construction of this object and returning it (that is, the step of main() function inside the var reaper = LeeksReaper(), the returned object is assigned to reaper).

Add attributes to the self object
Next, I added a lot of attributes to self. I will describe each attribute as follows, which can understand the purpose and intention of these attributes and variables quickly, facilitate the understanding of strategies, and avoid being confused when seeing the code.

python    self.numTick = 0         # It is used to record the number of transactions not triggered when the poll function is called. When the order is triggered and the order logic is executed, self.numTick is reset to 0
    self.lastTradeId = 0     # The transaction record ID of the order that has been transacted in the transaction market. This variable records the current transaction record ID of the market
    self.vol = 0             # Reference to the trading volume of each market inspection after weighted average calculation (market data is obtained once per loop, which can be interpreted as a time of market inspection)
    self.askPrice = 0        # The bill of lading price of the sales order can be understood as the price of the listing order after the strategy is calculated
    self.bidPrice = 0        # Purchase order bill of lading price
    self.orderBook = {Asks:[], Bids:[]}    # Record the currently obtained order book data, that is, depth data (sell one... sell n, buy one... buy n)
    self.prices = []                       # An array that records the prices on the time series after the calculation of the first three weighted averages in the order book, which means that each time the first three weighted averages of the order book are stored, they are placed in an array and used as a reference for subsequent strategy trading signals, so the variable name is prices, in plural form, indicating a set of prices
    self.tradeOrderId = 0    # Record the order ID after the current bill of lading is placed
    self.p = 0.5             # Position proportion: when the value of currency accounts for exactly half of the total asset value, the value is 0.5, that is, the equilibrium state
    self.account = null      # Record the account asset data, which is returned by the GetAccount() function
    self.preCalc = 0         # Record the timestamp of the last time when the revenue was calculated, in milliseconds, to control the frequency of triggering the execution of the revenue calculation code
    self.preNet = 0          # Record current return values

Add methods to self objects

After adding these attributes to self, start adding methods to the self object so that this object can do some work and have some functions.

The first function added:

go    self.updateTrades = function() {
        var trades = _C(exchange.GetTrades)  # Call the FMZ encapsulated interface GetTrades to obtain the latest market transaction data
        if (self.prices.length == 0) {       # When self.prices.length == 0, the self.prices array needs to be filled with numeric values, which will be triggered only when the strategy starts running
            while (trades.length == 0) {     # If there is no recent transaction record in the market, the while loop will keep executing until the latest transaction data is available and update the trades variable
                trades = trades.concat(_C(exchange.GetTrades))   # concat is a method of JS array type, which is used to concatenate two arrays, here is to concatenate the "trades" array and the array data returned by "_C(exchange.GetTrades)" into one array
            }
            for (var i = 0; i < 15; i++) {   # Fill in data to self.prices, and fill in 15 pieces of latest transaction prices
                self.prices[i] = trades[trades.length - 1].Price
            }
        }
        self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) {  # _. Reduce function is used for iterative calculation to accumulate the amount of the latest transaction records
            // Huobi not support trade.Id
            if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) {
                self.lastTradeId = Math.max(trade.Id == 0 ? trade.Time : trade.Id, self.lastTradeId)
                mem += trade.Amount
            }
            return mem
        }, 0)

    }

The function updateTrades is to get the latest market transaction data and do some calculations based on the data and record it for using in the subsequent logic of the strategy.
The line-by-line comments I wrote in the code above directly.
For _.reduce, someone who have no programming basic learning may be confused. Let's talk about it briefly, _.reduce is a function of the Underscore.js library. The FMZJS strategy supports this library, so it is very convenient for iterative calculation. The Underscore.js data link (https://underscorejs.net/#reduce)

The meaning is also very simple, for exmaple:

pythonfunction main () {
   var arr = [1, 2, 3, 4]
   var sum = _.reduce(arr, function(ret, ele){
       ret += ele

       return ret
   }, 0)

   Log("sum:", sum)    # sum = 10
}

That is, add up each number in the array [1, 2, 3, 4]. Back to our strategy, we add up the trading volume values of each transaction record data in the trades array. Get a total of the latest transaction volume self.vol = 0.7 * self.vol + 0.3 * _.reduce (...), here we use ... to replace the code. It is not difficult to see the calculation of self.vol is also a weighted average. That is, the newly generated trading volume accounts for 30% of the total, and the last weighted trading volume accounts for 70%. This ratio was set by the strategy author artificially and it may be related to the market rules.
As for your question, what if the interface to obtain the latest transaction data returned to the duplicate old data, then the data I got was wrong, and won't it be meaningful? Don't worry. This problem was considered in the strategy design, so the code has:

pythonif ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) {
    ...
}

the judgement. It can be judged based on the transaction ID in the transaction record. Accumulation is triggered only when the ID is greater than the ID of the last record, or if the exchange interface does not provide an ID, that is, trade.Id == 0, use the timestamp in the transaction record to judge. At this time, self.lastTradeId stores the timestamp of the transaction record instead of the ID.

The second function added:

python    self.updateOrderBook = function() {
        var orderBook = _C(exchange.GetDepth)
        self.orderBook = orderBook
        if (orderBook.Bids.length < 3 || orderBook.Asks.length < 3) {
            return
        }
        self.bidPrice = orderBook.Bids[0].Price * 0.618 + orderBook.Asks[0].Price * 0.382 + 0.01
        self.askPrice = orderBook.Bids[0].Price * 0.382 + orderBook.Asks[0].Price * 0.618 - 0.01
        self.prices.shift()
        self.prices.push(_N((orderBook.Bids[0].Price + orderBook.Asks[0].Price) * 0.35 +
            (orderBook.Bids[1].Price + orderBook.Asks[1].Price) * 0.1 +
            (orderBook.Bids[2].Price + orderBook.Asks[2].Price) * 0.05))
    }

Next, let's look at the function updateOrderBook. From the name of the function, we can see that it is used to update the order book. However, it doesn't update the order book only. The function starts to call the FMZ API function GetDepth() to obtain the current market order book data (sell one... sell n, buy one... buy n), and record the order book data in self.orderBook. Next, judge if the purchase order and sales order of the order book data are less than 3, if so, the invalid function will be returned directly.

After that, two pieces of data are calculated:

· Calculate the bill of lading price
The bill of lading price is also calculated by using the weighted average method. When calculating the purchase order, the weight given to the purchase price closest to the transaction price is 61.8% (0.618), and the weight given to the selling price closest to the transaction price is 38.2% (0.382)
When calculating the bill of lading bill of sale price, the same weight is given to the selling price closest to the transaction price. As for why is 0.618, it may be that the author prefers the golden section ratio. As for the last price (0.01), it is to offset to the center of the opening slightly.

· Update the weighted average price of the first three level of the order book on the time series
For the first three levels of purchase and sales order prices in the order book, the weighted average is calculated. The weight of the first level is 0.7, the weight of the second level is 0.2, and the weight of the third level is 0.1. Someone may say, "Oh, no, there are 0.7, 0.2, 0.1 in the code."
Let's expand the calculation:

pine(Buy one+Sell one) * 0.35+(Buy two+Sell two) * 0.1+(Buy three+Sell three) * 0.05
->
(Buy one+sell one)/2 * 2 * 0.35+(Buy two+sell two)/2 * 2 * 0.1+(Buy three+sell three)/2 * 2 * 0.05
->
(Buy one+sell one)/2 * 0.7+(Buy two+sell two)/2 * 0.2+(Buy three+sell three)/2 * 0.1
->
Average price of the first level * 0.7+average price of the second level * 0.2+average price of the third level * 0.1

As we can see here, the final calculated price is actually a response to the price position of the middle of the third opening in the current market.
Then use this calculated price to update the array self.prices, kicking out one of the oldest data (through the shift() function) and updating one of the newest data into it(through the push() function, shift and push functions are methods of the JS language array object, you can check the JS data for details). Thus forming the array self.prices, which is a stream of data with a time series order.

So let's have a rest here, and we will see you next issue ~

From: https://blog.mathquant.com/2022/11/04/leeksreaper-strategy-analysis1.html