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

+530
View File
@@ -0,0 +1,530 @@
const moment = require('moment');
const _ = require('lodash');
const Errors = require('../exchangeErrors');
const marketData = require('./binance-markets.json');
const exchangeUtils = require('../exchangeUtils');
const retry = exchangeUtils.retry;
const scientificToDecimal = exchangeUtils.scientificToDecimal;
const Binance = require('binance');
const Trader = function(config) {
_.bindAll(this, [
'roundAmount',
'roundPrice',
'isValidPrice',
'isValidLot'
]);
if (_.isObject(config)) {
this.key = config.key;
this.secret = config.secret;
this.currency = config.currency.toUpperCase();
this.asset = config.asset.toUpperCase();
}
let recvWindow = 6000;
if(config.optimizedConnection) {
// there is a bug in binance's API
// where some requests randomly take
// over a second, this tells binance
// to bail out after 500ms.
//
// As discussed in binance API
// telegram. TODO add link.
recvWindow = 500;
}
this.pair = this.asset + this.currency;
this.name = 'binance';
this.market = _.find(Trader.getCapabilities().markets, (market) => {
return market.pair[0] === this.currency && market.pair[1] === this.asset
});
this.binance = new Binance.BinanceRest({
key: this.key,
secret: this.secret,
timeout: 15000,
recvWindow,
disableBeautification: false,
handleDrift: true,
});
if(config.key && config.secret) {
// Note non standard func:
//
// On binance we might pay fees in BNB
// if we do we CANNOT calculate feePercent
// since we don't track BNB price (when we
// are not trading on a BNB market).
//
// Though we can deduce feePercent based
// on user fee tracked through `this.getFee`.
// Set default here, overwrite in getFee.
this.fee = 0.001;
// Set the proper fee asap.
this.getFee(_.noop);
this.oldOrder = false;
}
};
const recoverableErrors = [
'SOCKETTIMEDOUT',
'TIMEDOUT',
'CONNRESET',
'CONNREFUSED',
'NOTFOUND',
'Error -1021',
'Response code 429',
'Response code 5',
'Response code 403',
'ETIMEDOUT',
'EHOSTUNREACH',
// getaddrinfo EAI_AGAIN api.binance.com api.binance.com:443
'EAI_AGAIN',
'ENETUNREACH'
];
const includes = (str, list) => {
if(!_.isString(str))
return false;
return _.some(list, item => str.includes(item));
}
Trader.prototype.handleResponse = function(funcName, callback) {
return (error, body) => {
if (body && body.code) {
error = new Error(`Error ${body.code}: ${body.msg}`);
}
if(error) {
if(_.isString(error)) {
error = new Error(error);
}
if(includes(error.message, recoverableErrors)) {
error.notFatal = true;
}
if(funcName === 'cancelOrder' && error.message.includes('UNKNOWN_ORDER')) {
console.log(new Date, 'cancelOrder', 'UNKNOWN_ORDER');
// order got filled in full before it could be
// cancelled, meaning it was NOT cancelled.
return callback(false, {filled: true});
}
if(funcName === 'checkOrder' && error.message.includes('Order does not exist.')) {
console.log(new Date, 'Binance doesnt know this order, retrying up to 10 times..');
error.retry = 10;
}
if(funcName === 'addOrder' && error.message.includes('Account has insufficient balance')) {
console.log(new Date, 'insufficientFunds');
error.type = 'insufficientFunds';
}
return callback(error);
}
return callback(undefined, body);
}
};
Trader.prototype.getTrades = function(since, callback, descending) {
const processResults = (err, data) => {
if (err) return callback(err);
var parsedTrades = [];
_.each(
data,
function(trade) {
parsedTrades.push({
tid: trade.aggTradeId,
date: moment(trade.timestamp).unix(),
price: parseFloat(trade.price),
amount: parseFloat(trade.quantity),
});
},
this
);
if (descending) callback(null, parsedTrades.reverse());
else callback(undefined, parsedTrades);
};
var reqData = {
symbol: this.pair,
};
if (since) {
var endTs = moment(since)
.add(1, 'h')
.valueOf();
var nowTs = moment().valueOf();
reqData.startTime = moment(since).valueOf();
reqData.endTime = endTs > nowTs ? nowTs : endTs;
}
const fetch = cb => this.binance.aggTrades(reqData, this.handleResponse('getTrades', cb));
retry(undefined, fetch, processResults);
};
Trader.prototype.getPortfolio = function(callback) {
const setBalance = (err, data) => {
if (err) return callback(err);
const findAsset = item => item.asset === this.asset;
const assetAmount = parseFloat(_.find(data.balances, findAsset).free);
const findCurrency = item => item.asset === this.currency;
const currencyAmount = parseFloat(_.find(data.balances, findCurrency).free);
if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
assetAmount = 0;
}
if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
currencyAmount = 0;
}
const portfolio = [
{ name: this.asset, amount: assetAmount },
{ name: this.currency, amount: currencyAmount },
];
return callback(undefined, portfolio);
};
const fetch = cb => this.binance.account({}, this.handleResponse('getPortfolio', cb));
retry(undefined, fetch, setBalance);
};
Trader.prototype.getFee = function(callback) {
// binance does NOT tell us whether the user is using BNB to pay
// for fees, which means a discount (effectively lower fees)
const handle = (err, data) => {
if(err) {
return callback(err);
}
const basepoints = data.makerCommission;
/** Binance raw response
{ makerCommission: 10,
takerCommission: 10,
buyerCommission: 0,
sellerCommission: 0,
canTrade: true,
canWithdraw: true,
canDeposit: true,
So to get decimal representation of fee we actually need to divide by 10000
*/
// note non standard func, see constructor
this.fee = basepoints / 10000;
callback(undefined, this.fee);
}
const fetch = cb => this.binance.account({}, this.handleResponse('getFee', cb));
retry(undefined, fetch, handle);
};
Trader.prototype.getTicker = function(callback) {
const setTicker = (err, data) => {
if (err)
return callback(err);
var result = _.find(data, ticker => ticker.symbol === this.pair);
if(!result)
return callback(new Error(`Market ${this.pair} not found on Binance`));
var ticker = {
ask: parseFloat(result.askPrice),
bid: parseFloat(result.bidPrice),
};
callback(undefined, ticker);
};
const handler = cb => this.binance._makeRequest({}, this.handleResponse('getTicker', cb), 'api/v1/ticker/allBookTickers');
retry(undefined, handler, setTicker);
};
// Effectively counts the number of decimal places, so 0.001 or 0.234 results in 3
Trader.prototype.getPrecision = function(tickSize) {
if (!isFinite(tickSize)) return 0;
var e = 1, p = 0;
while (Math.round(tickSize * e) / e !== tickSize) { e *= 10; p++; }
return p;
};
Trader.prototype.round = function(amount, tickSize) {
var precision = 100000000;
var t = this.getPrecision(tickSize);
if(Number.isInteger(t))
precision = Math.pow(10, t);
amount *= precision;
amount = Math.floor(amount);
amount /= precision;
// https://gist.github.com/jiggzson/b5f489af9ad931e3d186
amount = scientificToDecimal(amount);
return amount;
};
Trader.prototype.roundAmount = function(amount) {
return this.round(amount, this.market.minimalOrder.amount);
}
Trader.prototype.roundPrice = function(price) {
return this.round(price, this.market.minimalOrder.price);
}
Trader.prototype.isValidPrice = function(price) {
return price >= this.market.minimalOrder.price;
}
Trader.prototype.isValidLot = function(price, amount) {
return amount * price >= this.market.minimalOrder.order;
}
Trader.prototype.outbidPrice = function(price, isUp) {
let newPrice;
if(isUp) {
newPrice = price + this.market.minimalOrder.price;
} else {
newPrice = price - this.market.minimalOrder.price;
}
return this.roundPrice(newPrice);
}
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
const setOrder = (err, data) => {
if (err) return callback(err);
const txid = data.orderId;
callback(undefined, txid);
};
const reqData = {
symbol: this.pair,
side: tradeType.toUpperCase(),
type: 'LIMIT',
timeInForce: 'GTC',
quantity: amount,
price: price,
timestamp: new Date().getTime()
};
const handler = cb => this.binance.newOrder(reqData, this.handleResponse('addOrder', cb));
retry(undefined, handler, setOrder);
};
Trader.prototype.getOpenOrders = function(callback) {
const get = (err, data) => {
if(err) {
return callback(err);
}
callback(null, data.map(o => o.orderId));
}
const reqData = {
symbol: this.pair
}
const handler = cb => this.binance.openOrders(reqData, this.handleResponse('getOpenOrders', cb));
retry(undefined, handler, get);
}
Trader.prototype.getOrder = function(order, callback) {
const get = (err, data) => {
if (err) return callback(err);
let price = 0;
let amount = 0;
let date = moment(0);
const fees = {};
if(!data.length) {
return callback(new Error('Binance did not return any trades'));
}
const trades = _.filter(data, t => {
// note: the API returns a string after creating
return t.orderId == order;
});
if(!trades.length) {
console.log('cannot find trades!', { order, list: data.map(t => t.orderId).reverse() });
const reqData = {
symbol: this.pair,
orderId: order,
};
this.binance.queryOrder(reqData, (err, resp) => {
console.log('couldnt find any trade for order, here is order:', {err, resp});
callback(new Error('Trades not found'));
});
return;
}
_.each(trades, trade => {
date = moment(trade.time);
price = ((price * amount) + (+trade.price * trade.qty)) / (+trade.qty + amount);
amount += +trade.qty;
if(fees[trade.commissionAsset])
fees[trade.commissionAsset] += (+trade.commission);
else
fees[trade.commissionAsset] = (+trade.commission);
});
let feePercent;
if(_.keys(fees).length === 1) {
if(fees.BNB && this.asset !== 'BNB' && this.currency !== 'BNB') {
// we paid fees in BNB, right now that means the fee is always 75%
// of base fee. We cannot calculate since we do not have the BNB rate.
feePercent = this.fee * 0.75;
} else {
if(fees[this.asset]) {
feePercent = fees[this.asset] / amount * 100;
} else if(fees.currency) {
feePercent = fees[this.currency] / price / amount * 100;
} else {
// use user fee of 10 basepoints
feePercent = this.fee;
}
}
} else {
// we paid fees in multiple currencies?
// assume user fee
feePercent = this.fee;
}
callback(undefined, { price, amount, date, fees, feePercent });
}
const reqData = {
symbol: this.pair,
// if this order was not part of the last 500 trades we won't find it..
limit: 1000,
};
const handler = cb => this.binance.myTrades(reqData, this.handleResponse('getOrder', cb));
retry(undefined, handler, get);
};
Trader.prototype.buy = function(amount, price, callback) {
this.addOrder('buy', amount, price, callback);
};
Trader.prototype.sell = function(amount, price, callback) {
this.addOrder('sell', amount, price, callback);
};
Trader.prototype.checkOrder = function(order, callback) {
const check = (err, data) => {
if (err) {
return callback(err);
}
if(data.filled === true) {
// binance responsed with order not found
return callback(undefined, { executed: true, open: false });
}
const status = data.status;
if(
status === 'CANCELED' ||
status === 'REJECTED' ||
// for good measure: GB does not
// submit orders that can expire yet
status === 'EXPIRED'
) {
return callback(undefined, { executed: false, open: false });
} else if(
status === 'NEW' ||
status === 'PARTIALLY_FILLED'
) {
return callback(undefined, { executed: false, open: true, filledAmount: +data.executedQty });
} else if(status === 'FILLED') {
return callback(undefined, { executed: true, open: false })
}
console.log('what status?', status);
throw status;
};
const reqData = {
symbol: this.pair,
orderId: order,
};
const fetcher = cb => this.binance.queryOrder(reqData, this.handleResponse('checkOrder', cb));
retry(undefined, fetcher, check);
};
Trader.prototype.cancelOrder = function(order, callback) {
const cancel = (err, data) => {
this.oldOrder = order;
if(err) {
return callback(err);
}
if(data && data.filled) {
return callback(undefined, true);
}
return callback(undefined, false);
};
let reqData = {
symbol: this.pair,
orderId: order,
};
const fetcher = cb => this.binance.cancelOrder(reqData, this.handleResponse('cancelOrder', cb));
retry(undefined, fetcher, cancel);
};
Trader.getCapabilities = function() {
return {
name: 'Binance',
slug: 'binance',
currencies: marketData.currencies,
assets: marketData.assets,
markets: marketData.markets,
requires: ['key', 'secret'],
providesHistory: 'date',
providesFullHistory: true,
tid: 'tid',
tradable: true,
gekkoBroker: 0.6,
limitedCancelConfirmation: true
};
};
module.exports = Trader;