Skip to content
This repository has been archived by the owner on Feb 15, 2022. It is now read-only.

Tbw add binanceus #2394

Merged
merged 2 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/stale.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- enhancement
- bug
- Sticky
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
5 changes: 5 additions & 0 deletions conf-sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ c.binance = {}
c.binance.key = 'YOUR-API-KEY'
c.binance.secret = 'YOUR-SECRET'

// to enable Binance US trading, enter your API credentials:
c.binanceus = {}
c.binanceus.key = 'YOUR-API-KEY'
c.binanceus.secret = 'YOUR-SECRET'

// to enable Bittrex trading, enter your API credentials:
c.bittrex = {}
c.bittrex.key = 'YOUR-API-KEY'
Expand Down
319 changes: 319 additions & 0 deletions extensions/exchanges/binanceus/exchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
const ccxt = require('ccxt')
, path = require('path')
// eslint-disable-next-line no-unused-vars
, colors = require('colors')
, _ = require('lodash')

module.exports = function binanceus (conf) {
var public_client, authed_client

function publicClient () {
if (!public_client) public_client = new ccxt.binanceus({ 'apiKey': '', 'secret': '', 'options': { 'adjustForTimeDifference': true } })
return public_client
}

function authedClient () {
if (!authed_client) {
if (!conf.binanceus || !conf.binanceus.key || conf.binanceus.key === 'YOUR-API-KEY') {
throw new Error('please configure your BinanceUS credentials in ' + path.resolve(__dirname, 'conf.js'))
}
authed_client = new ccxt.binanceus({ 'apiKey': conf.binanceus.key, 'secret': conf.binanceus.secret, 'options': { 'adjustForTimeDifference': true }, enableRateLimit: true })
}
return authed_client
}

/**
* Convert BNB-BTC to BNB/BTC
*
* @param product_id BNB-BTC
* @returns {string}
*/
function joinProduct(product_id) {
let split = product_id.split('-')
return split[0] + '/' + split[1]
}

function retry (method, args, err) {
if (method !== 'getTrades') {
console.error(('\nBinanceUS API is down! unable to call ' + method + ', retrying in 20s').red)
if (err) console.error(err)
console.error(args.slice(0, -1))
}
setTimeout(function () {
exchange[method].apply(exchange, args)
}, 20000)
}

var orders = {}

var exchange = {
name: 'binanceus',
historyScan: 'forward',
historyScanUsesTime: true,
makerFee: 0.1,
takerFee: 0.1,

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

getTrades: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = publicClient()
var startTime = null
var args = {}
if (opts.from) {
startTime = opts.from
} else {
startTime = parseInt(opts.to, 10) - 3600000
args['endTime'] = opts.to
}

const symbol = joinProduct(opts.product_id)
client.fetchTrades(symbol, startTime, undefined, args).then(result => {

if (result.length === 0 && opts.from) {
// client.fetchTrades() only returns trades in an 1 hour interval.
// So we use fetchOHLCV() to detect trade appart from more than 1h.
// Note: it's done only in forward mode.
const time_diff = client.options['timeDifference']
if (startTime + time_diff < (new Date()).getTime() - 3600000) {
// startTime is older than 1 hour ago.
return client.fetchOHLCV(symbol, undefined, startTime)
.then(ohlcv => {
return ohlcv.length ? client.fetchTrades(symbol, ohlcv[0][0]) : []
})
}
}
return result
}).then(result => {
var trades = result.map(trade => ({
trade_id: trade.id,
time: trade.timestamp,
size: parseFloat(trade.amount),
price: parseFloat(trade.price),
side: trade.side
}))
cb(null, trades)
}).catch(function (error) {
console.error('An error occurred', error)
return retry('getTrades', func_args)
})

},

getBalance: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = authedClient()
client.fetchBalance().then(result => {
var balance = {asset: 0, currency: 0}
Object.keys(result).forEach(function (key) {
if (key === opts.currency) {
balance.currency = result[key].free + result[key].used
balance.currency_hold = result[key].used
}
if (key === opts.asset) {
balance.asset = result[key].free + result[key].used
balance.asset_hold = result[key].used
}
})
cb(null, balance)
})
.catch(function (error) {
console.error('An error occurred', error)
return retry('getBalance', func_args)
})
},

getQuote: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = publicClient()
client.fetchTicker(joinProduct(opts.product_id)).then(result => {
cb(null, { bid: result.bid, ask: result.ask })
})
.catch(function (error) {
console.error('An error occurred', error)
return retry('getQuote', func_args)
})
},

getDepth: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = publicClient()
client.fetchOrderBook(joinProduct(opts.product_id), {limit: opts.limit}).then(result => {
cb(null, result)
})
.catch(function(error) {
console.error('An error ocurred', error)
return retry('getDepth', func_args)
})
},

cancelOrder: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = authedClient()
client.cancelOrder(opts.order_id, joinProduct(opts.product_id)).then(function (body) {
if (body && (body.message === 'Order already done' || body.message === 'order not found')) return cb()
cb(null)
}, function(err){
// match error against string:
// "binanceus {"code":-2011,"msg":"UNKNOWN_ORDER"}"

if (err) {
// decide if this error is allowed for a retry

if (err.message && err.message.match(new RegExp(/-2011|UNKNOWN_ORDER/))) {
console.error(('\ncancelOrder retry - unknown Order: ' + JSON.stringify(opts) + ' - ' + err).cyan)
} else {
// retry is allowed for this error

return retry('cancelOrder', func_args, err)
}
}

cb()
})
},

buy: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = authedClient()
if (typeof opts.post_only === 'undefined') {
opts.post_only = true
}
opts.type = 'limit'
var args = {}
if (opts.order_type === 'taker') {
delete opts.price
delete opts.post_only
opts.type = 'market'
} else {
args.timeInForce = 'GTC'
}
opts.side = 'buy'
delete opts.order_type
var order = {}
client.createOrder(joinProduct(opts.product_id), opts.type, opts.side, this.roundToNearest(opts.size, opts), opts.price, args).then(result => {
if (result && result.message === 'Insufficient funds') {
order = {
status: 'rejected',
reject_reason: 'balance'
}
return cb(null, order)
}
order = {
id: result ? result.id : null,
status: 'open',
price: opts.price,
size: this.roundToNearest(opts.size, opts),
post_only: !!opts.post_only,
created_at: new Date().getTime(),
filled_size: '0',
ordertype: opts.order_type
}
orders['~' + result.id] = order
cb(null, order)
}).catch(function (error) {
console.error('An error occurred', error)

// decide if this error is allowed for a retry:
// {"code":-1013,"msg":"Filter failure: MIN_NOTIONAL"}
// {"code":-2010,"msg":"Account has insufficient balance for requested action"}

if (error.message.match(new RegExp(/-1013|MIN_NOTIONAL|-2010/))) {
return cb(null, {
status: 'rejected',
reject_reason: 'balance'
})
}

return retry('buy', func_args)
})
},

sell: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = authedClient()
if (typeof opts.post_only === 'undefined') {
opts.post_only = true
}
opts.type = 'limit'
var args = {}
if (opts.order_type === 'taker') {
delete opts.price
delete opts.post_only
opts.type = 'market'
} else {
args.timeInForce = 'GTC'
}
opts.side = 'sell'
delete opts.order_type
var order = {}
client.createOrder(joinProduct(opts.product_id), opts.type, opts.side, this.roundToNearest(opts.size, opts), opts.price, args).then(result => {
if (result && result.message === 'Insufficient funds') {
order = {
status: 'rejected',
reject_reason: 'balance'
}
return cb(null, order)
}
order = {
id: result ? result.id : null,
status: 'open',
price: opts.price,
size: this.roundToNearest(opts.size, opts),
post_only: !!opts.post_only,
created_at: new Date().getTime(),
filled_size: '0',
ordertype: opts.order_type
}
orders['~' + result.id] = order
cb(null, order)
}).catch(function (error) {
console.error('An error occurred', error)

// decide if this error is allowed for a retry:
// {"code":-1013,"msg":"Filter failure: MIN_NOTIONAL"}
// {"code":-2010,"msg":"Account has insufficient balance for requested action"}

if (error.message.match(new RegExp(/-1013|MIN_NOTIONAL|-2010/))) {
return cb(null, {
status: 'rejected',
reject_reason: 'balance'
})
}

return retry('sell', func_args)
})
},

roundToNearest: function(numToRound, opts) {
var numToRoundTo = _.find(this.getProducts(), { 'asset': opts.product_id.split('-')[0], 'currency': opts.product_id.split('-')[1] }).min_size
numToRoundTo = 1 / (numToRoundTo)

return Math.floor(numToRound * numToRoundTo) / numToRoundTo
},

getOrder: function (opts, cb) {
var func_args = [].slice.call(arguments)
var client = authedClient()
var order = orders['~' + opts.order_id]
client.fetchOrder(opts.order_id, joinProduct(opts.product_id)).then(function (body) {
if (body.status !== 'open' && body.status !== 'canceled') {
order.status = 'done'
order.done_at = new Date().getTime()
order.filled_size = parseFloat(body.amount) - parseFloat(body.remaining)
return cb(null, order)
}
cb(null, order)
}, function(err) {
return retry('getOrder', func_args, err)
})
},

getCursor: function (trade) {
return (trade.time || trade)
}
}
return exchange
}
Loading