gloomberb 0.7.3 → 0.7.5

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 (80) hide show
  1. package/package.json +1 -1
  2. package/src/app.tsx +59 -30
  3. package/src/components/chart/chart-renderer.ts +109 -32
  4. package/src/components/chart/chart-types.ts +18 -0
  5. package/src/components/chart/chart.test.ts +152 -5
  6. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  7. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  8. package/src/components/chart/comparison-stock-chart.tsx +45 -8
  9. package/src/components/chart/market-session.ts +187 -0
  10. package/src/components/chart/native/chart-rasterizer.ts +65 -2
  11. package/src/components/chart/stock-chart-renderer-switch.test.tsx +96 -0
  12. package/src/components/chart/stock-chart.tsx +36 -8
  13. package/src/components/chart/time-axis-label.tsx +56 -2
  14. package/src/components/command-bar/command-bar.test.tsx +1 -1
  15. package/src/components/command-bar/command-bar.tsx +26 -2
  16. package/src/components/command-bar/list-model.ts +8 -4
  17. package/src/components/command-bar/view-model.test.ts +20 -0
  18. package/src/components/command-bar/view-model.ts +15 -3
  19. package/src/components/layout/shell.test.tsx +26 -0
  20. package/src/components/layout/shell.tsx +69 -21
  21. package/src/components/ticker-list-table.tsx +4 -15
  22. package/src/core/state/session-persistence.ts +25 -0
  23. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  24. package/src/market-data/coordinator.test.ts +122 -2
  25. package/src/market-data/coordinator.ts +192 -55
  26. package/src/market-data/hooks.test.tsx +31 -0
  27. package/src/market-data/hooks.tsx +1 -0
  28. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  29. package/src/plugins/builtin/chat-controller.ts +8 -7
  30. package/src/plugins/builtin/chat.test.tsx +55 -0
  31. package/src/plugins/builtin/chat.tsx +20 -6
  32. package/src/plugins/builtin/holders/index.test.ts +30 -1
  33. package/src/plugins/builtin/holders/index.tsx +7 -3
  34. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  35. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  36. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  37. package/src/plugins/builtin/news-wire/index.tsx +0 -2
  38. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  39. package/src/plugins/builtin/portfolio-list/pane.tsx +26 -9
  40. package/src/plugins/builtin/portfolio-list.test.tsx +29 -7
  41. package/src/plugins/builtin/sectors/index.tsx +30 -9
  42. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -1
  43. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -1
  44. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  45. package/src/plugins/pane-manager.test.ts +31 -0
  46. package/src/plugins/pane-manager.ts +32 -6
  47. package/src/renderers/electrobun/bun/index.ts +83 -3
  48. package/src/renderers/electrobun/shared/protocol.ts +8 -1
  49. package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
  50. package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
  51. package/src/renderers/electrobun/view/main.tsx +38 -24
  52. package/src/renderers/electrobun/view/styles.css +48 -4
  53. package/src/sources/gloomberb-cloud.test.ts +55 -2
  54. package/src/sources/gloomberb-cloud.ts +180 -29
  55. package/src/sources/provider-router.test.ts +567 -10
  56. package/src/sources/provider-router.ts +323 -93
  57. package/src/sources/yahoo-finance.test.ts +37 -0
  58. package/src/sources/yahoo-finance.ts +48 -2
  59. package/src/state/app-bootstrap.test.ts +50 -23
  60. package/src/state/app-bootstrap.ts +26 -10
  61. package/src/state/session-persistence.test.ts +5 -3
  62. package/src/state/use-inline-tickers.ts +3 -0
  63. package/src/state/use-quote-streaming.ts +11 -0
  64. package/src/types/data-provider.ts +5 -1
  65. package/src/types/financials.ts +1 -0
  66. package/src/utils/api-client.test.ts +120 -0
  67. package/src/utils/api-client.ts +66 -19
  68. package/src/utils/exchanges.test.ts +16 -0
  69. package/src/utils/exchanges.ts +33 -0
  70. package/src/utils/options.ts +1 -1
  71. package/src/utils/price-history.test.ts +28 -1
  72. package/src/utils/price-history.ts +16 -0
  73. package/src/utils/quote-freshness.test.ts +150 -0
  74. package/src/utils/quote-freshness.ts +42 -1
  75. package/src/utils/quote-resolution.ts +2 -0
  76. package/src/utils/quote-time.test.ts +34 -0
  77. package/src/utils/quote-time.ts +18 -5
  78. package/src/utils/ticker-search.ts +72 -1
  79. package/src/version.ts +1 -1
  80. 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
 
@@ -280,9 +281,19 @@ describe("renderChart", () => {
280
281
  expect(result.axisLabels.every((entry) => entry.label.includes("."))).toBe(true);
281
282
  });
282
283
 
283
- test("includes visible indicator overlays in the chart price range", () => {
284
+ test("keeps indicator overlays out of the chart price range", () => {
284
285
  const projection = projectChartData(chartFixture, 12, "line", false);
285
- 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, {
286
297
  width: 12,
287
298
  height: 6,
288
299
  showVolume: false,
@@ -300,6 +311,63 @@ describe("renderChart", () => {
300
311
  { index: 1, value: 18 },
301
312
  ],
302
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
+ }],
303
371
  emaLines: [],
304
372
  bollinger: null,
305
373
  rsi: null,
@@ -307,8 +375,9 @@ describe("renderChart", () => {
307
375
  },
308
376
  });
309
377
 
310
- expect(scene?.min).toBe(6);
311
- 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);
312
381
  });
313
382
 
314
383
  test("normalizes fractional web dimensions before drawing volume bars", () => {
@@ -434,6 +503,31 @@ describe("renderChart", () => {
434
503
  expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
435
504
  });
436
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
+
437
531
  test("positions desktop cursor x-axis overlay from exact pixels", () => {
438
532
  const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
439
533
  const width = 72;
@@ -499,6 +593,59 @@ describe("renderChart", () => {
499
593
  expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
500
594
  });
501
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
+
502
649
  test("uses source time-axis dates instead of projected extrema dates when provided", () => {
503
650
  const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
504
651
  date: new Date(Date.UTC(2026, 2, 25, index * 3)),
@@ -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);
@@ -98,6 +98,7 @@ import {
98
98
  import { resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
99
99
  import { TimeAxisLabel } from "./time-axis-label";
100
100
  import { PriceAxisLabels } from "./price-axis-labels";
101
+ import { getChartMarketSessionKey, resolveChartMarketSession } from "./market-session";
101
102
 
102
103
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
103
104
  area: "A",
@@ -392,6 +393,7 @@ function buildComparisonNativeBitmapKey(
392
393
  pixelWidth: number,
393
394
  pixelHeight: number,
394
395
  paletteKey: string,
396
+ marketSessionKey: string,
395
397
  ): string {
396
398
  const fingerprint = projection.series
397
399
  .map((series) => [
@@ -412,6 +414,7 @@ function buildComparisonNativeBitmapKey(
412
414
  pixelWidth,
413
415
  pixelHeight,
414
416
  paletteKey,
417
+ marketSessionKey,
415
418
  fingerprint,
416
419
  ].join("::");
417
420
  }
@@ -598,7 +601,16 @@ function ComparisonStockChartView({
598
601
  bgColor: colors.bg,
599
602
  gridColor: blendHex(colors.bg, colors.border, 0.55),
600
603
  crosshairColor: colors.borderFocused,
604
+ preMarketBgColor: blendHex(colors.bg, "#1d4ed8", 0.18),
605
+ postMarketBgColor: blendHex(colors.bg, "#b45309", 0.18),
601
606
  }), [colors.bg, colors.border, colors.borderFocused]);
607
+ const marketSession = useMemo(() => resolveChartMarketSession(symbolSources.map((source) => ({
608
+ exchange: source.exchange,
609
+ primaryExchange: source.instrument?.primaryExchange,
610
+ currency: source.currency,
611
+ assetCategory: source.instrument?.secType,
612
+ }))), [symbolSources]);
613
+ const marketSessionKey = useMemo(() => getChartMarketSessionKey(marketSession), [marketSession]);
602
614
 
603
615
  const setSelectedSymbol = (symbol: string) => {
604
616
  setViewState((current) => (
@@ -1088,7 +1100,8 @@ function ComparisonStockChartView({
1088
1100
  cursorY: null,
1089
1101
  selectedSymbol: viewState.selectedSymbol,
1090
1102
  colors: chartColors,
1091
- }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1103
+ marketSession,
1104
+ }), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
1092
1105
  const displayScene = useMemo(() => buildComparisonChartScene(projection, {
1093
1106
  width: chartWidth,
1094
1107
  height: chartHeight,
@@ -1096,7 +1109,8 @@ function ComparisonStockChartView({
1096
1109
  cursorY: displayCursorY,
1097
1110
  selectedSymbol: viewState.selectedSymbol,
1098
1111
  colors: chartColors,
1099
- }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
1112
+ marketSession,
1113
+ }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, marketSession, projection, viewState.selectedSymbol]);
1100
1114
 
1101
1115
  const staticResult = useMemo(() => renderComparisonChart(projection, {
1102
1116
  width: chartWidth,
@@ -1105,7 +1119,8 @@ function ComparisonStockChartView({
1105
1119
  cursorY: null,
1106
1120
  selectedSymbol: viewState.selectedSymbol,
1107
1121
  colors: chartColors,
1108
- }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1122
+ marketSession,
1123
+ }), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
1109
1124
 
1110
1125
  const interactiveResult = useMemo(() => (
1111
1126
  effectiveRenderer === "kitty" || useCanvasChart
@@ -1117,8 +1132,9 @@ function ComparisonStockChartView({
1117
1132
  cursorY: displayCursorY,
1118
1133
  selectedSymbol: viewState.selectedSymbol,
1119
1134
  colors: chartColors,
1135
+ marketSession,
1120
1136
  })
1121
- ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, useCanvasChart, viewState.selectedSymbol]);
1137
+ ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, marketSession, projection, useCanvasChart, viewState.selectedSymbol]);
1122
1138
 
1123
1139
  const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
1124
1140
  const timeAxisLabel = result.timeLabels || staticResult.timeLabels;
@@ -1282,7 +1298,14 @@ function ComparisonStockChartView({
1282
1298
  viewState.selectedSymbol,
1283
1299
  bitmapSize.pixelWidth,
1284
1300
  bitmapSize.pixelHeight,
1285
- [chartColors.bgColor, chartColors.gridColor, chartColors.crosshairColor].join(","),
1301
+ [
1302
+ chartColors.bgColor,
1303
+ chartColors.gridColor,
1304
+ chartColors.crosshairColor,
1305
+ chartColors.preMarketBgColor,
1306
+ chartColors.postMarketBgColor,
1307
+ ].join(","),
1308
+ marketSessionKey,
1286
1309
  );
1287
1310
  const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
1288
1311
  ? lastNativeBaseBitmapRef.current.bitmap
@@ -1312,7 +1335,10 @@ function ComparisonStockChartView({
1312
1335
  chartColors.bgColor,
1313
1336
  chartColors.crosshairColor,
1314
1337
  chartColors.gridColor,
1338
+ chartColors.postMarketBgColor,
1339
+ chartColors.preMarketBgColor,
1315
1340
  effectiveRenderer,
1341
+ marketSessionKey,
1316
1342
  nativeSurfaceManager,
1317
1343
  paneId,
1318
1344
  projection,
@@ -1685,9 +1711,10 @@ function ComparisonStockChartView({
1685
1711
  cursorY: null,
1686
1712
  selectedSymbol: viewState.selectedSymbol,
1687
1713
  colors: chartColors,
1714
+ marketSession,
1688
1715
  })
1689
1716
  : null
1690
- ), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, viewState.selectedSymbol]);
1717
+ ), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, marketSession, viewState.selectedSymbol]);
1691
1718
 
1692
1719
  const canvasBaseBitmapKey = useMemo(() => {
1693
1720
  if (!canvasBitmapSize || !canvasProjection || !hasChartData || isBlockingBody || bodyMessage) return null;
@@ -1697,7 +1724,14 @@ function ComparisonStockChartView({
1697
1724
  viewState.selectedSymbol,
1698
1725
  canvasBitmapSize.pixelWidth,
1699
1726
  canvasBitmapSize.pixelHeight,
1700
- [chartColors.bgColor, chartColors.gridColor, chartColors.crosshairColor].join(","),
1727
+ [
1728
+ chartColors.bgColor,
1729
+ chartColors.gridColor,
1730
+ chartColors.crosshairColor,
1731
+ chartColors.preMarketBgColor,
1732
+ chartColors.postMarketBgColor,
1733
+ ].join(","),
1734
+ marketSessionKey,
1701
1735
  );
1702
1736
  }, [
1703
1737
  bodyMessage,
@@ -1706,8 +1740,11 @@ function ComparisonStockChartView({
1706
1740
  chartColors.bgColor,
1707
1741
  chartColors.crosshairColor,
1708
1742
  chartColors.gridColor,
1743
+ chartColors.postMarketBgColor,
1744
+ chartColors.preMarketBgColor,
1709
1745
  hasChartData,
1710
1746
  isBlockingBody,
1747
+ marketSessionKey,
1711
1748
  symbols.length,
1712
1749
  viewState.selectedSymbol,
1713
1750
  ]);
@@ -1922,7 +1959,7 @@ function ComparisonStockChartView({
1922
1959
  cursorColumn={cursorTimeAxisColumn}
1923
1960
  cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
1924
1961
  cursorDate={cursorTimeAxisDate}
1925
- dates={projection.dates}
1962
+ dates={visibleWindow.dates}
1926
1963
  cursorColor={chartColors.crosshairColor}
1927
1964
  />
1928
1965
  </Box>