Created
January 5, 2021 21:12
-
-
Save cue232s/c23f049f002ac237cb45f0be1e99354c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// "use strict"; | |
const binance = require('node-binance-api'); | |
const chalk = require('chalk'); | |
const log = console.log; | |
const moment = require('moment'); | |
// percentage of gains targeted | |
const MIN_PROFIT_TARGET = 0.1; | |
const MAX_PROFIT_TARGET = 5.0; | |
const NUMBER_OF_TRADES_OPPORTUNITIES = 1; | |
let ARBITRAGE_OPP_COUNT = 0; | |
let ARB_GAIN_ACCUM = 0; | |
let ACT_BAL = 0; | |
let EXCHANGE_INFO; | |
const ACT_PERC_PER_TRADE = 0.6; | |
// tradding fee as a percentage | |
const TRADING_FEE = 0.05 / 100; | |
const SLIPPAGE_RANGE = 0.15; | |
const SLIPPAGE_ALLOWED = SLIPPAGE_RANGE / 100; | |
let tradeCandidates = []; | |
let iSRunning; | |
const config = () => { | |
binance.options({ | |
APIKEY: 'aeU3hOzGOn5ueapCbfJCUg1yqzuoa8B5BeSS6EaiwibKh8mE7uHtfZ4H1Yp3ZTId', | |
APISECRET: | |
'TSkA5FEmInjr6P0JrwdSYK27CQJppHfbeAJX1Ox9j7Vzp5FOfJXa5DTdrJtmqCZu' | |
}); | |
}; | |
let execPause = 0; | |
const runArbitrage = () => { | |
try { | |
if (iSRunning) throw 'Script already in process.. chill!'; | |
} catch (error) { | |
log(error); | |
} | |
iSRunning = true; | |
ARB_GAIN_ACCUM = 0; | |
ARBITRAGE_OPP_COUNT = 0; | |
const tickers = new Promise((resolve, reject) => { | |
binance.prices(function(ticker) { | |
// console.log("prices()", ticker); | |
binance.balance((balances, error) => { | |
ACT_BAL = balances.BTC.available; | |
binance.exchangeInfo(x => { | |
EXCHANGE_INFO = x; | |
resolve(ticker); | |
}); | |
}); | |
}); | |
}); | |
// at the moment BTC is quoting them all | |
tickers | |
.then(currencies => { | |
// console.log(currencies) | |
console.log('******STARTING BTC BALANCE: ' + ACT_BAL + '**********'); | |
let initialCurrency = 'BTC'; | |
let holdingCurrency = initialCurrency; | |
let startingPairs = getAllCurrencyPairsForCrypto( | |
holdingCurrency, | |
currencies | |
); | |
// startingPairs.splice(1, startingPairs.length) | |
let amountOf1stCrypto = ACT_BAL * 1.5; | |
// let amountOf1stCrypto = 4; | |
startingPairs.map(startingCrypto => { | |
let firstTransaction = buildTransaction( | |
amountOf1stCrypto, | |
holdingCurrency, | |
startingCrypto, | |
currencies | |
); | |
console.group(); | |
let secondHoldingCurrency = firstTransaction.buy.currency; | |
let amountOf2ndCrypto = firstTransaction.buy.volume; | |
// get all the currency pairs for the new currency you are holding | |
let crossPairs = getAllCurrencyPairsForCrypto( | |
secondHoldingCurrency, | |
currencies | |
); | |
crossPairs.splice(crossPairs.indexOf(startingCrypto), 1); | |
crossPairs.map(secondPair => { | |
let transactionPath = new Array(); | |
transactionPath.push(firstTransaction); | |
if (startingCrypto === secondPair) { | |
throw 'Error redundant Pair'; | |
} | |
secondTransaction = buildTransaction( | |
amountOf2ndCrypto, | |
secondHoldingCurrency, | |
secondPair, | |
currencies | |
); | |
console.group(); | |
let thirdHoldingCurrency = secondTransaction.buy.currency; | |
let thirdAmountOfThirdCurrency = secondTransaction.buy.volume; | |
finalPairs = getAllCurrencyPairsForCrypto( | |
thirdHoldingCurrency, | |
currencies | |
); | |
transactionPath.push(secondTransaction); | |
finalPairs.splice(finalPairs.indexOf(secondPair), 1); | |
let finalPair = finalPairs.filter(x => { | |
return x.includes(initialCurrency); | |
})[0]; | |
finalTransaction = buildTransaction( | |
thirdAmountOfThirdCurrency, | |
thirdHoldingCurrency, | |
finalPair, | |
currencies | |
); | |
transactionPath.push(finalTransaction); | |
// console.log(Math.abs(finalTransaction.buyAmount - amountOf1stCrypto)); | |
try { | |
let trueParity = finalTransaction.buy.volume - amountOf1stCrypto; | |
let potentialGain = trueParity / amountOf1stCrypto * 100; | |
// currently only trading parities in one direction (positive parity). | |
// Where we end up with more of the Initial coin then we began with. | |
// need to find negative parity and reverse order of transaction | |
if ( | |
potentialGain > MIN_PROFIT_TARGET && | |
potentialGain < MAX_PROFIT_TARGET | |
) { | |
console.log( | |
chalk.yellow( | |
'potential arbitrage for ' + potentialGain + '% profit' | |
) | |
); | |
tradeCandidates.push({ | |
trade: executeAribtrage(transactionPath, execPause * 2), | |
profitScore: potentialGain | |
}); | |
execPause++; | |
// executeAribtrage(transactionPath)() | |
transactionPath.forEach(x => { | |
log(chalk.cyan(x.description)); | |
}); | |
ARBITRAGE_OPP_COUNT++; | |
ARB_GAIN_ACCUM += potentialGain; | |
} | |
} catch (error) { | |
console.warn(error); | |
} | |
console.groupEnd(); | |
}); | |
console.groupEnd(); | |
}); | |
log( | |
chalk.blue( | |
'There are a total of ' + ARBITRAGE_OPP_COUNT + ' opportunities' | |
) | |
); | |
log( | |
chalk.blue( | |
'There average opportunity shows gains of ' + | |
(ARB_GAIN_ACCUM / ARBITRAGE_OPP_COUNT).toFixed(4) + | |
'%' | |
) | |
); | |
return runHighestRankingTrades( | |
NUMBER_OF_TRADES_OPPORTUNITIES, | |
tradeCandidates | |
); | |
}) | |
.catch(x => { | |
console.warn(chalk.bgYellow(x)); | |
}) | |
.then(x => { | |
// toggle the iRunning attribute to let the program kno | |
iSRunning = false; | |
}); | |
}; | |
runHighestRankingTrades = (numberOfTrades, listOfTrades) => { | |
let sortedList = listOfTrades.sort(function(a, b) { | |
var profitA = a.profitScore; // ignore upper and lowercase | |
var profitB = b.profitScore; // ignore upper and lowercase | |
if (profitA < profitB) { | |
return -1; | |
} | |
if (profitA > profitB) { | |
return 1; | |
} | |
// names must be equal | |
return 0; | |
}); | |
const executeTrades = sortedList.slice(0, numberOfTrades); | |
executeTrades.map(tradeGroup => { | |
setTimeout(() => { | |
tradeGroup.trade().then(x => console.log('trade executed :', x)); | |
}, 20); | |
}); | |
}; | |
executeAribtrage = (trnPath, executionPause) => { | |
let index = 0; | |
return function() { | |
return Promise.resolve( | |
executeTrade(trnPath[index], executionPause) | |
.then(order => { | |
index++; | |
if (order.status === 'FILLED') { | |
return executeTrade(trnPath[index]); | |
} | |
}) | |
.then(order => { | |
index++; | |
if (order.status === 'FILLED') { | |
return executeTrade(trnPath[index]); | |
} | |
}) | |
.then(order => { | |
index++; | |
if (order.status === 'FILLED') { | |
return executeTrade(trnPath[index]); | |
} | |
}) | |
.then(order => { | |
log(chalk.bgRed('Final order succeded!')); | |
}) | |
.catch(x => { | |
log(trnPath[index].pair + ' :index of trade: ', index); | |
log(trnPath[index].pair + ' :trade attempt : ', trnPath[index]); | |
log(trnPath[index].pair + ' :trade error: ', x); | |
log('end: ', chalk.red(moment().valueOf())); | |
return x; | |
}) | |
); | |
}; | |
}; | |
// return order2 = executeTrade(trnPath[index]).then((x) => { | |
// index++; | |
// let finalOrder = executeTrade(trnPath[index]).then((x) => { | |
// // log('finished the arbitrage') | |
// }); | |
// }); | |
// }).catch(x => console.log(x)); | |
// let order = executeTrade(trnPath[index]).then((x) => { | |
// index++; | |
// let order2 = executeTrade(trnPath[index]).then((x) => { | |
// index++; | |
// let finalOrder = executeTrade(trnPath[index]).then((x) => { | |
// log('finished the arbitrage') | |
// }); | |
// }); | |
// }).catch(x => console.log(x)); | |
getRuleCompliantNumber = (currencyPair, number, ruleType) => { | |
let compliantNumber; | |
// number = number.toFixed(10); | |
if (ruleType === 'LOT_SIZE') { | |
// never round up on lot. | |
let lotFilter = EXCHANGE_INFO.symbols.find(x => { | |
return x.symbol === currencyPair; | |
}).filters[1].stepSize; | |
// compliantNumber = number - (number % lotFilter); | |
// compliantNumber = Math.round(number / lotFilter) * lotFilter | |
compliantNumber = getVolume(number, lotFilter); | |
log('volume modified: ', parseFloat(compliantNumber)); | |
return parseFloat(compliantNumber); | |
} else { | |
let priceFilter = EXCHANGE_INFO.symbols.find(x => { | |
return x.symbol === currencyPair; | |
}).filters[0].minPrice; | |
// compliantNumber = number - (number % priceFilter).toFixed(10); | |
compliantNumber = getPrice(number, priceFilter); | |
// compliantNumber = number.toFixed(getPrecision(parseFloat(EXCHANGE_INFO.symbols.find((x) => { | |
// return x.symbol === currencyPair | |
// }).filters[0].minPrice))); | |
return parseFloat(compliantNumber); | |
} | |
}; | |
function getPrecision(number) { | |
var n = parseFloat(number) | |
.toString() | |
.split('.'); | |
return n.length > 1 ? n[1].length : 0; | |
} | |
function getPricePrecision(number) { | |
const realNumber = parseFloat(number); | |
if (realNumber.toString().indexOf('-') >= 0) { | |
const precision = realNumber.toString().slice(3); | |
return precision; | |
} else { | |
var n = realNumber.toString().split('.'); | |
if (!n[1]) { | |
n = number.toString().split('.'); | |
} | |
return n.length > 1 ? n[1].length : 0; | |
} | |
} | |
getPrice = (price, stepSize) => { | |
const precision = getPricePrecision(stepSize); | |
log('precison of price is: ', precision); | |
// realPrice = parseFloat(price); | |
// if(realPrice.toString().indexOf('-') >= 0) { | |
// const | |
// } else { | |
// } | |
n = parseFloat(price) | |
.toString() | |
.split('.'); | |
preDecimal = n[0]; | |
postDecimal = n[1]; | |
try { | |
return preDecimal + '.' + postDecimal.slice(0, precision); | |
} catch (error) { | |
return error; | |
} | |
}; | |
getVolume = (desiredVol, stepSize) => { | |
const precision = getPrecision(stepSize); | |
n = parseFloat(desiredVol) | |
.toString() | |
.split('.'); | |
preDecimal = n[0]; | |
postDecimal = n[1]; | |
try { | |
return preDecimal + '.' + postDecimal.slice(0, precision); | |
} catch (error) { | |
return error; | |
} | |
}; | |
// given an orderId continually check for if it has been filled | |
checkOrderStatus = (orderId, orderPair, checkCount = null) => { | |
checkCount = checkCount || 0; | |
return new Promise(resolve => { | |
binance.orderStatus(orderPair, orderId, (orderStatus, symbol) => { | |
resolve(orderStatus); | |
}); | |
}).then(orderStatus => { | |
if (orderStatus.status === 'FILLED') { | |
console.log(orderId + ' order status:', orderStatus.status); | |
return orderStatus; | |
} else if (orderStatus.status === 'CANCELED') { | |
console.log(orderId + ' order status:', orderStatus.status); | |
return orderStatus; | |
} else if (!orderStatus.status) { | |
log(chalk.yellow(orderId + ' bad order status:', orderStatus)); | |
return orderStatus; | |
} else { | |
console.log( | |
orderId + ' order status:', | |
chalk.red(moment().valueOf()) + '--' + orderStatus.status | |
); | |
return checkOrderStatus(orderId, orderPair, checkCount + 1); | |
} | |
}); | |
}; | |
executeTrade = (trx, timeoutX = 0) => { | |
return new Promise((r, j) => { | |
setTimeout(() => { | |
log(chalk.red(moment().valueOf())); | |
try { | |
if (trx.dealType === 'BUY') { | |
binance.buy( | |
trx.pair, | |
trx.buy.volume, | |
trx.limitPairPrice, | |
{ | |
type: 'LIMIT' | |
}, | |
(resp, err) => { | |
if (!resp.orderId) { | |
log( | |
chalk.bgYellow( | |
trx.pair + 'trade exited immediately: ', | |
resp.checkOrderStatus | |
) | |
); | |
return j(resp.msg); | |
} | |
return r( | |
checkOrderStatus(resp.orderId, trx.pair).catch(x => log) | |
); | |
} | |
); | |
} else { | |
binance.sell( | |
trx.pair, | |
trx.buy.volume, | |
trx.limitPairPrice, | |
{ | |
type: 'LIMIT' | |
}, | |
(resp, err) => { | |
if (!resp.orderId) { | |
log( | |
chalk.bgYellow( | |
trx.pair + 'trade exited immediately: ', | |
resp.checkOrderStatus | |
) | |
); | |
return j(resp.msg); | |
} | |
return r( | |
checkOrderStatus(resp.orderId, trx.pair).catch(x => log) | |
); | |
} | |
); | |
} | |
} catch (error) { | |
j(error); | |
} | |
}, timeoutX * 100); | |
}); | |
// orderStr = 'binance.' + trx.dealType.toLowerCase() + '("' + trx.pair + '",' + trx.buy.volume + ',' + trx.limitPairPrice + ', type:"LIMIT"}, (resp) => { })'; | |
// let order = eval(orderStr); | |
// return checkOrderStatus(order.orderId); | |
}; | |
transactionDetail = (currencyPair, heldCurrency) => { | |
let buying; | |
let selling; | |
let currentPos; | |
if (currencyPair.indexOf(heldCurrency) === 0) { | |
currentPos = 'BASE'; | |
newCurrency = getCryptoQuoteCurrency(currencyPair); | |
} else { | |
currentPos = 'QUOTE'; | |
newCurrency = getCryptoBaseCurrency(currencyPair); | |
} | |
let deal = { | |
buy: newCurrency, | |
sell: heldCurrency, | |
sellPostion: currentPos | |
}; | |
return deal; | |
}; | |
// this fuction is supplied the type of currency we are currently holding | |
// how much of said currency we are holding | |
// and the currecy pair that we are transacting with | |
// the result of this function is an array of arbitrage opportunities | |
buildTransaction = (amountHeld, holdingCurency, newPair, currencies) => { | |
let buyAmount; | |
let sellAmount = amountHeld; | |
let adjustedCurrencyPrice; | |
let dealAction; | |
try { | |
let deal = transactionDetail(newPair, holdingCurency); | |
let buyCurrency = deal.buy; | |
let slippageFee = Number(currencies[newPair] * SLIPPAGE_ALLOWED); | |
log(chalk.bgRed('currency price: ' + currencies[newPair])); | |
log(chalk.bgRed('cost of slippage: ' + slippageFee)); | |
if (deal.sellPostion === 'QUOTE') { | |
dealAction = 'BUY'; | |
adjustedCurrencyPrice = getRuleCompliantNumber( | |
newPair, | |
currencies[newPair] * 1 + slippageFee, | |
'PRICE' | |
); | |
log(chalk.bgRed('adjusted price: ' + adjustedCurrencyPrice)); | |
// if currency in hand is the quote currency of the new pair in thransaction | |
// (pair being - the curency you hold and the currency you are buying) | |
// divide amount of currency you hold (selling) by the price of the pair | |
buyAmount = amountHeld / adjustedCurrencyPrice; | |
} else { | |
dealAction = 'SELL'; | |
adjustedCurrencyPrice = getRuleCompliantNumber( | |
newPair, | |
currencies[newPair] * 1 - slippageFee, | |
'PRICE' | |
); | |
log(chalk.bgRed('adjusted price: ' + adjustedCurrencyPrice)); | |
// if currency in hand is the base currency of the new pair in thransaction | |
// (pair being - the curency you hold and the currency you are buying) | |
// multiply amount of currency you hold (selling) by the price of the pair | |
buyAmount = amountHeld * adjustedCurrencyPrice; | |
} | |
log(chalk.bgRed('pre-fee amount bought: ' + buyAmount)); | |
const tradingFee = buyAmount * TRADING_FEE; | |
const volume = getRuleCompliantNumber( | |
newPair, | |
buyAmount - tradingFee, | |
'LOT_SIZE' | |
); | |
let exchange = { | |
currentPairPrice: currencies[newPair], | |
limitPairPrice: adjustedCurrencyPrice, | |
pair: newPair, | |
fee: tradingFee, | |
dealType: dealAction, | |
buy: { | |
volume: volume, | |
currency: buyCurrency | |
}, | |
sell: { | |
volume: sellAmount, | |
currency: holdingCurency | |
}, | |
description: | |
' Sell ' + | |
sellAmount + | |
' of ' + | |
holdingCurency + | |
' for ' + | |
buyAmount + | |
' of ' + | |
buyCurrency | |
}; | |
console.log( | |
newPair + | |
': going from ' + | |
chalk.red(exchange.sell.volume + ' ' + exchange.sell.currency) + | |
' => ' + | |
chalk.green(exchange.buy.volume + ' ' + exchange.buy.currency) | |
); | |
return exchange; | |
} catch (error) { | |
console.warn(error); | |
} | |
}; | |
getCryptoQuoteCurrency = currency => { | |
let quoteCurr; | |
if (currency.slice(currency.length - 4) === 'USDT') { | |
quoteCurr = 'USDT'; | |
} else { | |
quoteCurr = currency.slice(currency.length - 3); | |
} | |
return quoteCurr; | |
}; | |
getCryptoBaseCurrency = currency => { | |
let baseCurr; | |
if (currency.slice(currency.length - 4) === 'USDT') { | |
baseCurr = currency.slice(0, currency.length - 4); | |
} else { | |
baseCurr = currency.slice(0, currency.length - 3); | |
} | |
return baseCurr; | |
}; | |
getAllBaseCryptos = (BaseCrypto, cryptos) => { | |
return Object.keys(cryptos).filter(x => { | |
return x.startsWith(BaseCrypto); | |
}); | |
}; | |
getAllCurrencyPairsForCrypto = (currency, cryptos) => { | |
return Object.keys(cryptos).filter(x => { | |
return x.includes(currency) && !x.includes('BNB'); | |
}); | |
}; | |
getAllQuoteCryptos = (QuoteCrypto, cryptos) => { | |
return Object.keys(cryptos).filter(x => { | |
return x.endsWith(QuoteCrypto); | |
}); | |
}; | |
module.exports = { | |
aribitrageChecker: runArbitrage, | |
buildTransaction: buildTransaction, | |
checkOrderStatus: checkOrderStatus, | |
config: config, | |
executeAribtrage: executeAribtrage, | |
executeTrade: executeTrade, | |
getAllBaseCryptos: getAllBaseCryptos, | |
getAllQuoteCryptos: getAllQuoteCryptos, | |
getAllCurrencyPairsForCrypto: getAllCurrencyPairsForCrypto, | |
getCryptoQuoteCurrency: getCryptoQuoteCurrency, | |
getCryptoBaseCurrency: getCryptoBaseCurrency, | |
getPrecision: getPrecision, | |
getRuleCompliantNumber: getRuleCompliantNumber, | |
transactionDetail: transactionDetail | |
}; | |
module.exports.config(); | |
setInterval(() => { | |
module.exports.aribitrageChecker(); | |
}, 500); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment