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 = ''; renderPriceChart(tickerSelect.value, 'NC1'); renderPriceChart(tickerSelect.value, 'CI1'); renderPriceChart(tickerSelect.value, 'IC1'); renderPriceChart(tickerSelect.value, 'AI1'); } async function renderPriceChart(ticker: string, cx: string) { const cxpc: PriceChartPoint[] = await cachedFetchJSON(`https://rest.fnar.net/exchange/cxpc/${ticker}.${cx}`); const filtered = cxpc.filter((p) => p.Interval === 'HOUR_TWELVE').map((p) => ({ ...p, Date: new Date(p.DateEpochMs) })); const maxPrice = Math.max(...filtered.map((p) => p.High)); const maxTraded = Math.max(...filtered.map((t) => t.Traded)); charts.appendChild(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 * 2, 10), { label: 'traded', anchor: 'right', y: (d) => (d / maxTraded) * maxPrice / 3, }), Plot.rectY(filtered, { x: 'Date', y: (t) => (t.Traded / maxTraded) * maxPrice / 3, // scale traded to price interval: 'day', fill: '#272', fillOpacity: 0.1, }), Plot.ruleX(filtered, { x: 'Date', y1: 'Low', y2: 'High', strokeWidth: 5, stroke: '#42a', }), Plot.dot(filtered, { x: 'Date', y: (p) => p.Volume / p.Traded, fill: '#a37', r: 2, }), Plot.crosshairX(filtered, { x: 'Date', 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; }