|
// issue - global URL |
|
const GDAX_URL = "https://api.pro.coinbase.com/", |
|
SOCKET_URL = "wss://ws-feed.pro.coinbase.com"; |
|
|
|
const insertIf = (condition, ...elements) => (condition ? elements : []); |
|
|
|
const scatterConfig = { |
|
// received orders are the only ones with size |
|
filter: [["type", "==", "received"], ["side", "==", "buy"]], |
|
row_pivot: ["product_id"], |
|
column_pivot: [], |
|
aggregate: [ |
|
{ op: "avg", column: "price" }, |
|
{ op: "avg", column: "size" }, |
|
{ op: "count", column: "order_id" } |
|
], |
|
sort: [] |
|
}; |
|
|
|
(async () => { |
|
let initialised = false, |
|
table, |
|
scatterView, |
|
suppress = false; |
|
|
|
const res = await fetch(`${GDAX_URL}products`); |
|
const products = await res.json(); |
|
|
|
const color = d3 |
|
.scaleOrdinal(d3.schemeCategory10) |
|
.domain(products.map(d => d.base_currency)); |
|
|
|
let buffer = []; |
|
setInterval(async () => { |
|
// to do - raise buffer.length bug |
|
if (initialised && buffer.length > 0 && !suppress) { |
|
try { |
|
table.update(buffer); |
|
render(); |
|
} catch (e) { |
|
console.error(e); |
|
} |
|
buffer = []; |
|
} |
|
}, 50); |
|
|
|
const render = async () => { |
|
const scatterCols = await scatterView.to_columns(); |
|
const series = scatterCols.__ROW_PATH__ |
|
.map((p, i) => ({ |
|
price: scatterCols.price[i], |
|
size: scatterCols.size[i], |
|
count: scatterCols.order_id[i], |
|
instrument: p[0] |
|
})) |
|
.slice(1); |
|
|
|
const labelPadding = 2; |
|
const textLabel = fc |
|
.layoutTextLabel() |
|
.padding(labelPadding) |
|
.value(d => d.instrument); |
|
|
|
const labels = fc |
|
.layoutLabel(fc.layoutGreedy()) |
|
.key(d => d.instrument) |
|
.size((d, i, g) => { |
|
const textSize = g[i].getElementsByTagName("text")[0].getBBox(); |
|
return [ |
|
textSize.width + labelPadding * 2, |
|
textSize.height + labelPadding * 2 |
|
]; |
|
}) |
|
.position(d => [d.size, d.price]) |
|
.component(textLabel); |
|
|
|
const size = d3 |
|
.scaleLinear() |
|
.range([20, 3600]) |
|
.domain(fc.extentLinear().accessors([d => d.count])(series)); |
|
|
|
const pointSeries = fc |
|
.seriesSvgPoint() |
|
.key(d => d.instrument) |
|
.crossValue(d => d.size) |
|
.mainValue(d => d.price) |
|
.size(d => size(d.count)) |
|
.decorate(sel => { |
|
sel.attr("fill", d => color(d.instrument.split("-")[0])); |
|
}); |
|
|
|
const multi = fc |
|
.seriesSvgMulti() |
|
.series([pointSeries, ...insertIf(suppress, labels)]); |
|
|
|
const chart = fc |
|
.chartCartesian(d3.scaleLog(), d3.scaleLog()) |
|
.xDomain([0.01, 10000]) |
|
.yDomain([0.001, 10000]) |
|
.xTickFormat(d3.format(",")) |
|
.yTickFormat(d3.format(",")) |
|
.xLabel("size") |
|
.yLabel("price") |
|
.xTickValues([0.1, 1, 10, 100, 1000]) |
|
.yTickValues([0.001, 0.1, 1, 10, 100, 1000]) |
|
.svgPlotArea(multi); |
|
|
|
d3.select("#chart") |
|
.datum(series) |
|
.transition() |
|
.duration(200) |
|
.call(chart); |
|
|
|
d3.select("#chart") |
|
.on("mouseover", () => { |
|
suppress = true; |
|
render(); |
|
}) |
|
.on("mouseout", () => { |
|
suppress = false; |
|
render(); |
|
}); |
|
}; |
|
|
|
const ws = new WebSocket(SOCKET_URL); |
|
|
|
ws.onopen = () => { |
|
ws.send( |
|
JSON.stringify({ |
|
type: "subscribe", |
|
product_ids: products.map(p => p.id) |
|
}) |
|
); |
|
}; |
|
|
|
ws.onmessage = msg => { |
|
if (document.hidden) { |
|
return; |
|
} |
|
|
|
const data = JSON.parse(msg.data); |
|
buffer.push(data); |
|
|
|
if (!initialised) { |
|
if (buffer.length > 200) { |
|
table = perspective.worker().table(buffer, { |
|
limit: 5000 |
|
}); |
|
|
|
initialised = true; |
|
|
|
scatterView = table.view(scatterConfig); |
|
render(); |
|
} |
|
} |
|
}; |
|
|
|
ws.onerror = console.log; |
|
ws.onclose = e => console.log(e.code, e.reason); |
|
})(); |