Análisis de la estrategia de LeeksReaper (1)

El autor:- ¿ Por qué?, Creado: 2022-11-04 15:42:26, Actualizado: 2023-09-15 21:08:48

img

Análisis de la estrategia de LeeksReaper (1)

Recientemente, se ha debatido intensamente sobre elprint moneyUna estrategia muy antigua ha entrado en los ojos de los Quants de nuevo: LeeksReaper. El principio de negociación del robot deprint moneyAsí que, leí la estrategia original cuidadosamente de nuevo y miré la versión trasplantada del transplante OKCoin en el FMZ Quant. La estrategia de la recolección de puerros trasplantados basada en la plataforma FMZ Quant se analiza para explorar la idea de la estrategia. En este artículo, analizaremos más desde los aspectos de la estrategia idea e intención para minimizar el contenido aburrido relacionado con la programación.

Código fuente de la estrategia [Transplantando 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)
    }
}

Resumen de la estrategia

En general, cuando se obtiene una estrategia para estudiar, primero se debe echar un vistazo a la estructura general del programa. El código de estrategia no es muy largo, con menos de 200 líneas de código, es muy conciso, y la estrategia original está muy restaurada, casi igual.main()Todo el código de la estrategia, excepto el código de lamain(), es una función llamadaLeeksReaper()El.LeeksReaper()La función es muy fácil de entender, se puede entender como el constructor del módulo lógico de estrategia porreaper (un objeto).LeeksReaper()El objetivo de este programa es el desarrollo de una lógica de intercambio de productos para la recolección de puerros.

Palabras clave:

img

· La primera línea de la estrategiamainFunción:var reaper = LeeksReaper(), el código declara una variable localreapery luego llama a la función LeeksReaper() para construir un objeto lógico estrategia que asigna un valor areaper.

El siguiente paso de la estrategiamainFunción:

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

Introduzca unawhileciclo interminable y seguir ejecutando la función de procesamientopoll()de lasreaperel objeto, elpoll()la función es exactamente donde la lógica principal de la estrategia de negociación se encuentra y todo el programa de estrategia comienza a ejecutar la lógica de negociación una y otra vez. En cuanto a la líneaSleep(TickInterval), es fácil de entender, es para controlar el tiempo de pausa después de cada ejecución de la lógica de negociación general, con el propósito de controlar la frecuencia de rotación de la lógica de negociación.

AnálisisLeeksReaper()constructor

Mira cómo elLeeksReaper()La función construye un objeto lógico de estrategia.

ElLeeksReaper()La función comienza declarando un objeto vacío.var self = {}, y durante la ejecución delLeeksReaper()La función añadirá gradualmente algunos métodos y atributos a este objeto vacío, finalmente completando la construcción de este objeto y devolviéndolo (es decir, el paso demain()Función en el interior delvar reaper = LeeksReaper(), el objeto devuelto se asigna areaper).

Añadir atributos a laselfObjeto A continuación, añadí muchos atributos aselfDescribiré cada atributo de la siguiente manera, que puede entender el propósito y la intención de estos atributos y variables rápidamente, facilitar la comprensión de las estrategias, y evitar ser confundido al ver el 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

Añadir métodos a los objetos self

Después de añadir estos atributos a sí mismo, empezar a añadir métodos a laselfobjeto para que este objeto pueda hacer algún trabajo y tener algunas funciones.

La primera función añadió:

    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)

    }

La funciónupdateTradesEl objetivo principal es obtener los últimos datos de transacciones de mercado y hacer algunos cálculos basados en los datos y registrarlos para usarlos en la lógica posterior de la estrategia. Los comentarios línea por línea que escribí en el código de arriba directamente. Para el_.reduce, alguien que no tiene conocimientos básicos de programación puede estar confundido._.reducees una función de la biblioteca Underscore.js. La estrategia FMZJS soporta esta biblioteca, por lo que es muy conveniente para el cálculo iterativo. El enlace de datos Underscore.js (https://underscorejs.net/#reduce)

El significado también es muy simple, 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 = 10
}

Es decir, sumar cada número en la matriz[1, 2, 3, 4]Volviendo a nuestra estrategia, sumamos los valores del volumen de negociación de cada registro de transacciones en eltradesObtener un total del último volumen de transaccionesself.vol = 0.7 * self.vol + 0.3 * _.reduce (...), aquí usamos...No es difícil ver el cálculo deself.volEs decir, el volumen de operaciones recién generado representa el 30% del total y el último volumen de operaciones ponderado representa el 70%. En cuanto a su pregunta, ¿qué pasa si la interfaz para obtener los últimos datos de transacción regresó a los datos antiguos duplicados, entonces los datos que obtuve estaban mal, y no será significativo? No se preocupe. Este problema fue considerado en el diseño de la estrategia, por lo que el código tiene:

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

La acumulación se activa solo cuando el ID es mayor que el ID del último registro, o si la interfaz de intercambio no proporciona un ID, es decir,trade.Id == 0, usar la marca de tiempo en el registro de transacción para juzgar.self.lastTradeIdalmacena la marca de tiempo del registro de transacciones en lugar del ID.

La segunda función añadió:

    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 la funciónupdateOrderBook. Desde el nombre de la función, podemos ver que se utiliza para actualizar el libro de pedidos. Sin embargo, no actualiza sólo el libro de pedidos. La función comienza a llamar a la función FMZ APIGetDepth()para obtener los datos actuales del libro de pedidos del mercado (vender uno... vender n, comprar uno... comprar n), y registrar los datos del libro de pedidos enself.orderBook. A continuación, juzgue si la orden de compra y la orden de venta de los datos del libro de pedidos son menores de 3, en caso afirmativo, la función no válida será devuelta directamente.

Después de eso, se calculan dos datos:

· Calcular el precio del conocimiento de embarque El precio del conocimiento de embarque también se calcula utilizando el método de la media ponderada. Al calcular la orden de compra, el peso dado al precio de compra más cercano al precio de transacción es del 61,8% (0,618) y el peso dado al precio de venta más cercano al precio de transacción es del 38,2% (0,382) Cuando se calcula el precio de la factura de embarque, el mismo peso se da al precio de venta más cercano al precio de la transacción. En cuanto a por qué es 0.618, puede ser que el autor prefiera la relación de sección dorada. En cuanto al último precio (0.01), se debe desplazar ligeramente al centro de la apertura.

· 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 precios de las órdenes de compra y venta en el libro de pedidos, se calcula el 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. Vamos a ampliar el 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 aquí, el precio final calculado es en realidad una respuesta a la posición de precios de la mitad de la tercera apertura en el mercado actual. Luego utiliza este precio calculado para actualizar la matrizself.prices, eliminando uno de los datos más antiguos (a través de lashift()En la actualidad, la información de los datos de la Unión Europea se encuentra disponible en la página web de la Comisión Europea (en inglés).push()función, shift y push son métodos del objeto de la matriz del lenguaje JS, puede comprobar los datos de JS para obtener detalles).self.prices, que es un flujo de datos con un orden de serie temporal.

Así que vamos a tener un descanso aquí, y nos vemos en el próximo número ~


Relacionados

Más.