Skip to content

Commit

Permalink
feat(cli): add orderbook command
Browse files Browse the repository at this point in the history
  • Loading branch information
Karl Ranna committed Jun 12, 2019
1 parent 13a5db5 commit e13aec3
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 4 deletions.
12 changes: 8 additions & 4 deletions lib/cli/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface GrpcResponse {
toObject: Function;
}

export const callback = (argv: Arguments, formatOutput?: Function) => {
export const callback = (argv: Arguments, formatOutput?: Function, displayJson?: Function) => {
return (error: Error | null, response: GrpcResponse) => {
if (error) {
console.error(`${error.name}: ${error.message}`);
Expand All @@ -47,9 +47,13 @@ export const callback = (argv: Arguments, formatOutput?: Function) => {
if (Object.keys(responseObj).length === 0) {
console.log('success');
} else {
!argv.json && formatOutput
? formatOutput(responseObj)
: console.log(JSON.stringify(responseObj, undefined, 2));
if (!argv.json && formatOutput) {
formatOutput(responseObj, argv);
} else {
displayJson
? displayJson(responseObj, argv)
: console.log(JSON.stringify(responseObj, undefined, 2));
}
}
}
};
Expand Down
179 changes: 179 additions & 0 deletions lib/cli/commands/orderbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Arguments } from 'yargs';
import { callback, loadXudClient } from '../command';
import { ListOrdersRequest, ListOrdersResponse, Order } from '../../proto/xudrpc_pb';
import Table, { HorizontalTable } from 'cli-table3';
import colors from 'colors/safe';
import { satsToCoinsStr } from '../utils';

type FormattedOrderbook = {
pairId: string,
rows: string[][],
};

type BucketDepth = {
price: number,
depth: number;
};

type OrderbookJson = {
pairId: string,
sell: BucketDepth[],
buy: BucketDepth[],
};

const COLUMNS = [19, 19, 19, 19];
const COLUMNS_IN_ORDER_SIDE = COLUMNS.length / 2;
const HEADER = [
{ content: colors.green('Buy'), colSpan: 2 },
{ content: colors.red('Sell'), colSpan: 2 },
];
const SECONDARY_HEADER = [
colors.green('Price'),
colors.green('Depth'),
colors.red('Price'),
colors.red('Depth'),
];

const addSide = (buckets: BucketDepth[]): string[] => {
const bucket = buckets.pop();
if (bucket) {
return [
bucket.price.toString(),
satsToCoinsStr(bucket.depth),
];
} else {
return Array.from(Array(COLUMNS_IN_ORDER_SIDE)).map(() => '');
}
};

export const createOrderbook = (orders: ListOrdersResponse.AsObject, precision: number) => {
const formattedOrderbooks: FormattedOrderbook[] = [];
orders.ordersMap.forEach((tradingPair) => {
const buy = createOrderbookSide(tradingPair[1].buyOrdersList, precision);
const sell = createOrderbookSide(tradingPair[1].sellOrdersList, precision);
const totalRows = buy.length < sell.length
? sell.length : buy.length;
const orderbookRows = Array.from(Array(totalRows))
.map(() => {
return addSide(buy).concat(addSide(sell));
});
formattedOrderbooks.push({
pairId: tradingPair[0],
rows: orderbookRows,
});
});
return formattedOrderbooks;
};

const createTable = () => {
const table = new Table({
colWidths: COLUMNS,
}) as HorizontalTable;
table.push(HEADER);
table.push(SECONDARY_HEADER);
return table;
};

const displayOrderbook = (orderbook: FormattedOrderbook) => {
const table = createTable();
orderbook.rows.forEach(row => table.push(row));
console.log(colors.underline(colors.bold(`\nTrading pair: ${orderbook.pairId}`)));
console.log(table.toString());
};

const displayTables = (orders: ListOrdersResponse.AsObject, argv: Arguments) => {
createOrderbook(orders, argv.precision).forEach(displayOrderbook);
};

const getPriceBuckets = (orders: Order.AsObject[], count = 8): number[] => {
const uniquePrices = [
...new Set(
orders.map(order => order.price),
),
];
return uniquePrices.splice(0, count);
};

const getDepthForBuckets = (
orders: Order.AsObject[],
priceBuckets: number[],
filledBuckets: BucketDepth[] = [],
): BucketDepth[] => {
// go through all the available price buckets
const price = priceBuckets.shift();
if (!price) {
// stop recursion when we're out of buckets to fill
return filledBuckets;
}
let filteredOrders = orders;
// filter to specific bucket when the next one exists
if (priceBuckets.length !== 0) {
filteredOrders = orders
.filter(order => order.price === price);
}
// calculate depth of the bucket
const depth = filteredOrders
.reduce((total, order) => {
return total + order.price * order.quantity;
}, 0);
filledBuckets.push({ price, depth });
// filter orders for the next cycle
const restOfOrders = orders.filter(order => order.price !== price);
return getDepthForBuckets(restOfOrders, priceBuckets, filledBuckets);
};

export const createOrderbookSide = (orders: Order.AsObject[], precision = 5) => {
// round prices down to the desired precision
orders.forEach((order) => {
order.price = parseFloat(order.price.toFixed(precision));
});
// get price buckets in which to divide orders to
const priceBuckets = getPriceBuckets(orders);
// divide prices into buckets
return getDepthForBuckets(orders, priceBuckets);
};

export const command = 'orderbook [pair_id] [precision]';

export const describe = 'list the order book';

export const builder = {
pair_id: {
describe: 'trading pair for which to retrieve the order book',
type: 'string',
},
precision: {
describe: 'the number of digits following the decimal point',
type: 'number',
default: 5,
},
};

const displayJson = (orders: ListOrdersResponse.AsObject, argv: Arguments) => {
const jsonOrderbooks: OrderbookJson[] = [];
const depthInSatoshisPerCoin = (bucket: BucketDepth) => {
bucket.depth = parseFloat(
satsToCoinsStr(bucket.depth),
);
};
orders.ordersMap.forEach((tradingPair) => {
const buy = createOrderbookSide(tradingPair[1].buyOrdersList, argv.precision);
buy.forEach(depthInSatoshisPerCoin);
const sell = createOrderbookSide(tradingPair[1].sellOrdersList, argv.precision);
sell.forEach(depthInSatoshisPerCoin);
jsonOrderbooks.push({
sell,
buy,
pairId: tradingPair[0],
});
});
console.log(JSON.stringify(jsonOrderbooks, undefined, 2));
};

export const handler = (argv: Arguments) => {
const request = new ListOrdersRequest();
const pairId = argv.pair_id ? argv.pair_id.toUpperCase() : undefined;
request.setPairId(pairId);
request.setIncludeOwnOrders(true);
loadXudClient(argv).listOrders(request, callback(argv, displayTables, displayJson));
};
80 changes: 80 additions & 0 deletions test/unit/Command.orderbook.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import uuidv1 from 'uuid';
import assert from 'assert';
import { expect } from 'chai';
import { Order } from '../../lib/proto/xudrpc_pb';
import { createOrderbookSide } from '../../lib/cli/commands/orderbook';
import { performance } from 'perf_hooks';

const createOrders = (
amount: number,
price: number,
quantity: number,
randomAmounts = false,
): Order.AsObject[] => {
assert(amount >= 1, 'amount must greater than 0');
const randomNumber = () => {
return Math.round(Math.random() * (Number.MAX_SAFE_INTEGER - 1) + 1);
};
return Array.from(Array(amount))
.map(() => {
const id = uuidv1();
return {
id,
price: randomAmounts ? randomNumber() : price,
quantity: randomAmounts ? randomNumber() : quantity,
localId: id,
pairId: 'LTC/BTC',
peerPubKey: '123',
createdAt: Date.now(),
side: 0,
isOwnOrder: false,
hold: 0,
};
});
};

describe('Command.orderbook.createOrderbookSide', () => {
it.skip('generates orderbook from 10000 million orders', () => {
const orders: Order.AsObject[] = createOrders(10000, 0.012160, 100000, true);
const startTime = performance.now();
createOrderbookSide(orders);
const endTime = performance.now();
const timeSpent = endTime - startTime;
expect(timeSpent < 1000).to.equal(true);
});

it('precision 5', () => {
const orders: Order.AsObject[] = createOrders(100, 0.012160, 100000)
.concat(createOrders(20, 0.011191, 100000))
.concat(createOrders(90, 0.011187, 100000))
.concat(createOrders(30, 0.011181, 100000))
.concat(createOrders(40, 0.011171, 100000))
.concat(createOrders(50, 0.011156, 100000))
.concat(createOrders(60, 0.011151, 100000))
.concat(createOrders(70, 0.011141, 100000))
.concat(createOrders(80, 0.011131, 100000))
.concat(createOrders(100, 0.011111, 100000));
expect(createOrderbookSide(orders)).to.deep.equal([
{ price: 0.01216, depth: 121600 },
{ price: 0.01119, depth: 123090 },
{ price: 0.01118, depth: 33540 },
{ price: 0.01117, depth: 44680 },
{ price: 0.01116, depth: 55800 },
{ price: 0.01115, depth: 66900 },
{ price: 0.01114, depth: 77980 },
{ price: 0.01113, depth: 200140 },
]);
});

it('precision 3', () => {
const orders: Order.AsObject[] = createOrders(100, 0.012160, 100000)
.concat(createOrders(20, 0.011191, 100000))
.concat(createOrders(90, 0.011187, 100000))
.concat(createOrders(30, 0.011181, 100000))
.concat(createOrders(40, 0.011171, 100000));
expect(createOrderbookSide(orders, 3)).to.deep.equal([
{ price: 0.012, depth: 120000 },
{ price: 0.011, depth: 198000 },
]);
});
});

0 comments on commit e13aec3

Please sign in to comment.