Análisis de la estrategia de cosecha de beneficios (1)

El autor:No lo sé., Creado: 2022-04-26 11:31:51, Actualizado:

Análisis de la estrategia de cosecha de beneficios (1)

Recientemente, los usuarios en el grupo FMZ Quant WeChat discutieron sobre el bot deprint moneyLa discusión fue muy acalorada, y una estrategia muy antigua ha vuelto a entrar en la visión de los cuantos:ganancia cosechadora- ¿ Qué? El principio de negociación de bots deprint moneyme culpo de no entender muy bien la estrategia de cosecha de ganancias en ese momento así que leí la estrategia original de nuevo seriamente y también leí la versión portada:Portar el cosechador de ganancias OKCoin- ¿ Qué? Tomando la versión portada de la estrategia de cosechadora de ganancias de FMZ como ejemplo, vamos a analizar esta estrategia y extraer las ideas de la estrategia, para que nuestros usuarios de la plataforma puedan aprender esta idea de estrategia. En este artículo, analizamos más desde el nivel de pensamiento estratégico, intención, etc., para minimizar el contenido aburrido relacionado con la programación.

[Port OKCoin Recolector de ganancias] Estrategia Código de fuente:

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

Resumen de la estrategia

Generalmente, cuando se obtiene una estrategia para aprender, al leer, primero se mira a la estructura general del programa. El código de estrategia no es largo, menos de 200 líneas; se puede decir que es muy simplificado, y tiene una alta restauración para la versión original de la estrategia, que es básicamente el mismo que este.main()Todo el código de la estrategia, exceptomain(), sólo tiene una función llamadaLeeksReaper()El.LeeksReaper()Esta función también es muy fácil de entender. Esta función se puede entender como el constructor del módulo lógico de estrategia de cosechadora de ganancias (un objeto).LeeksReaper()es responsable de construir la lógica de negociación de un cosechador de ganancias.

  • La primera línea de lamainfunción en la estrategia:var reaper = LeeksReaper(); el código declara una variable localreaper, y luego llama a la función LeeksReaper() para construir un objeto lógico de estrategia y lo asigna areaper.

  • La siguiente parte de lamainFunción:

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

    Introduzca unawhileel ciclo infinito, ejecutar continuamente la función de procesamiento de lareaperobjetospoll(), y elpoll()La función es la lógica principal de la estrategia, por lo que todo el programa de estrategia comienza a ejecutar continuamente la lógica de negociación.

    En cuanto a la línea deSleep(TickInterval), es fácil de entender, que es controlar el tiempo de pausa después de cada ejecución de la lógica de negociación general, y cuyo propósito es controlar la frecuencia de rotación de la lógica de negociación.

Analizar el ConstructorLeeksReaper()

Veamos cómo la funciónLeeksReaper()Construye un objeto en la lógica de la estrategia.

ElLeeksReaper()la función comienza y declara un objeto nulo,var self = {}Durante la ejecución de laLeeksReaper()Finalmente, la construcción del objeto se completa, y el objeto se devuelve (es decir, elvar reaper = LeeksReaper()En elmain()función, y el objeto devuelto se asigna areaper).

Añadir atributo al objetoself

A continuación, añadí muchos atributos aself. Voy a describir cada atributo a continuación. Usted puede entender rápidamente el propósito y la intención de estos atributos y variables, lo que hará que sea más fácil de entender la estrategia, y evitar la confusión cuando se ve la pila 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  

Añadir método al objetoself

Después de añadir estos atributos a self, empezar a añadir métodos a laselfobjeto, de modo que este objeto puede hacer algunas operaciones y tener algunas funciones.

Primera función añadida:

    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 función de losupdateTradesEl objetivo es obtener los últimos datos de ejecución en el mercado, y hacer algunos cálculos y registrar de acuerdo con los datos, y proporcionar los resultados para su uso en la lógica de la estrategia posterior. He escrito directamente los comentarios línea por línea en el código de arriba. Los estudiantes que no tienen conocimientos básicos de programación estarán confundidos acerca de_.reduce, así que aquí hay una breve introducción._.reducees una función deUnderscore.jsPor lo tanto, es muy conveniente usarlo para hacer cálculos iterativos.Enlace de información de Underscore.js

Muy fácil de entender, por ejemplo:

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
}

Esto es la suma de cada número en las matrices, a saber,[1, 2, 3, 4]Volviendo a nuestra estrategia, es sumar el valor de volumen de cada registro de ejecución de datos en eltradesEn este caso, el volumen de ejecución es el valor de la matriz, lo que resulta en un total del último volumen de ejecución.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...), permítanme reemplazar ese fragmento de código con...No es difícil ver aquí que el cálculo deself.voles también una media ponderada, es decir, el volumen total de ejecución recién generado representa el 30% y el volumen de ejecución anterior calculado representa el 70%.

En cuanto a su pregunta ¿qué debo hacer si la interfaz para obtener los últimos datos de ejecución me devuelve los datos antiguos duplicados? Entonces, los datos obtenidos son todos erróneos, así que ¿hay algún sentido para usar?" No se preocupe, la estrategia ha sido diseñada con eso en mente, por lo que el código tiene el siguiente contenido:

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

El juicio puede basarse en el ID de ejecución en el registro de ejecución de operaciones. Sólo cuando el ID es mayor que el ID de la última vez, se activa la acumulación.trade.Id == 0, utilizar la marca de tiempo en el registro de ejecución de la negociación para juzgar.self.lastTradeIdes la marca de tiempo del registro de ejecución, no la identificación.

Segunda función añadida:

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

A continuación, vamos a ver elupdateOrderBookLa función, en un primer momento, llama a la función FMZ API.GetDepth()para obtener los datos actuales del libro de pedidos del mercado (vender 1...vender n, comprar 1...comprar n) y registrar los datos del libro de pedidos enin self.orderBookA continuación, juzga si el nivel de los datos del libro de pedidos para órdenes de compra y de venta es inferior a 3 niveles; en caso afirmativo, se considera que es una función no válida y devuelve directamente.

Después se realizaron dos cálculos de datos:

  • Calcular el precio de la orden de entrega El cálculo del precio de entrega también se basa en el cálculo de la media ponderada. Al calcular el precio de oferta de una orden de compra, dar un mayor peso al precio de compra 1, a saber, 61.8% (0.618), y el precio de venta 1 representa el peso restante del 38.2% (0.382). Lo mismo es cierto al calcular el precio de pedido de la orden de entrega, dando más peso al precio de venta 1.

  • Actualizar el precio medio ponderado de los tres primeros niveles de la cartera de pedidos en las series temporales Para los tres primeros niveles de órdenes de compra y de venta en el libro de pedidos, se realiza un cálculo promedio ponderado. El peso del primer nivel es de 0.7, el peso del segundo nivel es de 0.2, y el peso del tercer nivel es de 0.1.

    Veamos el cálculo ampliado:

    (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
    

    Se puede observar que el precio final calculado refleja en realidad la posición media de los precios de los terceros niveles en el mercado actual. Luego utilice este precio calculado para actualizar elself.pricesarray, expulsar los datos más antiguos (por elshift()En la actualidad, el número de personas que se encuentran en el mercado de trabajo espush()función; shift y push funciones son todos los métodos de objetos de matriz JS, puede buscar materiales relacionados con JS para más detalles).self.priceses un flujo de datos en orden de serie temporal.

    Vamos a terminar el análisis aquí, y nos vemos la próxima vez!


Más.