accumulate_distribute/util/generate_order.js

'use strict'

const _isFinite = require('lodash/isFinite')
const _isObject = require('lodash/isObject')
const { Order } = require('bfx-api-node-models')
const genCID = require('../../util/gen_client_id')

/**
 * Generates an atomic order to fill one slice of an AccumulateDistribute
 * instance.
 *
 * @memberOf module:AccumulateDistribute
 * @name module:AccumulateDistribute~generateOrder
 *
 * @param {AOInstance} instance - AO instance
 * @returns {Order} o - null if awaiting data
 */
const generateOrder = (instance = {}) => {
  const { state = {}, h = {} } = instance
  const { debug } = h
  const {
    args = {}, orderAmounts, currentOrder, lastBook, lastTrade,
    capIndicator, offsetIndicator, remainingAmount
  } = state

  const {
    symbol, orderType, relativeOffset, relativeCap, limitPrice, _margin, hidden,
    lev, _futures
  } = args

  const scheduledAmount = orderAmounts[Math.min(currentOrder, orderAmounts.length - 1)]
  const amount = scheduledAmount > 0
    ? Math.min(scheduledAmount, remainingAmount)
    : Math.max(scheduledAmount, remainingAmount)

  const sharedOrderParams = {
    symbol,
    amount,
    hidden
  }

  if (_futures) {
    sharedOrderParams.lev = lev
  }

  if (orderType === 'MARKET') {
    return new Order({
      ...sharedOrderParams,
      cid: genCID(),
      type: _margin || _futures ? 'MARKET' : 'EXCHANGE MARKET',
      meta: { _HF: 1 }
    })
  } else if (orderType === 'LIMIT') {
    return new Order({
      ...sharedOrderParams,
      price: limitPrice,
      cid: genCID(),
      type: _margin || _futures ? 'LIMIT' : 'EXCHANGE LIMIT',
      meta: { _HF: 1 }
    })
  } else if (orderType !== 'RELATIVE') {
    throw new Error(`unknown order type: ${orderType}`)
  }

  let offsetPrice

  switch (relativeOffset.type) {
    case 'trade': {
      if (!lastTrade) return null
      offsetPrice = lastTrade.price
      break
    }

    case 'bid': {
      if (!lastBook) return null
      offsetPrice = lastBook.topBid()
      break
    }

    case 'ask': {
      if (!lastBook) return null
      offsetPrice = lastBook.topAsk()
      break
    }

    case 'mid': {
      if (!lastBook) return null
      offsetPrice = lastBook.midPrice()
      break
    }

    case 'ema': {
      if (offsetIndicator.l() === 0) return null
      offsetPrice = offsetIndicator.v() // guaranteed seeded
      break
    }

    case 'ma': {
      if (offsetIndicator.l() === 0) return null
      offsetPrice = offsetIndicator.v() // guaranteed seeded
      break
    }

    default: {
      throw new Error(`unknown relative offset type: ${relativeOffset.type}`)
    }
  }

  if (!_isFinite(offsetPrice)) {
    return null // awaiting data
  }

  debug('resolved offset price %f', offsetPrice)

  let finalPrice = offsetPrice + relativeOffset.delta

  if (_isObject(relativeCap) && relativeCap.type !== 'none') {
    let priceCap

    switch (relativeCap.type) {
      case 'trade': {
        if (!lastTrade) return null
        priceCap = lastTrade.price
        break
      }

      case 'bid': {
        if (!lastBook) return null
        priceCap = lastBook.topBid()
        break
      }

      case 'ask': {
        if (!lastBook) return null
        priceCap = lastBook.topAsk()
        break
      }

      case 'mid': {
        if (!lastBook) return null
        priceCap = lastBook.midPrice()
        break
      }

      case 'ema': {
        if (capIndicator.l() === 0) return null
        priceCap = capIndicator.v()
        break
      }

      case 'ma': {
        if (capIndicator.l() === 0) return null
        priceCap = capIndicator.v()
        break
      }

      default: {
        throw new Error(`unknown relative cap type: ${relativeCap.type}`)
      }
    }

    if (!_isFinite(priceCap)) {
      return null
    }

    priceCap += relativeCap.delta

    debug('resolved cap price %f', priceCap)

    finalPrice = Math.min(finalPrice, priceCap)
  }

  return new Order({
    ...sharedOrderParams,
    price: finalPrice,
    cid: genCID(),
    type: _margin || _futures ? 'LIMIT' : 'EXCHANGE LIMIT',
    meta: { _HF: 1 }
  })
}

module.exports = {
  gen: generateOrder
} // exported in object so we can be mocked