Skip to content

Commit

Permalink
Add support for QuadrigaCX API (DeviaVir#342)
Browse files Browse the repository at this point in the history
* Docker compose update to use internal data volume. Makes it work with Docker for Windows

* Initial exchange implemented

Quadriga API public and private queries are working, testing the getQuote and getBalance.

* JS Beautifier Config

* Adjusting minimum and increments for quadriga products

* Bug fixes and formatting

* Fix bug in buy function (typo) prevent callback
* Change formatting to follow the rest of zenbot

* Fix markdown percentage option description

* Fixed issues with buy command

* Fix error handling in API calls

* Prevent spamming calls to getOrder

There is a general fall through to setTimer at the end of the function that must always run, the conditionals were causing additional uncessary checks which increases probability of hitting API rate limits.

* Sell function tested and fixes

* Fix formatting remove update-selectors script

* Remove archives mapping from docker-compose.yml

Not related to this chance, didn't serve a particularly usefull purpose

* Update docker-compose.yml
  • Loading branch information
cmroche authored and DeviaVir committed Jul 3, 2017
1 parent 3bd0aaf commit fc2c59a
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .jsbeautifyrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"indent_size": 2,
"js": {
"preserve-newlines": true
}
}
2 changes: 1 addition & 1 deletion commands/buy.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = function container (get, set, clear) {
.option('--pct <pct>', 'buy with this % of currency balance', Number, c.buy_pct)
.option('--order_type <type>', 'order type to use (maker/taker)', /^(maker|taker)$/i, c.order_type)
.option('--size <size>', 'buy specific size of currency')
.option('--markup_pct <pct>', '% to mark up ask price', Number, c.markup_pct)
.option('--markup_pct <pct>', '% to mark down ask price', Number, c.markup_pct)
.option('--order_adjust_time <ms>', 'adjust bid on this interval to keep order competitive', Number, c.order_adjust_time)
.option('--max_slippage_pct <pct>', 'avoid buying at a slippage pct above this float', c.max_slippage_pct)
.option('--debug', 'output detailed debug info')
Expand Down
11 changes: 11 additions & 0 deletions conf-sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ c.bitfinex.secret = 'YOUR-SECRET'
// May use 'exchange' or 'trading' wallet balances. However margin trading may not work...read the API documentation.
c.bitfinex.wallet = 'exchange'

// to enable QuadrigaCX tranding, enter your API credentials:
c.quadriga = {}
c.quadriga.key = 'YOUR-API-KEY';

// this is the manual secret key entered by editing the API access
// and NOT the md5 hash you see in the summary
c.quadriga.secret = 'YOUR-SECRET';

// replace with the client id used at login, as a string, not number
c.quadriga.client_id = 'YOUR-CLIENT-ID';

// Optional stop-order triggers:

// sell if price drops below this % of bought price (0 to disable)
Expand Down
7 changes: 7 additions & 0 deletions extensions/exchanges/quadriga/_codemap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
_ns: 'zenbot',
_name: 'quadriga',

'exchanges.quadriga': require('./exchange'),
'exchanges.list[]': '#exchanges.quadriga'
}
278 changes: 278 additions & 0 deletions extensions/exchanges/quadriga/exchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
var QuadrigaCX = require('quadrigacx'),
path = require('path'),
colors = require('colors'),
n = require('numbro')

module.exports = function container(get, set, clear) {
var c = get('conf')
var shownWarnings = false

var public_client, authed_client

function publicClient() {
if (!public_client) public_client = new QuadrigaCX("1", "", "");
return public_client
}

function authedClient() {
if (!authed_client) {
if (!c.quadriga || !c.quadriga.key || !c.quadriga.key === 'YOUR-API-KEY') {
throw new Error('please configure your Quadriga credentials in ' + path.resolve(__dirname, 'conf.js'))
}

authed_client = new QuadrigaCX(c.quadriga.client_id, c.quadriga.key, c.quadriga.secret);
}
return authed_client
}

function joinProduct(product_id) {
return (product_id.split('-')[0] + '_' + product_id.split('-')[1]).toLowerCase()
}

function retry(method, args, error) {
if (error.code === 200) {
console.error(('\QuadrigaCX API rate limit exceeded! unable to call ' + method + ', aborting').red)
return;
}

if (method !== 'getTrades') {
console.error(('\QuadrigaCX API is down! unable to call ' + method + ', retrying in 30s').red)
}
setTimeout(function() {
exchange[method].apply(exchange, args)
}, 30000)
}

var orders = {}

var exchange = {
name: 'quadriga',
historyScan: 'backward',
makerFee: 0.5,

getProducts: function() {
return require('./products.json')
},

getTrades: function(opts, cb) {
var func_args = [].slice.call(arguments)
var args = {
book: joinProduct(opts.product_id),
time: 'hour'
}

var client = publicClient()
client.api('transactions', args, function(err, trades) {
if (!shownWarnings) {
console.log('please note: the quadriga api does not support backfilling (trade/paper only).')
console.log('please note: make sure to set the period to 1h')
shownWarnings = true;
}

if (err) return retry('getTrades', func_args, err)
if (trades.error) return retry('getTrades', func_args, trades.error)

var trades = trades.map(function(trade) {
return {
trade_id: trade.tid,
time: trade.date,
size: trade.amount,
price: trade.price,
side: trade.side
}
})

cb(null, trades)
})
},

getBalance: function(opts, cb) {
var client = authedClient()
client.api('balance', function(err, wallet) {
if (err) return retry('getBalance', null, err)
if (wallet.error) return retry('getBalance', null, wallet.error)

var currency = opts.currency.toLowerCase()
var asset = opts.asset.toLowerCase()

var balance = {
asset: 0,
currency: 0
}

balance.currency = wallet[currency + '_balance'];
balance.asset = wallet[asset + '_balance'];

balance.currency_hold = wallet[currency + '_reserved']
balance.asset_hold = wallet[asset + '_reserved']
cb(null, balance)
})
},

getQuote: function(opts, cb) {
var func_args = [].slice.call(arguments)

var params = {
book: joinProduct(opts.product_id)
}

var client = publicClient()
client.api('ticker', params, function(err, quote) {
if (err) return retry('getQuote', func_args, err)
if (quote.error) return retry('getQuote', func_args, quote.error)

var r = {
bid: quote.bid,
ask: quote.ask
}

cb(null, r)
})
},

cancelOrder: function(opts, cb) {
var func_args = [].slice.call(arguments)
var params = {
id: opts.order_id
}

var client = authedClient()
client.api('cancel_order', params, function(err, body) {
if (err) return retry('cancelOrder', func_args, err)
if (body.error) return retry('cancelOrder', func_args, body.error)
cb()
})
},

buy: function(opts, cb) {
var params = {
amount: opts.size,
book: joinProduct(opts.product_id)
}

if (opts.order_type === 'maker') {
params.price = n(opts.price).format('0.00')
}

var client = authedClient()
client.api('buy', params, function(err, body) {
var order = {
id: null,
status: 'open',
price: opts.price,
size: opts.size,
created_at: new Date().getTime(),
filled_size: '0',
ordertype: opts.order_type
}

if (err) return cb(err)
if (body.error) return cb(body.error.message)

if (opts.order_type === 'taker') {
order.status = 'done'
order.done_at = new Date().getTime();

if (body.orders_matched) {
var asset_total = 0
var price_total = 0.0
var order_count = body.orders_matched.length
for (var idx = 0; idx < order_count; idx++) {
asset_total = asset_total + body.orders_matched[idx].amount
price_total = price_total + (body.orders_matched[idx].amount * body.orfers_matched[idx].price)
}

order.price = price_total / asset_total
order.size = asset_total
} else {
order.price = body.price
order.size = body.amount
}
}

order.id = body.id
orders['~' + body.id] = order
cb(null, order)
})
},

sell: function(opts, cb) {
var params = {
amount: opts.size,
book: joinProduct(opts.product_id)
}

if (opts.order_type === 'maker' && typeof opts.type === 'undefined') {
params.price = n(opts.price).format('0.00')
}

var client = authedClient()
client.api('sell', params, function(err, body) {
var order = {
id: null,
status: 'open',
price: opts.price,
size: opts.size,
created_at: new Date().getTime(),
filled_size: '0',
ordertype: opts.order_type
}

if (err) return cb(err)
if (body.error) return cb(body.error.message)

if (opts.order_type === 'taker') {
order.status = 'done'
order.done_at = new Date().getTime();

if (body.orders_matched) {
var asset_total = 0
var price_total = 0.0
var order_count = body.orders_matched.length
for (var idx = 0; idx < order_count; idx++) {
asset_total = asset_total + body.orders_matched[idx].amount
price_total = price_total + (body.orders_matched[idx].amount * body.orfers_matched[idx].price)
}

order.price = price_total / asset_total
order.size = asset_total
} else {
order.price = body.price
order.size = body.amount
}
}

order.id = body.id
orders['~' + body.id] = order
cb(null, order)
})
},

getOrder: function(opts, cb) {
var order = orders['~' + opts.order_id]
var params = {
id: opts.order_id
}

var client = authedClient()
client.api('lookup_order', params, function(err, body) {
if (err) return cb(err)
if (body.error) return cb(body.error.message)

if (body.status === 2) {
order.status = 'done'
order.done_at = new Date().getTime()
order.filled_size = body.amount
return cb(null, order)
}
cb(null, order)
})
},

// return the property used for range querying.
getCursor: function(trade) {
return trade.time
}
}
return exchange
}
8 changes: 8 additions & 0 deletions extensions/exchanges/quadriga/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "zenbot_quadrigacx",
"version": "0.0.1",
"description": "Zenbot supporting code for QuadrigaCX",
"dependencies": {
"quadrigacx": "^0.0.7",
}
}
38 changes: 38 additions & 0 deletions extensions/exchanges/quadriga/products.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"id": "BTCUSD",
"asset": "BTC",
"currency": "USD",
"min_size": "0.00001",
"max_size": "10000",
"increment": "0.00001",
"label": "BTC/USD"
},
{
"id": "BTCCAD",
"asset": "BTC",
"currency": "CAD",
"min_size": "0.00001",
"max_size": "10000",
"increment": "0.00001",
"label": "BTC/CAD"
},
{
"id": "ETHCAD",
"asset": "ETH",
"currency": "CAD",
"min_size": "0.00001",
"max_size": "10000",
"increment": "0.00001",
"label": "ETH/CAD"
},
{
"id": "ETHBTC",
"asset": "ETH",
"currency": "BTC",
"min_size": "0.00001",
"max_size": "1000000",
"increment": "0.00001",
"label": "ETH/BTC"
}
]
5 changes: 0 additions & 5 deletions lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ module.exports = function container (get, set, clear) {
}
else {
order.local_time = new Date().getTime()
setTimeout(checkOrder, so.order_poll_time)
}
}
else {
Expand All @@ -314,14 +313,10 @@ module.exports = function container (get, set, clear) {
}
else {
order.local_time = new Date().getTime()
setTimeout(checkOrder, so.order_poll_time)
}
}
})
}
else {
setTimeout(checkOrder, so.order_poll_time)
}
})
}
setTimeout(checkOrder, so.order_poll_time)
Expand Down
Loading

0 comments on commit fc2c59a

Please sign in to comment.