gloomberb 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -7,8 +7,9 @@ import {
7
7
  } from "./chart-data";
8
8
  import { stepCursorTowards } from "./cursor-motion";
9
9
  import { buildChartScene, buildCursorTimeAxisSegments, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
10
- import { buildCursorTimeAxisOverlay } from "./time-axis-label";
10
+ import { buildCursorTimeAxisOverlay, resolveCursorDateFromAxis } from "./time-axis-label";
11
11
  import { buildCursorPriceAxisOverlay } from "./price-axis-labels";
12
+ import { resolveChartMarketSession, resolveExtendedHoursBackgroundSpans } from "./market-session";
12
13
  import type { PricePoint } from "../../types/financials";
13
14
  import type { ChartRenderMode } from "./chart-types";
14
15
 
@@ -183,6 +184,8 @@ describe("resolveRenderMode", () => {
183
184
  expect(resolveRenderMode("candles", 28)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
184
185
  expect(resolveRenderMode("candles", 39)).toMatchObject({ effectiveMode: "ohlc", fallbackMode: "ohlc" });
185
186
  expect(resolveRenderMode("candles", 40)).toMatchObject({ effectiveMode: "candles", fallbackMode: null });
187
+ expect(resolveRenderMode("hlc", 27)).toMatchObject({ effectiveMode: "line", fallbackMode: "line" });
188
+ expect(resolveRenderMode("hlc", 28)).toMatchObject({ effectiveMode: "hlc", fallbackMode: null });
186
189
  });
187
190
 
188
191
  test("forces compact charts back to area mode", () => {
@@ -192,7 +195,7 @@ describe("resolveRenderMode", () => {
192
195
  expect(projection.fallbackMode).toBeNull();
193
196
  });
194
197
 
195
- test("uses fewer projected buckets for candles and ohlc to preserve spacing", () => {
198
+ test("uses fewer projected buckets for candles and ohlc-style bars to preserve spacing", () => {
196
199
  const denseSeries = Array.from({ length: 100 }, (_, i) => ({
197
200
  date: new Date(2024, 0, i + 1),
198
201
  open: 100 + i,
@@ -204,6 +207,7 @@ describe("resolveRenderMode", () => {
204
207
 
205
208
  expect(projectChartData(denseSeries, 80, "candles", false).points).toHaveLength(34);
206
209
  expect(projectChartData(denseSeries, 80, "ohlc", false).points).toHaveLength(34);
210
+ expect(projectChartData(denseSeries, 80, "hlc", false).points).toHaveLength(34);
207
211
  expect(projectChartData(denseSeries, 80, "line", false).points).toHaveLength(80);
208
212
  });
209
213
 
@@ -277,9 +281,19 @@ describe("renderChart", () => {
277
281
  expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
278
282
  });
279
283
 
280
- test("includes visible indicator overlays in the chart price range", () => {
284
+ test("keeps indicator overlays out of the chart price range", () => {
281
285
  const projection = projectChartData(chartFixture, 12, "line", false);
282
- const scene = buildChartScene(projection.points, {
286
+ const baseScene = buildChartScene(projection.points, {
287
+ width: 12,
288
+ height: 6,
289
+ showVolume: false,
290
+ volumeHeight: 0,
291
+ cursorX: null,
292
+ cursorY: null,
293
+ mode: projection.effectiveMode,
294
+ colors: palette,
295
+ });
296
+ const sceneWithIndicators = buildChartScene(projection.points, {
283
297
  width: 12,
284
298
  height: 6,
285
299
  showVolume: false,
@@ -297,6 +311,63 @@ describe("renderChart", () => {
297
311
  { index: 1, value: 18 },
298
312
  ],
299
313
  }],
314
+ emaLines: [{
315
+ period: 2,
316
+ color: "#00ffff",
317
+ points: [
318
+ { index: 0, value: 4 },
319
+ { index: 1, value: 20 },
320
+ ],
321
+ }],
322
+ bollinger: {
323
+ color: "#ffaa00",
324
+ upper: [
325
+ { index: 0, value: 22 },
326
+ { index: 1, value: 23 },
327
+ ],
328
+ middle: [
329
+ { index: 0, value: 14 },
330
+ { index: 1, value: 15 },
331
+ ],
332
+ lower: [
333
+ { index: 0, value: 3 },
334
+ { index: 1, value: 4 },
335
+ ],
336
+ },
337
+ rsi: null,
338
+ macd: null,
339
+ },
340
+ });
341
+
342
+ expect(sceneWithIndicators?.min).toBe(baseScene?.min);
343
+ expect(sceneWithIndicators?.max).toBe(baseScene?.max);
344
+ });
345
+
346
+ test("draws indicator overlays below price pixels", () => {
347
+ const points: PricePoint[] = [
348
+ { date: new Date("2024-01-02"), close: 10, volume: 100 },
349
+ { date: new Date("2024-01-03"), close: 11, volume: 120 },
350
+ { date: new Date("2024-01-04"), close: 12, volume: 140 },
351
+ ];
352
+ const result = renderChart(points, {
353
+ width: 12,
354
+ height: 6,
355
+ showVolume: false,
356
+ volumeHeight: 0,
357
+ cursorX: null,
358
+ cursorY: null,
359
+ mode: "line",
360
+ colors: palette,
361
+ indicators: {
362
+ smaLines: [{
363
+ period: 2,
364
+ color: "#ff00ff",
365
+ points: [
366
+ { index: 0, value: 10 },
367
+ { index: 1, value: 11 },
368
+ { index: 2, value: 12 },
369
+ ],
370
+ }],
300
371
  emaLines: [],
301
372
  bollinger: null,
302
373
  rsi: null,
@@ -304,8 +375,9 @@ describe("renderChart", () => {
304
375
  },
305
376
  });
306
377
 
307
- expect(scene?.min).toBe(6);
308
- expect(scene?.max).toBe(18);
378
+ const pixels = result.pixelBuffer?.pixels.flat().filter(Boolean) ?? [];
379
+ expect(pixels.some((pixel) => pixel?.color === palette.lineColor)).toBe(true);
380
+ expect(pixels.some((pixel) => pixel?.color === "#ff00ff")).toBe(false);
309
381
  });
310
382
 
311
383
  test("normalizes fractional web dimensions before drawing volume bars", () => {
@@ -431,6 +503,31 @@ describe("renderChart", () => {
431
503
  expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
432
504
  });
433
505
 
506
+ test("keeps day precision for cursor labels when the axis coarsens to months", () => {
507
+ const dates = Array.from({ length: 12 }, (_, index) => new Date(2025, index, 1));
508
+ const width = 48;
509
+ const segments = buildCursorTimeAxisSegments({
510
+ timeLabels: buildTimeAxis(dates, width),
511
+ width,
512
+ cursorColumn: 27,
513
+ cursorDate: new Date(2025, 8, 15),
514
+ dates,
515
+ });
516
+
517
+ expect(segments.find((segment) => segment.highlighted)?.text).toBe("Sep 15 2025");
518
+ });
519
+
520
+ test("resolves cursor dates from the full source axis between coarse month ticks", () => {
521
+ const dates = Array.from({ length: 32 }, (_, index) => new Date(2025, 2, index + 1));
522
+ const resolved = resolveCursorDateFromAxis({
523
+ dates,
524
+ width: 32,
525
+ cursorColumn: 15,
526
+ });
527
+
528
+ expect(resolved?.toISOString()).toBe(dates[15]!.toISOString());
529
+ });
530
+
434
531
  test("positions desktop cursor x-axis overlay from exact pixels", () => {
435
532
  const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
436
533
  const width = 72;
@@ -468,7 +565,7 @@ describe("renderChart", () => {
468
565
  cellHeightPx,
469
566
  });
470
567
 
471
- expect(overlay.labelText).toBe("$214.03 ");
568
+ expect(overlay.labelText).toBe("$214.03");
472
569
  expect(overlay.topPercent).toBeCloseTo((cursorPixelY / (height * cellHeightPx - 1)) * 100, 5);
473
570
  });
474
571
 
@@ -496,6 +593,59 @@ describe("renderChart", () => {
496
593
  expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
497
594
  });
498
595
 
596
+ test("marks US premarket and postmarket bars for chart backgrounds", () => {
597
+ const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
598
+ const dates = [
599
+ new Date("2026-05-14T11:00:00Z"),
600
+ new Date("2026-05-14T14:00:00Z"),
601
+ new Date("2026-05-14T20:30:00Z"),
602
+ ];
603
+
604
+ expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([
605
+ { kind: "pre", startIndex: 0, endIndex: 0 },
606
+ { kind: "post", startIndex: 2, endIndex: 2 },
607
+ ]);
608
+ });
609
+
610
+ test("omits extended-hours backgrounds for wide intraday windows", () => {
611
+ const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
612
+ const dates = Array.from({ length: 12 }).flatMap((_, dayIndex) => {
613
+ const day = 4 + dayIndex;
614
+ return [
615
+ new Date(Date.UTC(2026, 4, day, 11, 0)),
616
+ new Date(Date.UTC(2026, 4, day, 14, 0)),
617
+ new Date(Date.UTC(2026, 4, day, 20, 30)),
618
+ ];
619
+ });
620
+
621
+ expect(resolveExtendedHoursBackgroundSpans(dates, session)).toEqual([]);
622
+ });
623
+
624
+ test("paints extended-hours chart cells as background colors", () => {
625
+ const session = resolveChartMarketSession([{ exchange: "NASDAQ", currency: "USD", assetCategory: "STK" }]);
626
+ const points: PricePoint[] = [
627
+ { date: new Date("2026-05-14T11:00:00Z"), close: 10, volume: 100 },
628
+ { date: new Date("2026-05-14T14:00:00Z"), close: 12, volume: 120 },
629
+ { date: new Date("2026-05-14T20:30:00Z"), close: 11, volume: 110 },
630
+ ];
631
+
632
+ const result = renderChart(points, {
633
+ width: 12,
634
+ height: 4,
635
+ showVolume: false,
636
+ volumeHeight: 0,
637
+ cursorX: null,
638
+ cursorY: null,
639
+ mode: "line",
640
+ colors: palette,
641
+ marketSession: session,
642
+ });
643
+
644
+ const backgrounds = result.lines.flatMap((line) => line.chunks.map((chunk) => chunk.bg));
645
+ expect(backgrounds).toContain(palette.preMarketBgColor);
646
+ expect(backgrounds).toContain(palette.postMarketBgColor);
647
+ });
648
+
499
649
  test("uses source time-axis dates instead of projected extrema dates when provided", () => {
500
650
  const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
501
651
  date: new Date(Date.UTC(2026, 2, 25, index * 3)),
@@ -528,6 +678,7 @@ describe("renderChart", () => {
528
678
  { mode: "line", width: 12, cursorX: 4 },
529
679
  { mode: "candles", width: 40, cursorX: 13 },
530
680
  { mode: "ohlc", width: 28, cursorX: 9 },
681
+ { mode: "hlc", width: 28, cursorX: 9 },
531
682
  ];
532
683
 
533
684
  for (const testCase of cases) {
@@ -553,6 +704,7 @@ describe("renderChart", () => {
553
704
  { mode: "area", width: 12, height: 6, cursorX: 5, cursorY: 4 },
554
705
  { mode: "candles", width: 40, height: 6, cursorX: 13, cursorY: 3 },
555
706
  { mode: "ohlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
707
+ { mode: "hlc", width: 28, height: 6, cursorX: 9, cursorY: 1 },
556
708
  ];
557
709
 
558
710
  for (const testCase of cases) {
@@ -638,12 +790,26 @@ describe("renderChart", () => {
638
790
  width: 28,
639
791
  height: 6,
640
792
  lines: [
641
- "⠁ ⠁ ⠁ ⢸ ⠁ ⠁ ⡄ ⠁ ⠁ ⠁",
642
- " ⢰ ⢸⠒⠂ ⠐⠒⡇ ",
643
- "⠁⢸⠒⠂ ⠁ ⠒⢺ ⠁ ⠁ ⡇ ⠁ ⠁ ⡆⠁",
644
- "⠤⢼ ⡀ ⡀ ⠸ ⡀ ⡀ ⡇ ⡀ ⡀ ⡏⠉",
645
- " ⠸ ⡧⠤ ⠠⠤⡇ ",
646
- "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⡇ ⡀ ⡀ ⠃⡀",
793
+ "⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
794
+ " ⢰⣶ ⢸⣿⠒⠂ ⠐⠒⣿⡇ ",
795
+ "⠁⢸⣿⠒⠂ ⠁ ⠒⢺⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
796
+ "⠤⢼⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
797
+ " ⠸⠿ ⣿⡧⠤ ⠠⠤⣿⡇ ",
798
+ "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
799
+ ],
800
+ timeLabels: "Jan 2 3 4 Jan 5",
801
+ },
802
+ {
803
+ mode: "hlc",
804
+ width: 28,
805
+ height: 6,
806
+ lines: [
807
+ "⠁ ⠁ ⠁ ⢸⣿ ⠁ ⠁ ⣤⡄ ⠁ ⠁ ⠁",
808
+ " ⢰⣶ ⢸⣿⠒⠂ ⣿⡇ ",
809
+ "⠁⢸⣿⠒⠂ ⠁ ⢸⣿ ⠁ ⠁ ⣿⡇ ⠁ ⠁⣶⡆⠁",
810
+ "⡀⢸⣿⡀ ⡀ ⠸⠿ ⡀ ⡀ ⣿⡇ ⡀ ⡀⣿⡏⠉",
811
+ " ⠸⠿ ⣿⡧⠤ ⣿⡇ ",
812
+ "⡀ ⡀ ⡀ ⡀ ⡀ ⡀ ⣿⡇ ⡀ ⡀⠛⠃⡀",
647
813
  ],
648
814
  timeLabels: "Jan 2 3 4 Jan 5",
649
815
  },
@@ -8,6 +8,14 @@ import {
8
8
  } from "./comparison-chart-renderer";
9
9
  import type { ComparisonChartSeries } from "./chart-types";
10
10
 
11
+ const comparisonColors = {
12
+ bgColor: "#000000",
13
+ gridColor: "#333333",
14
+ crosshairColor: "#ffffff",
15
+ preMarketBgColor: "#001b44",
16
+ postMarketBgColor: "#442300",
17
+ };
18
+
11
19
  function makeSeries(symbol: string, color: string, closes: number[]): ComparisonChartSeries {
12
20
  return {
13
21
  symbol,
@@ -51,11 +59,7 @@ describe("renderComparisonChart", () => {
51
59
  cursorX: null,
52
60
  cursorY: null,
53
61
  selectedSymbol: "AAPL",
54
- colors: {
55
- bgColor: "#000000",
56
- gridColor: "#333333",
57
- crosshairColor: "#ffffff",
58
- },
62
+ colors: comparisonColors,
59
63
  });
60
64
 
61
65
  expect(result.axisFractionDigits).toBeGreaterThanOrEqual(1);
@@ -78,11 +82,7 @@ describe("renderComparisonChart", () => {
78
82
  cursorX: null,
79
83
  cursorY: null,
80
84
  selectedSymbol: "AAPL",
81
- colors: {
82
- bgColor: "#000000",
83
- gridColor: "#333333",
84
- crosshairColor: "#ffffff",
85
- },
85
+ colors: comparisonColors,
86
86
  });
87
87
 
88
88
  expect(result.axisFractionDigits).toBe(1);
@@ -105,11 +105,7 @@ describe("renderComparisonChart", () => {
105
105
  cursorX: 5,
106
106
  cursorY: null,
107
107
  selectedSymbol: "MSFT",
108
- colors: {
109
- bgColor: "#000000",
110
- gridColor: "#333333",
111
- crosshairColor: "#ffffff",
112
- },
108
+ colors: comparisonColors,
113
109
  });
114
110
 
115
111
  expect(result.lines).toHaveLength(6);
@@ -134,11 +130,7 @@ describe("renderComparisonChart", () => {
134
130
  cursorX: null,
135
131
  cursorY: null,
136
132
  selectedSymbol: "AAPL",
137
- colors: {
138
- bgColor: "#000000",
139
- gridColor: "#333333",
140
- crosshairColor: "#ffffff",
141
- },
133
+ colors: comparisonColors,
142
134
  });
143
135
 
144
136
  expect(result.lines.length).toBeGreaterThan(0);
@@ -160,11 +152,7 @@ describe("renderComparisonChart", () => {
160
152
  cursorX: null,
161
153
  cursorY: null,
162
154
  selectedSymbol: "AAPL",
163
- colors: {
164
- bgColor: "#000000",
165
- gridColor: "#333333",
166
- crosshairColor: "#ffffff",
167
- },
155
+ colors: comparisonColors,
168
156
  });
169
157
 
170
158
  const chunks = result.lines.flatMap((line) => line.chunks);
@@ -188,11 +176,7 @@ describe("renderComparisonChart", () => {
188
176
  cursorX: 9,
189
177
  cursorY: null,
190
178
  selectedSymbol: "NVDA",
191
- colors: {
192
- bgColor: "#000000",
193
- gridColor: "#333333",
194
- crosshairColor: "#ffffff",
195
- },
179
+ colors: comparisonColors,
196
180
  });
197
181
 
198
182
  expect(scene).not.toBeNull();
@@ -15,10 +15,12 @@ import {
15
15
  resolveAxisFractionDigits,
16
16
  type StyledContent,
17
17
  } from "./chart-renderer";
18
- import type { ChartAxisMode, ComparisonChartRenderMode } from "./chart-types";
18
+ import type { ChartAxisMode, ChartColors, ChartMarketSession, ChartSessionBackgroundSpan, ComparisonChartRenderMode } from "./chart-types";
19
+ import { resolveExtendedHoursBackgroundSpans } from "./market-session";
19
20
 
20
21
  const LAYER_FILL = 1;
21
22
  const LAYER_DATA = 2;
23
+ type ComparisonChartColors = Pick<ChartColors, "bgColor" | "gridColor" | "crosshairColor" | "preMarketBgColor" | "postMarketBgColor">;
22
24
 
23
25
  export interface ComparisonChartScene {
24
26
  dates: Date[];
@@ -29,11 +31,8 @@ export interface ComparisonChartScene {
29
31
  mode: ComparisonChartRenderMode;
30
32
  axisMode: ChartAxisMode;
31
33
  selectedSymbol: string | null;
32
- colors: {
33
- bgColor: string;
34
- gridColor: string;
35
- crosshairColor: string;
36
- };
34
+ colors: ComparisonChartColors;
35
+ sessionBackgroundSpans: ChartSessionBackgroundSpan[];
37
36
  min: number;
38
37
  max: number;
39
38
  activeIdx: number;
@@ -55,11 +54,8 @@ export interface RenderComparisonChartOptions {
55
54
  cursorX: number | null;
56
55
  cursorY: number | null;
57
56
  selectedSymbol: string | null;
58
- colors: {
59
- bgColor: string;
60
- gridColor: string;
61
- crosshairColor: string;
62
- };
57
+ colors: ComparisonChartColors;
58
+ marketSession?: ChartMarketSession | null;
63
59
  }
64
60
 
65
61
  export interface RenderComparisonChartResult {
@@ -90,6 +86,16 @@ function getComparisonDotX(index: number, count: number, width: number): number
90
86
  return Math.round((index / (count - 1)) * Math.max(width - 1, 0));
91
87
  }
92
88
 
89
+ function getComparisonPointBand(index: number, pointCount: number, width: number): { left: number; right: number } {
90
+ const currentX = getComparisonDotX(index, pointCount, width);
91
+ const previousX = index > 0 ? getComparisonDotX(index - 1, pointCount, width) : currentX;
92
+ const nextX = index < pointCount - 1 ? getComparisonDotX(index + 1, pointCount, width) : currentX;
93
+ return {
94
+ left: index === 0 ? 0 : Math.floor((previousX + currentX) / 2) + 1,
95
+ right: index === pointCount - 1 ? Math.max(width - 1, 0) : Math.floor((currentX + nextX) / 2),
96
+ };
97
+ }
98
+
93
99
  function getScaledY(value: number, min: number, max: number, chartTop: number, chartBottom: number): number {
94
100
  const range = max - min || 1;
95
101
  const chartHeight = chartBottom - chartTop;
@@ -223,6 +229,29 @@ function drawComparisonAreaSeries(
223
229
  }
224
230
  }
225
231
 
232
+ function drawSessionBackgrounds(
233
+ buf: ReturnType<typeof createPixelBuffer>,
234
+ spans: readonly ChartSessionBackgroundSpan[],
235
+ pointCount: number,
236
+ yTop: number,
237
+ yBottom: number,
238
+ colors: ComparisonChartColors,
239
+ ) {
240
+ if (spans.length === 0 || pointCount === 0) return;
241
+ for (const span of spans) {
242
+ const startIndex = Math.max(Math.min(span.startIndex, pointCount - 1), 0);
243
+ const endIndex = Math.max(Math.min(span.endIndex, pointCount - 1), startIndex);
244
+ const startBand = getComparisonPointBand(startIndex, pointCount, buf.width);
245
+ const endBand = getComparisonPointBand(endIndex, pointCount, buf.width);
246
+ const color = span.kind === "pre" ? colors.preMarketBgColor : colors.postMarketBgColor;
247
+ for (let y = Math.max(yTop, 0); y <= Math.min(yBottom, Math.max(buf.height - 1, 0)); y += 1) {
248
+ for (let x = Math.max(startBand.left, 0); x <= Math.min(endBand.right, Math.max(buf.width - 1, 0)); x += 1) {
249
+ buf.backgrounds[y]![x] = color;
250
+ }
251
+ }
252
+ }
253
+ }
254
+
226
255
  function getActiveIndex(pointCount: number, width: number, cursorX: number | null): number {
227
256
  if (pointCount <= 0) return 0;
228
257
  if (cursorX === null || cursorX < 0 || cursorX >= width) {
@@ -294,6 +323,7 @@ export function buildComparisonChartScene(
294
323
  const crosshairValue = cursorY === null
295
324
  ? null
296
325
  : max - (cursorY / Math.max(chartRows - 1, 1)) * range;
326
+ const sessionBackgroundSpans = resolveExtendedHoursBackgroundSpans(projection.dates, opts.marketSession);
297
327
 
298
328
  return {
299
329
  dates: projection.dates,
@@ -305,6 +335,7 @@ export function buildComparisonChartScene(
305
335
  axisMode: projection.effectiveAxisMode,
306
336
  selectedSymbol: selectedSeries?.symbol ?? null,
307
337
  colors: opts.colors,
338
+ sessionBackgroundSpans,
308
339
  min,
309
340
  max,
310
341
  activeIdx,
@@ -354,6 +385,14 @@ export function renderComparisonChart(
354
385
  getComparisonAxisFractionDigitFloor(priceRange),
355
386
  )
356
387
  : null;
388
+ drawSessionBackgrounds(
389
+ buf,
390
+ scene.sessionBackgroundSpans,
391
+ scene.dates.length,
392
+ 0,
393
+ chartDotBottom,
394
+ scene.colors,
395
+ );
357
396
  drawGridLines(buf, gridLines.map((line) => line.y), scene.colors.gridColor);
358
397
 
359
398
  const selectedSeries = getSelectedSeries(scene.series, scene.selectedSymbol);