Analyse de la stratégie de l'exploitant de la récolte des profits (1)

Auteur:Je suis désolée., Créé: 2022-04-26 11:31:51, mis à jour:

Analyse de la stratégie de l'exploitant de la récolte des profits (1)

Récemment, les utilisateurs du groupe FMZ Quant WeChat ont discuté du bot deprint moneyLa discussion a été très chaude et une stratégie très ancienne est revenue dans la vision des quantités:Résultat RécolteuseJe suis désolée. Le principe du trading de robotsprint moneyJe me reproche de ne pas avoir bien compris la stratégie de la récolte des profits à l'époque.Porting OKCoin profit harvesterJe suis désolée. Prenant comme exemple la version portée de la stratégie de récolte de profit de FMZ, nous allons analyser cette stratégie et extraire les idées de la stratégie, afin que nos utilisateurs de plateforme puissent apprendre cette idée de stratégie. Dans cet article, nous analysons davantage du niveau de la pensée stratégique, de l'intention, etc., afin de minimiser le contenu ennuyeux lié à la programmation.

Le code source de la stratégie:

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

Vue d'ensemble de la stratégie

Généralement, lorsque vous obtenez une stratégie à apprendre, lorsque vous lisez, regardez d'abord la structure globale du programme. Le code de stratégie n'est pas long, moins de 200 lignes; on peut dire qu'il est très simplifié, et a une restauration élevée pour la stratégie de la version originale, qui est essentiellement la même que celle-ci.main()L'ensemble du code de la stratégie, à l'exception de lamain(), a seulement une fonction nomméeLeeksReaper()LeLeeksReaper()Cette fonction peut être comprise comme le constructeur du module logique de stratégie de la récolte de profit (un objet).LeeksReaper()est responsable de la construction de la logique de négociation d'un récolteur de profit.

  • La première ligne de lamainfonction dans la stratégie:var reaper = LeeksReaper(); le code déclare une variable localereaper, puis appelle la fonction LeeksReaper() pour construire un objet logique de stratégie et l'affecte àreaper.

  • La partie suivante de lamainfonction:

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

    Entrez unewhileLa fonction de traitement de l'appareil est exécutée en continu par une boucle infinie.reaperobjetpoll(), et lepoll()La fonction est la logique principale de la stratégie, donc l'ensemble du programme de stratégie commence à exécuter continuellement la logique de trading.

    En ce qui concerne la ligne deSleep(TickInterval), il est facile à comprendre, qui est de contrôler le temps de pause après chaque exécution de la logique globale de négociation, et dont le but est de contrôler la fréquence de rotation de la logique de négociation.

Analysez le constructeurLeeksReaper()

Voyons comment la fonctionLeeksReaper()construit un objet dans la logique de stratégie.

LeLeeksReaper()fonction commence et déclare un objet nul,var self = {}Au cours de l'exécution de laLeeksReaper()En fin de compte, la construction de l'objet est terminée, et l'objet est retourné (c'est-à-dire, levar reaper = LeeksReaper()dans lemain()fonction, et l'objet retourné est attribué àreaper).

Ajouter un attribut à l'objetself

Ensuite, j'ai ajouté beaucoup d'attributs àselfVous pouvez rapidement comprendre le but et l'intention de ces attributs et variables, ce qui facilitera la compréhension de la stratégie, et éviter d'être confus lorsque vous voyez la pile de 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  

Ajouter une méthode à l'objetself

Après avoir ajouté ces attributs à self, commencer à ajouter des méthodes à laselfobjet, de sorte que cet objet peut faire certaines opérations et avoir certaines fonctions.

Première fonction ajoutée:

    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)

    }

La fonction deupdateTradesIl s'agit d'obtenir les données d'exécution les plus récentes sur le marché, de faire des calculs et d'enregistrer en fonction des données, et de fournir les résultats pour une utilisation dans la logique de stratégie ultérieure. J'ai directement écrit les commentaires ligne par ligne dans le code ci-dessus. Les étudiants qui n'ont peut-être pas de base de programmation seront confus_.reduce, voici donc une brève introduction._.reduceest une fonction deUnderscore.jsIl est donc très pratique de l'utiliser pour effectuer des calculs itératifs.lien d'information de Underscore.js

Très facile à comprendre, par exemple:

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
}

C' est l' addition de chaque nombre dans les tableaux, à savoir:[1, 2, 3, 4]Pour revenir à notre stratégie, il s'agit d'additionner la valeur de volume de chaque enregistrement d'exécution dans letradesLe nombre total de volumes d'exécution est le nombre total de volumes d'exécution.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...)Permettez-moi de remplacer ce morceau de code par...Il n'est pas difficile de voir ici que le calcul deself.volest également une moyenne pondérée, c'est-à-dire que le volume total d'exécution nouvellement généré représente 30% et le volume d'exécution précédemment calculé représente 70%.

En ce qui concerne votre question que dois-je faire si l'interface pour obtenir les dernières données d'exécution me renvoie les anciennes données en double? alors, les données obtenues sont toutes fausses, donc y a-t-il un sens à utiliser? " Ne vous inquiétez pas, la stratégie a été conçue dans cet esprit, donc le code a le contenu suivant:

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

Le jugement peut être basé sur l'identifiant d'exécution dans l'enregistrement d'exécution des transactions. Seulement lorsque l'identifiant est plus grand que l'identifiant de la dernière fois, l'accumulation est déclenchée. Ou, si l'interface de la plate-forme ne fournit pas d'identifiant, c'est-à-dire,trade.Id == 0, utilisez l'horodatage dans l'enregistrement d'exécution de la transaction pour juger.self.lastTradeIdest l'horodatage de l'enregistrement d'exécution et non l'identifiant.

Deuxième fonction ajoutée:

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

Ensuite, regardons leupdateOrderBookfonction. D'après la signification du nom de la fonction, on peut voir que la fonction est de mettre à jour le carnet de commandes. Cependant, elle ne met pas simplement à jour le carnet de commandes. La fonction appelle d'abord la fonction FMZ API.GetDepth()pour obtenir les données actuelles du carnet de commandes du marché (vendre 1...vendre n, acheter 1...acheter n), et enregistrer les données du carnet de commandes dans lein self.orderBookEnsuite, il juge si le niveau des données du carnet de commandes pour les ordres d'achat et les ordres de vente est inférieur à 3 niveaux; dans ce cas, il est jugé comme une fonction invalide et renvoie directement.

Ensuite, deux calculs de données ont été effectués:

  • Calculer le prix de la commande de livraison Le calcul du prix de livraison est également basé sur le calcul de la moyenne pondérée. Lors du calcul du prix d'enchère d'un ordre d'achat, donner une plus grande pondération au prix d'achat 1, à savoir 61,8% (0,618), et le prix de vente 1 représente le poids restant de 38,2% (0,382). Il en va de même lors du calcul du prix d'enchère de l'ordre de livraison, en donnant plus de poids au prix de vente 1. En ce qui concerne la raison pour laquelle il est de 0,618, il se peut que le développeur préfère le ratio d'or. En ce qui concerne le peu de prix (0,01) ajouté ou soustrait à la fin, il s'agit de compenser un peu plus loin au centre du marché.

  • Mise à jour du prix moyen pondéré des trois premiers niveaux du carnet de commandes sur les séries chronologiques Pour les trois premiers niveaux d'ordres d'achat et de vente dans le carnet de commandes, un calcul moyen pondéré est effectué. Le poids du premier niveau est de 0,7, le poids du deuxième niveau est de 0,2 et le poids du troisième niveau est de 0,1.

    Voyons le calcul élargi:

    (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
    

    Il ressort de ce qui précède que le prix calculé final reflète en fait la position moyenne des prix des tiers niveaux sur le marché actuel. Ensuite, utilisez ce prix calculé pour mettre à jour leself.pricesL'ensemble des données de base (par l'intermédiaire de lashift()Les résultats de l'enquête sont publiés dans les journaux de l'Union européenne.push()fonction; shift et push fonction sont toutes des méthodes d'objets de tableau JS, vous pouvez rechercher des matériaux liés à JS pour plus de détails). Ainsi, le tableau généréself.pricesest un flux de données dans l'ordre des séries temporelles.

    On va finir l'analyse ici, et on se voit la prochaine fois!


Plus de