Profit Harvester Strategy Analysis (1)

Author: Ninabadass, Created: 2022-04-26 11:31:51, Updated:

Profit Harvester Strategy Analysis (1)

Recently, users in FMZ Quant WeChat group discussed about the bot of print money. The discussion was very hot, and a very old strategy has re-entered the vision of quants: profit Harvester. The bot trading principle of print money draws on the profit harvester strategy. I blame myself for not understanding the profit harvester strategy too well at that time. So, I read the original strategy again seriously, and also read the ported version: Porting OKCoin Profit Harvester. Taking the ported version of the profit harvester strategy of FMZ as example, we are going to analyze this strategy and dig out the ideas of the strategy, so that our platform users can learn this strategy idea. In this article, we analyze more from the level of strategic thinking, intention, etc., to minimize the boring content related to programming.

[Port OKCoin Profit Harvester] Strategy Source Code:

function 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 to 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 to 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)
    }
}

Overview of the Strategy

Generally, when you get a strategy to learn, when reading, first look at the overall program structure. The strategy code is not long, less than 200 lines; it can be said to be very simplified, and has a high restoration for the original version strategy, which is basically the same as this one. When the strategy code is running, it starts from the main() function. The whole strategy code, except main(), only has a function named LeeksReaper(). The LeeksReaper() function is also very easy to understand. This function can be understood as the constructor of the profit harvester strategy logic module (an object). Simply put, LeeksReaper() is responsible for constructing the trading logic of a profit harvester.

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

  • The following part of the main function:

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

    Enter a while infinite loop, continuously execute the processing function of the reaper object poll(), and the poll() function is the main logic of the strategy, so the entire strategy program begins to continuously execute the trading logic.

    As for the line of Sleep(TickInterval), it is easy to understand, which is to control the pause time after each execution of the overall trading logic, and whose purpose is to control the rotation frequency of the trading logic.

Analyze the Constructor LeeksReaper()

Let’s see how the function LeeksReaper() constructs an object in the strategy logic.

The LeeksReaper() function begins and declares a null object, var self = {}. During the execution of the LeeksReaper() function, this null object will be gradually modified by adding some methods and attributes in it. Finally, the construction of the object is completed, and the object is returned (that is, the var reaper = LeeksReaper() in the main() function, and the returned object is assigned to reaper).

Add attribute to the object self

Next, I added a lot of attributes to self. I will describe each attribute below. You can quickly understand the purpose and intent of these attributes and variables, which will make it easier to understand the strategy, and avoid being confused when you see the pile of code.

    self.numTick = 0         # it is used to record the number of times that the trading is not triggered when the poll function is called. When placing an order is triggered and the order logic is executed, reset self.numTick to 0
    self.lastTradeId = 0     # the trading record ID of the order that has been executed in the trading market; this variable records the current latest execution record ID in the market
    self.vol = 0             # after the weighted average calculation, the trading volume reference of the market at each inspection (the market data is obtained once per time of the loop, which can be understood as the inspection of the market once)
    self.askPrice = 0        # ask price of delivery order, which can be understood as the price of the pending sell order calculated by the strategy 
    self.bidPrice = 0        # bid price of delivery order
    self.orderBook = {Asks:[], Bids:[]}    # record the currently obtained order book data, that is, depth data (sell 1...sell n, buy 1...buy n)
    self.prices = []                       # an array that records the price in the time series after the weighted average calculation of the first three levels in the order book. Simply put, it is the weighted average price of the first three levels of the order book obtained by storing each time, and put them in an array as reference of the subsequent strategic trading signals. Therefore, the variable name is "prices", plural, which means a set of prices
    self.tradeOrderId = 0    # record the order ID after currently lading and ordering 
    self.p = 0.5             # position proportion; when the currency value is exactly half of the total asset value, the value of it is 0.5, which means the balanced state 
    self.account = null      # record the account asset data, which will be returned by the function GetAccount()
    self.preCalc = 0         # record the timestamp when calculating the return of the latest time, in milliseconds, which is used to control the frequency of triggering execution of the return calculation code
    self.preNet = 0          # record the current return value  

Add method to the object self

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

First Added Function:

    self.updateTrades = function() {
        var trades = _C(exchange.GetTrades)  # call the encapsulated interface "GetTrades" of FMZ, to obtain the currently latest execution data in the market
        if (self.prices.length == 0) {       # when self.prices.length == 0, you need to fill values in the array of self.prices, which is only triggered when the strategy is started 
            while (trades.length == 0) {     # if there is no latest execution record in the market, the while loop will run infinitely, until there is new execution data; the, update the variable "trades"
                trades = trades.concat(_C(exchange.GetTrades))   # "concat" is a method of JS array type, which is used to match two arrays; here we use it to match the array "trades" and the array returned by "_C(exchange.GetTrades)" into one array 
            }
            for (var i = 0; i < 15; i++) {   # fill in values for "self.prices"; fill 15 latest execution prices 
                self.prices[i] = trades[trades.length - 1].Price
            }
        }
        self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) {  # _.reduce function iteratively calculates the accumulated execution volume of the latest execution record 
            // 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 of updateTrades is to obtain the latest execution data in the market, and to do some calculations and record according to the data, and provide the results for use in the subsequent strategy logic. I have directly written the comments line by line in the code above. Students who may not have programming foundation will be confused about _.reduce, so here is a brief introduction. _.reduce is a function of Underscore.js library, supported by FMZJS strategy. Therefore, it is very convenient to use it to do iterative calculations. information link of Underscore.js

Very easy to understand, for example:

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

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

This is adding up each number in the arrays , namely [1, 2, 3, 4]. Back to our strategy, it is to add up the volume value of each execution record data in the trades array, resulting in a total of the latest execution volume. For self.vol = 0.7 * self.vol + 0.3 * _.reduce(...), allow me to replace that piece of code with .... It is not difficult to see here that the calculation of self.vol is also a weighted average. That is, the newly generated total execution volume accounts for 30%, and the previous execution volume calculated accounts for 70%. This ratio is artificially set by the strategy developer, which may be related to the observation of market rules.

As for your question —— “what should I do if the interface for obtaining the latest execution data returns me the duplicate old data? Then, the data obtained is all wrong, so is there any meaning to use?" Don’t worry, the strategy has been designed with that in mind, so the code has the following content:

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

The judgment can be based on the execution ID in the trading execution record. Only when the ID is bigger than the ID of last time, accumulation is triggered. Or, if the platform interface does not provide an ID, that is, trade.Id == 0, use the timestamp in the trading execution record to judge. At this time, what is stored by self.lastTradeId is the timestamp of the execution record, not the ID.

Second Added Function:

    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 updateOrderBook function. From the meaning of the function name, it can be seen that the function is to update the order book. However, it not just updates the order book. The function, at first, calls the FMZ API function GetDepth() to obtain the current market order book data (sell 1…sell n, buy 1…buy n), and record the order book data in in self.orderBook. Next, it judges if the level of the order book data for buy orders and sell orders is less than 3 levels; if so, it is judged to be an invalid function, and returns directly.

After that, two data calculations were performed:

  • Calculate the delivery order price The calculation of the delivery price is also based on the weighted average calculation. When calculating the bid price of a buy order, give a bigger weight to the buy 1 price, namely 61.8% (0.618), and the sell 1 price accounts for the remaining weight of 38.2% (0.382). The same is true when calculating the ask price of the delivery order, giving more weight to the sell 1 price. As for why it is 0.618, it may be that the developer prefers the golden ratio. As for the little bit of price (0.01) added or subtracted at the end, it is to offset a little further to the center of the market.

  • Update the weighted average price of the first three levels of the order book on the time series For the first three levels of buy orders and sell orders in the order book, a weighted average calculation is performed. 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. Some students may say: “Oh, wrong, there is no 0.7, 0.2, or 0.1 in the code!”

    Let’s see the expanded calculation:

    (Buy 1 + Sell 1) * 0.35 + (Buy 2 + Sell 2) * 0.1 + (Buy 3 + Sell 3) * 0.05
    ->
    (Buy 1 + Sell 1) / 2 * 2 * 0.35 + (Buy 2 + Sell 2) / 2 * 2 * 0.1 + (Buy 3 + Sell 3) / 2 * 2 * 0.05
    ->
    (Buy 1 + Sell 1) / 2 * 0.7 + (Buy 2 + Sell 2) / 2 * 0.2 + (Buy 3 + Sell 3) / 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
    

    It can be seen here that the final calculated price actually reflects the medium price position of the third levels in the current market. Then use this calculated price to update the self.prices array, kick out the oldest data (by the shift() function), and update it with the newest data (by push() function; “shift” and “push” functions are all methods of JS array objects, you can search JS related materials for more details). Thus, the generated array self.prices is a data flow in time series order.

    Take a break. Let’s finish the analysis here, and see you next time!


More