-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Karl Ranna
committed
Jun 12, 2019
1 parent
13a5db5
commit e13aec3
Showing
3 changed files
with
267 additions
and
4 deletions.
There are no files selected for viewing
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
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
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)); | ||
}; |
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
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 }, | ||
]); | ||
}); | ||
}); |