mat.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  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. const maxPrice = Math.max(...cxpc.flatMap((cxPrices) => cxPrices.map((p) => p.High)));
  31. const maxTraded = Math.max(...cxpc.flatMap((cxPrices) => cxPrices.map((t) => t.Traded)));
  32. charts.append(
  33. renderPriceChart('NC1', maxPrice, maxTraded, cxpc[0]), renderPriceChart('CI1', maxPrice, maxTraded, cxpc[1]),
  34. renderPriceChart('IC1', maxPrice, maxTraded, cxpc[2]), renderPriceChart('AI1', maxPrice, maxTraded, cxpc[3]),
  35. );
  36. }
  37. async function getCXPC(ticker: string, cx: string): Promise<PriceChartPoint[]> {
  38. const cxpc: PriceChartPoint[] = await cachedFetchJSON(`https://rest.fnar.net/exchange/cxpc/${ticker}.${cx}`);
  39. return cxpc.filter((p) => p.Interval === 'HOUR_TWELVE');
  40. }
  41. function renderPriceChart(cx: string, maxPrice: number, maxTraded: number, cxpc: PriceChartPoint[]):
  42. SVGSVGElement | HTMLElement {
  43. return Plot.plot({
  44. grid: true,
  45. width: charts.getBoundingClientRect().width / 2 - 10,
  46. height: 400,
  47. y: {axis: 'left', label: cx, domain: [0, maxPrice * 1.1]},
  48. marks: [
  49. Plot.axisY(d3.ticks(0, maxTraded * 1.5, 10), {
  50. label: 'traded',
  51. anchor: 'right',
  52. y: (d) => (d / maxTraded) * maxPrice / 3,
  53. }),
  54. Plot.rectY(cxpc, {
  55. x: (p) => new Date(p.DateEpochMs),
  56. y: (p) => (p.Traded / maxTraded) * maxPrice / 3, // scale traded to price
  57. interval: 'day',
  58. fill: '#272',
  59. fillOpacity: 0.2,
  60. }),
  61. Plot.ruleX(cxpc, {
  62. x: (p) => new Date(p.DateEpochMs),
  63. y1: 'Low',
  64. y2: 'High',
  65. strokeWidth: 5,
  66. stroke: '#42a',
  67. }),
  68. Plot.dot(cxpc, {
  69. x: (p) => new Date(p.DateEpochMs),
  70. y: (p) => p.Volume / p.Traded,
  71. fill: '#a37',
  72. r: 2,
  73. }),
  74. Plot.crosshairX(cxpc, {
  75. x: (p) => new Date(p.DateEpochMs),
  76. y: (p) => p.Volume / p.Traded,
  77. textStrokeWidth: 0,
  78. })
  79. ]
  80. });
  81. }
  82. interface Material {
  83. Ticker: string;
  84. Name: string;
  85. }
  86. interface PriceChartPoint {
  87. Interval: 'MINUTE_FIVE' | 'MINUTE_FIFTEEN' | 'MINUTE_THIRTY' | 'HOUR_ONE' | 'HOUR_TWO' | 'HOUR_FOUR' | 'HOUR_SIX' | 'HOUR_TWELVE' | 'DAY_ONE' | 'DAY_THREE';
  88. DateEpochMs: number;
  89. High: number;
  90. Low: number;
  91. Volume: number;
  92. Traded: number;
  93. }