gloomberb 0.3.7 → 0.4.0

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 (133) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +359 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +1 -1
  37. package/src/components/layout/pane-header.tsx +26 -2
  38. package/src/components/layout/shell.tsx +62 -50
  39. package/src/components/layout/status-bar.test.tsx +66 -0
  40. package/src/components/layout/status-bar.tsx +26 -5
  41. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  42. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  43. package/src/components/ticker-list-table.test.tsx +83 -0
  44. package/src/components/ticker-list-table.tsx +174 -97
  45. package/src/components/ui/data-table.tsx +27 -2
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/ui/page-stack-view.tsx +60 -0
  48. package/src/components/use-double-click-activation.ts +31 -0
  49. package/src/index.tsx +10 -10
  50. package/src/market-data/coordinator.test.ts +92 -4
  51. package/src/market-data/coordinator.ts +32 -5
  52. package/src/market-data/hooks.test.tsx +120 -0
  53. package/src/market-data/hooks.tsx +53 -16
  54. package/src/market-data/request-types.ts +4 -2
  55. package/src/market-data/selectors.ts +24 -58
  56. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  57. package/src/plugins/builtin/chat-controller.ts +310 -35
  58. package/src/plugins/builtin/chat.test.tsx +118 -6
  59. package/src/plugins/builtin/chat.tsx +175 -116
  60. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  61. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  62. package/src/plugins/builtin/news.tsx +1 -11
  63. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  64. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  65. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  66. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  67. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  68. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  69. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  70. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  71. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  72. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  73. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  74. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  75. package/src/plugins/builtin/sec.tsx +1 -11
  76. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  77. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  78. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  79. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  80. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  81. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  82. package/src/plugins/catalog.ts +65 -0
  83. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  84. package/src/plugins/ibkr/gateway-service.test.ts +45 -1
  85. package/src/plugins/ibkr/gateway-service.ts +141 -7
  86. package/src/plugins/pane-manager.test.ts +1 -1
  87. package/src/plugins/plugin-runtime.tsx +27 -7
  88. package/src/plugins/prediction-markets/cli.ts +28 -0
  89. package/src/plugins/prediction-markets/controller-data.ts +11 -3
  90. package/src/plugins/prediction-markets/controller.ts +141 -121
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/plugins/prediction-markets/launch.ts +51 -16
  93. package/src/plugins/prediction-markets/metrics.ts +9 -5
  94. package/src/plugins/prediction-markets/pane.test.tsx +119 -57
  95. package/src/plugins/prediction-markets/pane.tsx +74 -153
  96. package/src/plugins/prediction-markets/table.tsx +6 -3
  97. package/src/plugins/prediction-markets/types.ts +0 -1
  98. package/src/sources/gloomberb-cloud.test.ts +29 -0
  99. package/src/sources/gloomberb-cloud.ts +89 -1
  100. package/src/sources/provider-router.test.ts +265 -3
  101. package/src/sources/provider-router.ts +239 -11
  102. package/src/sources/yahoo-finance.test.ts +26 -0
  103. package/src/sources/yahoo-finance.ts +54 -3
  104. package/src/state/app-context-selector.test.tsx +99 -0
  105. package/src/state/app-context.test.ts +18 -0
  106. package/src/state/app-context.tsx +170 -65
  107. package/src/state/selectors.ts +25 -0
  108. package/src/theme/colors.ts +8 -8
  109. package/src/theme/themes.test.ts +2 -0
  110. package/src/theme/themes.ts +22 -10
  111. package/src/types/broker.ts +21 -0
  112. package/src/types/config.ts +17 -1
  113. package/src/types/data-provider.ts +18 -0
  114. package/src/types/financials.ts +31 -1
  115. package/src/types/plugin.ts +70 -0
  116. package/src/utils/api-client.test.ts +57 -0
  117. package/src/utils/api-client.ts +35 -12
  118. package/src/utils/back-navigation.ts +27 -0
  119. package/src/utils/format.test.ts +14 -0
  120. package/src/utils/format.ts +3 -3
  121. package/src/utils/market-status.ts +46 -1
  122. package/src/utils/quote-freshness.ts +127 -0
  123. package/src/utils/quote-resolution.test.ts +277 -0
  124. package/src/utils/quote-resolution.ts +641 -0
  125. package/src/utils/slugify.ts +3 -0
  126. package/src/utils/timestamp.ts +25 -0
  127. package/src/version.ts +1 -1
  128. package/src/cli/commands/portfolio.ts +0 -210
  129. package/src/cli/commands/watchlist.ts +0 -210
  130. package/src/plugins/builtin/data-providers.ts +0 -16
  131. package/src/sources/exchange-rates.ts +0 -18
  132. package/src/utils/ascii-chart.ts +0 -172
  133. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -1,5 +1,4 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { useKeyboard } from "@opentui/react";
3
2
  import type { GloomPlugin, DetailTabProps } from "../../types/plugin";
4
3
  import type { SecFilingItem } from "../../types/data-provider";
5
4
  import { useResolvedEntryValue, useSecFilingContent, useSecFilingsQuery } from "../../market-data/hooks";
@@ -195,15 +194,6 @@ function SecTab({ width, height, focused }: DetailTabProps) {
195
194
  }
196
195
  }, [cachedSelectedContent, filingContent, filingContentEntry?.phase, selected]);
197
196
 
198
- useKeyboard((event) => {
199
- if (!focused || filings.length === 0) return;
200
- if (event.name === "j" || event.name === "down") {
201
- setSelectedIdx((index) => Math.min(index + 1, filings.length - 1));
202
- } else if (event.name === "k" || event.name === "up") {
203
- setSelectedIdx((index) => Math.max(index - 1, 0));
204
- }
205
- });
206
-
207
197
  useEffect(() => {
208
198
  if (filings.length > 0 && selectedIdx >= filings.length) {
209
199
  setSelectedIdx(Math.max(0, filings.length - 1));
@@ -220,13 +210,13 @@ function SecTab({ width, height, focused }: DetailTabProps) {
220
210
  <DetailFeedView
221
211
  width={width}
222
212
  height={height}
213
+ focused={focused}
223
214
  items={toFeedItems(filings, selected?.accessionNumber, contentCache, loadingContent)}
224
215
  selectedIdx={selectedIdx}
225
216
  hoveredIdx={hoveredIdx}
226
217
  onSelect={setSelectedIdx}
227
218
  onHover={setHoveredIdx}
228
219
  listVariant="single-line"
229
- splitListWidthRatio={0.33}
230
220
  />
231
221
  );
232
222
  }
@@ -1,6 +1,8 @@
1
1
  import { useTerminalDimensions } from "@opentui/react";
2
2
  import type { ChartAxisMode } from "../../../components/chart/chart-types";
3
- import { StockChart } from "../../../components/chart/stock-chart";
3
+ import { ResolvedStockChart } from "../../../components/chart/stock-chart";
4
+ import type { TickerFinancials } from "../../../types/financials";
5
+ import type { TickerRecord } from "../../../types/ticker";
4
6
 
5
7
  export function ChartTab({
6
8
  width,
@@ -9,6 +11,9 @@ export function ChartTab({
9
11
  interactive,
10
12
  axisMode,
11
13
  onActivate,
14
+ symbol,
15
+ ticker,
16
+ financials,
12
17
  }: {
13
18
  width?: number;
14
19
  height?: number;
@@ -16,6 +21,9 @@ export function ChartTab({
16
21
  interactive: boolean;
17
22
  axisMode: ChartAxisMode;
18
23
  onActivate?: () => void;
24
+ symbol: string | null;
25
+ ticker: TickerRecord | null;
26
+ financials: TickerFinancials | null;
19
27
  }) {
20
28
  const { width: termWidth, height: termHeight } = useTerminalDimensions();
21
29
 
@@ -31,12 +39,15 @@ export function ChartTab({
31
39
  if (!interactive) onActivate?.();
32
40
  }}
33
41
  >
34
- <StockChart
42
+ <ResolvedStockChart
35
43
  width={chartWidth}
36
44
  height={chartHeight}
37
45
  focused={focused}
38
46
  interactive={interactive}
39
47
  axisMode={axisMode}
48
+ symbol={symbol}
49
+ ticker={ticker}
50
+ financials={financials}
40
51
  />
41
52
  </box>
42
53
  );
@@ -217,6 +217,27 @@ export function FinancialsTab({
217
217
  bodyScrollId?: string;
218
218
  }) {
219
219
  const { financials } = usePaneTicker();
220
+ return (
221
+ <ResolvedFinancialsTab
222
+ focused={focused}
223
+ financials={financials}
224
+ headerScrollId={headerScrollId}
225
+ bodyScrollId={bodyScrollId}
226
+ />
227
+ );
228
+ }
229
+
230
+ export function ResolvedFinancialsTab({
231
+ focused,
232
+ financials,
233
+ headerScrollId,
234
+ bodyScrollId,
235
+ }: {
236
+ focused: boolean;
237
+ financials: ReturnType<typeof usePaneTicker>["financials"];
238
+ headerScrollId?: string;
239
+ bodyScrollId?: string;
240
+ }) {
220
241
  const annualStatements = financials?.annualStatements ?? [];
221
242
  const quarterlyStatements = financials?.quarterlyStatements ?? [];
222
243
  const hasAnnualStatements = annualStatements.length > 0;
@@ -1,7 +1,7 @@
1
1
  import { TextAttributes } from "@opentui/core";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { useFxRatesMap } from "../../../market-data/hooks";
4
- import { useAppState, usePaneTicker } from "../../../state/app-context";
4
+ import { useAppSelector } from "../../../state/app-context";
5
5
  import { colors, priceColor } from "../../../theme/colors";
6
6
  import {
7
7
  convertCurrency,
@@ -16,14 +16,27 @@ import {
16
16
  exchangeShortName,
17
17
  marketStateColor,
18
18
  marketStateLabel,
19
+ quoteSourceLabel,
19
20
  } from "../../../utils/market-status";
20
21
  import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
21
22
  import { EmptyState, FieldRow } from "../../../components";
22
- import { StockChart } from "../../../components/chart/stock-chart";
23
+ import { ResolvedStockChart } from "../../../components/chart/stock-chart";
24
+ import type { TickerFinancials } from "../../../types/financials";
25
+ import type { TickerRecord } from "../../../types/ticker";
23
26
 
24
- export function OverviewTab({ width }: { width?: number }) {
25
- const { state } = useAppState();
26
- const { ticker, financials } = usePaneTicker();
27
+ export function OverviewTab({
28
+ width,
29
+ symbol,
30
+ ticker,
31
+ financials,
32
+ }: {
33
+ width?: number;
34
+ symbol: string | null;
35
+ ticker: TickerRecord | null;
36
+ financials: TickerFinancials | null;
37
+ }) {
38
+ const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
39
+ const exchangeRatesState = useAppSelector((state) => state.exchangeRates);
27
40
  const { width: termWidth } = useTerminalDimensions();
28
41
 
29
42
  if (!ticker) return <EmptyState title="No ticker selected." />;
@@ -31,20 +44,31 @@ export function OverviewTab({ width }: { width?: number }) {
31
44
  const quote = financials?.quote;
32
45
  const fundamentals = financials?.fundamentals;
33
46
  const profile = financials?.profile;
34
- const baseCurrency = state.config.baseCurrency;
35
47
  const exchangeRates = useFxRatesMap([
36
48
  baseCurrency,
37
49
  ticker.metadata.currency,
38
50
  quote?.currency,
39
51
  ...ticker.metadata.positions.map((position) => position.currency),
40
52
  ]);
41
- const effectiveExchangeRates = selectEffectiveExchangeRates(exchangeRates, state.exchangeRates);
53
+ const effectiveExchangeRates = selectEffectiveExchangeRates(exchangeRates, exchangeRatesState);
42
54
  const quoteCurrency = quote?.currency ?? ticker.metadata.currency ?? baseCurrency;
43
55
  const toBase = (value: number, fromCurrency: string) =>
44
56
  convertCurrency(value, fromCurrency, baseCurrency, effectiveExchangeRates);
45
57
  const sector = ticker.metadata.sector ?? profile?.sector;
46
58
  const industry = ticker.metadata.industry ?? profile?.industry;
47
59
  const description = profile?.description?.trim();
60
+ const listingVenue = exchangeShortName(
61
+ quote?.listingExchangeName ?? quote?.exchangeName,
62
+ quote?.listingExchangeFullName ?? quote?.fullExchangeName,
63
+ );
64
+ const routingVenue = exchangeShortName(quote?.routingExchangeName, quote?.routingExchangeFullName);
65
+ const priceSource = quote?.provenance?.price ? quoteSourceLabel(quote.provenance.price, "price") : "";
66
+ const sessionSource = quote?.provenance?.session ? quoteSourceLabel(quote.provenance.session, "session") : "";
67
+ const metadataParts = [
68
+ priceSource ? `Price source: ${priceSource}` : "",
69
+ sessionSource ? `Session source: ${sessionSource}` : "",
70
+ routingVenue && routingVenue !== listingVenue ? `Route: ${routingVenue}` : "",
71
+ ].filter((part) => part.length > 0);
48
72
 
49
73
  const chartWidth = Math.max((width || Math.floor(termWidth * 0.5)) - 4, 20);
50
74
  const hasHistory = (financials?.priceHistory?.length ?? 0) > 2;
@@ -61,9 +85,9 @@ export function OverviewTab({ width }: { width?: number }) {
61
85
  {" "}- {ticker.metadata.name || quote?.name || ""}
62
86
  </text>
63
87
  )}
64
- {quote?.exchangeName && (
88
+ {listingVenue && (
65
89
  <text fg={colors.textDim}>
66
- {" "}({exchangeShortName(quote.exchangeName, quote.fullExchangeName)})
90
+ {" "}({listingVenue})
67
91
  </text>
68
92
  )}
69
93
  {quote?.marketState && (
@@ -108,11 +132,22 @@ export function OverviewTab({ width }: { width?: number }) {
108
132
  </text>
109
133
  </box>
110
134
  )}
135
+ {metadataParts.length > 0 && (
136
+ <text fg={colors.textDim}>{metadataParts.join(" | ")}</text>
137
+ )}
111
138
  </box>
112
139
  )}
113
140
 
114
141
  {hasHistory && (
115
- <StockChart width={chartWidth} height={8} focused={false} compact />
142
+ <ResolvedStockChart
143
+ width={chartWidth}
144
+ height={8}
145
+ focused={false}
146
+ compact
147
+ symbol={symbol}
148
+ ticker={ticker}
149
+ financials={financials}
150
+ />
116
151
  )}
117
152
 
118
153
  <box flexDirection="column">
@@ -1,23 +1,24 @@
1
1
  import { useKeyboard } from "@opentui/react";
2
- import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import type { DetailTabDef, PaneProps } from "../../../types/plugin";
4
4
  import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
5
5
  import {
6
- useAppState,
6
+ useAppDispatch,
7
+ useAppSelector,
7
8
  usePaneCollection,
8
9
  usePaneInstance,
9
10
  usePaneStateValue,
10
11
  usePaneTicker,
11
12
  } from "../../../state/app-context";
12
13
  import { useQuoteStreaming } from "../../../state/use-quote-streaming";
13
- import { getCollectionName, getCollectionTickers } from "../../../state/selectors";
14
+ import { getCollectionName, getCollectionTickerCount } from "../../../state/selectors";
14
15
  import { getSharedRegistry } from "../../registry";
15
16
  import { EmptyState } from "../../../components";
16
17
  import { TabBar } from "../../../components/tab-bar";
17
18
  import { getConfiguredIbkrGatewayInstances } from "../../ibkr/instance-selection";
18
19
  import { useOptionsAvailability } from "../options-availability";
19
20
  import { ChartTab } from "./chart-tab";
20
- import { FinancialsTab } from "./financials-tab";
21
+ import { ResolvedFinancialsTab } from "./financials-tab";
21
22
  import { OverviewTab } from "./overview-tab";
22
23
  import {
23
24
  buildVisibleDetailTabs,
@@ -26,11 +27,13 @@ import {
26
27
  } from "./settings";
27
28
 
28
29
  export function TickerDetailPane({ focused, width, height }: PaneProps) {
29
- const { state, dispatch } = useAppState();
30
+ const dispatch = useAppDispatch();
31
+ const config = useAppSelector((state) => state.config);
30
32
  const paneInstance = usePaneInstance();
31
- const { ticker, financials } = usePaneTicker();
33
+ const { symbol, ticker, financials } = usePaneTicker();
32
34
  const streamingTarget = quoteSubscriptionTargetFromTicker(ticker, ticker?.metadata.ticker, "provider");
33
- useQuoteStreaming(streamingTarget ? [streamingTarget] : []);
35
+ const streamingTargets = useMemo(() => (streamingTarget ? [streamingTarget] : []), [streamingTarget]);
36
+ useQuoteStreaming(streamingTargets);
34
37
 
35
38
  const { collectionId } = usePaneCollection();
36
39
  const [activeTabId, setActiveTabId] = usePaneStateValue<string>("activeTabId", "overview");
@@ -38,15 +41,20 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
38
41
  const [pluginCaptured, setPluginCaptured] = useState(false);
39
42
  const paneSettings = getTickerDetailPaneSettings(paneInstance?.settings);
40
43
  const hasOptionsChain = useOptionsAvailability(ticker);
41
- const collectionTickers = getCollectionTickers(state, collectionId);
42
- const collectionName = getCollectionName(state, collectionId);
44
+ const collectionTickerCount = useAppSelector((state) => getCollectionTickerCount(state, collectionId));
45
+ const collectionName = useAppSelector((state) => getCollectionName(state, collectionId));
43
46
 
44
- const disabledPlugins = state.config.disabledPlugins || [];
47
+ const disabledPlugins = config.disabledPlugins;
45
48
  const registry = getSharedRegistry();
46
- const pluginTabs: DetailTabDef[] = registry
47
- ? [...registry.detailTabs.values()].filter((tab) => !disabledPlugins.includes(tab.id))
48
- : [];
49
- const hasIbkrGatewayTrading = getConfiguredIbkrGatewayInstances(state.config).length > 0;
49
+ const pluginTabs = useMemo<DetailTabDef[]>(() => (
50
+ registry
51
+ ? [...registry.detailTabs.values()].filter((tab) => !disabledPlugins.includes(tab.id))
52
+ : []
53
+ ), [disabledPlugins, registry]);
54
+ const hasIbkrGatewayTrading = useMemo(
55
+ () => getConfiguredIbkrGatewayInstances(config).length > 0,
56
+ [config],
57
+ );
50
58
  const allTabs = buildVisibleDetailTabs(pluginTabs, ticker, financials, {
51
59
  hasIbkrGatewayTrading,
52
60
  hasOptionsChain,
@@ -135,7 +143,7 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
135
143
  useKeyboard(handleKeyboard);
136
144
 
137
145
  if (!ticker) {
138
- const isEmptyFollowCollection = paneInstance?.binding?.kind === "follow" && !!collectionId && collectionTickers.length === 0;
146
+ const isEmptyFollowCollection = paneInstance?.binding?.kind === "follow" && !!collectionId && collectionTickerCount === 0;
139
147
  const message = isEmptyFollowCollection
140
148
  ? `No tickers in ${collectionName || "this collection"}.`
141
149
  : "No ticker selected.";
@@ -157,8 +165,15 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
157
165
  />
158
166
  )}
159
167
 
160
- {resolvedTabId === "overview" && <OverviewTab width={width} />}
161
- {resolvedTabId === "financials" && <FinancialsTab focused={focused} />}
168
+ {resolvedTabId === "overview" && (
169
+ <OverviewTab
170
+ width={width}
171
+ symbol={symbol}
172
+ ticker={ticker}
173
+ financials={financials}
174
+ />
175
+ )}
176
+ {resolvedTabId === "financials" && <ResolvedFinancialsTab focused={focused} financials={financials} />}
162
177
  {resolvedTabId === "chart" && (
163
178
  <ChartTab
164
179
  width={width}
@@ -167,6 +182,9 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
167
182
  interactive={chartInteractive}
168
183
  axisMode={paneSettings.chartAxisMode}
169
184
  onActivate={() => setChartInteractiveEager(true)}
185
+ symbol={symbol}
186
+ ticker={ticker}
187
+ financials={financials}
170
188
  />
171
189
  )}
172
190
 
@@ -1,7 +1,12 @@
1
1
  import type { PaneSettingsDef, DetailTabDef } from "../../../types/plugin";
2
2
  import type { TickerFinancials } from "../../../types/financials";
3
3
  import type { TickerRecord } from "../../../types/ticker";
4
- import type { ChartAxisMode } from "../../../components/chart/chart-types";
4
+ import type { ChartAxisMode, TimeRange } from "../../../components/chart/chart-types";
5
+ import {
6
+ DEFAULT_TICKER_CHART_RANGE_PRESET,
7
+ DEFAULT_TICKER_CHART_RESOLUTION,
8
+ normalizeChartResolution,
9
+ } from "../../../components/chart/chart-resolution";
5
10
  import { getSharedRegistry } from "../../registry";
6
11
 
7
12
  type DetailTabSummary = { id: string; name: string; order: number };
@@ -14,6 +19,8 @@ export interface TickerDetailPaneSettings {
14
19
  hideTabs: boolean;
15
20
  lockedTabId: string;
16
21
  chartAxisMode: ChartAxisMode;
22
+ chartRangePreset: TimeRange;
23
+ chartResolution: ReturnType<typeof normalizeChartResolution>;
17
24
  }
18
25
 
19
26
  export function getTickerDetailPaneSettings(
@@ -23,6 +30,17 @@ export function getTickerDetailPaneSettings(
23
30
  hideTabs: settings?.hideTabs === true,
24
31
  lockedTabId: typeof settings?.lockedTabId === "string" ? settings.lockedTabId : "overview",
25
32
  chartAxisMode: settings?.chartAxisMode === "percent" ? "percent" : "price",
33
+ chartRangePreset: settings?.chartRangePreset === "1D"
34
+ || settings?.chartRangePreset === "1W"
35
+ || settings?.chartRangePreset === "1M"
36
+ || settings?.chartRangePreset === "3M"
37
+ || settings?.chartRangePreset === "6M"
38
+ || settings?.chartRangePreset === "1Y"
39
+ || settings?.chartRangePreset === "5Y"
40
+ || settings?.chartRangePreset === "ALL"
41
+ ? settings.chartRangePreset
42
+ : DEFAULT_TICKER_CHART_RANGE_PRESET,
43
+ chartResolution: normalizeChartResolution(settings?.chartResolution, DEFAULT_TICKER_CHART_RESOLUTION),
26
44
  };
27
45
  }
28
46
 
@@ -495,6 +495,53 @@ describe("TickerDetailPane", () => {
495
495
  expect(frame).toContain("Builds widgets for industrial customers.");
496
496
  });
497
497
 
498
+ test("shows listing venue, session, sources, and route separately in Overview", async () => {
499
+ setSharedRegistryForTests(makeRegistry());
500
+ setOptionsProvider(createProvider(false));
501
+
502
+ testSetup = await testRender(
503
+ <DetailHarness
504
+ config={createDetailConfig("AMD")}
505
+ ticker={makeTicker("AMD", "Advanced Micro Devices")}
506
+ financials={makeFinancials({
507
+ quote: {
508
+ symbol: "AMD",
509
+ providerId: "ibkr",
510
+ dataSource: "live",
511
+ price: 100,
512
+ currency: "USD",
513
+ change: 1,
514
+ changePercent: 1,
515
+ lastUpdated: Date.now(),
516
+ listingExchangeName: "NASDAQ",
517
+ listingExchangeFullName: "NASDAQ",
518
+ routingExchangeName: "SMART",
519
+ routingExchangeFullName: "SMART",
520
+ marketState: "PRE",
521
+ sessionConfidence: "derived",
522
+ preMarketPrice: 101,
523
+ preMarketChange: 2,
524
+ preMarketChangePercent: 2,
525
+ provenance: {
526
+ price: { providerId: "ibkr", dataSource: "live" },
527
+ session: { providerId: "yahoo", dataSource: "yahoo" },
528
+ },
529
+ },
530
+ })}
531
+ width={110}
532
+ />,
533
+ { width: 110, height: 24 },
534
+ );
535
+
536
+ await flushFrame();
537
+ const frame = testSetup.captureCharFrame();
538
+ expect(frame).toContain("NASDAQ");
539
+ expect(frame).toContain("PRE-MKT");
540
+ expect(frame).toContain("Price source: IBKR live");
541
+ expect(frame).toContain("Session source: Yahoo");
542
+ expect(frame).toContain("Route: SMART");
543
+ });
544
+
498
545
  test("keeps quote prices native while converting market cap and position totals to base currency", async () => {
499
546
  setSharedRegistryForTests(makeRegistry());
500
547
  setOptionsProvider(createProvider(false));
@@ -0,0 +1,65 @@
1
+ import type { GloomPlugin } from "../types/plugin";
2
+ import type { LoadedExternalPlugin } from "./loader";
3
+ import { portfolioListPlugin } from "./builtin/portfolio-list";
4
+ import { tickerDetailPlugin } from "./builtin/ticker-detail";
5
+ import { manualEntryPlugin } from "./builtin/manual-entry";
6
+ import { ibkrPlugin } from "./ibkr";
7
+ import { newsPlugin } from "./builtin/news";
8
+ import { secPlugin } from "./builtin/sec";
9
+ import { optionsPlugin } from "./builtin/options";
10
+ import { notesPlugin } from "./builtin/notes";
11
+ import { aiPlugin } from "./builtin/ai";
12
+ import { gloomberbCloudPlugin } from "./builtin/chat";
13
+ import { helpPlugin } from "./builtin/help";
14
+ import { comparisonChartPlugin } from "./builtin/comparison-chart";
15
+ import { debugPlugin } from "./builtin/debug";
16
+ import { layoutManagerPlugin } from "./builtin/layout-manager";
17
+ import { yahooPlugin } from "./builtin/yahoo";
18
+ import { predictionMarketsPlugin } from "./prediction-markets";
19
+
20
+ export interface PluginCatalogEntry {
21
+ plugin: GloomPlugin;
22
+ source: "builtin" | "external";
23
+ path?: string;
24
+ error?: string;
25
+ }
26
+
27
+ export const builtinPlugins: GloomPlugin[] = [
28
+ yahooPlugin,
29
+ gloomberbCloudPlugin,
30
+ portfolioListPlugin,
31
+ tickerDetailPlugin,
32
+ manualEntryPlugin,
33
+ ibkrPlugin,
34
+ layoutManagerPlugin,
35
+ newsPlugin,
36
+ secPlugin,
37
+ optionsPlugin,
38
+ notesPlugin,
39
+ aiPlugin,
40
+ helpPlugin,
41
+ comparisonChartPlugin,
42
+ predictionMarketsPlugin,
43
+ debugPlugin,
44
+ ];
45
+
46
+ export function getPluginCatalog(externalPlugins: LoadedExternalPlugin[] = []): PluginCatalogEntry[] {
47
+ return [
48
+ ...builtinPlugins.map((plugin) => ({
49
+ plugin,
50
+ source: "builtin" as const,
51
+ })),
52
+ ...externalPlugins.map((entry) => ({
53
+ plugin: entry.plugin,
54
+ source: "external" as const,
55
+ path: entry.path,
56
+ error: entry.error,
57
+ })),
58
+ ];
59
+ }
60
+
61
+ export function getLoadablePlugins(externalPlugins: LoadedExternalPlugin[] = []): GloomPlugin[] {
62
+ return getPluginCatalog(externalPlugins)
63
+ .filter((entry) => !entry.error)
64
+ .map((entry) => entry.plugin);
65
+ }
@@ -98,6 +98,34 @@ export const ibkrBroker: BrokerAdapter = {
98
98
  return ibkrGatewayManager.getService(instance.id).getPriceHistory(ticker, normalized.gateway, exchange, range, instrument);
99
99
  },
100
100
 
101
+ getChartResolutionSupport(ticker, instance, exchange, instrument) {
102
+ const normalized = normalizeIbkrConfig(instance.config);
103
+ if (normalized.connectionMode !== "gateway") {
104
+ throw new Error("Gateway mode is required for broker history");
105
+ }
106
+ return ibkrGatewayManager.getService(instance.id).getChartResolutionSupport(
107
+ ticker,
108
+ normalized.gateway,
109
+ exchange,
110
+ instrument,
111
+ );
112
+ },
113
+
114
+ async getPriceHistoryForResolution(ticker, instance, exchange, bufferRange, resolution, instrument) {
115
+ const normalized = normalizeIbkrConfig(instance.config);
116
+ if (normalized.connectionMode !== "gateway") {
117
+ throw new Error("Gateway mode is required for broker history");
118
+ }
119
+ return ibkrGatewayManager.getService(instance.id).getPriceHistoryForResolution(
120
+ ticker,
121
+ normalized.gateway,
122
+ exchange,
123
+ bufferRange,
124
+ resolution,
125
+ instrument,
126
+ );
127
+ },
128
+
101
129
  async getDetailedPriceHistory(ticker, instance, exchange, startDate, endDate, barSize, instrument) {
102
130
  const normalized = normalizeIbkrConfig(instance.config);
103
131
  if (normalized.connectionMode !== "gateway") {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { ConnectionState, SecType, type ContractDetails, type TickByTickAllLast } from "@stoqey/ib";
2
+ import { BarSizeSetting, ConnectionState, SecType, type ContractDetails, type TickByTickAllLast } from "@stoqey/ib";
3
3
  import { Subject, of } from "rxjs";
4
4
  import {
5
5
  applyTickByTickAllLastToQuote,
@@ -352,6 +352,50 @@ describe("resolveGatewayConnection", () => {
352
352
  });
353
353
 
354
354
  describe("IbkrGatewayService", () => {
355
+ test("reports its manual chart resolution capabilities", () => {
356
+ const service = new IbkrGatewayService("ibkr-test");
357
+ expect(service.getChartResolutionCapabilities("AAPL", {
358
+ host: "127.0.0.1",
359
+ marketDataType: "auto",
360
+ })).toEqual(["1m", "5m", "15m", "30m", "1h", "1d", "1wk", "1mo"]);
361
+ });
362
+
363
+ test("requests fixed-resolution chart history with the mapped IBKR bar size", async () => {
364
+ const service = new IbkrGatewayService("ibkr-test");
365
+ let requestArgs: { duration: string; barSize: string } | null = null;
366
+ (service as any).connect = async () => {};
367
+ (service as any).resolveContract = async () => ({ symbol: "AAPL", exchange: "SMART", currency: "USD" });
368
+ (service as any).getPrimaryContractDetails = async () => undefined;
369
+ (service as any).withMarketDataFallback = async (_config: unknown, task: () => Promise<unknown>) => task();
370
+ (service as any).api = {
371
+ getHistoricalData: async (_contract: unknown, _end: string, duration: string, barSize: string) => {
372
+ requestArgs = { duration, barSize };
373
+ return [{
374
+ time: "20260330",
375
+ open: 99,
376
+ high: 101,
377
+ low: 98,
378
+ close: 100,
379
+ volume: 10,
380
+ }];
381
+ },
382
+ };
383
+
384
+ const history = await service.getPriceHistoryForResolution(
385
+ "AAPL",
386
+ { host: "127.0.0.1", marketDataType: "auto" },
387
+ "NASDAQ",
388
+ "1Y",
389
+ "1wk",
390
+ );
391
+
392
+ expect(requestArgs).toEqual({
393
+ duration: "1 Y",
394
+ barSize: BarSizeSetting.WEEKS_ONE,
395
+ });
396
+ expect(history[0]?.close).toBe(100);
397
+ });
398
+
355
399
  test("enriches imported positions with portfolio snapshot metrics", async () => {
356
400
  const contract = {
357
401
  conId: 123456,