mat.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import * as Plot from '@observablehq/plot';
  2. import * as d3 from 'd3';
  3. import {cachedFetchJSON} from './cache';
  4. const tickerSelect = document.querySelector('select#ticker') as HTMLSelectElement;
  5. const charts = document.querySelector('#charts')!;
  6. (async function () {
  7. const materials: Material[] = await fetch('https://rest.fnar.net/material/allmaterials').then((r) => r.json());
  8. const selected = document.location.hash.substring(1);
  9. for (const mat of materials.sort((a, b) => a.Ticker.localeCompare(b.Ticker))) {
  10. const option = document.createElement('option');
  11. option.value = mat.Ticker;
  12. option.textContent = `${mat.Ticker} ${mat.Name}`;
  13. if (mat.Ticker === selected)
  14. option.selected = true;
  15. tickerSelect.appendChild(option);
  16. }
  17. if (selected)
  18. render();
  19. })();
  20. tickerSelect.addEventListener('change', async () => {
  21. await render();
  22. document.location.hash = tickerSelect.value;
  23. });
  24. async function render() {
  25. charts.innerHTML = '';
  26. const cxpc = await Promise.all([
  27. getCXPC(tickerSelect.value, 'NC1'), getCXPC(tickerSelect.value, 'CI1'),
  28. getCXPC(tickerSelect.value, 'IC1'), getCXPC(tickerSelect.value, 'AI1'),
  29. ]);
  30. let minDate = null, maxDate = null;
  31. let maxPrice = 0, maxTraded = 0;
  32. for (const cxPrices of cxpc)
  33. for (const p of cxPrices) {
  34. if (minDate === null || p.DateEpochMs < minDate) minDate = p.DateEpochMs;
  35. if (maxDate === null || p.DateEpochMs > maxDate) maxDate = p.DateEpochMs;
  36. if (p.High > maxPrice) maxPrice = p.High;
  37. if (p.Traded > maxTraded) maxTraded = p.Traded;
  38. }
  39. if (minDate === null || maxDate === null)
  40. throw new Error('no data');
  41. const dateRange: [Date, Date] = [new Date(minDate), new Date(maxDate)];
  42. charts.append(
  43. renderPriceChart('NC1', dateRange, maxPrice, maxTraded, cxpc[0]), renderPriceChart('CI1', dateRange, maxPrice, maxTraded, cxpc[1]),
  44. renderPriceChart('IC1', dateRange, maxPrice, maxTraded, cxpc[2]), renderPriceChart('AI1', dateRange, maxPrice, maxTraded, cxpc[3]),
  45. );
  46. }
  47. async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
  48. const cxpc: PriceChartPoint[] = await cachedFetchJSON(`https://rest.fnar.net/exchange/cxpc/${ticker}.${cx}`);
  49. const threshold = Date.now() - 100 * 24 * 60 * 60 * 1000; // work around FIO bug that shows old data
  50. return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE' && p.DateEpochMs > threshold);
  51. }
  52. function renderPriceChart(cx: string, dateRange: [Date, Date], maxPrice: number, maxTraded: number, cxpc: PriceChartPoint[]):
  53. SVGSVGElement | HTMLElement {
  54. return Plot.plot({
  55. grid: true,
  56. width: charts.getBoundingClientRect().width / 2 - 10,
  57. height: 400,
  58. x: {domain: dateRange},
  59. y: {axis: 'left', label: cx, domain: [0, maxPrice * 1.1]},
  60. marks: [
  61. Plot.axisY(d3.ticks(0, maxTraded * 1.5, 10), {
  62. label: 'traded',
  63. anchor: 'right',
  64. y: (d) => (d / maxTraded) * maxPrice / 3,
  65. }),
  66. Plot.rectY(cxpc, {
  67. x: (p) => new Date(p.DateEpochMs),
  68. y: (p) => (p.Traded / maxTraded) * maxPrice / 3, // scale traded to price
  69. interval: 'day',
  70. fill: '#272',
  71. fillOpacity: 0.2,
  72. }),
  73. Plot.ruleX(cxpc, {
  74. x: (p) => new Date(p.DateEpochMs),
  75. y1: 'Low',
  76. y2: 'High',
  77. strokeWidth: 5,
  78. stroke: '#42a',
  79. }),
  80. Plot.dot(cxpc, {
  81. x: (p) => new Date(p.DateEpochMs),
  82. y: (p) => p.Volume / p.Traded,
  83. fill: '#a37',
  84. r: 2,
  85. }),
  86. Plot.crosshairX(cxpc, {
  87. x: (p) => new Date(p.DateEpochMs),
  88. y: (p) => p.Volume / p.Traded,
  89. textStrokeWidth: 0,
  90. })
  91. ]
  92. });
  93. }
  94. interface Material {
  95. Ticker: string;
  96. Name: string;
  97. }
  98. interface PriceChartPoint {
  99. Interval: 'MINUTE_FIVE' | 'MINUTE_FIFTEEN' | 'MINUTE_THIRTY' | 'HOUR_ONE' | 'HOUR_TWO' | 'HOUR_FOUR' | 'HOUR_SIX' | 'HOUR_TWELVE' | 'DAY_ONE' | 'DAY_THREE';
  100. DateEpochMs: number;
  101. High: number;
  102. Low: number;
  103. Volume: number;
  104. Traded: number;
  105. }