diff --git a/CHANGELOG b/CHANGELOG
index 133d70fd..84e475ed 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+# 2.0.6
+- feature: OCOCO type
+
# 2.0.5
- feature: futures support
diff --git a/docs/ococo.md b/docs/ococo.md
new file mode 100644
index 00000000..8f06e154
--- /dev/null
+++ b/docs/ococo.md
@@ -0,0 +1,25 @@
+
+
+## OCOCO
+Order Creates OCO (or OCOCO) triggers an OCO order after an initial MARKET
+or LIMIT order fills.
+
+**Kind**: global variable
+
+| Param | Type | Description |
+| --- | --- | --- |
+| symbol | string
| symbol to trade on |
+| orderType | string
| initial order type, LIMIT or MARKET |
+| orderPrice | number
| price for initial order if `orderType` is LIMIT |
+| amount | number
| initial order amount |
+| _margin | boolean
| if false, order type is prefixed with EXCHANGE |
+| _futures | boolean
| if false, order type is prefixed with EXCHANGE |
+| action | string
| initial order direction, Buy or Sell |
+| limitPrice | number
| oco order limit price |
+| stopPrice | number
| oco order stop price |
+| ocoAmount | number
| oco order amount |
+| ocoAction | string
| oco order direction, Buy or Sell |
+| submitDelaySec | number
| submit delay in seconds |
+| cancelDelaySec | number
| cancel delay in seconds |
+| lev | number
| leverage for relevant markets |
+
diff --git a/index.js b/index.js
index 47592dbe..f889aa6e 100644
--- a/index.js
+++ b/index.js
@@ -7,5 +7,6 @@ module.exports = {
AccumulateDistribute: require('./lib/accumulate_distribute'),
PingPong: require('./lib/ping_pong'),
MACrossover: require('./lib/ma_crossover'),
+ OCOCO: require('./lib/ococo'),
NoDataError: require('./lib/errors/no_data')
}
diff --git a/lib/ococo/events/life_start.js b/lib/ococo/events/life_start.js
new file mode 100644
index 00000000..26a0fdf3
--- /dev/null
+++ b/lib/ococo/events/life_start.js
@@ -0,0 +1,8 @@
+'use strict'
+
+module.exports = async (instance = {}) => {
+ const { h = {} } = instance
+ const { emitSelf } = h
+
+ await emitSelf('submit_initial_order')
+}
diff --git a/lib/ococo/events/life_stop.js b/lib/ococo/events/life_stop.js
new file mode 100644
index 00000000..ceec7efe
--- /dev/null
+++ b/lib/ococo/events/life_stop.js
@@ -0,0 +1,3 @@
+'use strict'
+
+module.exports = async (instance = {}) => {}
diff --git a/lib/ococo/events/orders_order_cancel.js b/lib/ococo/events/orders_order_cancel.js
new file mode 100644
index 00000000..a1e16623
--- /dev/null
+++ b/lib/ococo/events/orders_order_cancel.js
@@ -0,0 +1,17 @@
+'use strict'
+
+module.exports = async (instance = {}, order) => {
+ const { state = {}, h = {} } = instance
+ const { args = {}, orders = {}, gid, timeout } = state
+ const { emit, debug } = h
+ const { cancelDelay } = args
+
+ debug('detected atomic cancelation, stopping...')
+
+ if (timeout) {
+ clearTimeout(timeout)
+ }
+
+ await emit('exec:order:cancel:all', gid, orders, cancelDelay)
+ await emit('exec:stop')
+}
diff --git a/lib/ococo/events/orders_order_fill.js b/lib/ococo/events/orders_order_fill.js
new file mode 100644
index 00000000..a4af7284
--- /dev/null
+++ b/lib/ococo/events/orders_order_fill.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const { Config } = require('bfx-api-node-core')
+const { DUST } = Config
+
+module.exports = async (instance = {}, order) => {
+ const { state = {}, h = {} } = instance
+ const { updateState, emitSelf } = h
+ const { initialOrderFilled } = state
+
+ if (order.amount > DUST) { // partial fill
+ return
+ }
+
+ if (initialOrderFilled) {
+ await emitSelf('exec:stop')
+ } else {
+ await updateState(instance, { initialOrderFilled: true })
+ await emitSelf('submit_oco_order')
+ }
+}
diff --git a/lib/ococo/events/self_submit_initial_order.js b/lib/ococo/events/self_submit_initial_order.js
new file mode 100644
index 00000000..cabb4387
--- /dev/null
+++ b/lib/ococo/events/self_submit_initial_order.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const generateInitialOrder = require('../util/generate_initial_order')
+
+module.exports = async (instance = {}) => {
+ const { state = {}, h = {} } = instance
+ const { emit, debug } = h
+ const { args = {}, gid } = state
+ const { submitDelay } = args
+
+ const order = generateInitialOrder(instance)
+
+ debug(
+ 'generated order %s for %f @ %f',
+ order.type, order.amount, order.price || 'MARKET'
+ )
+
+ await emit('exec:order:submit:all', gid, [order], submitDelay)
+}
diff --git a/lib/ococo/events/self_submit_oco_order.js b/lib/ococo/events/self_submit_oco_order.js
new file mode 100644
index 00000000..01301818
--- /dev/null
+++ b/lib/ococo/events/self_submit_oco_order.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const generateOCOOrder = require('../util/generate_oco_order')
+
+module.exports = async (instance = {}) => {
+ const { state = {}, h = {} } = instance
+ const { emit, debug } = h
+ const { args = {}, gid } = state
+ const { submitDelay } = args
+
+ const order = generateOCOOrder(instance)
+
+ debug(
+ 'generated order %s for %f @ %f',
+ order.type, order.amount, order.price || 'MARKET'
+ )
+
+ await emit('exec:order:submit:all', gid, [order], submitDelay)
+}
diff --git a/lib/ococo/index.js b/lib/ococo/index.js
new file mode 100644
index 00000000..f4c20e1a
--- /dev/null
+++ b/lib/ococo/index.js
@@ -0,0 +1,75 @@
+'use strict'
+
+const defineAlgoOrder = require('../define_algo_order')
+
+const validateParams = require('./meta/validate_params')
+const processParams = require('./meta/process_params')
+const initState = require('./meta/init_state')
+const onSelfSubmitInitialOrder = require('./events/self_submit_initial_order')
+const onSelfSubmitOCOOrder = require('./events/self_submit_oco_order')
+const onLifeStart = require('./events/life_start')
+const onLifeStop = require('./events/life_stop')
+const onOrdersOrderCancel = require('./events/orders_order_cancel')
+const onOrdersOrderFill = require('./events/orders_order_fill')
+const genOrderLabel = require('./meta/gen_order_label')
+const genPreview = require('./meta/gen_preview')
+const declareEvents = require('./meta/declare_events')
+const getUIDef = require('./meta/get_ui_def')
+const serialize = require('./meta/serialize')
+const unserialize = require('./meta/unserialize')
+
+/**
+ * Order Creates OCO (or OCOCO) triggers an OCO order after an initial MARKET
+ * or LIMIT order fills.
+ *
+ * @name OCOCO
+ * @param {string} symbol - symbol to trade on
+ * @param {string} orderType - initial order type, LIMIT or MARKET
+ * @param {number} orderPrice - price for initial order if `orderType` is LIMIT
+ * @param {number} amount - initial order amount
+ * @param {boolean} _margin - if false, order type is prefixed with EXCHANGE
+ * @param {boolean} _futures - if false, order type is prefixed with EXCHANGE
+ * @param {string} action - initial order direction, Buy or Sell
+ * @param {number} limitPrice - oco order limit price
+ * @param {number} stopPrice - oco order stop price
+ * @param {number} ocoAmount - oco order amount
+ * @param {string} ocoAction - oco order direction, Buy or Sell
+ * @param {number} submitDelaySec - submit delay in seconds
+ * @param {number} cancelDelaySec - cancel delay in seconds
+ * @param {number} lev - leverage for relevant markets
+ */
+const OCOCO = defineAlgoOrder({
+ id: 'bfx-ococo',
+ name: 'OCOCO',
+
+ meta: {
+ validateParams,
+ processParams,
+ declareEvents,
+ getUIDef,
+ genOrderLabel,
+ genPreview,
+ initState,
+ serialize,
+ unserialize
+ },
+
+ events: {
+ self: {
+ submit_initial_order: onSelfSubmitInitialOrder,
+ submit_oco_order: onSelfSubmitOCOOrder
+ },
+
+ life: {
+ start: onLifeStart,
+ stop: onLifeStop
+ },
+
+ orders: {
+ order_cancel: onOrdersOrderCancel,
+ order_fill: onOrdersOrderFill
+ }
+ }
+})
+
+module.exports = OCOCO
diff --git a/lib/ococo/meta/declare_events.js b/lib/ococo/meta/declare_events.js
new file mode 100644
index 00000000..f840739e
--- /dev/null
+++ b/lib/ococo/meta/declare_events.js
@@ -0,0 +1,9 @@
+'use strict'
+
+module.exports = (instance = {}, host) => {
+ const { h = {} } = instance
+ const { declareEvent } = h
+
+ declareEvent(instance, host, 'self:submit_initial_order', 'submit_initial_order')
+ declareEvent(instance, host, 'self:submit_oco_order', 'submit_oco_order')
+}
diff --git a/lib/ococo/meta/gen_order_label.js b/lib/ococo/meta/gen_order_label.js
new file mode 100644
index 00000000..a264394c
--- /dev/null
+++ b/lib/ococo/meta/gen_order_label.js
@@ -0,0 +1,14 @@
+'use strict'
+
+module.exports = (state = {}) => {
+ const { args = {} } = state
+ const {
+ orderType, orderPrice, amount, ocoAmount, limitPrice, stopPrice
+ } = args
+
+ return [
+ 'OCOCO',
+ ` | ${amount} @ ${orderPrice || orderType} `,
+ ` | triggers ${ocoAmount} @ ${limitPrice} (stop ${stopPrice})`
+ ].join('')
+}
diff --git a/lib/ococo/meta/gen_preview.js b/lib/ococo/meta/gen_preview.js
new file mode 100644
index 00000000..754902b6
--- /dev/null
+++ b/lib/ococo/meta/gen_preview.js
@@ -0,0 +1,5 @@
+'use strict'
+
+module.exports = (args = {}) => {
+ return []
+}
diff --git a/lib/ococo/meta/get_ui_def.js b/lib/ococo/meta/get_ui_def.js
new file mode 100644
index 00000000..7208933d
--- /dev/null
+++ b/lib/ococo/meta/get_ui_def.js
@@ -0,0 +1,146 @@
+module.exports = () => ({
+ label: 'Order Creates OcO',
+ id: 'bfx-ococo',
+
+ uiIcon: 'ma-crossover-active',
+ connectionTimeout: 10000,
+ actionTimeout: 10000,
+
+ header: {
+ component: 'ui.checkbox_group',
+ fields: ['hidden', 'postonly']
+ },
+
+ sections: [{
+ title: '',
+ name: 'general',
+ rows: [
+ ['orderType', 'orderPrice'],
+ ['amount', 'action']
+ ]
+ }, {
+ title: 'OcO Settings',
+ name: 'shortEMASettings',
+ fixed: true,
+
+ rows: [
+ ['limitPrice', 'stopPrice'],
+ ['ocoAmount', 'ocoAction']
+ ]
+ }, {
+ title: '',
+ name: 'submitSettings',
+ fixed: true,
+
+ rows: [
+ ['submitDelaySec', 'cancelDelaySec']
+ ]
+ }, {
+ title: '',
+ name: 'lev',
+ fullWidth: true,
+ rows: [
+ ['lev']
+ ],
+
+ visible: {
+ _context: { eq: 'f' }
+ }
+ }],
+
+ fields: {
+ submitDelaySec: {
+ component: 'input.number',
+ label: 'Submit Delay (sec)',
+ customHelp: 'Seconds to wait before submitting orders',
+ default: 1
+ },
+
+ cancelDelaySec: {
+ component: 'input.number',
+ label: 'Cancel Delay (sec)',
+ customHelp: 'Seconds to wait before cancelling orders',
+ default: 0
+ },
+
+ orderType: {
+ component: 'input.dropdown',
+ label: 'Order Type',
+ default: 'LIMIT',
+ options: {
+ LIMIT: 'Limit',
+ MARKET: 'Market'
+ }
+ },
+
+ amount: {
+ component: 'input.amount',
+ label: 'Amount $BASE',
+ customHelp: 'Initial Order amount',
+ priceField: 'orderPrice'
+ },
+
+ ocoAmount: {
+ component: 'input.amount',
+ label: 'Amount $BASE',
+ customHelp: 'OcO Order amount'
+ },
+
+ orderPrice: {
+ component: 'input.price',
+ label: 'Initial Order Price $QUOTE',
+
+ disabled: {
+ orderType: { eq: 'MARKET' }
+ }
+ },
+
+ limitPrice: {
+ component: 'input.price',
+ label: 'OcO Limit Price $QUOTE'
+ },
+
+ stopPrice: {
+ component: 'input.price',
+ label: 'OcO Stop Price $QUOTE'
+ },
+
+ lev: {
+ component: 'input.range',
+ label: 'Leverage',
+ min: 1,
+ max: 100,
+ default: 10
+ },
+
+ hidden: {
+ component: 'input.checkbox',
+ label: 'HIDDEN',
+ default: false
+ },
+
+ postonly: {
+ component: 'input.checkbox',
+ label: 'POST-ONLY',
+ default: false
+ },
+
+ action: {
+ component: 'input.radio',
+ label: 'Action',
+ options: ['Buy', 'Sell'],
+ inline: true,
+ default: 'Buy'
+ },
+
+ ocoAction: {
+ component: 'input.radio',
+ label: 'Action',
+ options: ['Buy', 'Sell'],
+ inline: true,
+ default: 'Buy'
+ }
+ },
+
+ actions: ['preview', 'submit']
+})
diff --git a/lib/ococo/meta/init_state.js b/lib/ococo/meta/init_state.js
new file mode 100644
index 00000000..d65e38b4
--- /dev/null
+++ b/lib/ococo/meta/init_state.js
@@ -0,0 +1,8 @@
+'use strict'
+
+module.exports = (args = {}) => {
+ return {
+ args,
+ initialOrderFilled: false
+ }
+}
diff --git a/lib/ococo/meta/process_params.js b/lib/ococo/meta/process_params.js
new file mode 100644
index 00000000..ac2867e3
--- /dev/null
+++ b/lib/ococo/meta/process_params.js
@@ -0,0 +1,52 @@
+'use strict'
+
+const _isFinite = require('lodash/isFinite')
+
+module.exports = (data) => {
+ const params = { ...data }
+
+ if (params._symbol) {
+ params.symbol = params._symbol
+ delete params._symbol
+ }
+
+ if (!params._futures) {
+ delete params.lev
+ }
+
+ if (params.cancelDelaySec) {
+ params.cancelDelay = params.cancelDelaySec * 1000
+ delete params.cancelDelaySec
+ }
+
+ if (params.submitDelaySec) {
+ params.submitDelay = params.submitDelaySec * 1000
+ delete params.submitDelaySec
+ }
+
+ if (!_isFinite(params.cancelDelay)) {
+ params.cancelDelay = 1000
+ }
+
+ if (!_isFinite(params.submitDelay)) {
+ params.submitDelay = 2000
+ }
+
+ if (params.action) {
+ if (params.action === 'Sell') {
+ params.amount = (+params.amount) * -1
+ }
+
+ delete params.action
+ }
+
+ if (params.ocoAction) {
+ if (params.ocoAction === 'Sell') {
+ params.ocoAmount = (+params.ocoAmount) * -1
+ }
+
+ delete params.ocoAction
+ }
+
+ return params
+}
diff --git a/lib/ococo/meta/serialize.js b/lib/ococo/meta/serialize.js
new file mode 100644
index 00000000..84d4633b
--- /dev/null
+++ b/lib/ococo/meta/serialize.js
@@ -0,0 +1,11 @@
+'use strict'
+
+module.exports = (state = {}) => {
+ const { args = {}, label, name } = state
+
+ return {
+ label,
+ name,
+ args
+ }
+}
diff --git a/lib/ococo/meta/unserialize.js b/lib/ococo/meta/unserialize.js
new file mode 100644
index 00000000..ab666845
--- /dev/null
+++ b/lib/ococo/meta/unserialize.js
@@ -0,0 +1,11 @@
+'use strict'
+
+module.exports = (loadedState = {}) => {
+ const { args = {}, name, label } = loadedState
+
+ return {
+ label,
+ name,
+ args
+ }
+}
diff --git a/lib/ococo/meta/validate_params.js b/lib/ococo/meta/validate_params.js
new file mode 100644
index 00000000..9df34d95
--- /dev/null
+++ b/lib/ococo/meta/validate_params.js
@@ -0,0 +1,35 @@
+'use strict'
+
+const _isFinite = require('lodash/isFinite')
+
+const ORDER_TYPES = ['MARKET', 'LIMIT']
+
+module.exports = (args = {}) => {
+ const {
+ orderPrice, amount, orderType, submitDelay, cancelDelay, limitPrice,
+ stopPrice, ocoAmount, lev, _futures, action, ocoAction
+ } = args
+
+ if (ORDER_TYPES.indexOf(orderType) === -1) return `Invalid order type: ${orderType}`
+ if (!_isFinite(amount)) return 'Invalid amount'
+ if (!_isFinite(submitDelay) || submitDelay < 0) return 'Invalid submit delay'
+ if (!_isFinite(cancelDelay) || cancelDelay < 0) return 'Invalid cancel delay'
+ if (orderType === 'LIMIT' && !_isFinite(orderPrice)) {
+ return 'Limit price required for LIMIT order type'
+ }
+
+ if (!_isFinite(limitPrice)) return 'Invalid OCO limit price'
+ if (!_isFinite(stopPrice)) return 'Invalid OCO stop price'
+ if (!_isFinite(ocoAmount)) return 'Invalid OCO amount'
+
+ if (action !== 'Buy' && action !== 'Sell') return `Invalid action: ${action}`
+ if (ocoAction !== 'Buy' && ocoAction !== 'Sell') return `Invalid OCO action: ${ocoAction}`
+
+ if (_futures) {
+ if (!_isFinite(lev)) return 'Invalid leverage'
+ if (lev < 1) return 'Leverage less than 1'
+ if (lev > 100) return 'Leverage greater than 100'
+ }
+
+ return null
+}
diff --git a/lib/ococo/util/generate_initial_order.js b/lib/ococo/util/generate_initial_order.js
new file mode 100644
index 00000000..d9662476
--- /dev/null
+++ b/lib/ococo/util/generate_initial_order.js
@@ -0,0 +1,41 @@
+'use strict'
+
+const { Order } = require('bfx-api-node-models')
+const { nonce } = require('bfx-api-node-util')
+
+module.exports = (instance = {}) => {
+ const { state = {} } = instance
+ const { args = {} } = state
+ const {
+ amount, symbol, orderType, orderPrice, hidden, postonly, lev, _margin,
+ _futures
+ } = args
+
+ const sharedOrderParams = {
+ symbol,
+ amount,
+ hidden,
+ postonly
+ }
+
+ if (_futures) {
+ sharedOrderParams.lev = lev
+ }
+
+ if (orderType === 'MARKET') {
+ return new Order({
+ ...sharedOrderParams,
+ cid: nonce(),
+ type: _margin || _futures ? 'MARKET' : 'EXCHANGE MARKET'
+ })
+ } else if (orderType === 'LIMIT') {
+ return new Order({
+ ...sharedOrderParams,
+ price: +orderPrice,
+ cid: nonce(),
+ type: _margin || _futures ? 'LIMIT' : 'EXCHANGE LIMIT'
+ })
+ } else {
+ throw new Error(`unknown order type: ${orderType}`)
+ }
+}
diff --git a/lib/ococo/util/generate_oco_order.js b/lib/ococo/util/generate_oco_order.js
new file mode 100644
index 00000000..2d70c9a1
--- /dev/null
+++ b/lib/ococo/util/generate_oco_order.js
@@ -0,0 +1,37 @@
+'use strict'
+
+const { Order } = require('bfx-api-node-models')
+const { nonce } = require('bfx-api-node-util')
+
+module.exports = (instance = {}) => {
+ const { state = {} } = instance
+ const { args = {} } = state
+ const {
+ ocoAmount, symbol, limitPrice, stopPrice, hidden, postonly, lev,
+ _margin, _futures
+ } = args
+
+ const cid = nonce()
+ const sharedOrderParams = {
+ symbol,
+ amount: ocoAmount
+ }
+
+ if (_futures) {
+ sharedOrderParams.lev = lev
+ }
+
+ const o = new Order({
+ ...sharedOrderParams,
+ price: +limitPrice,
+ priceAuxLimit: +stopPrice,
+ oco: true,
+ cid: cid,
+ cidOCO: cid,
+ type: _margin || _futures ? 'LIMIT' : 'EXCHANGE LIMIT',
+ hidden,
+ postonly
+ })
+
+ return o
+}
diff --git a/package.json b/package.json
index 912ac697..0ea75bcd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bfx-hf-algo",
- "version": "2.0.5",
+ "version": "2.0.6",
"description": "HF Algorithmic Order Module",
"main": "index.js",
"directories": {
@@ -29,7 +29,8 @@
"twap_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/twap/index.js > docs/twap.md",
"pingpong_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/ping_pong/index.js > docs/ping_pong.md",
"ad_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/accumulate_distribute/index.js > docs/accumulate_distribute.md",
- "docs": "npm run aohost_docs && npm run helpers_docs && npm run iceberg_docs && npm run twap_docs && npm run ad_docs && npm run macrossover_docs && npm run pingpong_docs"
+ "ococo_docs": "node_modules/jsdoc-to-markdown/bin/cli.js lib/ococo/index.js > docs/ococo.md",
+ "docs": "npm run aohost_docs && npm run helpers_docs && npm run iceberg_docs && npm run twap_docs && npm run ad_docs && npm run macrossover_docs && npm run pingpong_docs && npm run ococo_docs"
},
"repository": {
"type": "git",