import * as Plot from '@observablehq/plot'; 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', render); async function render() { charts.innerHTML = ''; renderPriceChart(tickerSelect.value, 'IC1'); } 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 plot = Plot.plot({ inset: 6, width: 928, grid: true, y: {label: ticker}, color: {domain: [-1, 0, 1], range: ['#e41a1c', '#000000', '#4daf4a']}, marks: [ Plot.lineY(filtered, { x: 'Date', y: (p) => p.Volume / p.Traded, }), Plot.ruleX(filtered, { x: 'Date', y1: 'Low', y2: 'High' }), /* Plot.ruleX(filtered, { x: 'Date', y: 'Traded', // stroke: (d) => Math.sign(d.Close - d.Open), // strokeWidth: 4, // strokeLinecap: 'round' }) */ Plot.crosshairX(filtered, { x: 'Date', y: (p) => p.Traded / p.Volume, tip: true, }) ] }) charts.appendChild(plot); } 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; }