import * as Plot from '@observablehq/plot'; import * as d3 from 'd3'; import {cachedFetchJSON} from './cache'; const tickerSelect = document.querySelector('select#ticker') as HTMLSelectElement; const charts = document.querySelector('#charts')!; (async function () { const materials: Material[] = await fetch('https://rest.fnar.net/material/allmaterials').then((r) => r.json()); const selected = document.location.hash.substring(1); for (const mat of materials.sort((a, b) => a.Ticker.localeCompare(b.Ticker))) { const option = document.createElement('option'); option.value = mat.Ticker; option.textContent = `${mat.Ticker} ${mat.Name}`; if (mat.Ticker === selected) option.selected = true; tickerSelect.appendChild(option); } if (selected) render(); })(); tickerSelect.addEventListener('change', async () => { await render(); document.location.hash = tickerSelect.value; }); async function render() { charts.innerHTML = ''; const cxpc = await Promise.all([ getCXPC(tickerSelect.value, 'NC1'), getCXPC(tickerSelect.value, 'CI1'), getCXPC(tickerSelect.value, 'IC1'), getCXPC(tickerSelect.value, 'AI1'), ]); const maxPrice = Math.max(...cxpc.flatMap((cxPrices) => cxPrices.map((p) => p.High))); const maxTraded = Math.max(...cxpc.flatMap((cxPrices) => cxPrices.map((t) => t.Traded))); charts.append( renderPriceChart('NC1', maxPrice, maxTraded, cxpc[0]), renderPriceChart('CI1', maxPrice, maxTraded, cxpc[1]), renderPriceChart('IC1', maxPrice, maxTraded, cxpc[2]), renderPriceChart('AI1', maxPrice, maxTraded, cxpc[3]), ); } async function getCXPC(ticker: string, cx: string): Promise { const cxpc: PriceChartPoint[] = await cachedFetchJSON(`https://rest.fnar.net/exchange/cxpc/${ticker}.${cx}`); return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE'); } function renderPriceChart(cx: string, maxPrice: number, maxTraded: number, cxpc: PriceChartPoint[]): SVGSVGElement | HTMLElement { return Plot.plot({ grid: true, width: charts.getBoundingClientRect().width / 2 - 10, height: 400, y: {axis: 'left', label: cx, domain: [0, maxPrice * 1.1]}, marks: [ Plot.axisY(d3.ticks(0, maxTraded * 1.5, 10), { label: 'traded', anchor: 'right', y: (d) => (d / maxTraded) * maxPrice / 3, }), Plot.rectY(cxpc, { x: (p) => new Date(p.DateEpochMs), y: (p) => (p.Traded / maxTraded) * maxPrice / 3, // scale traded to price interval: 'day', fill: '#272', fillOpacity: 0.2, }), Plot.ruleX(cxpc, { x: (p) => new Date(p.DateEpochMs), y1: 'Low', y2: 'High', strokeWidth: 5, stroke: '#42a', }), Plot.dot(cxpc, { x: (p) => new Date(p.DateEpochMs), y: (p) => p.Volume / p.Traded, fill: '#a37', r: 2, }), Plot.crosshairX(cxpc, { x: (p) => new Date(p.DateEpochMs), y: (p) => p.Volume / p.Traded, textStrokeWidth: 0, }) ] }); } interface Material { Ticker: string; Name: string; } interface PriceChartPoint { Interval: 'MINUTE_FIVE' | 'MINUTE_FIFTEEN' | 'MINUTE_THIRTY' | 'HOUR_ONE' | 'HOUR_TWO' | 'HOUR_FOUR' | 'HOUR_SIX' | 'HOUR_TWELVE' | 'DAY_ONE' | 'DAY_THREE'; DateEpochMs: number; High: number; Low: number; Volume: number; Traded: number; }