first commit

This commit is contained in:
Francois Klieber committed 2021-06-06 20:25:14 +02:00
commit aa7869eb33
386 files changed
+69375

No files matched your search

+430
View File
@@ -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;