| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- 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<PriceChartPoint[]> {
- 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;
- }
|