gloomberb 0.3.3 → 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 (143) hide show
  1. package/README.md +27 -16
  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-renderer.ts +7 -2
  17. package/src/components/chart/chart-resolution.test.ts +56 -0
  18. package/src/components/chart/chart-resolution.ts +362 -0
  19. package/src/components/chart/chart-types.ts +12 -4
  20. package/src/components/chart/chart-viewport.test.ts +25 -0
  21. package/src/components/chart/chart-viewport.ts +28 -0
  22. package/src/components/chart/chart.test.ts +26 -0
  23. package/src/components/chart/comparison-chart-data.ts +20 -27
  24. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  25. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  26. package/src/components/chart/stock-chart.tsx +484 -112
  27. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  28. package/src/components/command-bar/command-bar.test.tsx +650 -5
  29. package/src/components/command-bar/command-bar.tsx +359 -63
  30. package/src/components/command-bar/command-registry.ts +8 -0
  31. package/src/components/command-bar/helpers.test.ts +23 -1
  32. package/src/components/command-bar/helpers.ts +14 -5
  33. package/src/components/command-bar/pane-template-display.ts +12 -0
  34. package/src/components/command-bar/root-shortcuts.ts +2 -1
  35. package/src/components/command-bar/workflow-ops.ts +4 -2
  36. package/src/components/detail-feed-view.test.tsx +124 -49
  37. package/src/components/detail-feed-view.tsx +396 -160
  38. package/src/components/layout/floating-pane.tsx +1 -1
  39. package/src/components/layout/pane-header.tsx +26 -2
  40. package/src/components/layout/shell.tsx +62 -50
  41. package/src/components/layout/status-bar.test.tsx +66 -0
  42. package/src/components/layout/status-bar.tsx +26 -5
  43. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  44. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  45. package/src/components/ticker-list-table.test.tsx +83 -0
  46. package/src/components/ticker-list-table.tsx +174 -97
  47. package/src/components/ui/data-table.tsx +27 -2
  48. package/src/components/ui/index.ts +1 -0
  49. package/src/components/ui/page-stack-view.tsx +60 -0
  50. package/src/components/use-double-click-activation.ts +31 -0
  51. package/src/index.tsx +10 -10
  52. package/src/market-data/coordinator.test.ts +92 -4
  53. package/src/market-data/coordinator.ts +32 -5
  54. package/src/market-data/hooks.test.tsx +120 -0
  55. package/src/market-data/hooks.tsx +53 -16
  56. package/src/market-data/request-types.ts +4 -2
  57. package/src/market-data/selectors.ts +24 -58
  58. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  59. package/src/plugins/builtin/chat-controller.ts +310 -35
  60. package/src/plugins/builtin/chat.test.tsx +118 -6
  61. package/src/plugins/builtin/chat.tsx +175 -116
  62. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  63. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  64. package/src/plugins/builtin/news.tsx +1 -11
  65. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  66. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  67. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  68. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  69. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  70. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  71. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  72. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  73. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  74. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  75. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  76. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  77. package/src/plugins/builtin/sec.tsx +1 -11
  78. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  79. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  80. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  81. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  82. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  83. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  84. package/src/plugins/catalog.ts +65 -0
  85. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  86. package/src/plugins/ibkr/gateway-service.test.ts +75 -1
  87. package/src/plugins/ibkr/gateway-service.ts +179 -9
  88. package/src/plugins/pane-manager.test.ts +1 -1
  89. package/src/plugins/plugin-runtime.tsx +27 -7
  90. package/src/plugins/prediction-markets/chart.test.tsx +139 -0
  91. package/src/plugins/prediction-markets/chart.tsx +65 -436
  92. package/src/plugins/prediction-markets/cli.ts +28 -0
  93. package/src/plugins/prediction-markets/controller-data.ts +17 -5
  94. package/src/plugins/prediction-markets/controller.ts +141 -121
  95. package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
  96. package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
  97. package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
  98. package/src/plugins/prediction-markets/detail.test.tsx +4 -0
  99. package/src/plugins/prediction-markets/index.tsx +2 -0
  100. package/src/plugins/prediction-markets/launch.ts +51 -16
  101. package/src/plugins/prediction-markets/metrics.ts +9 -5
  102. package/src/plugins/prediction-markets/pane.test.tsx +160 -58
  103. package/src/plugins/prediction-markets/pane.tsx +74 -183
  104. package/src/plugins/prediction-markets/table.tsx +6 -3
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/sources/gloomberb-cloud.test.ts +29 -0
  107. package/src/sources/gloomberb-cloud.ts +89 -1
  108. package/src/sources/provider-router.test.ts +308 -3
  109. package/src/sources/provider-router.ts +247 -17
  110. package/src/sources/yahoo-finance.test.ts +26 -0
  111. package/src/sources/yahoo-finance.ts +54 -3
  112. package/src/state/app-context-selector.test.tsx +99 -0
  113. package/src/state/app-context.test.ts +18 -0
  114. package/src/state/app-context.tsx +170 -65
  115. package/src/state/selectors.ts +25 -0
  116. package/src/theme/colors.ts +8 -8
  117. package/src/theme/themes.test.ts +2 -0
  118. package/src/theme/themes.ts +22 -10
  119. package/src/types/broker.ts +21 -0
  120. package/src/types/config.ts +17 -1
  121. package/src/types/data-provider.ts +18 -0
  122. package/src/types/financials.ts +31 -1
  123. package/src/types/plugin.ts +70 -0
  124. package/src/utils/api-client.test.ts +57 -0
  125. package/src/utils/api-client.ts +35 -12
  126. package/src/utils/back-navigation.ts +27 -0
  127. package/src/utils/format.test.ts +14 -0
  128. package/src/utils/format.ts +3 -3
  129. package/src/utils/market-status.ts +46 -1
  130. package/src/utils/price-history.test.ts +25 -0
  131. package/src/utils/price-history.ts +29 -5
  132. package/src/utils/quote-freshness.ts +127 -0
  133. package/src/utils/quote-resolution.test.ts +277 -0
  134. package/src/utils/quote-resolution.ts +641 -0
  135. package/src/utils/slugify.ts +3 -0
  136. package/src/utils/timestamp.ts +25 -0
  137. package/src/version.ts +1 -1
  138. package/src/cli/commands/portfolio.ts +0 -210
  139. package/src/cli/commands/watchlist.ts +0 -210
  140. package/src/plugins/builtin/data-providers.ts +0 -16
  141. package/src/sources/exchange-rates.ts +0 -18
  142. package/src/utils/ascii-chart.ts +0 -172
  143. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -1,136 +1,25 @@
1
- import { useRef } from "react";
2
- import { TextAttributes, type BoxRenderable } from "@opentui/core";
3
- import { Spinner, TabBar } from "../../components";
1
+ import { TextAttributes } from "@opentui/core";
2
+ import { PageStackView, TabBar } from "../../components";
3
+ import type { PaneProps } from "../../types/plugin";
4
+ import { colors } from "../../theme/colors";
5
+ import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
4
6
  import { usePredictionMarketsController } from "./controller";
5
7
  import { PredictionMarketDetailPane } from "./detail/pane";
6
8
  import { BROWSE_TABS, VENUE_TABS } from "./navigation";
7
9
  import { PredictionMarketsTable } from "./table";
8
- import { formatPredictionTransportBadge } from "./status";
9
- import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
10
10
  import type { PredictionBrowseTab, PredictionCategoryId } from "./types";
11
- import type { PaneProps } from "../../types/plugin";
12
- import { colors } from "../../theme/colors";
13
- import { formatTimeAgo } from "../../utils/format";
14
-
15
- function clamp(value: number, min: number, max: number): number {
16
- return Math.min(Math.max(value, min), max);
17
- }
18
11
 
19
12
  export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
20
13
  const controller = usePredictionMarketsController({ focused });
21
- const rootRef = useRef<BoxRenderable>(null);
22
- const splitDragRef = useRef<{ startX: number; startRatio: number } | null>(
23
- null,
24
- );
25
- const statusBadge = formatPredictionTransportBadge(
26
- controller.catalogLoadCount > 0 || controller.detailLoadCount > 0
27
- ? "loading"
28
- : controller.transportState,
29
- );
30
- const showSummaryUnitsHint = controller.effectiveVenueScope === "all";
31
- const minTableWidth = 44;
32
- const minDetailWidth = 36;
33
- const effectiveMinTableWidth = controller.selectedSummary
34
- ? Math.min(minTableWidth, Math.max(width - minDetailWidth - 1, 20))
35
- : width;
36
- const effectiveMinDetailWidth = controller.selectedSummary
37
- ? Math.min(minDetailWidth, Math.max(width - effectiveMinTableWidth - 1, 20))
38
- : 0;
39
- const listWidth = controller.selectedSummary
40
- ? clamp(
41
- Math.round(width * controller.detailSplitRatio),
42
- effectiveMinTableWidth,
43
- Math.max(effectiveMinTableWidth, width - effectiveMinDetailWidth - 1),
44
- )
45
- : width;
46
- const detailWidth = controller.selectedSummary
47
- ? Math.max(width - listWidth - 1, effectiveMinDetailWidth)
48
- : 0;
49
- const dividerColor =
50
- controller.focusRegion === "detail" ? colors.borderFocused : colors.border;
51
14
 
52
- const handlePaneMouse = (event: {
53
- type?: string;
54
- x?: number;
55
- preventDefault?: () => void;
56
- stopPropagation?: () => void;
57
- }) => {
58
- if (!controller.selectedSummary || !rootRef.current) {
59
- splitDragRef.current = null;
60
- return;
61
- }
62
-
63
- const localX = (event.x ?? 0) - rootRef.current.x;
64
- const dividerX = listWidth;
65
-
66
- if (
67
- event.type === "down" &&
68
- localX >= dividerX &&
69
- localX <= dividerX + 1
70
- ) {
71
- splitDragRef.current = {
72
- startX: localX,
73
- startRatio: controller.detailSplitRatio,
74
- };
75
- event.preventDefault?.();
76
- event.stopPropagation?.();
77
- return;
78
- }
79
-
80
- if (!splitDragRef.current) return;
81
-
82
- if (event.type === "drag") {
83
- const delta = localX - splitDragRef.current.startX;
84
- const nextListWidth = clamp(
85
- Math.round(width * splitDragRef.current.startRatio) + delta,
86
- effectiveMinTableWidth,
87
- Math.max(effectiveMinTableWidth, width - effectiveMinDetailWidth - 1),
88
- );
89
- controller.actions.setDetailSplitRatio(nextListWidth / Math.max(width, 1));
90
- event.preventDefault?.();
91
- event.stopPropagation?.();
92
- return;
93
- }
94
-
95
- if (event.type === "up" || event.type === "drag-end") {
96
- splitDragRef.current = null;
97
- event.preventDefault?.();
98
- event.stopPropagation?.();
99
- }
100
- };
101
-
102
- return (
15
+ const browseContent = (
103
16
  <box
104
- ref={rootRef}
105
17
  flexDirection="column"
106
18
  width={width}
107
19
  height={height}
108
20
  backgroundColor={colors.panel}
109
- onMouse={handlePaneMouse}
110
21
  >
111
- {!controller.paneSettings.hideHeader && (
112
- <box flexDirection="row" height={1} paddingX={1}>
113
- <text fg={colors.textBright} attributes={TextAttributes.BOLD}>
114
- Prediction Markets
115
- </text>
116
- <box flexGrow={1} />
117
- {showSummaryUnitsHint && (
118
- <text fg={colors.textDim}>VOL = native venue units</text>
119
- )}
120
- <box width={1} />
121
- {controller.searchLoading && <Spinner />}
122
- {controller.searchLoading && <box width={1} />}
123
- <text fg={statusBadge.color}>{statusBadge.label}</text>
124
- <box width={1} />
125
- <text fg={colors.textDim}>
126
- {controller.lastRefreshAt
127
- ? `refresh ${formatTimeAgo(new Date(controller.lastRefreshAt))}`
128
- : "waiting"}
129
- </text>
130
- </box>
131
- )}
132
-
133
- {!controller.paneSettings.hideTabs && (
22
+ {!controller.paneSettings.hideTabs ? (
134
23
  <TabBar
135
24
  tabs={VENUE_TABS.map((tab) => ({
136
25
  label: tab.label,
@@ -140,7 +29,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
140
29
  onSelect={controller.actions.setVenue}
141
30
  compact
142
31
  />
143
- )}
32
+ ) : null}
144
33
 
145
34
  <box flexDirection="row" height={1} paddingX={1} gap={2}>
146
35
  <box
@@ -148,9 +37,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
148
37
  onMouseDown={controller.actions.focusSearch}
149
38
  width={Math.max(18, Math.floor(width * 0.32))}
150
39
  >
151
- <text fg={colors.textDim}>
152
- {controller.searchFocused ? "?" : "/"}
153
- </text>
40
+ <text fg={colors.textDim}>{controller.searchFocused ? "?" : "/"}</text>
154
41
  <box width={1} />
155
42
  {controller.searchFocused ? (
156
43
  <input
@@ -198,7 +85,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
198
85
  </box>
199
86
  </box>
200
87
 
201
- {PREDICTION_CATEGORY_OPTIONS.length > 1 && (
88
+ {PREDICTION_CATEGORY_OPTIONS.length > 1 ? (
202
89
  <scrollbox height={1} scrollX focusable={false}>
203
90
  <box flexDirection="row" paddingX={1} gap={2}>
204
91
  {PREDICTION_CATEGORY_OPTIONS.map((category) => {
@@ -224,71 +111,75 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
224
111
  })}
225
112
  </box>
226
113
  </scrollbox>
227
- )}
114
+ ) : null}
228
115
 
229
- {controller.catalogError && (
116
+ {controller.catalogError ? (
230
117
  <box height={1} paddingX={1}>
231
118
  <text fg={colors.negative}>{controller.catalogError}</text>
232
119
  </box>
233
- )}
234
-
235
- <box flexDirection="row" flexGrow={1}>
236
- <box
237
- width={listWidth}
238
- flexDirection="column"
239
- onMouseDown={controller.actions.focusList}
240
- >
241
- <PredictionMarketsTable
242
- columns={controller.visibleColumns}
243
- rows={controller.visibleRows}
244
- selectedRowKey={controller.selectedRow?.key ?? null}
245
- hoveredIdx={controller.hoveredIdx}
246
- setHoveredIdx={controller.actions.setHoveredIdx}
247
- setSelectedRowKey={controller.actions.selectRow}
248
- watchlist={controller.watchlistSet}
249
- onToggleWatchlist={controller.actions.toggleWatchlist}
250
- sortPreference={controller.sortPreference}
251
- onHeaderClick={controller.actions.handleSortHeaderClick}
252
- headerScrollRef={controller.headerScrollRef}
253
- scrollRef={controller.scrollRef}
254
- syncHeaderScroll={controller.layout.syncHeaderScroll}
255
- onBodyScrollActivity={controller.layout.onBodyScrollActivity}
256
- />
257
- </box>
258
-
259
- {controller.selectedSummary && (
260
- <>
261
- <box width={1}>
262
- <text fg={dividerColor}>│</text>
263
- </box>
264
-
265
- <box
266
- width={detailWidth}
267
- flexDirection="column"
268
- paddingX={1}
269
- onMouseDown={controller.actions.focusDetail}
270
- >
271
- <PredictionMarketDetailPane
272
- detail={controller.detail}
273
- detailError={controller.detailError}
274
- detailLoadCount={controller.detailLoadCount}
275
- detailTab={controller.detailTab}
276
- detailWidth={Math.max(detailWidth - 2, 24)}
277
- focused={controller.focusRegion === "detail"}
278
- height={height}
279
- historyRange={controller.historyRange}
280
- onDetailTabChange={controller.actions.setDetailTab}
281
- onHistoryRangeChange={controller.actions.setHistoryRange}
282
- onPreviewOrder={controller.actions.previewOrder}
283
- onSelectMarket={controller.actions.selectMarket}
284
- scrollRef={controller.detailScrollRef}
285
- selectedRow={controller.selectedRow}
286
- selectedSummary={controller.selectedSummary}
287
- />
288
- </box>
289
- </>
290
- )}
120
+ ) : null}
121
+
122
+ <box flexDirection="column" flexGrow={1}>
123
+ <PredictionMarketsTable
124
+ columns={controller.visibleColumns}
125
+ rows={controller.visibleRows}
126
+ selectedRowKey={controller.selectedRow?.key ?? null}
127
+ hoveredIdx={controller.hoveredIdx}
128
+ setHoveredIdx={controller.actions.setHoveredIdx}
129
+ onSelectRow={(rowKey) => controller.actions.setBrowseSelection(rowKey)}
130
+ onOpenRow={controller.actions.openSelectedRow}
131
+ watchlist={controller.watchlistSet}
132
+ onToggleWatchlist={controller.actions.toggleWatchlist}
133
+ sortPreference={controller.sortPreference}
134
+ onHeaderClick={controller.actions.handleSortHeaderClick}
135
+ headerScrollRef={controller.headerScrollRef}
136
+ scrollRef={controller.scrollRef}
137
+ syncHeaderScroll={controller.layout.syncHeaderScroll}
138
+ onBodyScrollActivity={controller.layout.onBodyScrollActivity}
139
+ />
291
140
  </box>
292
141
  </box>
293
142
  );
143
+
144
+ const detailContent =
145
+ controller.selectedSummary && controller.selectedRow ? (
146
+ <box
147
+ flexDirection="column"
148
+ flexGrow={1}
149
+ width={width}
150
+ height={Math.max(height - 1, 1)}
151
+ paddingX={1}
152
+ backgroundColor={colors.panel}
153
+ >
154
+ <PredictionMarketDetailPane
155
+ detail={controller.detail}
156
+ detailError={controller.detailError}
157
+ detailLoadCount={controller.detailLoadCount}
158
+ detailTab={controller.detailTab}
159
+ detailWidth={Math.max(width - 2, 24)}
160
+ focused={focused && controller.detailOpen}
161
+ height={Math.max(height - 1, 1)}
162
+ historyRange={controller.historyRange}
163
+ onDetailTabChange={controller.actions.setDetailTab}
164
+ onHistoryRangeChange={controller.actions.setHistoryRange}
165
+ onPreviewOrder={controller.actions.previewOrder}
166
+ onSelectMarket={controller.actions.selectMarket}
167
+ scrollRef={controller.detailScrollRef}
168
+ selectedRow={controller.selectedRow}
169
+ selectedSummary={controller.selectedSummary}
170
+ />
171
+ </box>
172
+ ) : (
173
+ <box flexGrow={1} backgroundColor={colors.panel} />
174
+ );
175
+
176
+ return (
177
+ <PageStackView
178
+ focused={focused}
179
+ detailOpen={controller.detailOpen && !!controller.selectedSummary}
180
+ onBack={controller.actions.closeDetail}
181
+ rootContent={browseContent}
182
+ detailContent={detailContent}
183
+ />
184
+ );
294
185
  }
@@ -14,7 +14,8 @@ export function PredictionMarketsTable({
14
14
  selectedRowKey,
15
15
  hoveredIdx,
16
16
  setHoveredIdx,
17
- setSelectedRowKey,
17
+ onSelectRow,
18
+ onOpenRow,
18
19
  watchlist,
19
20
  onToggleWatchlist,
20
21
  sortPreference,
@@ -29,7 +30,8 @@ export function PredictionMarketsTable({
29
30
  selectedRowKey: string | null;
30
31
  hoveredIdx: number | null;
31
32
  setHoveredIdx: (index: number | null) => void;
32
- setSelectedRowKey: (rowKey: string) => void;
33
+ onSelectRow: (rowKey: string) => void;
34
+ onOpenRow: (rowKey: string) => void;
33
35
  watchlist: Set<string>;
34
36
  onToggleWatchlist: (row: PredictionListRow) => void;
35
37
  sortPreference: PredictionSortPreference;
@@ -56,7 +58,8 @@ export function PredictionMarketsTable({
56
58
  setHoveredIdx={setHoveredIdx}
57
59
  getItemKey={(row) => row.key}
58
60
  isSelected={(row) => selectedRowKey === row.key}
59
- onSelect={(row) => setSelectedRowKey(row.key)}
61
+ onSelect={(row) => onSelectRow(row.key)}
62
+ onActivate={(row) => onOpenRow(row.key)}
60
63
  virtualize
61
64
  renderCell={(row, column, _index, rowState) => {
62
65
  const watchlisted = row.watchMarketKeys.some((marketKey) =>
@@ -5,7 +5,6 @@ export type PredictionVenue = "polymarket" | "kalshi";
5
5
  export type PredictionVenueScope = "all" | PredictionVenue;
6
6
  export type PredictionBrowseTab = "top" | "ending" | "new" | "watchlist";
7
7
  export type PredictionDetailTab = "overview" | "book" | "trades" | "rules";
8
- export type PredictionFocusRegion = "list" | "detail";
9
8
  export type PredictionHistoryRange = "1D" | "1W" | "1M" | "ALL";
10
9
  export type PredictionVolumeUnit = "usd" | "contracts";
11
10
  export type PredictionSortDirection = "asc" | "desc";
@@ -26,6 +26,11 @@ afterEach(() => {
26
26
  });
27
27
 
28
28
  describe("GloomberbCloudProvider", () => {
29
+ test("reports its manual chart resolution capabilities", () => {
30
+ const provider = new GloomberbCloudProvider();
31
+ expect(provider.getChartResolutionCapabilities()).toEqual(["1m", "5m", "15m", "30m", "45m", "1h", "1d", "1wk", "1mo"]);
32
+ });
33
+
29
34
  test("fetches detailed intraday chart history with Twelve Data intervals", async () => {
30
35
  apiClient.ensureVerifiedSession = async () => verifiedUser;
31
36
 
@@ -91,6 +96,30 @@ describe("GloomberbCloudProvider", () => {
91
96
  });
92
97
  });
93
98
 
99
+ test("fetches fixed-resolution chart history with the requested interval", async () => {
100
+ apiClient.ensureVerifiedSession = async () => verifiedUser;
101
+
102
+ let requestArgs: Record<string, string | number | undefined> | null = null;
103
+ apiClient.getCloudHistory = async (_symbol, _exchange, params = {}) => {
104
+ requestArgs = params;
105
+ return {
106
+ status: "success",
107
+ data: [{
108
+ date: "2026-03-27",
109
+ close: 250.12,
110
+ }],
111
+ };
112
+ };
113
+
114
+ const provider = new GloomberbCloudProvider();
115
+ const history = await provider.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1wk");
116
+
117
+ expect(requestArgs?.interval).toBe("1week");
118
+ expect(requestArgs?.startDate).toBeDefined();
119
+ expect(requestArgs?.endDate).toBeDefined();
120
+ expect(history[0]?.close).toBe(250.12);
121
+ });
122
+
94
123
  test("normalizes sub-unit cloud quotes to their main currency", async () => {
95
124
  apiClient.ensureVerifiedSession = async () => verifiedUser;
96
125
  apiClient.getCloudQuote = async () => ({
@@ -1,4 +1,9 @@
1
1
  import type { TimeRange } from "../components/chart/chart-types";
2
+ import {
3
+ normalizeChartResolutionSupport,
4
+ type ChartResolutionSupport,
5
+ type ManualChartResolution,
6
+ } from "../components/chart/chart-resolution";
2
7
  import type { DataProvider, MarketDataRequestContext, NewsItem, QuoteSubscriptionTarget, SearchRequestContext, SecFilingItem } from "../types/data-provider";
3
8
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
9
  import type { InstrumentSearchResult } from "../types/instrument";
@@ -14,6 +19,17 @@ import { normalizePriceValueByDivisor, resolveCurrencyUnit } from "../utils/curr
14
19
  import { createProviderMiss } from "./provider-errors";
15
20
 
16
21
  const providerId = "gloomberb-cloud" as const;
22
+ const CLOUD_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
23
+ { resolution: "1m", maxRange: "1W" },
24
+ { resolution: "5m", maxRange: "1M" },
25
+ { resolution: "15m", maxRange: "3M" },
26
+ { resolution: "30m", maxRange: "6M" },
27
+ { resolution: "45m", maxRange: "6M" },
28
+ { resolution: "1h", maxRange: "1Y" },
29
+ { resolution: "1d", maxRange: "ALL" },
30
+ { resolution: "1wk", maxRange: "ALL" },
31
+ { resolution: "1mo", maxRange: "ALL" },
32
+ ]);
17
33
  const CLOUD_PROVIDER_MISS_PATTERNS = [
18
34
  /data not found/i,
19
35
  /symbol.*missing or invalid/i,
@@ -39,6 +55,8 @@ async function withCloudFallback<T>(load: () => Promise<T>, message: string): Pr
39
55
 
40
56
  function mapQuote(quote: CloudQuotePayload): Quote {
41
57
  const { currency, divisor } = resolveCurrencyUnit(quote.currency);
58
+ const listingExchangeName = quote.listingExchangeName ?? quote.exchangeName;
59
+ const listingExchangeFullName = quote.listingExchangeFullName ?? quote.fullExchangeName ?? listingExchangeName;
42
60
  return {
43
61
  ...quote,
44
62
  currency: currency || quote.currency,
@@ -57,6 +75,10 @@ function mapQuote(quote: CloudQuotePayload): Quote {
57
75
  preMarketChange: normalizePriceValueByDivisor(quote.preMarketChange, divisor),
58
76
  postMarketPrice: normalizePriceValueByDivisor(quote.postMarketPrice, divisor),
59
77
  postMarketChange: normalizePriceValueByDivisor(quote.postMarketChange, divisor),
78
+ listingExchangeName,
79
+ listingExchangeFullName,
80
+ exchangeName: listingExchangeName,
81
+ fullExchangeName: listingExchangeFullName,
60
82
  providerId,
61
83
  };
62
84
  }
@@ -92,7 +114,7 @@ function toCloudInterval(interval: string): string {
92
114
  return "45min";
93
115
  case "1d":
94
116
  return "1day";
95
- case "1w":
117
+ case "1wk":
96
118
  return "1week";
97
119
  case "1mo":
98
120
  return "1month";
@@ -118,12 +140,45 @@ function formatCloudDateTime(date: Date, includeTime: boolean): string {
118
140
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
119
141
  }
120
142
 
143
+ function getRangeStartDate(range: TimeRange, endDate = new Date()): Date {
144
+ const startDate = new Date(endDate);
145
+ switch (range) {
146
+ case "1D":
147
+ startDate.setDate(startDate.getDate() - 1);
148
+ break;
149
+ case "1W":
150
+ startDate.setDate(startDate.getDate() - 7);
151
+ break;
152
+ case "1M":
153
+ startDate.setMonth(startDate.getMonth() - 1);
154
+ break;
155
+ case "3M":
156
+ startDate.setMonth(startDate.getMonth() - 3);
157
+ break;
158
+ case "6M":
159
+ startDate.setMonth(startDate.getMonth() - 6);
160
+ break;
161
+ case "1Y":
162
+ startDate.setFullYear(startDate.getFullYear() - 1);
163
+ break;
164
+ case "5Y":
165
+ startDate.setFullYear(startDate.getFullYear() - 5);
166
+ break;
167
+ case "ALL":
168
+ startDate.setFullYear(startDate.getFullYear() - 20);
169
+ break;
170
+ }
171
+ return startDate;
172
+ }
173
+
121
174
  function toHistoryRequest(range: TimeRange): {
122
175
  interval: string;
123
176
  outputsize: number;
124
177
  rangeKey: TimeRange;
125
178
  } {
126
179
  switch (range) {
180
+ case "1D":
181
+ return { interval: "5min", outputsize: 24 * 12, rangeKey: range };
127
182
  case "1W":
128
183
  return { interval: "1h", outputsize: 7 * 24, rangeKey: range };
129
184
  case "1M":
@@ -179,6 +234,14 @@ export class GloomberbCloudProvider implements DataProvider {
179
234
  readonly name = "Gloomberb Cloud";
180
235
  readonly priority = 100;
181
236
 
237
+ getChartResolutionSupport(): ChartResolutionSupport[] {
238
+ return CLOUD_RESOLUTION_SUPPORT;
239
+ }
240
+
241
+ getChartResolutionCapabilities(): ManualChartResolution[] {
242
+ return CLOUD_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
243
+ }
244
+
182
245
  async canProvide(): Promise<boolean> {
183
246
  return !!(await apiClient.ensureVerifiedSession());
184
247
  }
@@ -260,6 +323,31 @@ export class GloomberbCloudProvider implements DataProvider {
260
323
  .map((point) => mapPricePoint(point, divisor));
261
324
  }
262
325
 
326
+ async getPriceHistoryForResolution(
327
+ ticker: string,
328
+ exchange: string,
329
+ bufferRange: TimeRange,
330
+ resolution: ManualChartResolution,
331
+ _context?: MarketDataRequestContext,
332
+ ): Promise<PricePoint[]> {
333
+ await requireVerifiedSession();
334
+ const interval = toCloudInterval(resolution);
335
+ const endDate = new Date();
336
+ const startDate = getRangeStartDate(bufferRange, endDate);
337
+ const includeTime = /(min|h)$/i.test(interval);
338
+ const response = await withCloudFallback(
339
+ () => apiClient.getCloudHistory(ticker, exchange, {
340
+ interval,
341
+ startDate: formatCloudDateTime(startDate, includeTime),
342
+ endDate: formatCloudDateTime(endDate, includeTime),
343
+ }),
344
+ `Cloud chart data is unavailable for ${ticker}`,
345
+ );
346
+ const { divisor } = resolveCurrencyUnit(response.providerMeta?.currency);
347
+ return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
348
+ .map((point) => mapPricePoint(point, divisor));
349
+ }
350
+
263
351
  async getDetailedPriceHistory(
264
352
  ticker: string,
265
353
  exchange: string,