Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cyber-storm-200712 committed Feb 19, 2022
0 parents commit f8c29b3
Show file tree
Hide file tree
Showing 10 changed files with 754 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules

config.js
package-lock.json
.idea
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# How it works

Cryptocurrency Arbitrage Bot is a node.js trading system that does automatic long/short arbitrage between the two biggest Bitcoin exchanges: [Poloniex](https://poloniex.com/) and [Bitfinex](https://www.bitfinex.com/). The purpose of this bot is to automatically profit from these temporary price differences whilst remaining market-neutral.

Here is a real example where an arbitrage opportunity exists between Poloniex (long) and Bitfinex (short):

![image](http://i.imgur.com/t9Pnjz1.png)
At the first vertical line, the spread between the exchanges is high so the bot buys Poloniex and short sells Bitfinex. Then, when the spread closes (second vertical line), the bot exits the market by selling Poloniex and buying Bitfinex back. Note that this methodology means that profits are realised *even in if the price of your asset decreases.*

### Advantages

Unlike other Bitcoin arbitrage systems, this bot doesn't sell but actually short sells Bitcoin (and other Cryptos) on the short exchange. This feature offers two important advantages:

1. The strategy is market-neutral: the Bitcoin market's moves (up or down) don't impact the strategy returns. This removes a huge risk from the strategy. The Bitcoin market could suddenly lose twice its value that this won't make any difference in the strategy returns.

2. The strategy doesn't need to transfer funds (USD or BTC) between Bitcoin exchanges. The buy/sell and sell/buy trading activities are done in parallel on two different exchanges, independently. This means that there is no need to deal with transfer latency issues.

A situational explanation can be found [in the wiki](https://github.com/joepegler/Cryptocurrency-Arbitrage-Bot/wiki)

### Installation

This bot requires [Node.js](https://nodejs.org/) v4+ to run.

Install the dependencies and devDependencies and start the server.

```sh
$ npm install -d
```

To test the bot and investigate it's features:
```sh
$ node playground
```

To start the bot and begin listening for opportunities:
```sh
$ node app
```
147 changes: 147 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use strict";

const exchanges = {
poloniex: require('./exchanges/poloniex'),
bitfinex: require('./exchanges/bitfinex'),
};

const logger = require('./utils/logger');
const messenger = require('./utils/messenger');
const _ = require('lodash');
const SUPPORTED_PAIRS = ['LTCBTC','ETHBTC','XRPBTC','XMRBTC','DSHBTC'];
const OPPORTUNITY_THRESHOLD_PERCENTAGE = 1;
let interval;


(function init(){
Promise.all([exchanges.poloniex.init(), exchanges.bitfinex.init()]).then((messages)=>{
logger.log(messages[0]);
logger.log(messages[1]);
interval = setInterval(tick, 3000);
}).catch(logger.error);
function tick(){
getPrices(SUPPORTED_PAIRS).then(getOrderSize).then(messenger.broadcast).catch(logger.error);
}
}());

/*
function placeOrders(orders){
return new Promise((resolve, reject) => {
let short = orders[0];
let long = orders[1];
//pair, amount, price, side
// exchange[short.exchangeName].order(short.pair, short.price);
logger.log(long);
logger.log(short);
let message = `Placing a ${long.pair} buy order on ${long.exchangeName} at a price of ${long.price}. Placing a ${short.pair} sell order on ${short.exchangeName} at a price of ${short.price}.`;
logger.log(message);
resolve();
});
}
*/

function getOrderSize(opportunity){
/*
*
* Determines the order size by retrieving the balance. The min balance from both exchanges is used, and added to the opportunity object.
*
* {
* pair: 'ETHUSD'
* shortExchange: 'poloniex',
* ...
* orderSize: 1.713
* }
*
* */
return new Promise((resolve, reject) => {
const balancePromises = [
exchanges[opportunity.shortExchange].balance(opportunity.pair).catch(reject),
exchanges[opportunity.longExchange].balance(opportunity.pair).catch(reject)
];
Promise.all(balancePromises).then(balances => {
opportunity.orderSize = _.min(balances);
resolve(opportunity);
}).catch(reject);
});
}

function getPrices(pairs) {
/*
*
* Returns the best available opportunity for arbitrage (if any). The delta is calculated as:
*
* 100 - (longExchange.lowestAsk / shortExchange.highestBid)
*
* because those are the prices that orders are most likely to be filled at.
*
* args:
*
* ['LTCBTC','ETHBTC','XRPBTC','XMRBTC','DASHBTC']
*
* return:
*
* {
* pair: 'ETHUSD'
* shortExchange: 'poloniex',
* longExchange: 'bitfinex',
* shortExchangeAsk: 322,
* shortExchangeBid: 320,
* shortExchangeMid: 321,
* longExchangeAsk: 305,
* longExchangeBid: 301,
* longExchangeMid: 303,
* delta: 4.68,
* }
*
* */
// logger.log('pairs: ' + JSON.stringify(pairs));
return new Promise((resolve, reject) => {
const pricePromises = [
exchanges.poloniex.tick(pairs).catch(reject),
exchanges.bitfinex.tick(pairs).catch(reject)
];
Promise.all(pricePromises).then(prices => {
let opportunity = {};
let poloniexPrices = prices[0];
let bitfinexPrices = prices[1];
// prices = [{exchange: 'bitfinex', pair: 'ETHUSD', ask: 312, bid: 310, mid: 311}, {exchange: 'bitfinex', pair: 'LTCUSD', ask: 46, bid: 44, mid: 45}, {exchange: 'bitfinex', pair: 'BTCUSD', ask: 3800, bid: 3700, mid: 3750}]
_.each(poloniexPrices, (poloniexPrice) =>{
_.each(bitfinexPrices, (bitfinexPrice) =>{
if(poloniexPrice.pair === bitfinexPrice.pair){
let ordered =_.sortBy([poloniexPrice, bitfinexPrice], ['mid']);
let longExchange = ordered[0];
let shortExchange = ordered[1];
let delta = parseFloat(100 - (longExchange.ask / shortExchange.bid * 100)).toFixed(2);
if ( delta > OPPORTUNITY_THRESHOLD_PERCENTAGE ){
// logger.log(`Opportunity found for ${shortExchange.pair}: [[${longExchange.ask}][${shortExchange.bid}] - [${delta}]]`);
if((opportunity && (opportunity.delta < delta)) || _.isEmpty(opportunity)){
opportunity = {
pair: poloniexPrice.pair,
shortExchange: shortExchange.exchange,
longExchange: longExchange.exchange,
shortExchangeAsk: shortExchange.ask,
shortExchangeBid: shortExchange.bid,
shortExchangeMid: shortExchange.mid,
longExchangeAsk: longExchange.ask,
longExchangeBid: longExchange.bid,
longExchangeMid: longExchange.mid,
delta: delta,
}
}
}
else{
// logger.log(`No opportunity for ${shortExchange.pair}: [[${longExchange.ask}][${shortExchange.bid}] - [${delta}]]`);
}
}
})
});
if(_.isEmpty(opportunity)){
reject('No opportunity.')
}
else{
resolve(opportunity);
}
});
});
}

124 changes: 124 additions & 0 deletions exchanges/bitfinex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
module.exports = (function() {
"use strict";

const SETTINGS = require('./config')['BITFINEX'];
const Promise = require('promise');
const _ = require('lodash');
const BFX = require('bitfinex-api-node');
const logger = require('../utils/logger');
const bitfinex = new BFX(SETTINGS.API_KEY, SETTINGS.API_SECRET, {version: 1, transform: true}).rest;
const bitfinex_websocket = new BFX('', '', { version: 2, transform: true }).ws;
const devMode = process.argv.includes('dev');
let prices = {}, dollarBalance = 0;

return {
tick: (pairArray) => {
/*
*
* Returns an array of price values corresponding to the pairArray provided. e.g.
*
* args:
* ['ETHUSD', 'LTCUSD', 'BTCUSD']
*
* returns:
* [{exchange: 'bitfinex', pair: 'ETHUSD', ask: 312, bid: 310, mid: 311}, {exchange: 'bitfinex', pair: 'LTCUSD', ask: 46, bid: 44, mid: 45}, {exchange: 'bitfinex', pair: 'BTCUSD', ask: 3800, bid: 3700, mid: 3750}]
*
* */
return new Promise((resolve) => {
resolve(pairArray.map(pair => {return prices[pair];}));
});
},
balance(pair) {
/*
*
* Returns a single float value of approximate balance of the selected coin.
* It is slightly adjust to give a margin of error for the exchange rate e.g.
*
* args:
* 'LTC'
*
* returns:
* 1.235
*
* */
return new Promise((resolve, reject) => {
if(_.isNumber(dollarBalance)) {
// For bitfinex we must translate the price to bitcoin first.
let bitcoinPrice = _.find(prices, {pair: 'BTCUSD'}).mid;
let pairPriceInBitcoin = _.find(prices, {pair: pair}).mid;
let bitcoinBalance = parseFloat( dollarBalance / bitcoinPrice );
let coinBalance = parseFloat( bitcoinBalance / pairPriceInBitcoin );
resolve(coinBalance);
}
else{
reject(`Bitfinex could retrieve the balance`)
}
});
},
order(pair, amount, price, side) {
/*
*
* Place an order
*
* */
return new Promise((resolve, reject) => {
// [symbol, amount, price, exchange, side, type, is_hidden, postOnly, cb]
bitfinex.new_order(SETTINGS.COINS[pair], amount.toFixed(7), price.toFixed(9), 'bitfinex', side, 'limit', (err, data) => {
if (!err) {
// {"id":3341017504,"cid":1488258364,"cid_date":"2017-08-13","gid":null,"symbol":"ethbtc","exchange":"bitfinex","price":"0.078872","avg_execution_price":"0.0","side":"sell","type":"limit","timestamp":"1502583888.325827284","is_live":true,"is_cancelled":false,"is_hidden":false,"oco_order":null,"was_forced":false,"original_amount":"0.01","remaining_amount":"0.01","executed_amount":"0.0","src":"api","order_id":3341017504}
resolve(data);
}
else {
logger.error(err);
reject(err);
}
});
});
},
init(){
/*
*
* Initiating the exchange will start the ticker and also retrieve the balance for trading.
* It returns a simple success message (String)
*
* */
let once;
return new Promise((resolve, reject) => {
const invertedMap = (_.invert(SETTINGS.COINS));
bitfinex_websocket.on('open', () => {
_.each(SETTINGS.COINS, pair => {
bitfinex_websocket.subscribeTicker(pair);
});
bitfinex_websocket.on('ticker', (ePair, ticker) => {
let pair = invertedMap[ePair.substring(1)];
prices[pair] = {
exchange: 'bitfinex',
pair: pair,
ask: parseFloat(ticker.ASK) + (devMode ? (parseFloat(ticker.ASK) * .02) : 0),
bid: parseFloat(ticker.BID) + (devMode ? (parseFloat(ticker.ASK) * .02) : 0),
mid: parseFloat((parseFloat(ticker.ASK) + parseFloat(ticker.BID)) / 2)
};
if (!once) {
once = true;
bitfinex.margin_infos((err, data) => {
if (!err) {
try {
//[{"margin_balance":"72.84839221","tradable_balance":"182.120980525","unrealized_pl":"0.0","unrealized_swap":"0.0","net_value":"72.84839221","required_margin":"0.0","leverage":"2.5","margin_requirement":"13.0","margin_limits":[{"on_pair":"BTCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"LTCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"LTCBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ETHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETCBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"ETCUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ZECUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"ZECBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"XMRUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"XMRBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"DSHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"DSHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"IOTUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"IOTBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"IOTETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"EOSUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"EOSBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"EOSETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"OMGUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"OMGBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"OMGETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"BCHUSD","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"},{"on_pair":"BCHBTC","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"233.895656370333333333"},{"on_pair":"BCHETH","initial_margin":"30.0","margin_requirement":"15.0","tradable_balance":"220.973456370333333333"}],"message":"Margin requirement, leverage and tradable balance are now per pair. Values displayed in the root of the JSON message are incorrect (deprecated). You will find the correct ones under margin_limits, for each pair. Please update your code as soon as possible."}]
dollarBalance = parseFloat(data[0].margin_balance);
resolve(`Successfully initiated bitfinex. Your balance is: ${dollarBalance} Dollars. `);
}
catch (e) {
reject(e);
}
}
else {
reject(err);
}
});
}
});
});
});
}
};
})();
25 changes: 25 additions & 0 deletions exchanges/config-example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
POLONIEX : {
API_KEY: 'X',
API_SECRET: 'X',
COINS: {
BTCUSD:'BTC_USDT',
LTCBTC:'BTC_LTC',
ETHBTC:'BTC_ETH',
XRPBTC:'BTC_XRP',
XMRBTC:'BTC_XMR',
DASHBTC:'BTC_DASH'
}
},
BITFINEX : {
API_KEY: 'X',
API_SECRET: 'X',
COINS: {
LTCBTC:'LTCBTC',
ETHBTC:'ETHBTC',
XRPBTC:'XRPBTC',
XMRBTC:'XMRBTC',
DASHBTC:'DSHBTC'
}
}
};
Loading

0 comments on commit f8c29b3

Please sign in to comment.