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
@@ -6,7 +6,6 @@ import { useShortcut } from "../../react/input";
6
6
  import { useAppDispatch, useAppSelector, usePaneInstance, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
7
7
  import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "../layout/pane-footer";
8
8
  import { ShortcutHint } from "../ui";
9
- import { saveConfig } from "../../data/config-store";
10
9
  import { getSharedMarketData } from "../../plugins/registry";
11
10
  import { colors } from "../../theme/colors";
12
11
  import { useThemeColors } from "../../theme/theme-context";
@@ -129,12 +128,14 @@ import type { PricePoint, TickerFinancials } from "../../types/financials";
129
128
  import type { TickerRecord } from "../../types/ticker";
130
129
  import { TimeAxisLabel } from "./time-axis-label";
131
130
  import { PriceAxisLabels } from "./price-axis-labels";
131
+ import { getChartMarketSessionKey, resolveChartMarketSession } from "./market-session";
132
132
 
133
133
  const MODE_CHIPS: Record<ChartRenderMode, string> = {
134
134
  area: "A",
135
135
  line: "L",
136
136
  candles: "C",
137
137
  ohlc: "O",
138
+ hlc: "H",
138
139
  };
139
140
 
140
141
  const MODE_LABELS: Record<ChartRenderMode, string> = {
@@ -142,6 +143,7 @@ const MODE_LABELS: Record<ChartRenderMode, string> = {
142
143
  line: "LINE",
143
144
  candles: "CANDLES",
144
145
  ohlc: "OHLC",
146
+ hlc: "HLC",
145
147
  };
146
148
 
147
149
  const AXIS_MEASURE_PALETTE = resolveChartPalette({
@@ -1085,6 +1087,7 @@ function buildNativeBitmapKey(
1085
1087
  showVolume: boolean,
1086
1088
  paletteId: string,
1087
1089
  indicatorKey: string,
1090
+ marketSessionKey: string,
1088
1091
  ): string {
1089
1092
  const fingerprint = points
1090
1093
  .map((point) => {
@@ -1092,7 +1095,7 @@ function buildNativeBitmapKey(
1092
1095
  return `${date}:${point.open}:${point.high}:${point.low}:${point.close}:${point.volume ?? 0}`;
1093
1096
  })
1094
1097
  .join("|");
1095
- return [pointCount, pixelWidth, pixelHeight, mode, showVolume ? "1" : "0", paletteId, indicatorKey, fingerprint].join("::");
1098
+ return [pointCount, pixelWidth, pixelHeight, mode, showVolume ? "1" : "0", paletteId, indicatorKey, marketSessionKey, fingerprint].join("::");
1096
1099
  }
1097
1100
 
1098
1101
  function buildNativeCrosshairBitmapKey(
@@ -1151,9 +1154,18 @@ function resolveSelectionCursor(
1151
1154
  };
1152
1155
  }
1153
1156
 
1154
- const INDICATOR_COLORS = [
1155
- "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8",
1156
- ];
1157
+ function getIndicatorColor(index: number): string {
1158
+ const indicatorColors = [
1159
+ colors.negative,
1160
+ colors.positive,
1161
+ colors.borderFocused,
1162
+ colors.warning,
1163
+ colors.textBright,
1164
+ colors.neutral,
1165
+ colors.textMuted,
1166
+ ];
1167
+ return indicatorColors[((index % indicatorColors.length) + indicatorColors.length) % indicatorColors.length]!;
1168
+ }
1157
1169
 
1158
1170
  function getIndicatorWarmupPeriod(config: IndicatorConfig): number {
1159
1171
  const periods = [
@@ -1269,7 +1281,7 @@ function computeIndicatorOverlays(
1269
1281
  config: IndicatorConfig,
1270
1282
  ): ChartIndicatorOverlays {
1271
1283
  let colorIdx = 0;
1272
- const nextColor = () => INDICATOR_COLORS[colorIdx++ % INDICATOR_COLORS.length]!;
1284
+ const nextColor = () => getIndicatorColor(colorIdx++);
1273
1285
 
1274
1286
  const smaLines = (config.sma ?? []).map((period) => ({
1275
1287
  period,
@@ -1439,7 +1451,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1439
1451
  }: ResolvedStockChartProps) {
1440
1452
  const themeColors = useThemeColors();
1441
1453
  const renderer = useNativeRenderer();
1442
- const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
1454
+ const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1, fractionalViewport = false } = useUiCapabilities();
1443
1455
  const dispatch = useAppDispatch();
1444
1456
  const config = useAppSelector((state) => state.config);
1445
1457
  const paneId = usePaneInstanceId();
@@ -1449,6 +1461,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1449
1461
  const preferredRenderer = config.chartPreferences.renderer;
1450
1462
  const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
1451
1463
  const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
1464
+ const [storedRenderMode, setStoredRenderMode] = usePaneSettingValue<ChartRenderMode>("chartRenderMode", defaultRenderMode);
1452
1465
  const persistChartControls = usePersistChartControlSelection("chartRangePreset");
1453
1466
  const [viewState, setViewState] = useState<StockChartViewportState>({
1454
1467
  presetRange: compact ? "1Y" : storedRangePreset,
@@ -1458,7 +1471,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1458
1471
  zoomLevel: 1,
1459
1472
  cursorX: null,
1460
1473
  cursorY: null,
1461
- renderMode: defaultRenderMode,
1474
+ renderMode: storedRenderMode,
1462
1475
  });
1463
1476
  // Read indicator config from the chart caller, falling back to legacy pane settings.
1464
1477
  const indicatorConfig: IndicatorConfig = indicatorConfigOverride ?? ((pane?.settings?.indicators as IndicatorConfig) ?? EMPTY_INDICATOR_CONFIG);
@@ -1683,6 +1696,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1683
1696
  });
1684
1697
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1685
1698
  }, [compact, selectionSupportMap, storedRangePreset, storedResolution]);
1699
+ useEffect(() => {
1700
+ if (compact) return;
1701
+ setViewState((current) => (
1702
+ current.renderMode === storedRenderMode ? current : { ...current, renderMode: storedRenderMode }
1703
+ ));
1704
+ }, [compact, storedRenderMode]);
1686
1705
  const effectiveResolution: ChartResolution = !compact
1687
1706
  && requestedResolution !== "auto"
1688
1707
  && resolutionSupport !== null
@@ -1698,7 +1717,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1698
1717
  !compact && instrumentRef
1699
1718
  ? {
1700
1719
  instrument: instrumentRef,
1701
- bufferRange: indicatorBufferRange,
1720
+ bufferRange: viewState.bufferRange,
1702
1721
  granularity: "range",
1703
1722
  }
1704
1723
  : null,
@@ -1724,9 +1743,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1724
1743
  const axisSectionWidthBudget = compact
1725
1744
  ? axisMode === "percent" ? 11 : 8
1726
1745
  : axisMode === "percent" ? 11 : 10;
1727
- const axisRightPadding = 1;
1746
+ const axisRightPadding = compact || fractionalViewport ? 0 : 1;
1728
1747
  const minimumAxisWidth = axisMode === "percent" ? 5 : 4;
1729
- const axisGap = axisSectionWidthBudget > 0 ? 1 : 0;
1748
+ const axisGap = axisSectionWidthBudget > 0 ? fractionalViewport ? 0.5 : 1 : 0;
1730
1749
  const minChartWidth = compact ? 12 : 20;
1731
1750
  const measurementChartWidth = Math.max(width - axisSectionWidthBudget - axisGap, minChartWidth);
1732
1751
  const headerRows = compact ? 0 : 2;
@@ -1734,7 +1753,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1734
1753
  const timeAxisRow = 1;
1735
1754
  // Native/canvas surfaces can cover the last physical row after pixel rounding;
1736
1755
  // keep the text x-axis one row clear of the pane footer.
1737
- const nativeSurfaceGuardRow = !compact && (useCanvasChart || effectiveRenderer === "kitty") ? 1 : 0;
1756
+ const nativeSurfaceGuardRow = !compact && fractionalViewport && (useCanvasChart || effectiveRenderer === "kitty") ? 1 : 0;
1738
1757
  const volumeHeight = showVolume && !compact ? 3 : 0;
1739
1758
  const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow - nativeSurfaceGuardRow, 4);
1740
1759
  const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
@@ -2360,7 +2379,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2360
2379
  colors: AXIS_MEASURE_PALETTE,
2361
2380
  timeAxisDates: measuredTimeAxisDates,
2362
2381
  });
2363
- const cursorSamples = measuredScene
2382
+ const cursorSamples = !compact && measuredScene
2364
2383
  ? [
2365
2384
  formatCursorAxisValue(
2366
2385
  measuredScene.min,
@@ -2401,6 +2420,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2401
2420
  chartHeight,
2402
2421
  compact,
2403
2422
  displayedDateWindow,
2423
+ fractionalViewport,
2404
2424
  history,
2405
2425
  historyOverride,
2406
2426
  minChartWidth,
@@ -2545,18 +2565,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2545
2565
  };
2546
2566
  }, [preferredRenderer, renderer]);
2547
2567
 
2548
- const persistDefaultRenderMode = (nextMode: ChartRenderMode) => {
2549
- if (nextMode === defaultRenderMode) return;
2550
- const nextConfig = {
2551
- ...config,
2552
- chartPreferences: {
2553
- ...config.chartPreferences,
2554
- defaultRenderMode: nextMode,
2555
- },
2556
- };
2557
- dispatch({ type: "SET_CONFIG", config: nextConfig });
2558
- saveConfig(nextConfig).catch(() => {});
2559
- };
2568
+ const persistRenderMode = useCallback((nextMode: ChartRenderMode) => {
2569
+ if (!compact && nextMode !== storedRenderMode) {
2570
+ setStoredRenderMode(nextMode);
2571
+ }
2572
+ }, [compact, setStoredRenderMode, storedRenderMode]);
2560
2573
 
2561
2574
  const expandBufferRange = (action: PendingExpansionAction): boolean => {
2562
2575
  if (compact) return false;
@@ -2658,7 +2671,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2658
2671
  };
2659
2672
 
2660
2673
  const setRenderMode = (mode: ChartRenderMode) => {
2661
- persistDefaultRenderMode(mode);
2674
+ persistRenderMode(mode);
2662
2675
  setViewState((current) => ({ ...current, renderMode: mode }));
2663
2676
  };
2664
2677
 
@@ -2872,7 +2885,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2872
2885
  const activeMode = current.renderMode ?? "area";
2873
2886
  const index = CHART_RENDER_MODES.indexOf(activeMode);
2874
2887
  const nextMode = CHART_RENDER_MODES[(index + 1) % CHART_RENDER_MODES.length]!;
2875
- persistDefaultRenderMode(nextMode);
2888
+ persistRenderMode(nextMode);
2876
2889
  return { ...current, renderMode: nextMode };
2877
2890
  });
2878
2891
  return;
@@ -3033,6 +3046,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3033
3046
  themeColors.text,
3034
3047
  themeColors.textDim,
3035
3048
  ]);
3049
+ const marketSession = useMemo(() => resolveChartMarketSession(ticker
3050
+ ? [{
3051
+ exchange: ticker.metadata.exchange,
3052
+ currency: ticker.metadata.currency,
3053
+ assetCategory: ticker.metadata.assetCategory,
3054
+ }]
3055
+ : []), [ticker]);
3056
+ const marketSessionKey = useMemo(() => getChartMarketSessionKey(marketSession), [marketSession]);
3036
3057
 
3037
3058
  const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
3038
3059
  const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
@@ -3060,7 +3081,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3060
3081
  axisMode,
3061
3082
  colors: chartColors,
3062
3083
  timeAxisDates,
3063
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
3084
+ marketSession,
3085
+ }), [axisMode, chartColors, chartHeight, chartWidth, compact, marketSession, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
3064
3086
  const displayScene = useMemo(() => buildChartScene(projection.points, {
3065
3087
  width: chartWidth,
3066
3088
  height: chartHeight,
@@ -3073,7 +3095,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3073
3095
  colors: chartColors,
3074
3096
  timeAxisDates,
3075
3097
  indicators,
3076
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3098
+ marketSession,
3099
+ }), [axisMode, chartColors, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3077
3100
  const snappedSelectionCursorX = selectionScene
3078
3101
  ? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
3079
3102
  : null;
@@ -3119,7 +3142,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3119
3142
  colors: chartColors,
3120
3143
  timeAxisDates,
3121
3144
  indicators,
3122
- }), [axisMode, chartColors, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3145
+ marketSession,
3146
+ }), [axisMode, chartColors, chartHeight, chartWidth, compact, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3123
3147
 
3124
3148
  const staticResult = useMemo(() => renderChart(projection.points, {
3125
3149
  width: chartWidth,
@@ -3135,7 +3159,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3135
3159
  colors: chartColors,
3136
3160
  timeAxisDates,
3137
3161
  indicators,
3138
- }), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3162
+ marketSession,
3163
+ }), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3139
3164
 
3140
3165
  const interactiveResult = useMemo(() => (
3141
3166
  effectiveRenderer === "kitty" || useCanvasChart
@@ -3154,8 +3179,9 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3154
3179
  colors: chartColors,
3155
3180
  timeAxisDates,
3156
3181
  indicators,
3182
+ marketSession,
3157
3183
  })
3158
- ), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, useCanvasChart, volumeHeight]);
3184
+ ), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, marketSession, projection.effectiveMode, projection.points, showVolume, timeAxisDates, useCanvasChart, volumeHeight]);
3159
3185
 
3160
3186
  const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
3161
3187
 
@@ -3289,8 +3315,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3289
3315
  chartColors.volumeDown,
3290
3316
  chartColors.candleUp,
3291
3317
  chartColors.candleDown,
3318
+ chartColors.preMarketBgColor,
3319
+ chartColors.postMarketBgColor,
3292
3320
  ].join(","),
3293
3321
  indicatorRenderKey,
3322
+ marketSessionKey,
3294
3323
  );
3295
3324
  const cachedBitmap = lastNativeBaseBitmapRef.current?.key === bitmapKey
3296
3325
  ? lastNativeBaseBitmapRef.current.bitmap
@@ -3320,11 +3349,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3320
3349
  chartColors.fillColor,
3321
3350
  chartColors.gridColor,
3322
3351
  chartColors.lineColor,
3352
+ chartColors.postMarketBgColor,
3353
+ chartColors.preMarketBgColor,
3323
3354
  chartColors.volumeDown,
3324
3355
  chartColors.volumeUp,
3325
3356
  compact,
3326
3357
  effectiveRenderer,
3327
3358
  indicatorRenderKey,
3359
+ marketSessionKey,
3328
3360
  nativeBaseScene,
3329
3361
  nativeBaseSurfaceId,
3330
3362
  nativeSurfaceManager,
@@ -3412,7 +3444,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3412
3444
 
3413
3445
  const hasHistory = chartWindow.points.length > 0;
3414
3446
  const requestedMode = projection.requestedMode;
3415
- const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc";
3447
+ const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc" || projection.effectiveMode === "hlc";
3416
3448
  const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
3417
3449
  const activePoint = showOhlcSummary ? (selectionScene?.activePoint ?? null) : null;
3418
3450
  const visiblePriceRange = selectionScene
@@ -3463,7 +3495,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3463
3495
  const activeMode = current.renderMode ?? "area";
3464
3496
  const index = CHART_RENDER_MODES.indexOf(activeMode);
3465
3497
  const nextMode = CHART_RENDER_MODES[(index + 1) % CHART_RENDER_MODES.length]!;
3466
- persistDefaultRenderMode(nextMode);
3498
+ persistRenderMode(nextMode);
3467
3499
  return { ...current, renderMode: nextMode };
3468
3500
  });
3469
3501
  };
@@ -3535,6 +3567,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3535
3567
  footerHints,
3536
3568
  history,
3537
3569
  navigableDateWindow,
3570
+ persistRenderMode,
3538
3571
  resolutionChips,
3539
3572
  selectedResolution,
3540
3573
  selectionSupportMap,
@@ -3552,26 +3585,33 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3552
3585
  priceRange: visiblePriceRange,
3553
3586
  });
3554
3587
 
3588
+ const parts: PaneFooterSegment["parts"] = [
3589
+ ...(projection.effectiveMode === "hlc"
3590
+ ? []
3591
+ : [
3592
+ { text: "O", tone: "label" as const },
3593
+ { text: formatPrice(activePoint.open), tone: "value" as const },
3594
+ ]),
3595
+ { text: "H", tone: "label" },
3596
+ { text: formatPrice(activePoint.high), tone: "value" },
3597
+ { text: "L", tone: "label" },
3598
+ { text: formatPrice(activePoint.low), tone: "value" },
3599
+ { text: "C", tone: "label" },
3600
+ { text: formatPrice(activePoint.close), tone: "value" },
3601
+ { text: "V", tone: "label" },
3602
+ { text: formatCompact(activePoint.volume), tone: "value" },
3603
+ ];
3604
+
3555
3605
  return [{
3556
- id: "ohlc",
3557
- parts: [
3558
- { text: "O", tone: "label" },
3559
- { text: formatPrice(activePoint.open), tone: "value" },
3560
- { text: "H", tone: "label" },
3561
- { text: formatPrice(activePoint.high), tone: "value" },
3562
- { text: "L", tone: "label" },
3563
- { text: formatPrice(activePoint.low), tone: "value" },
3564
- { text: "C", tone: "label" },
3565
- { text: formatPrice(activePoint.close), tone: "value" },
3566
- { text: "V", tone: "label" },
3567
- { text: formatCompact(activePoint.volume), tone: "value" },
3568
- ],
3606
+ id: projection.effectiveMode === "hlc" ? "hlc" : "ohlc",
3607
+ parts,
3569
3608
  }];
3570
3609
  }, [
3571
3610
  activePoint,
3572
3611
  chartAssetCategory,
3573
3612
  chartCurrency,
3574
3613
  compact,
3614
+ projection.effectiveMode,
3575
3615
  showOhlcSummary,
3576
3616
  visiblePriceRange,
3577
3617
  ]);
@@ -3810,9 +3850,10 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3810
3850
  colors: chartColors,
3811
3851
  timeAxisDates,
3812
3852
  indicators: canvasIndicators,
3853
+ marketSession,
3813
3854
  })
3814
3855
  : null
3815
- ), [axisMode, canvasIndicators, canvasProjection, chartColors, chartHeight, compact, showVolume, timeAxisDates, volumeHeight]);
3856
+ ), [axisMode, canvasIndicators, canvasProjection, chartColors, chartHeight, compact, marketSession, showVolume, timeAxisDates, volumeHeight]);
3816
3857
 
3817
3858
  const canvasBaseBitmapKey = useMemo(() => {
3818
3859
  if (!canvasBitmapSize || !canvasProjection || !hasHistory || isBlockingBody || bodyMessage) return null;
@@ -3831,8 +3872,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3831
3872
  chartColors.volumeDown,
3832
3873
  chartColors.candleUp,
3833
3874
  chartColors.candleDown,
3875
+ chartColors.preMarketBgColor,
3876
+ chartColors.postMarketBgColor,
3834
3877
  ].join(","),
3835
3878
  canvasIndicatorRenderKey,
3879
+ marketSessionKey,
3836
3880
  );
3837
3881
  }, [
3838
3882
  bodyMessage,
@@ -3844,11 +3888,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3844
3888
  chartColors.fillColor,
3845
3889
  chartColors.gridColor,
3846
3890
  chartColors.lineColor,
3891
+ chartColors.postMarketBgColor,
3892
+ chartColors.preMarketBgColor,
3847
3893
  chartColors.volumeDown,
3848
3894
  chartColors.volumeUp,
3849
3895
  compact,
3850
3896
  hasHistory,
3851
3897
  isBlockingBody,
3898
+ marketSessionKey,
3852
3899
  showVolume,
3853
3900
  ]);
3854
3901
 
@@ -4062,7 +4109,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
4062
4109
  </Box>
4063
4110
  ) : (
4064
4111
  <Box flexDirection="row" gap={1}>
4065
- <Text fg={colors.textDim}>mode:{MODE_LABELS[requestedMode]}</Text>
4066
4112
  {projection.fallbackMode && (
4067
4113
  <Text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</Text>
4068
4114
  )}
@@ -23,6 +23,51 @@ function clamp(value: number, min: number, max: number): number {
23
23
  return Math.max(min, Math.min(max, value));
24
24
  }
25
25
 
26
+ function normalizeDate(value: Date | string | number): Date | null {
27
+ const date = value instanceof Date ? value : new Date(value);
28
+ return Number.isFinite(date.getTime()) ? date : null;
29
+ }
30
+
31
+ export function resolveCursorDateFromAxis({
32
+ dates,
33
+ width,
34
+ cursorColumn,
35
+ cursorPixelX,
36
+ cellWidthPx,
37
+ }: {
38
+ dates: Array<Date | string | number>;
39
+ width: number;
40
+ cursorColumn: number | null;
41
+ cursorPixelX?: number | null;
42
+ cellWidthPx?: number | null;
43
+ }): Date | null {
44
+ const normalizedDates = dates
45
+ .map(normalizeDate)
46
+ .filter((date): date is Date => date !== null);
47
+ if (normalizedDates.length === 0) return null;
48
+ if (normalizedDates.length === 1) return normalizedDates[0]!;
49
+
50
+ let ratio: number | null = null;
51
+ if (
52
+ cursorPixelX !== null
53
+ && cursorPixelX !== undefined
54
+ && Number.isFinite(cursorPixelX)
55
+ && cellWidthPx !== null
56
+ && cellWidthPx !== undefined
57
+ && Number.isFinite(cellWidthPx)
58
+ && cellWidthPx > 0
59
+ ) {
60
+ const pixelWidth = Math.max(width * cellWidthPx, 1);
61
+ ratio = clamp(cursorPixelX / Math.max(pixelWidth - 1, 1), 0, 1);
62
+ } else if (cursorColumn !== null && Number.isFinite(cursorColumn) && width > 1) {
63
+ ratio = clamp(cursorColumn / Math.max(width - 1, 1), 0, 1);
64
+ }
65
+
66
+ if (ratio === null) return null;
67
+ const index = Math.round(ratio * (normalizedDates.length - 1));
68
+ return normalizedDates[clamp(index, 0, normalizedDates.length - 1)] ?? null;
69
+ }
70
+
26
71
  export function buildCursorTimeAxisOverlay({
27
72
  segments,
28
73
  width,
@@ -61,13 +106,22 @@ export function TimeAxisLabel({
61
106
  cursorColor,
62
107
  }: TimeAxisLabelProps) {
63
108
  const { cellWidthPx = 8, fractionalViewport = false } = useUiCapabilities();
109
+ const resolvedCursorDate = useMemo(() => {
110
+ return resolveCursorDateFromAxis({
111
+ dates,
112
+ width,
113
+ cursorColumn,
114
+ cursorPixelX: fractionalViewport ? cursorPixelX : null,
115
+ cellWidthPx,
116
+ }) ?? cursorDate;
117
+ }, [cellWidthPx, cursorColumn, cursorDate, cursorPixelX, dates, fractionalViewport, width]);
64
118
  const segments = useMemo(() => buildCursorTimeAxisSegments({
65
119
  timeLabels,
66
120
  width,
67
121
  cursorColumn,
68
- cursorDate,
122
+ cursorDate: resolvedCursorDate,
69
123
  dates,
70
- }), [cursorColumn, cursorDate, dates, timeLabels, width]);
124
+ }), [cursorColumn, dates, resolvedCursorDate, timeLabels, width]);
71
125
  const overlay = useMemo(() => buildCursorTimeAxisOverlay({
72
126
  segments,
73
127
  width,