剖析一个数字货币高频策略

更多精彩内容,欢迎关注公众号:数量技术宅,也可添加技术宅个人微信号:sljsz01,与我交流。

OKCoin韭菜收割机,201606-201701从6千块入市赚到25万,在此开源

这是一个在OKCoin比特币交易平台上的高频交易机器人程序,从2016年6月策略基本定型,到2017年1月中旬,这个策略成功的把最初投入的6000块钱刷到了250000。由于近日央行对比特币实行高压政策,各大平台都停止了配资,并且开始征收交易费,该策略实际上已经失效了。

本机器人程序基于两个主要策略:

  1. 趋势策略:在价格发生趋势性的波动时,及时下单跟进,即俗话说的追涨杀跌

  2. 平衡策略:仓位偏离50%时,放出小单使仓位逐渐回归50%,防止趋势末期的反转造成回撤,即收益落袋,不吃鱼尾

本程序要求平衡仓位,即(本金+融资=融币),使得仓位在50%时净资产不随着价格波动,也保证了发生趋势性波动时涨跌都赚

[移植 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()就是负责构造一个韭菜收割机交易逻辑用的。

  • 策略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数组是一个有时间序列顺序的数据流。

第三个添加的函数:

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

 

关注 “数量技术宅”不迷路,您的点赞、转发,是我输出干货,最大的动力

 


往期干货分享推荐阅读

Omega System Trading and Development Club内部分享策略Easylanguage源码

一个真实数据集的完整机器学习解决方案(下)

一个真实数据集的完整机器学习解决方案(上)

如何使用交易开拓者(TB)开发数字货币策略

股指期货高频数据机器学习预测

如何使用TradingView(TV)回测数字货币交易策略

如何投资股票型基金?什么时间买?买什么?

【数量技术宅|量化投资策略系列分享】基于指数移动平均的股指期货交易策略

AMA指标原作者Perry Kaufman 100+套交易策略源码分享

【 数量技术宅 | 期权系列分享】期权策略的“独孤九剑”

【数量技术宅|金融数据系列分享】套利策略的价差序列计算,恐怕没有你想的那么简单

【数量技术宅|量化投资策略系列分享】成熟交易者期货持仓跟随策略

如何获取免费的数字货币历史数据

【数量技术宅|量化投资策略系列分享】多周期共振交易策略

【数量技术宅|金融数据分析系列分享】为什么中证500(IC)是最适合长期做多的指数

商品现货数据不好拿?商品季节性难跟踪?一键解决没烦恼的Python爬虫分享

【数量技术宅|金融数据分析系列分享】如何正确抄底商品期货、大宗商品

【数量技术宅|量化投资策略系列分享】股指期货IF分钟波动率统计策略

【数量技术宅 | Python爬虫系列分享】实时监控股市重大公告的Python爬虫

posted @ 2021-01-09 20:54  数量技术宅  阅读(643)  评论(0编辑  收藏  举报