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.
- package/README.md +27 -16
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-renderer.ts +7 -2
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/chart.test.ts +26 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +484 -112
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +308 -3
- package/src/sources/provider-router.ts +247 -17
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -1,136 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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.
|
|
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="
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 "
|
|
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,
|