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,12 +7,12 @@ import { useChartQueries } from "../../market-data/hooks";
7
7
  import { buildChartKey } from "../../market-data/selectors";
8
8
  import { useAppSelector, usePaneSettingValue } from "../../state/app-context";
9
9
  import { usePaneFooter, type PaneHint } from "../layout/pane-footer";
10
- import { blendHex, colors, getComparisonSeriesColor, priceColor } from "../../theme/colors";
10
+ import { blendHex, colors, getComparisonSeriesColor } from "../../theme/colors";
11
11
  import { useThemeColors } from "../../theme/theme-context";
12
12
  import type { BrokerContractRef } from "../../types/instrument";
13
13
  import type { PricePoint } from "../../types/financials";
14
14
  import { formatPercentRaw } from "../../utils/format";
15
- import { formatMarketPriceWithCurrency, formatSignedMarketPrice } from "../../utils/market-format";
15
+ import { formatMarketPriceWithCurrency } from "../../utils/market-format";
16
16
  import { getSharedMarketData } from "../../plugins/registry";
17
17
  import {
18
18
  applyComparisonZoomAroundAnchor,
@@ -30,7 +30,6 @@ import {
30
30
  import {
31
31
  buildVisibleDateWindow,
32
32
  clearActivePreset,
33
- formatVisibleSpanLabel,
34
33
  needsCanonicalPresetViewportReset,
35
34
  resolveChartBodyState,
36
35
  resolveCanonicalPresetViewport,
@@ -96,9 +95,10 @@ import {
96
95
  resolveNativeSurfaceVisibleRect,
97
96
  type NativeSurfaceRenderableNode,
98
97
  } from "./native/surface-visibility";
99
- import { formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
98
+ import { resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
100
99
  import { TimeAxisLabel } from "./time-axis-label";
101
100
  import { PriceAxisLabels } from "./price-axis-labels";
101
+ import { getChartMarketSessionKey, resolveChartMarketSession } from "./market-session";
102
102
 
103
103
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
104
104
  area: "A",
@@ -393,6 +393,7 @@ function buildComparisonNativeBitmapKey(
393
393
  pixelWidth: number,
394
394
  pixelHeight: number,
395
395
  paletteKey: string,
396
+ marketSessionKey: string,
396
397
  ): string {
397
398
  const fingerprint = projection.series
398
399
  .map((series) => [
@@ -413,6 +414,7 @@ function buildComparisonNativeBitmapKey(
413
414
  pixelWidth,
414
415
  pixelHeight,
415
416
  paletteKey,
417
+ marketSessionKey,
416
418
  fingerprint,
417
419
  ].join("::");
418
420
  }
@@ -445,6 +447,23 @@ function clipText(text: string, width: number): string {
445
447
  return `${text.slice(0, width - 3)}...`;
446
448
  }
447
449
 
450
+ function formatLegendSummary(
451
+ symbol: string,
452
+ rawValue: number | null,
453
+ baseValue: number | null,
454
+ currency: string,
455
+ priceRange: number | undefined,
456
+ ): string {
457
+ if (rawValue === null) return `${symbol} waiting`;
458
+ const price = formatMarketPriceWithCurrency(rawValue, currency, {
459
+ minimumFractionDigits: 2,
460
+ precisionOffset: 1,
461
+ priceRange,
462
+ });
463
+ if (baseValue === null || baseValue === 0) return `${symbol} ${price}`;
464
+ return `${symbol} ${price} ${formatPercentRaw(((rawValue - baseValue) / baseValue) * 100)}`;
465
+ }
466
+
448
467
  function getInitialComparisonMode(mode: string | undefined): ComparisonChartRenderMode {
449
468
  return mode === "line" ? "line" : "area";
450
469
  }
@@ -555,6 +574,7 @@ function ComparisonStockChartView({
555
574
  const mouseCrosshairDisabledRef = useRef(false);
556
575
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
557
576
  const animationFrameRef = useRef<number | null>(null);
577
+ const lastAppliedStoredSelectionKeyRef = useRef<string | null>(null);
558
578
  const pendingCanonicalResetRef = useRef(1);
559
579
  const appliedCanonicalResetRef = useRef(0);
560
580
  const pendingExpansionRef = useRef<PendingExpansionAction>(null);
@@ -564,7 +584,7 @@ function ComparisonStockChartView({
564
584
  const axisRightPadding = 1;
565
585
  const minimumAxisWidth = axisMode === "percent" ? 5 : 4;
566
586
  const axisGap = axisSectionWidthBudget > 0 ? 1 : 0;
567
- const headerRows = 1;
587
+ const headerRows = 0;
568
588
  const controlRows = 2;
569
589
  const timeAxisRows = 1;
570
590
  const helpRows = 0;
@@ -581,7 +601,16 @@ function ComparisonStockChartView({
581
601
  bgColor: colors.bg,
582
602
  gridColor: blendHex(colors.bg, colors.border, 0.55),
583
603
  crosshairColor: colors.borderFocused,
604
+ preMarketBgColor: blendHex(colors.bg, "#1d4ed8", 0.18),
605
+ postMarketBgColor: blendHex(colors.bg, "#b45309", 0.18),
584
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]);
585
614
 
586
615
  const setSelectedSymbol = (symbol: string) => {
587
616
  setViewState((current) => (
@@ -591,13 +620,35 @@ function ComparisonStockChartView({
591
620
  ));
592
621
  };
593
622
 
594
- const capabilityKey = useMemo(() => symbolSources.map((source) => [
595
- source.symbol,
596
- source.exchange,
597
- source.brokerId ?? "",
598
- source.brokerInstanceId ?? "",
599
- source.instrument?.conId ?? "",
600
- ].join(":")).join("|"), [symbolSources]);
623
+ const marketDataProvider = getSharedMarketData();
624
+ const capabilityDescriptor = useMemo(() => {
625
+ const sources = symbolSources.map((source) => ({
626
+ symbol: source.symbol,
627
+ exchange: source.exchange,
628
+ brokerId: source.brokerId,
629
+ brokerInstanceId: source.brokerInstanceId,
630
+ instrument: source.instrument,
631
+ }));
632
+ return {
633
+ key: sources.map((source) => [
634
+ source.symbol,
635
+ source.exchange,
636
+ source.brokerId ?? "",
637
+ source.brokerInstanceId ?? "",
638
+ source.instrument?.conId ?? "",
639
+ source.instrument?.localSymbol ?? "",
640
+ source.instrument?.symbol ?? "",
641
+ ].join(":")).join("|"),
642
+ sources,
643
+ };
644
+ }, [symbolSources]);
645
+ const effectiveResolutionSupport = useMemo<ChartResolutionSupport[]>(() => (
646
+ resolutionSupport ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS.map((resolution) => ({ resolution, maxRange: "ALL" as const }))
647
+ ), [resolutionSupport]);
648
+ const selectionSupportMap = useMemo(
649
+ () => buildChartResolutionSupportMap(effectiveResolutionSupport),
650
+ [effectiveResolutionSupport],
651
+ );
601
652
 
602
653
  useEffect(() => {
603
654
  if (symbols.includes(viewState.selectedSymbol ?? "")) return;
@@ -608,21 +659,25 @@ function ComparisonStockChartView({
608
659
  }, [symbols, viewState.selectedSymbol]);
609
660
 
610
661
  useEffect(() => {
662
+ const storedSelectionKey = `${storedRangePreset}:${storedResolution}`;
663
+ if (lastAppliedStoredSelectionKeyRef.current === storedSelectionKey) return;
664
+
665
+ lastAppliedStoredSelectionKeyRef.current = storedSelectionKey;
611
666
  pendingCanonicalResetRef.current += 1;
612
667
  setViewState((current) => (
613
668
  storedResolution === "auto"
614
- ? resolveStoredChartSelection(current, storedRangePreset, storedResolution, supportMap)
669
+ ? resolveStoredChartSelection(current, storedRangePreset, storedResolution, selectionSupportMap)
615
670
  : resolvePresetSelection(
616
671
  {
617
672
  ...current,
618
673
  resolution: storedResolution,
619
674
  },
620
675
  storedRangePreset,
621
- getSupportMaxRange(supportMap, storedResolution),
676
+ getSupportMaxRange(selectionSupportMap, storedResolution),
622
677
  )
623
678
  ));
624
679
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
625
- }, [storedRangePreset, storedResolution, supportMap]);
680
+ }, [selectionSupportMap, storedRangePreset, storedResolution]);
626
681
 
627
682
  const commitDisplayCursor = (next: DisplayCursorState) => {
628
683
  displayCursorRef.current = next;
@@ -705,15 +760,15 @@ function ComparisonStockChartView({
705
760
  ), [nativeSurfaceManager]);
706
761
 
707
762
  useEffect(() => {
708
- const provider = getSharedMarketData();
709
- if ((!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) || symbolSources.length === 0) {
763
+ const provider = marketDataProvider;
764
+ if ((!provider?.getChartResolutionSupport && !provider?.getChartResolutionCapabilities) || capabilityDescriptor.sources.length === 0) {
710
765
  setResolutionSupport(null);
711
766
  return;
712
767
  }
713
768
 
714
769
  let cancelled = false;
715
770
  setResolutionSupport(null);
716
- Promise.all(symbolSources.map(async (source) => {
771
+ Promise.all(capabilityDescriptor.sources.map(async (source) => {
717
772
  try {
718
773
  const support = provider.getChartResolutionSupport
719
774
  ? await provider.getChartResolutionSupport(
@@ -757,11 +812,7 @@ function ComparisonStockChartView({
757
812
  return () => {
758
813
  cancelled = true;
759
814
  };
760
- }, [capabilityKey, symbolSources]);
761
-
762
- const effectiveResolutionSupport = useMemo<ChartResolutionSupport[]>(() => (
763
- resolutionSupport ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS.map((resolution) => ({ resolution, maxRange: "ALL" as const }))
764
- ), [resolutionSupport]);
815
+ }, [capabilityDescriptor.key, marketDataProvider]);
765
816
  const availableManualResolutions = resolutionSupport?.map((entry) => entry.resolution) ?? DEFAULT_VISIBLE_MANUAL_CHART_RESOLUTIONS;
766
817
  const effectiveResolution: ChartResolution = viewState.resolution !== "auto"
767
818
  && resolutionSupport !== null
@@ -873,7 +924,6 @@ function ComparisonStockChartView({
873
924
  ]);
874
925
  const axisSectionWidth = axisWidth + axisRightPadding;
875
926
  const chartWidth = Math.max(width - axisSectionWidth - axisGap, minChartWidth);
876
- const maxCursorX = chartWidth - 1;
877
927
  const seriesDates = useMemo(() => getUniqueSortedSeriesDates(series), [series]);
878
928
  const visibleDateWindow = useMemo(() => buildVisibleDateWindow(seriesDates, viewState.panOffset, viewState.zoomLevel), [seriesDates, viewState.panOffset, viewState.zoomLevel]);
879
929
  const activePreset = resolveVisibleActivePreset(seriesDates, {
@@ -1006,6 +1056,16 @@ function ComparisonStockChartView({
1006
1056
  ));
1007
1057
  };
1008
1058
 
1059
+ const selectSymbolByOffset = (offset: number) => {
1060
+ setViewState((current) => {
1061
+ const currentIndex = Math.max(symbols.indexOf(current.selectedSymbol ?? symbols[0] ?? ""), 0);
1062
+ return {
1063
+ ...current,
1064
+ selectedSymbol: symbols[clamp(currentIndex + offset, 0, symbols.length - 1)] ?? current.selectedSymbol,
1065
+ };
1066
+ });
1067
+ };
1068
+
1009
1069
  useEffect(() => {
1010
1070
  const remapCursor = (cursor: DisplayCursorState) => buildDisplayCursorState(
1011
1071
  cursor.cellX,
@@ -1040,7 +1100,8 @@ function ComparisonStockChartView({
1040
1100
  cursorY: null,
1041
1101
  selectedSymbol: viewState.selectedSymbol,
1042
1102
  colors: chartColors,
1043
- }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1103
+ marketSession,
1104
+ }), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
1044
1105
  const displayScene = useMemo(() => buildComparisonChartScene(projection, {
1045
1106
  width: chartWidth,
1046
1107
  height: chartHeight,
@@ -1048,7 +1109,8 @@ function ComparisonStockChartView({
1048
1109
  cursorY: displayCursorY,
1049
1110
  selectedSymbol: viewState.selectedSymbol,
1050
1111
  colors: chartColors,
1051
- }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
1112
+ marketSession,
1113
+ }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, marketSession, projection, viewState.selectedSymbol]);
1052
1114
 
1053
1115
  const staticResult = useMemo(() => renderComparisonChart(projection, {
1054
1116
  width: chartWidth,
@@ -1057,7 +1119,8 @@ function ComparisonStockChartView({
1057
1119
  cursorY: null,
1058
1120
  selectedSymbol: viewState.selectedSymbol,
1059
1121
  colors: chartColors,
1060
- }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1122
+ marketSession,
1123
+ }), [chartColors, chartHeight, chartWidth, marketSession, projection, viewState.selectedSymbol]);
1061
1124
 
1062
1125
  const interactiveResult = useMemo(() => (
1063
1126
  effectiveRenderer === "kitty" || useCanvasChart
@@ -1069,8 +1132,9 @@ function ComparisonStockChartView({
1069
1132
  cursorY: displayCursorY,
1070
1133
  selectedSymbol: viewState.selectedSymbol,
1071
1134
  colors: chartColors,
1135
+ marketSession,
1072
1136
  })
1073
- ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, useCanvasChart, viewState.selectedSymbol]);
1137
+ ), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, marketSession, projection, useCanvasChart, viewState.selectedSymbol]);
1074
1138
 
1075
1139
  const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
1076
1140
  const timeAxisLabel = result.timeLabels || staticResult.timeLabels;
@@ -1234,7 +1298,14 @@ function ComparisonStockChartView({
1234
1298
  viewState.selectedSymbol,
1235
1299
  bitmapSize.pixelWidth,
1236
1300
  bitmapSize.pixelHeight,
1237
- [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,
1238
1309
  );
1239
1310
  const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
1240
1311
  ? lastNativeBaseBitmapRef.current.bitmap
@@ -1264,7 +1335,10 @@ function ComparisonStockChartView({
1264
1335
  chartColors.bgColor,
1265
1336
  chartColors.crosshairColor,
1266
1337
  chartColors.gridColor,
1338
+ chartColors.postMarketBgColor,
1339
+ chartColors.preMarketBgColor,
1267
1340
  effectiveRenderer,
1341
+ marketSessionKey,
1268
1342
  nativeSurfaceManager,
1269
1343
  paneId,
1270
1344
  projection,
@@ -1425,22 +1499,6 @@ function ComparisonStockChartView({
1425
1499
  case "t":
1426
1500
  onEditTickers?.();
1427
1501
  return;
1428
- case "h":
1429
- mouseCrosshairDisabledRef.current = false;
1430
- cursorMotionKindRef.current = "discrete";
1431
- setViewState((current) => ({
1432
- ...current,
1433
- cursorX: current.cursorX === null ? maxCursorX : Math.max(current.cursorX - 1, 0),
1434
- }));
1435
- return;
1436
- case "l":
1437
- mouseCrosshairDisabledRef.current = false;
1438
- cursorMotionKindRef.current = "discrete";
1439
- setViewState((current) => ({
1440
- ...current,
1441
- cursorX: current.cursorX === null ? 0 : Math.min(current.cursorX + 1, maxCursorX),
1442
- }));
1443
- return;
1444
1502
  case "escape":
1445
1503
  mouseCrosshairDisabledRef.current = true;
1446
1504
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
@@ -1451,42 +1509,20 @@ function ComparisonStockChartView({
1451
1509
  if (viewState.selectedSymbol) onOpenSymbol(viewState.selectedSymbol);
1452
1510
  return;
1453
1511
  case "left":
1454
- setViewState((current) => {
1455
- const currentIndex = Math.max(symbols.indexOf(current.selectedSymbol ?? symbols[0] ?? ""), 0);
1456
- return {
1457
- ...current,
1458
- selectedSymbol: symbols[Math.max(currentIndex - 1, 0)] ?? current.selectedSymbol,
1459
- };
1460
- });
1512
+ case "h":
1513
+ selectSymbolByOffset(-1);
1461
1514
  return;
1462
1515
  case "right":
1463
- setViewState((current) => {
1464
- const currentIndex = Math.max(symbols.indexOf(current.selectedSymbol ?? symbols[0] ?? ""), 0);
1465
- return {
1466
- ...current,
1467
- selectedSymbol: symbols[Math.min(currentIndex + 1, symbols.length - 1)] ?? current.selectedSymbol,
1468
- };
1469
- });
1516
+ case "l":
1517
+ selectSymbolByOffset(1);
1470
1518
  return;
1471
1519
  case "up":
1472
1520
  case "k":
1473
- setViewState((current) => {
1474
- const currentIndex = Math.max(symbols.indexOf(current.selectedSymbol ?? symbols[0] ?? ""), 0);
1475
- return {
1476
- ...current,
1477
- selectedSymbol: symbols[Math.max(currentIndex - legendColumns, 0)] ?? current.selectedSymbol,
1478
- };
1479
- });
1521
+ selectSymbolByOffset(-legendColumns);
1480
1522
  return;
1481
1523
  case "down":
1482
1524
  case "j":
1483
- setViewState((current) => {
1484
- const currentIndex = Math.max(symbols.indexOf(current.selectedSymbol ?? symbols[0] ?? ""), 0);
1485
- return {
1486
- ...current,
1487
- selectedSymbol: symbols[Math.min(currentIndex + legendColumns, symbols.length - 1)] ?? current.selectedSymbol,
1488
- };
1489
- });
1525
+ selectSymbolByOffset(legendColumns);
1490
1526
  return;
1491
1527
  }
1492
1528
 
@@ -1629,18 +1665,9 @@ function ComparisonStockChartView({
1629
1665
  const hasChartData = series.some((entry) => entry.points.length > 0);
1630
1666
  const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
1631
1667
  const cursorScene = hasDisplayCursor ? displayScene : staticScene;
1632
- const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
1633
- const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
1634
- const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
1635
- const selectedBaseValue = selectedSeries?.baseValue ?? null;
1636
- const selectedChange = selectedRawValue !== null && selectedBaseValue !== null
1637
- ? selectedRawValue - selectedBaseValue
1638
- : null;
1639
- const selectedChangePct = selectedChange !== null && selectedBaseValue
1640
- ? (selectedChange / selectedBaseValue) * 100
1668
+ const legendActiveIndex = hasChartData
1669
+ ? cursorScene?.activeIdx ?? staticScene?.activeIdx ?? null
1641
1670
  : null;
1642
- const selectedCurrency = selectedSeries?.currency ?? "USD";
1643
- const displayDate = cursorScene?.activeDate ?? staticResult.activeDate;
1644
1671
  const visiblePriceRange = result.priceRange ?? staticResult.priceRange ?? undefined;
1645
1672
  const axisLabels = new Map(staticResult.axisLabels.map((entry) => [entry.row, entry.label]));
1646
1673
  const cursorRow = useCanvasChart ? cursorScene?.cursorRow ?? null : result.cursorRow;
@@ -1652,10 +1679,6 @@ function ComparisonStockChartView({
1652
1679
  visiblePriceRange,
1653
1680
  )
1654
1681
  : null;
1655
- const visibleLabel = formatVisibleSpanLabel({
1656
- start: visibleWindow.dates[0] ?? null,
1657
- end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
1658
- });
1659
1682
  const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
1660
1683
  const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.activeDate ?? null : null;
1661
1684
 
@@ -1688,9 +1711,10 @@ function ComparisonStockChartView({
1688
1711
  cursorY: null,
1689
1712
  selectedSymbol: viewState.selectedSymbol,
1690
1713
  colors: chartColors,
1714
+ marketSession,
1691
1715
  })
1692
1716
  : null
1693
- ), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, viewState.selectedSymbol]);
1717
+ ), [canvasBitmapSize?.pixelWidth, canvasProjection, chartColors, chartHeight, chartWidth, marketSession, viewState.selectedSymbol]);
1694
1718
 
1695
1719
  const canvasBaseBitmapKey = useMemo(() => {
1696
1720
  if (!canvasBitmapSize || !canvasProjection || !hasChartData || isBlockingBody || bodyMessage) return null;
@@ -1700,7 +1724,14 @@ function ComparisonStockChartView({
1700
1724
  viewState.selectedSymbol,
1701
1725
  canvasBitmapSize.pixelWidth,
1702
1726
  canvasBitmapSize.pixelHeight,
1703
- [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,
1704
1735
  );
1705
1736
  }, [
1706
1737
  bodyMessage,
@@ -1709,8 +1740,11 @@ function ComparisonStockChartView({
1709
1740
  chartColors.bgColor,
1710
1741
  chartColors.crosshairColor,
1711
1742
  chartColors.gridColor,
1743
+ chartColors.postMarketBgColor,
1744
+ chartColors.preMarketBgColor,
1712
1745
  hasChartData,
1713
1746
  isBlockingBody,
1747
+ marketSessionKey,
1714
1748
  symbols.length,
1715
1749
  viewState.selectedSymbol,
1716
1750
  ]);
@@ -1852,32 +1886,6 @@ function ComparisonStockChartView({
1852
1886
  flexGrow={1}
1853
1887
  onMouseScroll={hasChartData && !isBlockingBody && !bodyMessage ? handlePlotScroll : undefined}
1854
1888
  >
1855
- <Box flexDirection="row" gap={2} height={1}>
1856
- <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
1857
- {viewState.selectedSymbol ?? "Compare"} - {getChartResolutionLabel(effectiveResolution)}
1858
- </Text>
1859
- <Text fg={colors.textDim}>{visibleLabel}</Text>
1860
- <Text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1861
- {selectedRawValue !== null
1862
- ? formatMarketPriceWithCurrency(selectedRawValue, selectedCurrency, {
1863
- minimumFractionDigits: 2,
1864
- precisionOffset: 1,
1865
- priceRange: visiblePriceRange,
1866
- })
1867
- : "—"}
1868
- </Text>
1869
- <Text fg={selectedChange !== null ? priceColor(selectedChange) : colors.textDim}>
1870
- {selectedChange !== null && selectedChangePct !== null
1871
- ? `${formatSignedMarketPrice(selectedChange, {
1872
- minimumFractionDigits: 2,
1873
- precisionOffset: 1,
1874
- priceRange: visiblePriceRange,
1875
- })} (${formatPercentRaw(selectedChangePct)})`
1876
- : "Waiting for data"}
1877
- </Text>
1878
- {displayDate && <Text fg={colors.textDim}>{formatDateShort(displayDate)}</Text>}
1879
- </Box>
1880
-
1881
1889
  <Box flexDirection="row" height={1}>
1882
1890
  <Box flexDirection="row" gap={1}>
1883
1891
  {TIME_RANGES.map((range, index) => (
@@ -1951,7 +1959,7 @@ function ComparisonStockChartView({
1951
1959
  cursorColumn={cursorTimeAxisColumn}
1952
1960
  cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
1953
1961
  cursorDate={cursorTimeAxisDate}
1954
- dates={projection.dates}
1962
+ dates={visibleWindow.dates}
1955
1963
  cursorColor={chartColors.crosshairColor}
1956
1964
  />
1957
1965
  </Box>
@@ -1964,19 +1972,11 @@ function ComparisonStockChartView({
1964
1972
  {legendRow.map((symbol) => {
1965
1973
  const item = projection.series.find((entry) => entry.symbol === symbol) ?? null;
1966
1974
  const isSelected = viewState.selectedSymbol === symbol;
1967
- const latestRaw = item?.latestRawValue ?? null;
1968
- const latestChange = latestRaw !== null && item?.baseValue != null ? latestRaw - item.baseValue : null;
1969
- const latestChangePct = latestChange !== null && item?.baseValue
1970
- ? (latestChange / item.baseValue) * 100
1971
- : null;
1975
+ const activeRaw = legendActiveIndex !== null
1976
+ ? item?.points[legendActiveIndex]?.rawValue ?? null
1977
+ : item?.latestRawValue ?? null;
1972
1978
  const currency = item?.currency ?? "USD";
1973
- const summary = latestRaw !== null && latestChangePct !== null
1974
- ? `${symbol} ${formatMarketPriceWithCurrency(latestRaw, currency, {
1975
- minimumFractionDigits: 2,
1976
- precisionOffset: 1,
1977
- priceRange: visiblePriceRange,
1978
- })} ${formatPercentRaw(latestChangePct)}`
1979
- : `${symbol} waiting`;
1979
+ const summary = formatLegendSummary(symbol, activeRaw, item?.baseValue ?? null, currency, visiblePriceRange);
1980
1980
 
1981
1981
  return (
1982
1982
  <Box