first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,430 @@
|
||||
const Gdax = require('gdax');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
const QUERY_DELAY = 350;
|
||||
|
||||
const marketData = require('./coinbase-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
this.post_only = true;
|
||||
this.use_sandbox = false;
|
||||
this.name = 'GDAX';
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
this.scanbackResults = [];
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
|
||||
this.api_url = 'https://api.pro.coinbase.com';
|
||||
this.api_sandbox_url = 'https://api-public.sandbox.pro.coinbase.com';
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.passphrase = config.passphrase;
|
||||
|
||||
this.pair = [config.asset, config.currency].join('-').toUpperCase();
|
||||
this.post_only =
|
||||
typeof config.post_only !== 'undefined' ? config.post_only : true;
|
||||
|
||||
if (config.sandbox) {
|
||||
this.use_sandbox = config.sandbox;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.gdax_public = new Gdax.PublicClient(
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
this.gdax = new Gdax.AuthenticatedClient(
|
||||
this.key,
|
||||
this.secret,
|
||||
this.passphrase,
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Rate limit exceeded',
|
||||
'Response code 5',
|
||||
'GDAX is currently under maintenance.',
|
||||
'HTTP 408 Error',
|
||||
'HTTP 504 Error',
|
||||
'HTTP 503 Error',
|
||||
'socket hang up',
|
||||
'EHOSTUNREACH',
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(method, next) {
|
||||
return (error, response, body) => {
|
||||
if(!error && body && !_.isEmpty(body.message)) {
|
||||
error = new Error(body.message);
|
||||
}
|
||||
|
||||
if(
|
||||
response &&
|
||||
response.statusCode < 200 &&
|
||||
response.statusCode >= 300
|
||||
) {
|
||||
error = new Error(`Response code ${response.statusCode}`);
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 1000;
|
||||
}
|
||||
|
||||
if(
|
||||
['buy', 'sell'].includes(method) &&
|
||||
error.message.includes('Insufficient funds')
|
||||
) {
|
||||
error.retry = 10;
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return next(undefined, body);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var portfolio = data.map(function(account) {
|
||||
return {
|
||||
name: account.currency.toUpperCase(),
|
||||
amount: parseFloat(account.available),
|
||||
};
|
||||
});
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
callback(undefined, { bid: +data.bid, ask: +data.ask });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
//https://www.gdax.com/fees
|
||||
// const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
|
||||
const fee = 0;
|
||||
|
||||
//There is no maker fee, not sure if we need taker fee here
|
||||
//If post only is enabled, gdax only does maker trades which are free
|
||||
callback(undefined, this.post_only ? 0 : fee);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return this.getMaxDecimalsNumber(amount);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const buyParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({buyParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.buy(buyParams, this.processResponse('buy', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const sellParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({sellParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data.message && data.message.includes('Insufficient funds')) {
|
||||
err = new Error(data.message);
|
||||
err.retryOnce = true;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.sell(sellParams, this.processResponse('sell', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// @link:
|
||||
// https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
|
||||
var status = data.status;
|
||||
if(status == 'pending') {
|
||||
// technically not open yet, but will be soon
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: 0 });
|
||||
} if (status === 'done' || status === 'settled') {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
} else if (status === 'rejected') {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} else if(status === 'open' || status === 'active') {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
|
||||
}
|
||||
|
||||
callback(new Error('Unknown status ' + status));
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const price = parseFloat(data.price);
|
||||
const amount = parseFloat(data.filled_size);
|
||||
const date = moment(data.done_at);
|
||||
const fees = {
|
||||
// you always pay fee in the base currency on gdax
|
||||
[this.currency]: +data.fill_fees
|
||||
}
|
||||
const feePercent = +data.fill_fees / price / amount * 100;
|
||||
|
||||
callback(undefined, { price, amount, date, fees, feePercent });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('getOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
// callback for cancelOrder should be true if the order was already filled, otherwise false
|
||||
const result = (err, data) => {
|
||||
if(err) {
|
||||
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var lastScan = 0;
|
||||
|
||||
const handle = function(err, data) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var result = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.trade_id,
|
||||
amount: parseFloat(trade.size),
|
||||
date: moment.utc(trade.time).format('X'),
|
||||
price: parseFloat(trade.price),
|
||||
};
|
||||
});
|
||||
|
||||
if (this.scanback) {
|
||||
var last = _.last(data);
|
||||
var first = _.first(data);
|
||||
|
||||
// Try to find trade id matching the since date
|
||||
if (!this.scanbackTid) {
|
||||
// either scan for new ones or we found it.
|
||||
if (moment.utc(last.time) < moment.utc(since)) {
|
||||
this.scanbackTid = last.trade_id;
|
||||
} else {
|
||||
console.log('Scanning backwards...' + last.time);
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{
|
||||
after: last.trade_id - BATCH_SIZE * lastScan,
|
||||
limit: BATCH_SIZE,
|
||||
},
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
lastScan++;
|
||||
if (lastScan > 100) {
|
||||
lastScan = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scanbackTid) {
|
||||
// if scanbackTid is set we need to move forward again
|
||||
console.log(
|
||||
'Backwards: ' +
|
||||
last.time +
|
||||
' (' +
|
||||
last.trade_id +
|
||||
') to ' +
|
||||
first.time +
|
||||
' (' +
|
||||
first.trade_id +
|
||||
')'
|
||||
);
|
||||
|
||||
this.scanbackResults = this.scanbackResults.concat(result.reverse());
|
||||
|
||||
if (this.scanbackTid != first.trade_id) {
|
||||
this.scanbackTid = first.trade_id;
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
} else {
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
|
||||
console.log('Scan finished: data found:' + this.scanbackResults.length);
|
||||
callback(null, this.scanbackResults);
|
||||
|
||||
this.scanbackResults = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback(null, result.reverse());
|
||||
}
|
||||
};
|
||||
|
||||
if (since || this.scanback) {
|
||||
this.scanback = true;
|
||||
if (this.scanbackTid) {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
} else if (since) {
|
||||
console.log('Scanning back in the history needed...', since);
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
|
||||
var decimalNumber = parseFloat(number);
|
||||
|
||||
// The ^-?\d*\. strips off any sign, integer portion, and decimal point
|
||||
// leaving only the decimal fraction.
|
||||
// The 0+$ strips off any trailing zeroes.
|
||||
var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
|
||||
.length;
|
||||
|
||||
var decimalMultiplier = 1;
|
||||
for (i = 0; i < decimalLimit; i++) {
|
||||
decimalMultiplier *= 10;
|
||||
}
|
||||
|
||||
return decimalCount <= decimalLimit
|
||||
? decimalNumber.toString()
|
||||
: (
|
||||
Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
|
||||
).toFixed(decimalLimit);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'GDAX',
|
||||
slug: 'gdax',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret', 'passphrase'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: false,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user