Loading ...

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

Author: 小小梦, Created: 2020-11-16 10:03:52, Updated: 2020-11-16 15:13:17

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

接着上回内容讲解。

第三个添加的函数:

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

构造函数LeeksReaper()构造对象时,给对象添加的balanceAccount()函数作用是更新账户资产信息,储存在self.account,即构造的对象的account属性。定时计算收益数值并打印。接着根据最新的账户资产信息,计算现货钱币平衡比例(现货仓位平衡),在触发偏移阈值时,进行小单平仓,让钱币(仓位)重回平衡状态。等待一定时间成交,然后取消所有挂单,下一轮执行该函数,会再次检测平衡并且做出对应的处理。

我们来逐句看下这个函数的代码: 首先第一句var account = exchange.GetAccount()是声明了一个局部变量account,并且调用发明者API接口exchange.GetAccount()函数,获取当前的账户最新数据,赋值给account变量。然后判断account这个变量,如果变量为null值(例如超时、网络、交易所接口异常等问题获取失败)就直接返回(对应if (!account){...}这里)。

self.account = account这句是把局部变量account赋值给构造的对象的account属性用以记录最新的账户信息在构造的对象中。

var now = new Date().getTime()这句声明一个局部变量now,并且调用JavaScript语言的时间日期对象的getTime()函数返回当前时间戳。赋值给now变量。

if (self.orderBook.Bids.length > 0 && now - self.preCalc > (CalcNetInterval * 1000)) {...}这句代码判断当前时间戳和上次记录的时间戳差值如果超过参数CalcNetInterval * 1000即代表从上次更新,到现在超过了CalcNetInterval * 1000毫秒(CalcNetInterval秒),实现定时打印收益的功能,由于计算收益时要用到盘口买一的价格,所以条件中还限定了self.orderBook.Bids.length > 0这个条件(深度数据,买单列表中必须有有效的档位信息)。当这个if语句条件触发时,执行self.preCalc = now更新最近一次打印收益的时间戳变量self.preCalc为当前时间戳now。这里收益统计采用的是净值计算方法,代码为var net = _N(account.Balance + account.FrozenBalance + self.orderBook.Bids[0].Price * (account.Stocks + account.FrozenStocks)),即按照当前买一价格把币换算成钱(计价币),然后和账户中的钱数加在一起赋值给声明的局部变量net。判断当前的总净值和上次记录的总净值是否一致:

            if (net != self.preNet) {
                self.preNet = net
                LogProfit(net)
            }

如果不一致,即net != self.preNet为真,就用net变量更新用于记录净值的属性self.preNet。然后打印这个net总净值数据到发明者量化交易平台机器人的收益曲线图表上(可以在FMZ API文档查询LogProfit这个函数)。

如果没有触发定时打印收益,那么就继续以下流程,将account.Stocks(当前账户可用币数)、account.Balance(当前账户可用钱数)记录在self.btcself.cny。计算偏移比例并赋值记录在self.p

self.p = self.btc * self.prices[self.prices.length-1] / (self.btc * self.prices[self.prices.length-1] + self.cny)

算法也很简单,就是计算币的当前价值占账户总净值的百分比。

那么判断何时触发钱币(仓位)平衡呢? 作者这里以50%上下2个百分点作为缓冲,超过了缓冲区执行平衡,即self.p < 0.48钱币平衡偏移触发,认为币少了,就在盘口买一位置开始每次价格增加0.01,布置三个小单。同理钱币平衡self.p > 0.52,认为币多了,就在盘口卖一放出小单。最后根据参数设置等待一定时间Sleep(BalanceTimeout)之后取消所有订单。

        var orders = exchange.GetOrders()                  # 获取当前所有挂单,存在orders变量
        if (orders) {                                      # 如果获取当前挂单数据的变量orders不为null
            for (var i = 0; i < orders.length; i++) {      # 循环遍历orders,逐个取消订单
                if (orders[i].Id != self.tradeOrderId) {
                    exchange.CancelOrder(orders[i].Id)     # 调用exchange.CancelOrder,根据orders[i].Id取消订单
                }
            }
        }

第四个添加的函数:

策略核心部分,重头戏来了,self.poll = function() {...}函数是整个策略的主要逻辑,上一篇文章中我们也讲了,在main()函数开始执行,进入while死循环之前,我们使用var reaper = LeeksReaper()构造了韭菜收割机对象,然后在main()函数中循环调用reaper.poll()就是调用的该函数。

self.poll函数开始执行,做了一些每次循环前的准备工作,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          # 声明交易数量变量,初始为0

接下来就是判断当前短期行情是牛还是熊了。

        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
        }

还记得,上一篇文章中的self.updateOrderBook()函数么,在其中我们使用加权平均的算法构造了一个时间序列为顺序的prices数组。本段代码中使用了三个新的函数_.min_.maxslice这三个函数也非常好理解,

  • _.min:作用是求出参数数组中最小的那个值。

  • _.max:作用是求出参数数组中最大的那个值。

  • slice:该函数是JavaScript数组对象的一个成员函数,作用是把数组中按照索引截取一部分返回,举个例子:

    function main() {
        // index     .. -8 -7 -6 -5 -4 -3 -2 -1
        var arr = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
        Log(arr.slice(-5, -1))    // 会截取 4 ~ 1 这几个元素,返回一个新数组:[4,3,2,1]
    }
    

    img

这里判断熊、牛的条件就是:

  • self.numTick > 2要成立,就是说在新一轮的检测价格爆发时,至少要经过三轮的检测才触发,避免开始就触发。
  • 价格序列self.prices中的最后一个数据,也就是最新的数据与self.prices数组中之前一段范围内的最大或者最小价格之差要突破burstPrice这个爆发价格。

如果条件都成立,则标记bull或者bear,为true,并且给tradeAmount变量赋值,计划梭哈交易。

再根据之前self.updateTrades()函数中更新、计算的self.vol,对于参数BurstThresholdVol决定是否减小交易力度(减小计划交易的量)。

        if (self.vol < BurstThresholdVol) {
            tradeAmount *= self.vol / BurstThresholdVol   // 缩减计划交易量,缩减为之前量的self.vol / BurstThresholdVol 倍
        }
        
        if (self.numTick < 5) {
            tradeAmount *= 0.8      // 缩减为计划的80%
        }
        
        if (self.numTick < 10) {    // 缩减为计划的80%
            tradeAmount *= 0.8
        }

接下来判断交易信号、交易量是否符合要求:

        if ((!bull && !bear) || tradeAmount < MinStock) {   # 如果非牛市并且也非熊市,或者计划交易的量tradeAmount小于参数设置的最小交易量MinStock,poll函数直接返回,不做交易操作
            return
        }

通过以上判断之后,执行var tradePrice = bull ? self.bidPrice : self.askPrice根据是熊市还是牛市,设置交易价格,用对应的提单价格赋值。

最后进入一个while循环,该循环唯一的停止跳出条件是tradeAmount >= MinStock计划交易的量小于了最小交易量。 在循环中根据当前是牛市状态,还是熊市状态,执行下单。并记录下单ID在变量orderId。每轮循环下单后Sleep(200)等待200毫秒。循环中接着判断orderId是否为真(如果下单失败,不会返回订单ID,就不会触发该if条件),如果条件为真。拿到了订单ID赋值给self.tradeOrderId

声明一个用以储存订单数据的变量order初始赋值为null。然后循环获取这个ID的订单数据,并且判断订单是否是挂单状态,如果是挂单状态,取消该ID的订单,如果不是挂单状态则跳出这个检测循环。

                var order = null           // 声明一个变量用于保存订单数据
                while (true) {             // 一个while循环
                    order = exchange.GetOrder(orderId)    // 调用GetOrder查询订单ID为 orderId的订单数据
                    if (order) {                          // 如果查询到订单数据,查询失败order为null,不会触发当前if条件
                        if (order.Status == ORDER_STATE_PENDING) {   // 判断订单状态是不是正在挂单中
                            exchange.CancelOrder(orderId)            // 如果当前正在挂单,取消该订单
                            Sleep(200)
                        } else {                                     // 否则执行break跳出当前while循环
                            break
                        }
                    }
                }

接着执行以下流程:

                self.tradeOrderId = 0              // 重置self.tradeOrderId
                tradeAmount -= order.DealAmount    // 更新tradeAmount,减去提单的订单已经成交的数量
                tradeAmount *= 0.9                 // 减小下单力度
                if (order.Status == ORDER_STATE_CANCELED) {     // 如果订单已经是取消了
                    self.updateOrderBook()                      // 更新订单薄等数据
                    while (bull && self.bidPrice - tradePrice > 0.1) {   // 牛市时,更新后的提单价格超过当前交易价格0.1就减小交易力度,略微调整交易价格
                        tradeAmount *= 0.99
                        tradePrice += 0.1
                    }
                    while (bear && self.askPrice - tradePrice < -0.1) {  // 熊市时,更新后的提单价格超过当前交易价格0.1就减小交易力度,略微调整交易价格
                        tradeAmount *= 0.99
                        tradePrice -= 0.1
                    }
                }

当程序流程跳出while (tradeAmount >= MinStock) {...}这个循环时,说明本次价格爆发交易流程执行完毕了。 执行self.numTick = 0,即重置self.numTick为0。

LeeksReaper()构造函数执行最后将self对象返回,就是var reaper = LeeksReaper()时,返回给了reaper

至此LeeksReaper()构造函数是如何构造这个韭菜收割机对象的以及韭菜收割机对象各个方法,主要逻辑函数的执行流程,我们剖析了一遍,相信您看完本文应该对这个高频策略算法流程有了一个比较清晰的理解。


More

lu4kyd0y 有微信交流群嘛

小康 BurstThresholdVol 这个参数是干嘛的?该怎么设置啊

evan1987 细节满满,看了1小时才勉强理解细节,赞

rootme 梦总666,研究明白后我能写出跟print money 一样的韭菜收割机么

lu4kyd0y 主页的微信还没有通过

小小梦 FMZ平台首页,有群组信息,可以加QQ 或者微信群。

小小梦 爆发量,这个是策略参数,人为设置的,详细看下策略、文章,就知道这个变量控制什么了。

小小梦 原理应该是差不多吧