Análise da Estratégia de Recolha de Lucro (1)

Autora:Ninabadass, Criado: 2022-04-26 11:31:51, Atualizado:

Análise da Estratégia de Recolha de Lucro (1)

Recentemente, usuários no grupo FMZ Quant WeChat discutiram sobre o bot deprint moneyA discussão foi muito acalorada, e uma estratégia muito antiga voltou a entrar na visão dos quantes:Benefício Recolhedor- Não. O princípio de negociação de botsprint moneyEu culpo-me por não ter entendido a estratégia da colheita de lucro muito bem naquele momento, então, eu li a estratégia original novamente seriamente, e também li a versão portada:Portar o Recolhimento de Lucro da OKCoin- Não. Tomando a versão portada da estratégia de colheita de lucro da FMZ como exemplo, vamos analisar essa estratégia e extrair as ideias da estratégia, para que nossos usuários da plataforma possam aprender essa ideia de estratégia. Neste artigo, analisamos mais a partir do nível de pensamento estratégico, intenção, etc., para minimizar o conteúdo chato relacionado à programação.

Estratégia Código Fonte:

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

Visão geral da Estratégia

Geralmente, quando você recebe uma estratégia para aprender, ao ler, primeiro olhe para a estrutura geral do programa. O código de estratégia não é longo, menos de 200 linhas; pode-se dizer que é muito simplificado e tem uma alta restauração para a estratégia da versão original, que é basicamente a mesma que esta.main()O código de estratégia inteiro, excetomain(), só tem uma função chamadaLeeksReaper(). OLeeksReaper()Esta função pode ser entendida como o construtor do módulo lógico de estratégia de colheita de lucro (um objeto).LeeksReaper()é responsável pela construção da lógica de negociação de um colhedor de lucro.

  • A primeira linha domainfunção na estratégia:var reaper = LeeksReaper(); o código declara uma variável localreaper, e então chama a função LeeksReaper() para construir um objeto lógico estratégia e atribui-lo parareaper.

  • A parte seguinte domainFunção:

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

    Insira umwhileLoop infinito, executar continuamente a função de processamento doreaperObjetopoll(), e opoll()Função é a lógica principal da estratégia, por isso todo o programa de estratégia começa a executar continuamente a lógica de negociação.

    Quanto à linha deSleep(TickInterval), é fácil de entender, que é controlar o tempo de pausa após cada execução da lógica de negociação geral, e cujo propósito é controlar a frequência de rotação da lógica de negociação.

Analise o ConstrutorLeeksReaper()

Vamos ver como a funçãoLeeksReaper()Constrói um objeto na lógica da estratégia.

OLeeksReaper()função inicia e declara um objeto nulo,var self = {}Durante a execução doLeeksReaper()Finalmente, a construção do objeto é concluída, e o objeto é devolvido (isto é, ovar reaper = LeeksReaper()emmain()função, e o objeto retornado é atribuído areaper).

Adicionar atributo ao objetoself

Em seguida, eu adicionei muitos atributos paraself. Eu vou descrever cada atributo abaixo. Você pode entender rapidamente o propósito e a intenção desses atributos e variáveis, o que tornará mais fácil entender a estratégia, e evitar ser confundido quando você vê a pilha de código.

    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  

Adicionar método ao objetoself

Depois de adicionar estes atributos para self, começar a adicionar métodos para oselfObjeto, para que este objeto possa fazer algumas operações e ter algumas funções.

Primeira função adicionada:

    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)

    }

A função deupdateTradesO objectivo principal é obter os dados de execução mais recentes no mercado, e fazer alguns cálculos e registar de acordo com os dados, e fornecer os resultados para utilização na lógica estratégica subsequente. Escrevi diretamente os comentários linha por linha no código acima. Os alunos que não têm base de programação podem estar confusos sobre_.reduce, por isso aqui está uma breve introdução._.reduceé uma função deUnderscore.jsPor isso, é muito conveniente usá-lo para fazer cálculos iterativos.link de informação do Underscore.js

Muito fácil de entender, 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 is 10
}

Isto é somar todos os números das matrizes, ou seja,[1, 2, 3, 4]De volta à nossa estratégia, é somar o valor de volume de cada registro de execução de dados notradesArquivo de dados para o volume de execução.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...), permita-me substituir esse pedaço de código com...Não é difícil ver aqui que o cálculo deself.volO volume total de execução recém-gerado representa 30%, e o volume de execução anterior calculado representa 70%. Este rácio é fixado artificialmente pelo desenvolvedor da estratégia, o que pode estar relacionado com a observação das regras de mercado.

Quanto à sua pergunta o que devo fazer se a interface para obter os dados de execução mais recentes me devolver os dados antigos duplicados? Então, os dados obtidos estão todos errados, então há algum sentido para usar? "Não se preocupe, a estratégia foi projetada com isso em mente, então o código tem o seguinte conteúdo:

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

O julgamento pode ser baseado no ID de execução no registro de execução de negociação. Somente quando o ID é maior do que o ID da última vez, o acúmulo é acionado. Ou, se a interface da plataforma não fornecer um ID, ou seja, o ID é maior do que o ID da última vez.trade.Id == 0, usar o carimbo de tempo no registro de execução de negociação para julgar.self.lastTradeIdé o carimbo do registo de execução, não a identificação.

Segunda função adicionada:

    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 oupdateOrderBookA função, a partir do significado do nome da função, pode ser visto que a função é para atualizar o livro de pedidos. No entanto, ele não apenas atualiza o livro de pedidos. A função, no início, chama a função FMZ APIGetDepth()Obter os dados do livro de ordens do mercado atual (vendendo 1...vendendo n, comprando 1...comprando n) e registar os dados do livro de ordens emin self.orderBookEm seguida, julga se o nível dos dados do livro de ordens para ordens de compra e ordens de venda é inferior a 3 níveis; se for o caso, julga-se que é uma função inválida e retorna directamente.

Em seguida, foram realizados dois cálculos de dados:

  • Calcular o preço da encomenda de entrega O cálculo do preço de entrega também é baseado no cálculo da média ponderada. Ao calcular o preço de licitação de uma ordem de compra, dar um peso maior ao preço de compra 1, ou seja, 61,8% (0,618), e o preço de venda 1 representa o peso restante de 38,2% (0,382). O mesmo é verdade ao calcular o preço de compra da ordem de entrega, dando mais peso ao preço de venda 1.

  • 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 ordens de compra e de venda no livro de ordens, é realizada uma média ponderada. O peso do primeiro nível é de 0,7, o peso do segundo nível é de 0,2 e o peso do terceiro nível é de 0,1.

    Vejamos o cálculo expandido:

    (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
    

    Pode-se observar aqui que o preço final calculado reflete efectivamente a posição de preço médio dos níveis 3 no mercado actual. Em seguida, use este preço calculado para atualizar oself.pricesarray, expulsar os dados mais antigos (peloshift()A Comissão deve, em seguida, proceder à sua análise e atualização com os dados mais recentes (porpush()função; shift e push funções são todos os métodos de objetos de matriz JS, você pode pesquisar materiais relacionados JS para mais detalhes). Assim, a matriz geradaself.pricesé um fluxo de dados em ordem de série temporal.

    Vamos terminar a análise aqui, e vemos-nos da próxima vez!


Mais.