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,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 {
|
|
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
|
-
<
|
|
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 {
|
|
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 {
|
|
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({
|
|
25
|
-
|
|
26
|
-
|
|
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,
|
|
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
|
-
{
|
|
88
|
+
{listingVenue && (
|
|
65
89
|
<text fg={colors.textDim}>
|
|
66
|
-
{" "}({
|
|
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
|
-
<
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
47
|
+
const disabledPlugins = config.disabledPlugins;
|
|
45
48
|
const registry = getSharedRegistry();
|
|
46
|
-
const pluginTabs
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 &&
|
|
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" &&
|
|
161
|
-
|
|
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,11 +1,12 @@
|
|
|
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,
|
|
6
6
|
applyTickByTickBidAskToQuote,
|
|
7
7
|
diagnoseLocalIbkrPortIssue,
|
|
8
8
|
IbkrGatewayService,
|
|
9
|
+
parseIbkrHistoricalBarTime,
|
|
9
10
|
resolveGatewayConnection,
|
|
10
11
|
summarizeBrokerAccount,
|
|
11
12
|
} from "./gateway-service";
|
|
@@ -239,6 +240,35 @@ describe("tick-by-tick quote updates", () => {
|
|
|
239
240
|
});
|
|
240
241
|
});
|
|
241
242
|
|
|
243
|
+
describe("parseIbkrHistoricalBarTime", () => {
|
|
244
|
+
test("parses compact day bars without treating them as epoch seconds", () => {
|
|
245
|
+
const date = parseIbkrHistoricalBarTime(20250328);
|
|
246
|
+
|
|
247
|
+
expect(Number.isNaN(date.getTime())).toBe(false);
|
|
248
|
+
expect(date.getFullYear()).toBe(2025);
|
|
249
|
+
expect(date.getMonth()).toBe(2);
|
|
250
|
+
expect(date.getDate()).toBe(28);
|
|
251
|
+
expect(date.getHours()).toBe(0);
|
|
252
|
+
expect(date.getMinutes()).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("parses compact intraday bar timestamps", () => {
|
|
256
|
+
const date = parseIbkrHistoricalBarTime("20250328 09:30:00");
|
|
257
|
+
|
|
258
|
+
expect(Number.isNaN(date.getTime())).toBe(false);
|
|
259
|
+
expect(date.getFullYear()).toBe(2025);
|
|
260
|
+
expect(date.getMonth()).toBe(2);
|
|
261
|
+
expect(date.getDate()).toBe(28);
|
|
262
|
+
expect(date.getHours()).toBe(9);
|
|
263
|
+
expect(date.getMinutes()).toBe(30);
|
|
264
|
+
expect(date.getSeconds()).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("preserves epoch-second timestamps for streaming-style bars", () => {
|
|
268
|
+
expect(parseIbkrHistoricalBarTime(1_700_000_000).getTime()).toBe(1_700_000_000_000);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
242
272
|
describe("diagnoseLocalIbkrPortIssue", () => {
|
|
243
273
|
test("reports a detected local IBKR listener on a different port", async () => {
|
|
244
274
|
const issue = await diagnoseLocalIbkrPortIssue({
|
|
@@ -322,6 +352,50 @@ describe("resolveGatewayConnection", () => {
|
|
|
322
352
|
});
|
|
323
353
|
|
|
324
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
|
+
|
|
325
399
|
test("enriches imported positions with portfolio snapshot metrics", async () => {
|
|
326
400
|
const contract = {
|
|
327
401
|
conId: 123456,
|