Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
bmino committed Jun 14, 2019
2 parents 1a99e5e + 303f886 commit 0dae169
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 69 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "binance-triangle-arbitrage",
"version": "5.1.1",
"version": "5.1.2",
"private": true,
"engines": {
"node": "11.10.1",
Expand Down
68 changes: 42 additions & 26 deletions src/main/ArbitrageExecution.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@ const ArbitrageExecution = {
executeCalculatedPosition(calculated) {
const startTime = new Date().getTime();

const { times } = calculated;
const { symbol } = calculated.trade;

if (!ArbitrageExecution.isSafeToExecute(calculated)) return false;

const { symbol } = calculated.trade;
const age = {
ab: startTime - calculated.depth.ab.eventTime,
bc: startTime - calculated.depth.bc.eventTime,
ca: startTime - calculated.depth.ca.eventTime
};

// Register position as being attempted
ArbitrageExecution.attemptedPositions[startTime] = calculated.id;
ArbitrageExecution.inProgressIds.add(calculated.id);
ArbitrageExecution.inProgressSymbols.add(symbol.a);
ArbitrageExecution.inProgressSymbols.add(symbol.b);
ArbitrageExecution.inProgressSymbols.add(symbol.c);

logger.execution.info(`Attempting to execute ${calculated.id} with an age of ${(startTime - Math.min(times.ab, times.bc, times.ca)).toFixed(0)} ms and expected profit of ${calculated.percent.toFixed(4)}%`);
logger.execution.info(`Attempting to execute ${calculated.id} with an age of ${Math.max(age.ab, age.bc, age.ca).toFixed(0)} ms and expected profit of ${calculated.percent.toFixed(4)}%`);
logger.execution.debug(`${calculated.trade.ab.ticker} depth cache age: ${age.ab.toFixed(0)} ms`);
logger.execution.debug(`${calculated.trade.bc.ticker} depth cache age: ${age.bc.toFixed(0)} ms`);
logger.execution.debug(`${calculated.trade.ca.ticker} depth cache age: ${age.ca.toFixed(0)} ms`);

return ArbitrageExecution.execute(calculated)
.then((actual) => {
Expand All @@ -35,30 +42,28 @@ const ArbitrageExecution = {
if (!CONFIG.TRADING.ENABLED) return;

logger.execution.debug();
logger.execution.debug(`AB Expected Conversion: ${calculated.start.toFixed(8)} ${symbol.a} into ${calculated.b.toFixed(8)} ${symbol.b}`);
logger.execution.debug(`AB Expected Conversion: ${calculated.a.spent.toFixed(8)} ${symbol.a} into ${calculated.b.earned.toFixed(8)} ${symbol.b}`);
logger.execution.debug(`AB Observed Conversion: ${actual.a.spent.toFixed(8)} ${symbol.a} into ${actual.b.earned.toFixed(8)} ${symbol.b}`);
logger.execution.debug();
logger.execution.debug(`BC Expected Conversion: ${calculated.b.toFixed(8)} ${symbol.b} into ${calculated.c.toFixed(8)} ${symbol.c}`);
logger.execution.debug(`BC Expected Conversion: ${calculated.b.spent.toFixed(8)} ${symbol.b} into ${calculated.c.earned.toFixed(8)} ${symbol.c}`);
logger.execution.debug(`BC Observed Conversion: ${actual.b.spent.toFixed(8)} ${symbol.b} into ${actual.c.earned.toFixed(8)} ${symbol.c}`);
logger.execution.debug();
logger.execution.debug(`CA Expected Conversion: ${calculated.c.toFixed(8)} ${symbol.c} into ${calculated.a.toFixed(8)} ${symbol.a}`);
logger.execution.debug(`CA Expected Conversion: ${calculated.c.spent.toFixed(8)} ${symbol.c} into ${calculated.a.earned.toFixed(8)} ${symbol.a}`);
logger.execution.debug(`CA Observed Conversion: ${actual.c.spent.toFixed(8)} ${symbol.c} into ${actual.a.earned.toFixed(8)} ${symbol.a}`);
logger.execution.debug();

const delta = {
a: actual.a.earned - actual.a.spent,
b: actual.b.earned - actual.b.spent,
c: actual.c.earned - actual.c.spent
};
logger.execution.trace(`Depth cache used for calculation:`);
logger.execution.trace(calculated.depth);

const percent = {
a: delta.a / actual.a.spent * 100,
b: delta.b / actual.b.spent * 100,
c: delta.c / actual.c.spent * 100
a: actual.a.delta / actual.a.spent * 100,
b: actual.b.delta / actual.b.spent * 100,
c: actual.c.delta / actual.c.spent * 100
};

logger.execution.info(`${symbol.a} delta:\t ${delta.a < 0 ? '' : ' '}${delta.a.toFixed(8)} (${percent.a < 0 ? '' : ' '}${percent.a.toFixed(4)}%)`);
logger.execution.info(`${symbol.b} delta:\t ${delta.b < 0 ? '' : ' '}${delta.b.toFixed(8)} (${percent.b < 0 ? '' : ' '}${percent.b.toFixed(4)}%)`);
logger.execution.info(`${symbol.c} delta:\t ${delta.c < 0 ? '' : ' '}${delta.c.toFixed(8)} (${percent.c < 0 ? '' : ' '}${percent.c.toFixed(4)}%)`);
logger.execution.info(`${symbol.a} delta:\t ${actual.a.delta < 0 ? '' : ' '}${actual.a.delta.toFixed(8)} (${percent.a < 0 ? '' : ' '}${percent.a.toFixed(4)}%)`);
logger.execution.info(`${symbol.b} delta:\t ${actual.b.delta < 0 ? '' : ' '}${actual.b.delta.toFixed(8)} (${percent.b < 0 ? '' : ' '}${percent.b.toFixed(4)}%)`);
logger.execution.info(`${symbol.c} delta:\t ${actual.c.delta < 0 ? '' : ' '}${actual.c.delta.toFixed(8)} (${percent.c < 0 ? '' : ' '}${percent.c.toFixed(4)}%)`);
logger.execution.info(`BNB commission: ${(-1 * actual.fees).toFixed(8)}`);
logger.execution.info();
})
Expand All @@ -79,28 +84,29 @@ const ArbitrageExecution = {

isSafeToExecute(calculated) {
const now = new Date().getTime();
const { symbol } = calculated.trade;

// Profit Threshold is Not Satisfied
if (calculated.percent < CONFIG.TRADING.PROFIT_THRESHOLD) return false;

// Age Threshold is Not Satisfied
const ageInMilliseconds = now - Math.min(calculated.times.ab, calculated.times.bc, calculated.times.ca);
if (ageInMilliseconds > CONFIG.TRADING.AGE_THRESHOLD) return false;
const ageInMilliseconds = now - Math.min(calculated.depth.ab.eventTime, calculated.depth.bc.eventTime, calculated.depth.ca.eventTime);
if (isNaN(ageInMilliseconds) || ageInMilliseconds > CONFIG.TRADING.AGE_THRESHOLD) return false;

if (CONFIG.TRADING.EXECUTION_CAP && ArbitrageExecution.getAttemptedPositionsCount() >= CONFIG.TRADING.EXECUTION_CAP) {
logger.execution.trace(`Blocking execution because ${ArbitrageExecution.getAttemptedPositionsCount()} executions have been attempted`);
return false;
}
if (ArbitrageExecution.inProgressSymbols.has(calculated.trade.symbol.a)) {
logger.execution.trace(`Blocking execution because ${calculated.trade.symbol.a} is currently involved in an execution`);
if (ArbitrageExecution.inProgressSymbols.has(symbol.a)) {
logger.execution.trace(`Blocking execution because ${symbol.a} is currently involved in an execution`);
return false;
}
if (ArbitrageExecution.inProgressSymbols.has(calculated.trade.symbol.b)) {
logger.execution.trace(`Blocking execution because ${calculated.trade.symbol.b} is currently involved in an execution`);
if (ArbitrageExecution.inProgressSymbols.has(symbol.b)) {
logger.execution.trace(`Blocking execution because ${symbol.b} is currently involved in an execution`);
return false;
}
if (ArbitrageExecution.inProgressSymbols.has(calculated.trade.symbol.c)) {
logger.execution.trace(`Blocking execution because ${calculated.trade.symbol.c} is currently involved in an execution`);
if (ArbitrageExecution.inProgressSymbols.has(symbol.c)) {
logger.execution.trace(`Blocking execution because ${symbol.c} is currently involved in an execution`);
return false;
}
if (ArbitrageExecution.getAttemptedPositionsCountInLastSecond() > 1) {
Expand Down Expand Up @@ -174,6 +180,10 @@ const ArbitrageExecution = {

[actual.c.spent, actual.a.earned, fees] = ArbitrageExecution.parseActualResults(calculated.trade.ca.method, resultsCA);
actual.fees += fees;

actual.a.delta = actual.a.earned - actual.a.spent;
actual.b.delta = actual.b.earned - actual.b.spent;
actual.c.delta = actual.c.earned - actual.c.spent;
}

return actual;
Expand Down Expand Up @@ -224,6 +234,12 @@ const ArbitrageExecution = {
actual.fees += fees;
}
return actual;
})
.then((actual) => {
actual.a.delta = actual.a.earned - actual.a.spent;
actual.b.delta = actual.b.earned - actual.b.spent;
actual.c.delta = actual.c.earned - actual.c.spent;
return actual;
});
},

Expand Down
14 changes: 14 additions & 0 deletions src/main/BinanceApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ const BinanceApi = {
});
},

cloneDepth(ticker) {
const tmp = binance.depthCache(ticker);
return {
eventTime: tmp.eventTime,
lastUpdateId: tmp.lastUpdateId,
asks: {...tmp.asks},
bids: {...tmp.bids}
};
},

cloneDepths(...tickers) {
return tickers.map(ticker => BinanceApi.cloneDepth(ticker));
},

marketBuy(ticker, quantity) {
logger.execution.info(`${binance.getOption('test') ? 'Test: Buying' : 'Buying'} ${quantity} ${ticker} @ market price`);
return new Promise((resolve, reject) => {
Expand Down
135 changes: 102 additions & 33 deletions src/main/CalculationNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const CONFIG = require('../../config/config');
const binance = require('node-binance-api')();
const BinanceApi = require('./BinanceApi');
const MarketCache = require('./MarketCache');

const CalculationNode = {
Expand All @@ -22,45 +22,70 @@ const CalculationNode = {
let calculated = {
id: `${trade.symbol.a}-${trade.symbol.b}-${trade.symbol.c}`,
trade: trade,
start: investmentA,
ab: 0,
bc: 0,
ca: 0,
times: {
ab: binance.depthCache(trade.ab.ticker).eventTime,
bc: binance.depthCache(trade.bc.ticker).eventTime,
ca: binance.depthCache(trade.ca.ticker).eventTime
depth: {
ab: BinanceApi.cloneDepth(trade.ab.ticker),
bc: BinanceApi.cloneDepth(trade.bc.ticker),
ca: BinanceApi.cloneDepth(trade.ca.ticker)
},
a: 0,
b: 0,
c: 0
a: {
spent: 0,
earned: 0,
delta: 0
},
b: {
spent: 0,
earned: 0,
delta: 0
},
c: {
spent: 0,
earned: 0,
delta: 0
}
};

if (trade.ab.method === 'Buy') {
const dustedAB = CalculationNode.orderBookConversion(investmentA, trade.symbol.a, trade.symbol.b, trade.ab.ticker);
calculated.b = calculated.ab = CalculationNode.calculateDustless(trade.ab.ticker, dustedAB);
// Buying BA
const dustedB = CalculationNode.orderBookConversion(investmentA, trade.symbol.a, trade.symbol.b, trade.ab.ticker);
calculated.b.earned = calculated.ab = CalculationNode.calculateDustless(trade.ab.ticker, dustedB);
calculated.a.spent = CalculationNode.orderBookReverseConversion(calculated.b.earned, trade.symbol.b, trade.symbol.a, trade.ab.ticker);
} else {
calculated.start = calculated.ab = CalculationNode.calculateDustless(trade.ab.ticker, investmentA);
calculated.b = CalculationNode.orderBookConversion(calculated.ab, trade.symbol.a, trade.symbol.b, trade.ab.ticker);
// Selling AB
calculated.a.spent = calculated.ab = CalculationNode.calculateDustless(trade.ab.ticker, investmentA);
calculated.b.earned = CalculationNode.orderBookConversion(calculated.a.spent, trade.symbol.a, trade.symbol.b, trade.ab.ticker);
}

if (trade.bc.method === 'Buy') {
const dustedBC = CalculationNode.orderBookConversion(calculated.b, trade.symbol.b, trade.symbol.c, trade.bc.ticker);
calculated.c = calculated.bc = CalculationNode.calculateDustless(trade.bc.ticker, dustedBC);
// Buying CB
const dustedC = CalculationNode.orderBookConversion(calculated.b.earned, trade.symbol.b, trade.symbol.c, trade.bc.ticker);
calculated.c.earned = calculated.bc = CalculationNode.calculateDustless(trade.bc.ticker, dustedC);
calculated.b.spent = CalculationNode.orderBookReverseConversion(calculated.c.earned, trade.symbol.c, trade.symbol.b, trade.bc.ticker);
} else {
calculated.bc = CalculationNode.calculateDustless(trade.bc.ticker, calculated.b);
calculated.c = CalculationNode.orderBookConversion(calculated.bc, trade.symbol.b, trade.symbol.c, trade.bc.ticker);
// Selling BC
calculated.b.spent = calculated.bc = CalculationNode.calculateDustless(trade.bc.ticker, calculated.b.earned);
calculated.c.earned = CalculationNode.orderBookConversion(calculated.b.spent, trade.symbol.b, trade.symbol.c, trade.bc.ticker);
}

if (trade.ca.method === 'Buy') {
const dustedCA = CalculationNode.orderBookConversion(calculated.c, trade.symbol.c, trade.symbol.a, trade.ca.ticker);
calculated.a = calculated.ca = CalculationNode.calculateDustless(trade.ca.ticker, dustedCA);
// Buying AC
const dustedA = CalculationNode.orderBookConversion(calculated.c.earned, trade.symbol.c, trade.symbol.a, trade.ca.ticker);
calculated.a.earned = calculated.ca = CalculationNode.calculateDustless(trade.ca.ticker, dustedA);
calculated.c.spent = CalculationNode.orderBookReverseConversion(calculated.a.earned, trade.symbol.a, trade.symbol.c, trade.ca.ticker);
} else {
calculated.ca = CalculationNode.calculateDustless(trade.ca.ticker, calculated.c);
calculated.a = CalculationNode.orderBookConversion(calculated.ca, trade.symbol.c, trade.symbol.a, trade.ca.ticker);
// Selling CA
calculated.c.spent = calculated.ca = CalculationNode.calculateDustless(trade.ca.ticker, calculated.c.earned);
calculated.a.earned = CalculationNode.orderBookConversion(calculated.c.spent, trade.symbol.c, trade.symbol.a, trade.ca.ticker);
}

calculated.percent = (calculated.a - calculated.start) / calculated.start * 100 - (CONFIG.TRADING.TAKER_FEE * 3);
// Calculate deltas
calculated.a.delta = calculated.a.earned - calculated.a.spent;
calculated.b.delta = calculated.b.earned - calculated.b.spent;
calculated.c.delta = calculated.c.earned - calculated.c.spent;

calculated.percent = (calculated.a.delta / calculated.a.spent * 100) - (CONFIG.TRADING.TAKER_FEE * 3);
if (!calculated.percent) calculated.percent = 0;

return calculated;
Expand All @@ -76,45 +101,89 @@ const CalculationNode = {
},

orderBookConversion(amountFrom, symbolFrom, symbolTo, ticker) {
let i, j, rate, quantity, exchangeableAmount;
let orderBook = binance.depthCache(ticker) || {};
if (amountFrom === 0) return 0;

let amountTo = 0;
let i, rate, quantity, exchangeableAmount;
let orderBook = BinanceApi.cloneDepth(ticker) || {};
const bidRates = Object.keys(orderBook.bids || {});
const askRates = Object.keys(orderBook.asks || {});
let amountTo = 0;

if (amountFrom === 0) return 0;
if (parseFloat(bidRates[0]) > parseFloat(askRates[0])) throw new Error(`Spread does not exist for ${ticker}`);

if (ticker === symbolFrom + symbolTo) {
for (i=0; i<bidRates.length; i++) {
rate = parseFloat(bidRates[i]);
quantity = orderBook.bids[bidRates[i]];
exchangeableAmount = quantity * rate;
if (quantity < amountFrom) {
amountFrom -= quantity;
amountTo += quantity * rate;
amountTo += exchangeableAmount;
} else {
// Last fill
return amountTo + amountFrom * rate;
return amountTo + (amountFrom * rate);
}
}
} else {
for (j=0; j<askRates.length; j++) {
rate = parseFloat(askRates[j]);
quantity = orderBook.asks[askRates[j]];
for (i=0; i<askRates.length; i++) {
rate = parseFloat(askRates[i]);
quantity = orderBook.asks[askRates[i]];
exchangeableAmount = quantity * rate;
if (exchangeableAmount < amountFrom) {
amountFrom -= quantity * rate;
amountFrom -= exchangeableAmount;
amountTo += quantity;
} else {
// Last fill
return amountTo + amountFrom / rate;
return amountTo + (amountFrom / rate);
}
}
}

throw new Error(`Bid depth (${bidRates.length}) or ask depth (${askRates.length}) too shallow to convert ${amountFrom} ${symbolFrom} to ${symbolTo} using ${ticker}`);
},

orderBookReverseConversion(amountFrom, symbolFrom, symbolTo, ticker) {
if (amountFrom === 0) return 0;

let amountTo = 0;
let i, rate, quantity, exchangeableAmount;
let orderBook = BinanceApi.cloneDepth(ticker) || {};
const bidRates = Object.keys(orderBook.bids || {});
const askRates = Object.keys(orderBook.asks || {});

if (parseFloat(bidRates[0]) > parseFloat(askRates[0])) throw new Error(`Spread does not exist for ${ticker}`);

if (ticker === symbolFrom + symbolTo) {
for (i=0; i<askRates.length; i++) {
rate = parseFloat(askRates[i]);
quantity = orderBook.asks[askRates[i]];
exchangeableAmount = quantity * rate;
if (quantity < amountFrom) {
amountFrom -= quantity;
amountTo += exchangeableAmount;
} else {
// Last fill
return amountTo + (amountFrom * rate);
}
}
} else {
for (i=0; i<bidRates.length; i++) {
rate = parseFloat(bidRates[i]);
quantity = orderBook.bids[bidRates[i]];
exchangeableAmount = quantity * rate;
if (exchangeableAmount < amountFrom) {
amountFrom -= exchangeableAmount;
amountTo += quantity;
} else {
// Last fill
return amountTo + (amountFrom / rate);
}
}
}

throw new Error(`Bid depth (${bidRates.length}) or ask depth (${askRates.length}) too shallow to reverse convert ${amountFrom} ${symbolFrom} to ${symbolTo} using ${ticker}`);
},

calculateDustless(ticker, amount) {
if (Number.isInteger(amount)) return amount;
const amountString = amount.toFixed(12);
Expand Down
Loading

0 comments on commit 0dae169

Please sign in to comment.