Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: Bin order tables by rate. #1090

Merged
merged 9 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/webserver/site/src/html/markets.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<thead>
<tr>
<th class="text-left pl-2">Quantity</th>
<th class="text-right pr-3"># Orders</th>
<th class="text-right pr-2">Rate</th>
<th class="text-right pr-2">Epoch</th>
</tr>
Expand All @@ -90,6 +91,7 @@
{{- /* This row is used by the app as a template. */ -}}
<tr id="rowTemplate">
<td class="text-left pl-2" data-type="qty">-</td>
<td class="text-right pr-3" data-type="orders">-</td>
<td class="text-right pr-2" data-type="rate">-</td>
<td class="text-right pr-3" data-type="epoch"></td>
</tr>
Expand Down
136 changes: 110 additions & 26 deletions client/webserver/site/src/js/markets.js
Original file line number Diff line number Diff line change
Expand Up @@ -1446,14 +1446,43 @@ export default class MarketsPage extends BasePage {
this.loadTableSide(false)
}

/* binOrdersByRateAndEpoch takes a list of sorted orders and returns the
same orders grouped into arrays. The orders are grouped by their rate
and whether or not they are epoch queue orders. Epoch queue orders
will come after non epoch queue orders with the same rate. */
binOrdersByRateAndEpoch (orders) {
if (!orders || !orders.length) return []
const bins = []
let currEpochBin = []
let currNonEpochBin = []
let currRate = orders[0].rate
if (orders[0].epoch) currEpochBin.push(orders[0])
else currNonEpochBin.push(orders[0])
for (let i = 1; i < orders.length; i++) {
if (orders[i].rate !== currRate) {
bins.push(currNonEpochBin)
bins.push(currEpochBin)
currEpochBin = []
currNonEpochBin = []
currRate = orders[i].rate
}
if (orders[i].epoch) currEpochBin.push(orders[i])
else currNonEpochBin.push(orders[i])
}
bins.push(currNonEpochBin)
bins.push(currEpochBin)
return bins.filter(bin => bin.length > 0)
}

/* loadTables loads the order book side into its table. */
loadTableSide (sell) {
const bookSide = sell ? this.book.sells : this.book.buys
const tbody = sell ? this.page.sellRows : this.page.buyRows
const cssClass = sell ? 'sellcolor' : 'buycolor'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cssClass is unused outside of OrderTableRowManager (via orderTableRow). I'd recommend moving this cssClass stuff into the OrderTableRowManager constructor, and just passing the sell boolean as an argument instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call.

Doc.empty(tbody)
if (!bookSide || !bookSide.length) return
bookSide.forEach(order => { tbody.appendChild(this.orderTableRow(order, cssClass)) })
const orderBins = this.binOrdersByRateAndEpoch(bookSide)
orderBins.forEach(bin => { tbody.appendChild(this.orderTableRow(bin, cssClass)) })
}

/* addTableOrder adds a single order to the appropriate table. */
Expand All @@ -1464,23 +1493,28 @@ export default class MarketsPage extends BasePage {
// Handle market order differently.
if (order.rate === 0) {
// This is a market order.
if (!row || row.order.rate !== 0) {
row = this.orderTableRow(order, cssClass)
if (row && row.getRate() === 0) {
row.insertOrder(order)
} else {
row = this.orderTableRow([order], cssClass)
tbody.insertBefore(row, tbody.firstChild)
}
row.addQty(order.qty)
return
}
// Must be a limit order. Sort by rate. Skip the market order row.
if (row && row.order.rate === 0) row = row.nextSibling
const tr = this.orderTableRow(order, cssClass)
if (row && row.getRate() === 0) row = row.nextSibling
while (row) {
if ((order.rate < row.order.rate) === order.sell) {
if (row.compare(order) === 0) {
row.insertOrder(order)
return
} else if (row.compare(order) > 0) {
const tr = this.orderTableRow([order], cssClass)
tbody.insertBefore(tr, row)
return
}
row = row.nextSibling
}
const tr = this.orderTableRow([order], cssClass)
tbody.appendChild(tr)
}

Expand All @@ -1489,8 +1523,7 @@ export default class MarketsPage extends BasePage {
const token = order.token
for (const tbody of [this.page.sellRows, this.page.buyRows]) {
for (const tr of Array.from(tbody.children)) {
if (tr.order.token === token) {
tr.remove()
if (tr.removeOrder(token)) {
return
}
}
Expand All @@ -1500,11 +1533,10 @@ export default class MarketsPage extends BasePage {
/* updateTableOrder looks for the order in the table and updates the qty */
updateTableOrder (update) {
const token = update.token
const qty = update.qty
for (const tbody of [this.page.sellRows, this.page.buyRows]) {
for (const tr of Array.from(tbody.children)) {
if (tr.order.token === token) {
const td = tr.querySelector('[data-type=qty]')
td.innerText = update.qty.toFixed(8)
if (tr.updateOrderQty(token, qty)) {
return
}
}
Expand All @@ -1525,53 +1557,105 @@ export default class MarketsPage extends BasePage {
*/
clearOrderTableEpochSide (tbody, newEpoch) {
for (const tr of Array.from(tbody.children)) {
if (tr.order.epoch && tr.order.epoch !== newEpoch) tr.remove()
tr.removeEpochOrders()
}
}

/*
* orderTableRow creates a new <tr> element to insert into an order table.
Takes a bin of orders with the same rate, and displays the total quantity.
*/
orderTableRow (order, cssClass) {
orderTableRow (orderBin, cssClass) {
const tr = this.page.rowTemplate.cloneNode(true)
tr.qty = order.qty
tr.order = order
const rate = order.rate
const { rate, sell } = orderBin[0]
const isEpoch = !!orderBin[0].epoch
const qty = orderBin.reduce((total, curr) => total + curr.qty, 0)
tr.orderBin = orderBin
bind(tr, 'click', () => {
this.reportClick(rate)
})
let qtyTD
let ordersTD
tr.querySelectorAll('td').forEach(td => {
switch (td.dataset.type) {
case 'qty':
qtyTD = td
td.innerText = order.qty.toFixed(8)
td.innerText = qty.toFixed(8)
break
case 'rate':
if (order.rate === 0) {
if (rate === 0) {
td.innerText = 'market'
} else {
td.innerText = order.rate.toFixed(8)
td.innerText = rate.toFixed(8)
td.classList.add(cssClass)
// Draw a line on the chart on hover.
Doc.bind(tr, 'mouseenter', e => {
const chart = this.chart
this.depthLines.hover = [{
rate: order.rate,
color: order.sell ? chart.theme.sellLine : chart.theme.buyLine
rate: rate,
color: sell ? chart.theme.sellLine : chart.theme.buyLine
}]
this.drawChartLines()
})
}
break
case 'orders':
ordersTD = td
td.innerText = orderBin.length
break
case 'epoch':
if (order.epoch) td.appendChild(check.cloneNode())
if (isEpoch) td.appendChild(check.cloneNode())
break
}
})
tr.addQty = (qty) => {
tr.qty += qty
qtyTD.innerText = tr.qty.toFixed(8)
tr.updateQtyAndNumOrders = () => {
const qty = tr.orderBin.reduce((total, curr) => total + curr.qty, 0)
qtyTD.innerText = qty.toFixed(8)
ordersTD.innerText = tr.orderBin.length
}
tr.insertOrder = (order) => {
tr.orderBin.push(order)
tr.updateQtyAndNumOrders()
}
tr.updateOrderQty = (token, qty) => {
for (let i = 0; i < tr.orderBin.length; i++) {
if (tr.orderBin[i].token === token) {
tr.orderBin[i].qty = qty
tr.updateQtyAndNumOrders()
return true
}
}
return false
}
tr.removeOrder = (token) => {
const index = tr.orderBin.findIndex(order => order.token === token)
if (index < 0) return false
tr.orderBin.splice(index, 1)
if (!tr.orderBin.length) tr.remove()
else tr.updateQtyAndNumOrders()
return true
}
tr.removeEpochOrders = (newEpoch) => {
tr.orderBin = tr.orderBin.filter((order) => {
return !(order.epoch && order.epoch !== newEpoch)
})
if (!tr.orderBin.length) tr.remove()
else tr.updateQtyAndNumOrders()
}
tr.getRate = () => {
return tr.orderBin[0].rate
}
tr.getEpoch = () => {
return tr.orderBin[0].epoch
}
tr.compare = (order) => {
if (tr.getRate() === order.rate && !!tr.getEpoch() === !!order.epoch) {
return 0
} else if (tr.getRate() !== order.rate) {
return (tr.getRate() > order.rate) === order.sell ? 1 : -1
} else {
return tr.getEpoch() ? 1 : -1
}
}
return tr
}
Expand Down