Loading ...

韭菜收割机策略剖析(1)

Author: 小小梦, Created: 2020-11-12 22:11:32, Updated: 2020-11-16 15:13:46

韭菜收割机策略剖析

最近发明者量化微信群内讨论print money的机器人,讨论的非常火热,一个非常古老的策略又重新进入了宽客们的视野:韭菜收割机print money的机器人交易原理借鉴了韭菜收割机策略,怪自己当时对于韭菜收割机策略没有看太明白,没有理解到位。所以,又重新认真的看了一遍原版策略,并且看了一遍在发明者量化上的移植版本移植 OKCoin 韭菜收割机。 就以发明者量化平台的移植版韭菜收割机策略,剖析该策略,挖掘该策略的思路。以便平台用户学习到这个策略思路。 本篇我们更多从策略思路、意图等层面剖析,尽量减少编程相关的枯燥内容。

[移植 OKCoin 韭菜收割机]策略源码:

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

策略通篇概览

一般拿到一个策略学习,阅读时,首先通篇看一下整体的程序结构。该策略代码并不多,只有不到200行代码,可谓非常精简,并且对于原版的策略还原度很高,基本上是一样的。策略代码运行时从main()函数开始执行,通篇策略代码,除了main(),就是一个名为LeeksReaper()的函数了,LeeksReaper()函数也很好理解,该函数可以理解为韭菜收割机策略逻辑模块(一个对象)的构造函数,简单说LeeksReaper()就是负责构造一个韭菜收割机交易逻辑用的。

关键字: img img

  • 策略main函数第一行: var reaper = LeeksReaper(),代码声明了一个局部变量reaper,然后调用LeeksReaper()函数构造了一个策略逻辑对象,赋值给reaper

  • 策略main函数接下来:

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

    进入一个while死循环,不停的执行reaper对象的处理函数poll()poll()函数正是交易策略的主要逻辑所在,整个策略程序就开始不停的执行交易逻辑了。 至于Sleep(TickInterval)这行很好理解,就是为了控制每次整体交易逻辑执行之后的暂停时间,目的是控制交易逻辑的轮转频率。

剖析LeeksReaper()构造函数

看看LeeksReaper()函数是如何构造一个策略逻辑对象的。

LeeksReaper()函数开始,声明了一个空对象,var self = {},在LeeksReaper()函数执行的过程中会逐步对这个空对象增加一些方法,属性,最终完成这个对象的构造,最后返回这个对象(也就是main()函数里面var reaper = LeeksReaper()这一步,返回的对象赋值给了reaper)。

self对象添加属性

接下来给self添加了很多属性,以下我对每个属性都加以描述,可以快速理解这些属性、变量的用途,意图,方便理解策略,避免看到这一堆代码时,被绕的云里雾里。

    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          # 记录当前收益数值

self对象添加方法

给self增加了这些属性之后,开始给self对象添加方法,让这个对象可以做一些工作,具备一些功能。

第一个添加的函数:

    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)

    }

updateTrades这个函数的作用是获取一次最新的市场成交数据,并且根据数据做一些计算并记录,提供给策略后续的逻辑中使用。 逐行的注释我直接写在上面代码中。 对于_.reduce可能没有编程基础的同学会困惑了,这里简单讲下,_.reduceUnderscore.js这个库的函数,FMZJS策略支持了这个库,所以用来迭代计算很方便,Underscore.js资料链接

意思也很简单,例如:

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
}

就是把数组[1, 2, 3, 4]中的每个数加起来。回到我们的策略中,就是把trades数组中的每个交易记录数据其中成交量数值累加起来。得出一个最新的成交记录交易量总计。self.vol = 0.7 * self.vol + 0.3 * _.reduce(...),请允许我用...代替那一堆代码。这里不难看出对于self.vol的计算也是加权平均。即最新产生的成交总成交量占权重30%,上一次的加权计算得出的成交量占70%。这个比例是策略作者人为设定的,可能与观察市场规律有关。 至于你问我,万一获取最近成交数据的接口给我返回了重复的旧数据肿么办,那我得出的数据都是错的,还有使用意义么?不用担心,策略设计时是考虑过此问题的,所以代码中便有了

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

这个判断。可以基于成交记录中成交ID判断,只有ID大于上次记录的ID时才触发累计,或者如果交易所接口不提供ID时,即trade.Id == 0,使用成交记录中的时间戳判断,此时self.lastTradeId储存的就是成交记录的时间戳,而不是ID了。

第二个添加的函数:

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

接下来看updateOrderBook这个函数,从函数名字面意思就能看出,这个函数作用是更新订单薄。然鹅,可不仅仅只是更新一下订单薄。函数开始调用FMZ的API函数GetDepth()获取当前市场订单薄数据(卖一…卖n,买一…买n),并且把订单薄数据记录在self.orderBook中。接下来判断如果订单薄数据买单、卖单少于3档,就判定无效函数直接返回。

之后,进行了两项数据的计算:

  • 计算提单价格 计算提单价格同样是使用加权平均计算,对于计算买单时,给买一的权重大些为61.8%(0.618),卖一占剩余的权重38.2%(0.382) 计算提单卖单价格时则同样,给与卖一价格权重大些。至于为什么是0.618,可能是作者比较喜欢黄金分割比例。至于最后加减的那一点点价格(0.01)是为了略微再向盘口正中央偏移一点。

  • 更新时间序列上订单薄前三档加权平均价格 对于订单薄前三档买单、卖单价格做加权平均计算,第一档权重0.7,第二档权重0.2,第三档权重0.1。可能有的同学会说:“诶,不对呀,代码中木有0.7,0.2,0.1呀” 我们把计算展开看下:

    (买一 + 卖一) * 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
    

    到这里可以看到,最后计算出的价格实际上是反应当前市场中盘口三档中位的价格位置。 然后用这个算出的价格,更新self.prices数组,踢出一个最旧的数据(通过shift()函数),更新进去一个最新的数据(通过push()函数,shift、push函数都是JS语言数组对象的方法,具体可以查询JS资料)。从而形成self.prices数组是一个有时间序列顺序的数据流。

咳咳,喝口水,先剖析到这里,我们下期再见~


More

bwxiaok 太棒了,没有梦总的解释还真的不能完全看懂,感谢大佬的耐心解释!

梵风衣 黄金分割0.618 0.382是用的斐波 梦总牛批

LEEEEEEEEO 确实牛批啊这么详细

evan1987 大大的赞,这么详细的解答.感谢分享

makebit 梦总牛p

iteh3712198 如果用python跑,和JS的差距会很大么?

shiyimjjcn 梦总,牛批!! 虽然注释了,但是看起来还是非常复杂.....

九个太阳 梦总,牛批!!

小小梦 感谢支持~如果喜欢,帮忙分享下,嘿嘿~

小小梦 感谢支持!

小小梦 感谢支持!

小小梦 感谢支持!

小小梦 上学那会儿,学到这个黄金分割比例记得特别清楚,说是这样比例长宽的矩形最美~~但是不知道为什么。。。。

小小梦 应该差不多。

小小梦 感谢支持。

小小梦 其实不复杂,这次注释的比较啰嗦,逐行都尽量用最容易理解的方式描述了。通篇看下来应该理解不难。