
Recientemente, el inventor del grupo de discusión cuantitativo de WeChatprint moneyLa discusión sobre los robots fue muy acalorada y una estrategia muy antigua volvió a entrar en la visión de los quants:Cosechadora de puerros。
print moneyEl principio de negociación del robot se basa en la estrategia de la cosechadora de puerros. Me culpo a mí mismo por no haber entendido claramente la estrategia de la cosechadora de puerros en ese momento. Entonces, revisé cuidadosamente nuevamente la estrategia original y también revisé la versión trasplantada en Inventor Quant.Trasplante de puerros con la cosechadora OKCoin。
Tomemos la versión trasplantada de la estrategia de cosechadora de puerros de la Plataforma Cuantitativa Inventor para analizar la estrategia y explorar las ideas detrás de ella. Para que los usuarios de la plataforma puedan conocer esta idea estratégica.
En este artículo analizaremos más desde la perspectiva del pensamiento estratégico, intenciones, etc., y trataremos de reducir el contenido aburrido relacionado con la programación.
[Código fuente de la estrategia de trasplante de OKCoin Leek Harvester:
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("开始平衡", 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("开始平衡", 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)
}
}
Generalmente, cuando recibes una estrategia para aprender, al leerla, primero debes mirar la estructura general del programa. Esta estrategia no tiene mucho código, solo menos de 200 líneas de código, lo cual es muy conciso y el grado de restauración a la estrategia original es muy alto y es básicamente la misma. El código de política se ejecuta desdemain()La función comienza a ejecutarse y se ejecuta todo el código de estrategia.main(), es unLeeksReaper()La función,LeeksReaper()La función también es fácil de entender. Esta función puede entenderse como el constructor del módulo lógico de estrategia de cosechadora de puerros (un objeto). En pocas palabrasLeeksReaper()Es responsable de construir una lógica de transacción de cosechadora de puerros.
Palabras clave:

EstrategiamainLa primera línea de la función:
var reaper = LeeksReaper(), el código declara una variable localreaperLuego llame a la función LeeksReaper() para construir un objeto de lógica de estrategia y asígnelo areaper。
EstrategiamainLa función es la siguiente:
while (true) {
reaper.poll()
Sleep(TickInterval)
}
Introduzca unawhileBucle muerto, ejecución sin finreaperFunción de procesamiento de objetospoll(),poll()La función es la lógica principal de la estrategia comercial, y todo el programa de estrategia comienza a ejecutar la lógica comercial continuamente.
ParaSleep(TickInterval)Esta línea es fácil de entender. Sirve para controlar el tiempo de pausa después de cada ejecución de la lógica de transacción general, con el fin de controlar la frecuencia de rotación de la lógica de transacción.
LeeksReaper()ConstructorEcha un vistazoLeeksReaper()Cómo las funciones construyen un objeto lógico de estrategia.
LeeksReaper()Al comienzo de la función, se declara un objeto vacío.var self = {},existirLeeksReaper()Durante la ejecución de la función, se irán añadiendo gradualmente algunos métodos y propiedades a este objeto vacío, y finalmente se completará la construcción de este objeto, y finalmente se retornará este objeto (es decir,main()Dentro de la funciónvar reaper = LeeksReaper()En este paso, el objeto devuelto se asigna areaper)。
selfAgregar propiedades a un objetoA continuación darselfSe han añadido muchas propiedades. A continuación, describiré cada una de ellas para que puedas comprender rápidamente el propósito y la intención de estas propiedades y variables, y entender la estrategia fácilmente, para evitar confundirte cuando veas esta pila de código.
self.numTick = 0 # 用来记录poll函数调用时未触发交易的次数,当触发下单并且下单逻辑执行完时,self.numTick重置为0
self.lastTradeId = 0 # 交易市场已经成交的订单交易记录ID,这个变量记录市场当前最新的成交记录ID
self.vol = 0 # 通过加权平均计算之后的市场每次考察时成交量参考(每次循环获取一次市场行情数据,可以理解为考察了行情一次)
self.askPrice = 0 # 卖单提单价格,可以理解为策略通过计算后将要挂卖单的价格
self.bidPrice = 0 # 买单提单价格
self.orderBook = {Asks:[], Bids:[]} # 记录当前获取的订单薄数据,即深度数据(卖一...卖n,买一...买n)
self.prices = [] # 一个数组,记录订单薄中前三档加权平均计算之后的时间序列上的价格,简单说就是每次储存计算得到的订单薄前三档加权平均价格,放在一个数组中,用于后续策略交易信号参考,所以该变量名是prices,复数形式,表示一组价格
self.tradeOrderId = 0 # 记录当前提单下单后的订单ID
self.p = 0.5 # 仓位比重,币的价值正好占总资产价值的一半时,该值为0.5,即平衡状态
self.account = null # 记录账户资产数据,由GetAccount()函数返回数据
self.preCalc = 0 # 记录最近一次计算收益时的时间戳,单位毫秒,用于控制收益计算部分代码触发执行的频率
self.preNet = 0 # 记录当前收益数值
selfMétodo de adición de objetosDespués de agregar estos atributos a sí mismo, comience aselfAgregar métodos a un objeto permite que el objeto realice algún trabajo y tenga algunas funciones.
La primera función agregada:
self.updateTrades = function() {
var trades = _C(exchange.GetTrades) # 调用FMZ封装的接口GetTrades,获取当前最新的市场成交数据
if (self.prices.length == 0) { # 当self.prices.length == 0时,需要给self.prices数组填充数值,只有策略启动运行时才会触发
while (trades.length == 0) { # 如果近期市场上没有更新的成交记录,这个while循环会一直执行,直到有最新成交数据,更新trades变量
trades = trades.concat(_C(exchange.GetTrades)) # concat 是JS数组类型的一个方法,用来拼接两个数组,这里就是把“trades”数组和“_C(exchange.GetTrades)”返回的数组数据拼接成一个数组
}
for (var i = 0; i < 15; i++) { # 给self.prices填充数据,填充15个最新成交价格
self.prices[i] = trades[trades.length - 1].Price
}
}
self.vol = 0.7 * self.vol + 0.3 * _.reduce(trades, function(mem, trade) { # _.reduce 函数迭代计算,累计最新成交记录的成交量
// 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)
}
updateTradesEl propósito de esta función es obtener los últimos datos de transacciones del mercado, realizar algunos cálculos basados en los datos y registrarlos para su uso en la lógica posterior de la estrategia.
Escribí los comentarios línea por línea directamente en el código anterior.
para_.reduceLos estudiantes que no tengan conocimientos básicos de programación pueden sentirse confundidos. A continuación, se ofrece una breve explicación._.reduceSíUnderscore.jsLas funciones de esta biblioteca están respaldadas por la estrategia FMZJS, por lo que resulta muy conveniente utilizar cálculos iterativos.Enlace de datos de Underscore.js
El significado también es muy sencillo, 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, la matriz[1, 2, 3, 4]Suma cada número en . Volviendo a nuestra estrategia,tradesSe suman los valores del volumen de transacciones de cada registro de transacción en la matriz. Obtenga el último registro de transacciones y el volumen total de transacciones.self.vol = 0.7 * self.vol + 0.3 * _.reduce(...)Por favor, permítame utilizar...En lugar de ese montón de código. No es difícil ver aquí queself.volEl cálculo también es un promedio ponderado. Es decir, el último volumen total de transacciones representa el 30% del peso, y el volumen de transacciones obtenido mediante el cálculo ponderado anterior representa el 70%. Esta relación la establece artificialmente el autor de la estrategia y puede estar relacionada con la observación de patrones del mercado.
En cuanto a tu pregunta, ¿qué pasa si la interfaz para obtener los datos de transacciones más recientes me devuelve datos antiguos duplicados? Entonces, los datos que obtengo serán incorrectos, ¿tiene sentido utilizarla? No te preocupes, este problema se tuvo en cuenta al diseñar la estrategia, por eso está incluido en el código.
if ((trade.Id > self.lastTradeId) || (trade.Id == 0 && trade.Time > self.lastTradeId)) {
...
}
Esta sentencia. Se puede juzgar en función del ID de transacción en el registro de transacción. 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 el ID, es decir,trade.Id == 0, utilizando la marca de tiempo en el registro de transacción para determinar, en este momentoself.lastTradeIdLo que se almacena es la marca de tiempo del registro de la transacción, no el ID.
La 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))
}
Próximo relojupdateOrderBookComo sugiere su nombre, esta función se utiliza para actualizar la cartera de pedidos. Pero es más que simplemente actualizar la cartera de pedidos. La función comienza a llamar a la función API de FMZGetDepth()Obtenga los datos del libro de órdenes del mercado actual (vender uno…vender n, comprar uno…comprar n) y registre los datos del libro de órdenes enself.orderBookmedio. A continuación, si los datos del libro de órdenes contienen menos de 3 órdenes de compra y venta, la función se considera inválida y regresa directamente.
Posteriormente se calcularon dos datos:
Calcular el precio del conocimiento de embarque El precio del conocimiento de embarque también se calcula utilizando el promedio ponderado. Al calcular la orden de compra, se le asigna a la orden de compra un peso mayor del 61,8 % (0,618) y a la orden de venta el peso restante del 38,2 % (0,382). Lo mismo se aplica al calcular el precio de venta del conocimiento de embarque, dándole mayor peso al precio de venta. En cuanto a por qué es 0,618, puede ser que el autor prefiera la proporción áurea. En cuanto al último ligero aumento o disminución del precio (0,01), es para desplazarlo ligeramente hacia el centro del mercado.
Actualizar el precio promedio ponderado de los primeros tres niveles de la cartera de pedidos en la serie temporal Se realiza un cálculo de promedio ponderado en los primeros tres niveles de precios de órdenes de compra y venta en el libro de órdenes, donde el primer nivel tiene un peso de 0,7, el segundo nivel tiene un peso de 0,2 y el tercer nivel tiene un peso de 0,1. . Algunos estudiantes pueden decir: “Oh, eso no es correcto, no hay 0,7, 0,2, 0,1 en el código”. Ampliemos el cálculo:
(买一 + 卖一) * 0.35 + (买二 + 卖二) * 0.1 + (买三 + 卖三) * 0.05
->
(买一 + 卖一) / 2 * 2 * 0.35 + (买二 + 卖二) / 2 * 2 * 0.1 + (买三 + 卖三) / 2 * 2 * 0.05
->
(买一 + 卖一) / 2 * 0.7 + (买二 + 卖二) / 2 * 0.2 + (买三 + 卖三) / 2 * 0.1
->
第一档平均的价格 * 0.7 + 第二档平均的价格 * 0.2 + 第三档平均的价格 * 0.1
Aquí puede ver que el precio final calculado en realidad refleja la posición del precio medio de los tres niveles en el mercado actual.
Luego use este precio calculado para actualizarself.pricesMatriz, expulsa los datos más antiguos (a través deshift()función), actualizar los datos más recientes (a través depush()Las funciones de función, desplazamiento y empuje son métodos de objetos de matriz del lenguaje JS. Puede consultar la información de JS para obtener más detalles). Así se formaself.pricesUna matriz es un flujo de datos ordenado en series temporales.
Ejem, bebe un poco de agua, me detendré aquí por ahora, nos vemos la próxima vez~