mat.ts 2.7 KB

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