Análise da estratégia do LeeksReaper (1)

Autora:Lydia., Criado: 2022-11-04 15:42:26, Atualizado: 2023-09-15 21:08:48

img

Análise da estratégia do LeeksReaper (1)

Recentemente, tem havido uma discussão acalorada sobre aprint moneyUma estratégia muito antiga voltou aos olhos dos Quants: o LeeksReaper. O princípio de negociação do robô deprint moneyEntão, eu li novamente a estratégia original cuidadosamente novamente e olhei para a versão transplantada do transplantado OKCoin leeksreaper no FMZ Quant. A estratégia de rebanho de alho transplantado baseada na plataforma FMZ Quant é analisada para explorar a ideia da estratégia. Neste artigo, vamos analisar mais a partir dos aspectos de estratégia ideia e intenção para minimizar o conteúdo chato relacionado à programação.

Código de origem da estratégia [Transplante OKCoin LeeksReaper]:

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

Estratégia geral

Geralmente, quando se tem uma estratégia para estudar, deve-se dar uma olhada na estrutura geral do programa primeiro. O código de estratégia não é muito longo, com menos de 200 linhas de código, é muito conciso, e a estratégia original é altamente restaurada, quase a mesma.main()O código de estratégia completo, excetomain(), é uma função chamadaLeeksReaper(). OLeeksReaper()função é muito fácil de entender, pode ser entendido como o construtor do módulo lógico estratégia poreksreaper (um objeto).LeeksReaper()É responsável pela construção de uma lógica de comércio de poros.

Palavras chave:

img

· A primeira linha da estratégiamainFunção:var reaper = LeeksReaper(), o código declara uma variável localreapere então chama a função LeeksReaper() para construir um objeto lógico estratégia que atribui um valor parareaper.

O próximo passo da estratégiamainFunção:

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

Insira umwhileciclo infinito e continuar a executar a função de processamentopoll()doreaperO objectivo, opoll()A função é exatamente onde a lógica principal da estratégia de negociação está e todo o programa de estratégia começa a executar a lógica de negociação repetidamente. Quanto à linhaSleep(TickInterval), é fácil de entender, é para controlar o tempo de pausa após cada execução da lógica de negociação geral, com o objetivo de controlar a frequência de rotação da lógica de negociação.

AnáliseLeeksReaper()Construtor

Veja como oLeeksReaper()A função constrói um objeto lógico de estratégia.

OLeeksReaper()A função inicia-se declarando um objeto vazio,var self = {}, e durante a execução doLeeksReaper()função irá gradualmente adicionar alguns métodos e atributos para este objeto vazio, finalmente concluindo a construção deste objeto e devolvendo-o (ou seja, a etapa demain()função no interior dovar reaper = LeeksReaper(), o objeto devolvido é atribuído areaper).

Adicionar atributos aoselfObjeto Em seguida, eu adicionei muitos atributos paraselfEu vou descrever cada atributo da seguinte forma, que pode entender o propósito e a intenção desses atributos e variáveis rapidamente, facilitar a compreensão de estratégias, e evitar ser confundido ao ver o código.

    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

Adicionar métodos a objetos self

Depois de adicionar estes atributos para auto, começar a adicionar métodos para oselfobjeto para que este objeto possa fazer algum trabalho e ter algumas funções.

A primeira função acrescentou:

    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)

    }

A funçãoupdateTradesO objectivo principal é obter os dados mais recentes das transacções de mercado e fazer alguns cálculos com base nos dados e registrá-los para utilização na lógica subsequente da estratégia. Os comentários linha por linha que escrevi no código acima diretamente. Para_.reduce, alguém que não tem aprendizado básico de programação pode ficar confuso._.reduceé uma função da biblioteca Underscore.js. A estratégia FMZJS suporta esta biblioteca, por isso é muito conveniente para cálculo iterativo.https://underscorejs.net/#reduce)

O significado também é muito simples, por exemplo:

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

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

Ou seja, somar cada número na matriz[1, 2, 3, 4]De volta à nossa estratégia, somamos os valores do volume de negociação de cada dado do registo de transacções notradesObtenha um total do volume de transações mais recenteself.vol = 0.7 * self.vol + 0.3 * _.reduce (...), aqui usamos...Não é difícil ver o cálculo deself.volO volume de negociação recentemente gerado representa 30% do total e o último volume de negociação ponderado representa 70%. Este rácio foi fixado artificialmente pelo autor da estratégia e pode estar relacionado com as regras do mercado. Quanto à sua pergunta, e se a interface para obter os dados de transação mais recentes retornar aos dados antigos duplicados, então os dados que obtive estavam errados e não serão significativos? Não se preocupe. Este problema foi considerado no projeto da estratégia, então o código tem:

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

A acumulação é desencadeada apenas quando o ID é maior que o ID do último registro, ou se a interface de troca não fornecer um ID, ou seja,trade.Id == 0, use o carimbo de hora no registro da transação para julgar.self.lastTradeIdArmazena o carimbo de data e hora do registo da transacção em vez do ID.

A segunda função acrescentou:

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

Em seguida, vamos olhar para a funçãoupdateOrderBook. A partir do nome da função, podemos ver que é usado para atualizar o livro de ordens. No entanto, ele não atualiza apenas o livro de ordens. A função começa a chamar a função FMZ API.GetDepth()Obter os dados do livro de ordens do mercado atual (vender um... vender n, comprar um... comprar n) e registar os dados do livro de ordens emself.orderBook. Em seguida, julgue se a ordem de compra e a ordem de venda dos dados do livro de encomendas são menores que 3, se assim for, a função inválida será devolvida diretamente.

Depois disso, são calculados dois dados:

· Calcular o preço do conhecimento de embarque O preço do conhecimento de embarque é igualmente calculado utilizando o método da média ponderada.No cálculo da ordem de compra, o peso atribuído ao preço de compra mais próximo do preço de transacção é de 61,8% (0,618) e o peso atribuído ao preço de venda mais próximo do preço de transacção é de 38,2% (0,382) Quando se calcula o preço da nota de embarque, o mesmo peso é dado ao preço de venda mais próximo do preço da transação. Quanto ao porquê é 0,618, pode ser que o autor prefira a proporção da seção dourada. Quanto ao último preço (0,01), é para compensar para o centro da abertura ligeiramente.

· Atualizar o preço médio ponderado dos três primeiros níveis da carteira de pedidos nas séries temporais Para os três primeiros níveis de preços das ordens de compra e venda na carteira de pedidos, a média ponderada é calculada. O peso do primeiro nível é 0,7, o peso do segundo nível é 0,2 e o peso do terceiro nível é 0,1. Alguém pode dizer: Oh, não, há 0,7, 0,2, 0,1 no código. Vamos expandir o cálculo:

(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

Como podemos ver aqui, o preço final calculado é na verdade uma resposta à posição de preço do meio da terceira abertura no mercado atual. Então use este preço calculado para atualizar a matrizself.prices, eliminando um dos dados mais antigos (através doshift()A função de informação e de informação (e de actualização de um dos dados mais recentes) através dapush()função, shift e push funções são métodos da linguagem JS array objeto, você pode verificar os dados JS para detalhes). Assim formando a matrizself.prices, que é um fluxo de dados com uma ordem de séries temporais.

Então vamos descansar aqui, e ver-nos-emos na próxima edição ~


Relacionados

Mais.