bot18
Version:
A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f
464 lines (392 loc) • 10.6 kB
JavaScript
'use strict'
const Promise = require('bluebird')
const Model = require('../model')
const { prepareAmount, preparePrice } = require('../util/precision')
const BOOL_FIELDS = ['notify']
const FIELDS = {
id: 0,
gid: 1,
cid: 2,
symbol: 3,
mtsCreate: 4,
mtsUpdate: 5,
amount: 6,
amountOrig: 7,
type: 8,
typePrev: 9,
flags: 12,
status: 13,
price: 16,
priceAvg: 17,
priceTrailing: 18,
priceAuxLimit: 19,
notify: 23,
placedId: 25
}
const FIELD_KEYS = Object.keys(FIELDS)
let lastCID = Date.now()
/**
* High level order model; provides methods for execution & can stay updated via
* a WSv2 connection
*/
class Order extends Model {
/**
* @param {Object|Array} data - either a map of order fields or a raw array
* @param {WSv2} ws - optional, saved for a later call to registerListeners()
*/
constructor (data = {}, ws) {
super(data, FIELDS, BOOL_FIELDS, FIELD_KEYS)
if (!this.flags) this.flags = 0
if (typeof data.oco !== 'undefined') this.setOCO(data.oco)
if (typeof data.hidden !== 'undefined') this.setHidden(data.hidden)
if (typeof data.postonly !== 'undefined') this.setPostOnly(data.postonly)
this._ws = ws
this._lastAmount = this.amount
this._onWSOrderNew = this._onWSOrderNew.bind(this)
this._onWSOrderUpdate = this._onWSOrderUpdate.bind(this)
this._onWSOrderClose = this._onWSOrderClose.bind(this)
if (isNaN(this.amountOrig) && !isNaN(this.amount)) {
this.amountOrig = this.amount
}
if (this.amount === 0 && this.amountOrig === 0) {
throw new Error('got 0 start & orig amount')
}
}
static unserialize (arr) {
return super.unserialize(arr, FIELDS, BOOL_FIELDS, FIELD_KEYS)
}
/**
* @return {boolean} oco
*/
isOCO () {
return !!(this.flags & Order.flags.OCO)
}
/**
* @return {boolean} hidden
*/
isHidden () {
return !!(this.flags & Order.flags.HIDDEN)
}
/**
* @return {boolean} postonly
*/
isPostOnly () {
return !!(this.flags & Order.flags.POSTONLY)
}
/**
* @return {boolean} includesVR
*/
includesVariableRates () {
return !(this.flags & Order.flags.NO_VR)
}
/**
* @return {boolean} posclose
*/
isPositionClose () {
return !!(this.flags & Order.flags.POS_CLOSE)
}
/**
* @return {boolean} reduceonly
*/
isReduceOnly () {
return !!(this.flags & Order.flags.REDUCE_ONLY)
}
/**
* @param {boolean} v
* @param {number?} stopPrice - optional, defaults to current value
*/
setOCO (v, stopPrice = this.priceAuxLimit) {
if (v) this.priceAuxLimit = stopPrice
this._modifyFlag(Order.flags.OCO, v)
}
/**
* @param {boolean} v
*/
setHidden (v) {
this._modifyFlag(Order.flags.HIDDEN, v)
}
/**
* @param {boolean} v
*/
setPostOnly (v) {
this._modifyFlag(Order.flags.POSTONLY, v)
}
/**
* @param {boolean} v
*/
setNoVariableRates (v) {
this._modifyFlag(Order.flags.NO_VR, v)
}
/**
* @param {boolean} v
*/
setPositionClose (v) {
this._modifyFlag(Order.flags.POS_CLOSE, v)
}
/**
* @param {boolean} v
*/
setReduceOnly (v) {
this._modifyFlag(Order.flags.REDUCE_ONLY, v)
}
_modifyFlag (flag, active) {
if (!!(this.flags & flag) === active) return
this.flags += active ? flag : -flag
}
/**
* Send an order update packet to the WS server, and update local state. This
* updates the order atomically without changing its position in the queue for
* its price level.
*
* Rejects with an error if an attempt is made to apply a delta to a missing
* amount.
*
* @param {Object} changes
* @param {WSv2} ws - optional, defaults to internal instance
* @return {Promise} p - resolves on ws2 confirmation, or rejects if no ws2
*/
update (changes = {}, ws = this._ws) {
const keys = Object.keys(changes)
// Apply change locally
keys.forEach(k => {
if (k === 'id') return
if (FIELD_KEYS.indexOf(k) !== -1) {
this[k] = changes[k]
} else if (k === 'price_trailing') {
this.priceTrailing = Number(changes[k])
} else if (k === 'price_oco_stop' || k === 'price_aux_limit') {
this.priceAuxLimit = Number(changes[k])
} else if (k === 'delta' && !Number.isNaN(+changes[k])) {
if (!Number.isNaN(+this.amount)) {
this.amount += Number(changes[k])
} else {
return Promise.reject(new Error('can\'t apply delta to missing amount'))
}
}
})
changes.id = this.id // tag with ID
if (changes.price) changes.price = preparePrice(changes.price)
if (changes.amount) changes.amount = prepareAmount(changes.amount)
if (changes.delta) changes.delta = prepareAmount(changes.delta)
if (changes.price_aux_limit) {
changes.price_aux_limit = preparePrice(changes.price_aux_limit)
}
if (changes.price_trailing) {
changes.price_trailing = preparePrice(changes.price_trailing)
}
return ws
? ws.updateOrder(changes)
: Promise.reject(new Error('no ws client available'))
}
/**
* @return {Object} preview
*/
toPreview () {
return {
gid: this.gid,
cid: this.cid,
symbol: this.symbol,
amount: this.amount,
type: this.type,
price: this.price,
notify: this.notify,
flags: this.flags
}
}
/**
* Registers for updates/persistence on the specified ws2 instance
*
* @param {WSv2} ws - optional, defaults to internal ws
*/
registerListeners (ws = this._ws) {
if (!ws) return
const chanData = {
symbol: this.symbol,
cid: this.cid || null,
cbGID: this.cbGID()
}
ws.onOrderNew(chanData, this._onWSOrderNew)
ws.onOrderUpdate(chanData, this._onWSOrderUpdate)
ws.onOrderClose(chanData, this._onWSOrderClose)
this._ws = ws
}
/**
* Removes update listeners from the specified ws2 instance
*
* @param {WSv2} ws - optional, defaults to internal ws
*/
removeListeners (ws = this._ws) {
if (ws) ws.removeListeners(this.cbGID())
}
/**
* @return {string} cbGID
*/
cbGID () {
return `${this.gid}.${this.cid}`
}
/**
* @param {WSv2} ws - optional, defaults to internal ws
* @return {Promise} p
*/
submit (ws = this._ws) {
if (!ws) return Promise.reject(new Error('no ws connection'))
return ws.submitOrder(this).then((orderArr) => {
Object.assign(this, Order.unserialize(orderArr))
return this
})
}
/**
* @param {WSv2} ws - optional, defaults to internal ws
* @return {Promise} p
*/
cancel (ws = this._ws) {
if (!ws) return Promise.reject(new Error('no ws connection'))
if (!this.id) return Promise.reject(new Error('order has no ID'))
return ws.cancelOrder(this.id)
}
/**
* Equivalent to calling cancel() followed by submit()
*
* @param {WSv2} ws - optional, defaults to internal ws
* @return {Promise} p
*/
recreate (ws = this._ws) {
if (!ws) return Promise.reject(new Error('no ws connection'))
if (!this.id) return Promise.reject(new Error('order has no ID'))
return this.cancel(ws).then(() => {
this.id = null
return this.submit(ws)
})
}
/**
* Query the amount that was filled on the last order update
*
* @return {number} amount
*/
getLastFillAmount () {
return this._lastAmount - this.amount
}
/**
* @return {string} currency
*/
getBaseCurrency () {
return this.symbol.substring(1, 4)
}
/**
* @return {string} currency
*/
getQuoteCurrency () {
return this.symbol.substring(4)
}
/**
* @return {number} value
*/
getNotionalValue () {
return Math.abs(this.amount * this.price)
}
/**
* @return {boolean} isPartiallyFilled
*/
isPartiallyFilled () {
const a = Math.abs(this.amount)
return a > 0 && a < Math.abs(this.amountOrig)
}
/**
* @param {Array} order
* @private
*/
_onWSOrderUpdate (order) {
this._lastAmount = this.amount
Object.assign(this, Order.unserialize(order))
this.emit('update', order, this)
}
/**
* @param {Array} order
* @private
*/
_onWSOrderClose (order) {
this._lastAmount = this.amount
Object.assign(this, Order.unserialize(order))
this.emit('update', order, this)
this.emit('close', order, this)
}
/**
* @param {Array} order
* @private
*/
_onWSOrderNew (order) {
this._lastAmount = this.amount
Object.assign(this, Order.unserialize(order))
this.emit('update', order, this)
this.emit('new', order, this)
}
/**
* Creates an order map that can be passed to the `on` command.
*
* @return {Object} o
*/
toNewOrderPacket () {
const data = {
gid: this.gid,
cid: Number.isNaN(+this.cid) ? lastCID++ : this.cid,
symbol: this.symbol,
type: this.type,
amount: this.amount + '',
flags: this.flags || 0,
meta: this.meta // optional
}
if (!Number.isNaN(+this.price)) {
data.price = this.price + ''
}
if (this.priceTrailing !== null && !Number.isNaN(+this.priceTrailing)) {
data.price_trailing = this.priceTrailing + ''
}
if (this.priceAuxLimit !== null && !Number.isNaN(+this.priceAuxLimit)) {
if (this.flags & Order.flags.OCO) {
data.price_oco_stop = this.priceAuxLimit + ''
} else {
data.price_aux_limit = this.priceAuxLimit + ''
}
}
return data
}
/**
* @param {Array} arr - order in ws2 array format
* @return {string} currency - base currency from symbol
*/
static getBaseCurrency (arr = []) {
return (arr[3] || '').substring(1, 4).toUpperCase()
}
/**
* @param {Array} arr - order in ws2 array format
* @return {string} currency - quote currency from symbol
*/
static getQuoteCurrency (arr = []) {
return (arr[3] || '').substring(4).toUpperCase()
}
}
Order.type = {}
Order.status = {}
const statuses = ['ACTIVE', 'EXECUTED', 'PARTIALLY FILLED', 'CANCELED']
const types = [
'MARKET', 'EXCHANGE MARKET', 'LIMIT', 'EXCHANGE LIMIT', 'STOP',
'EXCHANGE STOP', 'TRAILING STOP', 'EXCHANGE TRAILING STOP', 'FOK',
'EXCHANGE FOK', 'STOP LIMIT', 'EXCHANGE STOP LIMIT'
]
statuses.forEach((s) => {
Order.status[s] = s
Order.status[s.split(' ').join('_')] = s
})
types.forEach((t) => {
Order.type[t] = t
Order.type[t.split(' ').join('_')] = t
})
Order.flags = {
OCO: 2 ** 14, // 16384
POSTONLY: 2 ** 12, // 4096
HIDDEN: 2 ** 6, // 64
NO_VR: 2 ** 19, // 524288
POS_CLOSE: 2 ** 9, // 512
REDUCE_ONLY: 2 ** 10 // 1024
}
module.exports = Order