bitdo2
Version:
A cryptocurrency order-rule executor
281 lines (261 loc) • 8.67 kB
JavaScript
const parsers = require('../lib/parsers');
const log = require('../log');
const Plugins = require('../plugins');
const Exchanges = require('../exchanges');
const config = require('../config');
const moment = require('moment');
const _ = require('lodash');
const Promise = require('bluebird');
log.enableConsole();
exports.command = 'obslope <product>';
exports.desc = 'Runs analysis on the slop of an orderbook of a given product';
exports.builder = args => args
.describe('poll', 'Number of seconds between polling')
.number('poll')
.default('poll', 30)
.describe('window', 'Percentage to examine relative to ticker price')
.number('window')
.default('window', 0.2);
exports.handler = (args) => {
const product = parsers.parseProduct(args.product);
log.info(`Starting up orderbook analyzer on ${product.exchange} ${product.symbol}`);
const exchange = Exchanges.createExchange(product.exchange, config.exchanges[product.exchange]);
const plugins = Plugins.createFromConfig({
web: {
port: 8081,
},
});
function createGraphData(book) {
return _.reduce(book, (graph, item) => {
const prevSize = graph.length > 0 ? graph[graph.length - 1].y : 0;
graph.push({
x: item.price,
y: prevSize + item.size,
});
return graph;
}, []);
}
function discoverOrderbookWalls(currentPrice, book, wallThreshold = 2, slopeAdjustor = 100.0, ignoreEarlyPrice = 0.01, smalength = 50) {
const walls = [];
// "Center" of the book
walls.push({
price: book[0].price,
size: book[0].size,
slope: 0,
});
const sma = [0];
let net = book[0].size;
_.each(book, order => {
net += order.size;
if (order.size >= _.mean(sma) * wallThreshold && Math.abs(order.price - currentPrice) > currentPrice * ignoreEarlyPrice) {
walls.push({
price: order.price,
size: net,
slope: 0,
});
walls[walls.length - 2].slope = Math.abs((net - walls[walls.length - 2].size) / (order.price - walls[walls.length - 2].price)) / slopeAdjustor;
}
if (order.size > _.mean(sma) * 0.9)
sma.push(order.size);
while (sma.length > smalength) sma.shift();
});
return walls;
}
const outstandingOrders = [];
const priceHistory = [];
let velocity = 0;
const velocitySMA = [];
let lastPrice = null;
const velocityPeriods = 15;
let currentBuy = null;
function update() {
log.info('Updating OB analysis...');
return Promise.all([
exchange.getOrderBook(product.symbol, product.relation, 1),
exchange.getTicker(product.symbol, product.relation),
]).spread((orderbook, ticker) => {
priceHistory.push({ price: ticker.price, ts: new Date() });
const windowedBuys = _.filter(orderbook.buys, buy => buy.price > ticker.price - ticker.price * args.window);
const windowedSells = _.filter(orderbook.sells, buy => buy.price < ticker.price + ticker.price * args.window && buy.price > 0);
const buyWalls = discoverOrderbookWalls(ticker.price, _.reverse(_.clone(windowedBuys)));
const sellWalls = discoverOrderbookWalls(ticker.price, windowedSells);
/*
Basic algorithm thoughts:
- If volume velocity is sufficiently high enough
- If going-right has a SUFFICIENTLY lower slope than going left (aka, favoring buys)
- If it has well-defined walls
- Add a buy order right after the first wall, and a sell right before the one after
*/
// TODO: Right now just using price as velocity, but should really be volume
velocitySMA.push(ticker.price - (lastPrice || ticker.price));
while (velocitySMA.length > velocityPeriods)
velocitySMA.shift();
lastPrice = ticker.price;
velocity = _.mean(velocitySMA);
const slopeModifier = 2;
if (buyWalls.length >= 2 && sellWalls.length >= 3
&& buyWalls[0].slope < sellWalls[0].slope * slopeModifier
&& velocity > ticker.price * 0.002
&& !currentBuy) {
const buyPrice = sellWalls[1].price + 0.12;
log.info(`Triggering expected buy at ${buyPrice}`);
currentBuy = {
price: buyPrice,
size: 5,
};
outstandingOrders.push(currentBuy);
}
if (currentBuy) {
// Watch the buy price, and the walls, as they may be adjusting
// A) Update sell to change price
// B) Immediate sell in risky situations (or cancel the buy if not settled)
}
/*
Display graph to user
*/
plugins.graph(`${product.symbol}-${product.relation} Orderbook`, {
type: 'line',
data: {
datasets: [{
label: 'Buys',
steppedLine: 'before',
backgroundColor: 'rgba(0,255,0,0.3)',
borderColor: 'rgba(0,255,0,0.8)',
fill: true,
pointRadius: 0,
data: createGraphData(_.reverse(windowedBuys)),
yAxisID: 'y-axis-1',
}, {
label: 'Sells',
steppedLine: 'before',
backgroundColor: 'rgba(255,0,0,0.3)',
borderColor: 'rgba(255,0,0,0.8)',
fill: true,
pointRadius: 0,
data: createGraphData(windowedSells),
yAxisID: 'y-axis-1',
}, {
label: 'Buy-Walls',
pointRadius: 10,
pointStyle: 'triangle',
fill: false,
backgroundColor: 'white',
borderColor: 'rgba(255,255,255,0.5)',
lineTension: 0,
data: _.map(buyWalls, item => ({
x: item.price,
y: item.size,
text: `y=${~~item.slope}`,
})),
yAxisID: 'y-axis-1',
}, {
label: 'Sell-Walls',
pointRadius: 10,
pointHoverRadius: 15,
pointStyle: 'triangle',
fill: false,
backgroundColor: 'white',
borderColor: 'rgba(255,255,255,0.5)',
lineTension: 0,
data: _.map(sellWalls, item => ({
x: item.price,
y: item.size,
text: `y=${~~item.slope}`,
})),
yAxisID: 'y-axis-1',
}, {
label: 'Velocity',
pointRadius: 10,
pointHoverRadius: 15,
fill: false,
borderColor: 'rgba(255,255,255,0.5)',
lineTension: 0,
data: [{
x: ticker.price,
y: 0,
text: `v=${velocity}`,
}, {
x: ticker.price + velocity,
y: 0,
}],
yAxisID: 'y-axis-2',
}, {
label: 'Orders',
pointRadius: 10,
pointHoverRadius: 15,
pointStyle: 'star',
fill: false,
showLine: false,
data: _.map(outstandingOrders, order => ({
x: order.price,
y: order.size,
})),
yAxisID: 'y-axis-2',
}],
},
options: {
stacked: false,
scales: {
xAxes: [{
type: 'linear',
ticks: {
source: 'labels',
minRotation: 30,
autoSkip: true,
},
}],
yAxes: [{
type: 'linear',
display: true,
position: 'left',
id: 'y-axis-1',
}, {
type: 'linear',
display: true,
position: 'right',
id: 'y-axis-2',
gridLines: {
drawOnChartArea: false,
},
}],
},
},
html: {
width: '100%',
},
});
plugins.graph(`${product.symbol}-${product.relation} Ticker`, {
type: 'line',
data: {
datasets: [{
label: 'Price',
data: _.map(priceHistory, h => ({ x: moment(h.ts).format(), y: h.price })),
backgroundColor: 'rgba(255,255,255,0.7)',
pointRadius: 0,
}],
},
options: {
scales: {
xAxes: [{
type: 'time',
distribution: 'series',
ticks: {
source: 'labels',
minRotation: 30,
autoSkip: true,
},
time: {
tooltipFormat: 'll HH:mm',
},
}],
},
},
html: {
width: '100%',
},
});
});
}
update();
setInterval(update, args.poll * 1000);
};