gloomberb 0.9.1 → 0.9.3
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 +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +5 -2
- package/src/components/layout/header.tsx +13 -5
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-controls.tsx +19 -10
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/price-performance.tsx +183 -0
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
- package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +22 -2
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +9 -8
- package/src/renderers/electrobun/view/ui-host.tsx +102 -77
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { PricePoint, Quote } from "../types/financials";
|
|
2
|
+
|
|
3
|
+
export interface PriceReturnHorizon {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
amount: number;
|
|
7
|
+
unit: "month" | "year";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PriceReturnField {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
value: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PRICE_RETURN_HORIZONS: PriceReturnHorizon[] = [
|
|
17
|
+
{ id: "1M", label: "1M", amount: 1, unit: "month" },
|
|
18
|
+
{ id: "3M", label: "3M", amount: 3, unit: "month" },
|
|
19
|
+
{ id: "6M", label: "6M", amount: 6, unit: "month" },
|
|
20
|
+
{ id: "1Y", label: "1Y", amount: 1, unit: "year" },
|
|
21
|
+
{ id: "3Y", label: "3Y", amount: 3, unit: "year" },
|
|
22
|
+
{ id: "5Y", label: "5Y", amount: 5, unit: "year" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function coercePointDate(value: Date | string | number): Date | null {
|
|
26
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
27
|
+
return Number.isFinite(date.getTime()) ? date : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizePriceReturnHistory(points: readonly PricePoint[]): PricePoint[] {
|
|
31
|
+
const byTimestamp = new Map<number, PricePoint>();
|
|
32
|
+
for (const point of points) {
|
|
33
|
+
const date = coercePointDate(point.date as Date | string | number);
|
|
34
|
+
if (!date || !Number.isFinite(point.close)) continue;
|
|
35
|
+
byTimestamp.set(date.getTime(), { ...point, date });
|
|
36
|
+
}
|
|
37
|
+
return [...byTimestamp.entries()]
|
|
38
|
+
.sort((left, right) => left[0] - right[0])
|
|
39
|
+
.map(([, point]) => point);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function appendQuoteToPriceReturnHistory(
|
|
43
|
+
points: readonly PricePoint[],
|
|
44
|
+
quote: Pick<Quote, "price" | "lastUpdated"> | null | undefined,
|
|
45
|
+
): PricePoint[] {
|
|
46
|
+
const history = normalizePriceReturnHistory(points);
|
|
47
|
+
if (!quote || !Number.isFinite(quote.price) || !Number.isFinite(quote.lastUpdated)) {
|
|
48
|
+
return history;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const quoteDate = new Date(quote.lastUpdated);
|
|
52
|
+
if (!Number.isFinite(quoteDate.getTime())) return history;
|
|
53
|
+
|
|
54
|
+
const latestHistoryTime = history.at(-1)?.date.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
55
|
+
if (quoteDate.getTime() <= latestHistoryTime) return history;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
...history,
|
|
59
|
+
{
|
|
60
|
+
date: quoteDate,
|
|
61
|
+
close: quote.price,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function subtractHorizon(date: Date, horizon: PriceReturnHorizon): Date {
|
|
67
|
+
const target = new Date(date);
|
|
68
|
+
if (horizon.unit === "month") {
|
|
69
|
+
target.setMonth(target.getMonth() - horizon.amount);
|
|
70
|
+
} else {
|
|
71
|
+
target.setFullYear(target.getFullYear() - horizon.amount);
|
|
72
|
+
}
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function computePriceReturnForHorizon(
|
|
77
|
+
points: readonly PricePoint[],
|
|
78
|
+
horizon: PriceReturnHorizon,
|
|
79
|
+
): number | null {
|
|
80
|
+
const history = normalizePriceReturnHistory(points);
|
|
81
|
+
if (history.length < 2) return null;
|
|
82
|
+
|
|
83
|
+
const latest = history.at(-1)!;
|
|
84
|
+
const cutoff = subtractHorizon(latest.date, horizon);
|
|
85
|
+
let baseline: PricePoint | null = null;
|
|
86
|
+
|
|
87
|
+
for (const point of history) {
|
|
88
|
+
if (point.date.getTime() <= cutoff.getTime()) {
|
|
89
|
+
baseline = point;
|
|
90
|
+
} else {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!baseline || !Number.isFinite(baseline.close) || baseline.close === 0) return null;
|
|
96
|
+
if (!Number.isFinite(latest.close)) return null;
|
|
97
|
+
return (latest.close - baseline.close) / baseline.close;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildPriceReturnFields(
|
|
101
|
+
points: readonly PricePoint[],
|
|
102
|
+
horizons: readonly PriceReturnHorizon[] = PRICE_RETURN_HORIZONS,
|
|
103
|
+
): PriceReturnField[] {
|
|
104
|
+
return horizons.map((horizon) => ({
|
|
105
|
+
id: horizon.id,
|
|
106
|
+
label: horizon.label,
|
|
107
|
+
value: computePriceReturnForHorizon(points, horizon),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
@@ -267,6 +267,7 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
|
|
|
267
267
|
inputRef={searchInputRef}
|
|
268
268
|
onFocus={focusSearch}
|
|
269
269
|
onBlur={blurSearch}
|
|
270
|
+
onNavigateDown={blurSearch}
|
|
270
271
|
onQueryChange={updateFeedQuery}
|
|
271
272
|
/>
|
|
272
273
|
) : null;
|
|
@@ -309,6 +310,7 @@ export function TwitterFeedPane({ focused, width, height }: PaneProps) {
|
|
|
309
310
|
load={loadActiveFeed}
|
|
310
311
|
onResult={markFeedResult}
|
|
311
312
|
onError={markFeedError}
|
|
313
|
+
onFocusSearch={focusSearch}
|
|
312
314
|
emptyStateTitle={searchEnabled ? "No tweets" : "Enter a search query"}
|
|
313
315
|
emptyStateHint={searchEnabled ? activeFeedQuery : undefined}
|
|
314
316
|
/>
|
|
@@ -15,6 +15,7 @@ export function TwitterFeedSearchBar({
|
|
|
15
15
|
inputRef,
|
|
16
16
|
onFocus,
|
|
17
17
|
onBlur,
|
|
18
|
+
onNavigateDown,
|
|
18
19
|
onQueryChange,
|
|
19
20
|
}: {
|
|
20
21
|
feed: TwitterFeed;
|
|
@@ -25,6 +26,7 @@ export function TwitterFeedSearchBar({
|
|
|
25
26
|
inputRef: RefObject<InputRenderable | null>;
|
|
26
27
|
onFocus: () => void;
|
|
27
28
|
onBlur: () => void;
|
|
29
|
+
onNavigateDown?: () => void;
|
|
28
30
|
onQueryChange: (feedId: string, query: string) => void;
|
|
29
31
|
}) {
|
|
30
32
|
const updateQuery = useCallback((value: string) => {
|
|
@@ -43,6 +45,7 @@ export function TwitterFeedSearchBar({
|
|
|
43
45
|
debounceMs={TWEET_SEARCH_DEBOUNCE_MS}
|
|
44
46
|
onFocus={onFocus}
|
|
45
47
|
onBlur={onBlur}
|
|
48
|
+
onNavigateDown={onNavigateDown}
|
|
46
49
|
onQueryChange={updateQuery}
|
|
47
50
|
/>
|
|
48
51
|
);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
usePaneFooter,
|
|
7
7
|
type DataTableCell,
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
|
+
type DataTableRootKeyContext,
|
|
9
10
|
} from "../../../components";
|
|
10
11
|
import { TickerBadgeText } from "../../../components/ticker/badge/text";
|
|
11
12
|
import { RemoteImage } from "../../../components/ui";
|
|
@@ -15,6 +16,7 @@ import type { CloudTweetPayload, CloudTweetSearchResponse } from "../../../api-c
|
|
|
15
16
|
import { formatTimeAgo } from "../../../utils/format";
|
|
16
17
|
import { colors } from "../../../theme/colors";
|
|
17
18
|
import { CloudAuthNotice } from "../cloud/auth-actions";
|
|
19
|
+
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
|
|
18
20
|
import {
|
|
19
21
|
buildTweetColumns,
|
|
20
22
|
formatMetric,
|
|
@@ -152,6 +154,7 @@ export function TweetSearchTable({
|
|
|
152
154
|
load,
|
|
153
155
|
onResult,
|
|
154
156
|
onError,
|
|
157
|
+
onFocusSearch,
|
|
155
158
|
emptyStateTitle,
|
|
156
159
|
emptyStateHint,
|
|
157
160
|
}: {
|
|
@@ -165,6 +168,7 @@ export function TweetSearchTable({
|
|
|
165
168
|
load: () => Promise<CloudTweetSearchResponse>;
|
|
166
169
|
onResult?: (result: CloudTweetSearchResponse) => void;
|
|
167
170
|
onError?: (message: string) => void;
|
|
171
|
+
onFocusSearch?: () => void;
|
|
168
172
|
emptyStateTitle?: string;
|
|
169
173
|
emptyStateHint?: string;
|
|
170
174
|
}) {
|
|
@@ -219,13 +223,21 @@ export function TweetSearchTable({
|
|
|
219
223
|
));
|
|
220
224
|
}, []);
|
|
221
225
|
|
|
222
|
-
const handleRootKeyDown = useCallback((
|
|
226
|
+
const handleRootKeyDown = useCallback((
|
|
227
|
+
event: DataTableKeyEvent,
|
|
228
|
+
context: DataTableRootKeyContext,
|
|
229
|
+
) => {
|
|
230
|
+
if (onFocusSearch && context.selectedIndex <= 0 && isPlainArrowUp(event)) {
|
|
231
|
+
stopSearchFocusNavigation(event);
|
|
232
|
+
onFocusSearch();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
223
235
|
if (event.name !== "r") return false;
|
|
224
236
|
event.preventDefault?.();
|
|
225
237
|
event.stopPropagation?.();
|
|
226
238
|
reload();
|
|
227
239
|
return true;
|
|
228
|
-
}, [reload]);
|
|
240
|
+
}, [onFocusSearch, reload]);
|
|
229
241
|
|
|
230
242
|
const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
231
243
|
if (event.name !== "o") return false;
|
|
@@ -6,6 +6,7 @@ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../
|
|
|
6
6
|
import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane/footer";
|
|
7
7
|
import {
|
|
8
8
|
AppContext,
|
|
9
|
+
type AppAction,
|
|
9
10
|
createInitialState,
|
|
10
11
|
PaneInstanceProvider,
|
|
11
12
|
} from "../../../state/app/context";
|
|
@@ -13,7 +14,7 @@ import { Box } from "../../../ui";
|
|
|
13
14
|
import { cloneLayout, createDefaultConfig } from "../../../types/config";
|
|
14
15
|
import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
|
|
15
16
|
import type { DataProvider } from "../../../types/data-provider";
|
|
16
|
-
import type { TickerFinancials } from "../../../types/financials";
|
|
17
|
+
import type { PricePoint, TickerFinancials } from "../../../types/financials";
|
|
17
18
|
import type { PluginRegistry } from "../../registry";
|
|
18
19
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
|
|
19
20
|
import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../../registry";
|
|
@@ -30,6 +31,12 @@ let root: ReturnType<typeof createRoot> | undefined;
|
|
|
30
31
|
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
31
32
|
const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
|
32
33
|
|
|
34
|
+
interface ComparisonHarnessOptions {
|
|
35
|
+
dispatch?: (action: AppAction) => void;
|
|
36
|
+
focused?: boolean;
|
|
37
|
+
focusedPaneId?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function makeTicker(symbol: string, currency: string): TickerRecord {
|
|
34
41
|
return {
|
|
35
42
|
metadata: {
|
|
@@ -68,6 +75,35 @@ function makeFinancials(symbol: string, currency: string, closes: number[]): Tic
|
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
function makeDatedFinancials(symbol: string, currency: string, closes: Array<[string, number]>): TickerFinancials {
|
|
79
|
+
const latest = closes.at(-1);
|
|
80
|
+
const latestClose = latest?.[1] ?? 0;
|
|
81
|
+
const latestDate = latest ? Date.parse(`${latest[0]}T00:00:00Z`) : Date.now();
|
|
82
|
+
return {
|
|
83
|
+
annualStatements: [],
|
|
84
|
+
quarterlyStatements: [],
|
|
85
|
+
quote: {
|
|
86
|
+
symbol,
|
|
87
|
+
price: latestClose,
|
|
88
|
+
currency,
|
|
89
|
+
change: latestClose - (closes[0]?.[1] ?? latestClose),
|
|
90
|
+
changePercent: closes[0]?.[1] ? ((latestClose - closes[0]![1]) / closes[0]![1]) * 100 : 0,
|
|
91
|
+
lastUpdated: latestDate,
|
|
92
|
+
},
|
|
93
|
+
priceHistory: closes.map(([date, close]) => ({
|
|
94
|
+
date: new Date(`${date}T00:00:00Z`),
|
|
95
|
+
close,
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function makeDatedPriceHistory(closes: Array<[string, number]>): PricePoint[] {
|
|
101
|
+
return closes.map(([date, close]) => ({
|
|
102
|
+
date: new Date(`${date}T00:00:00Z`),
|
|
103
|
+
close,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
function createProvider(historyBySymbol: Record<string, number[]>, currencyBySymbol: Record<string, string>): DataProvider {
|
|
72
108
|
return {
|
|
73
109
|
id: "test-provider",
|
|
@@ -101,6 +137,46 @@ function createProvider(historyBySymbol: Record<string, number[]>, currencyBySym
|
|
|
101
137
|
};
|
|
102
138
|
}
|
|
103
139
|
|
|
140
|
+
function createDatedProvider(financialsBySymbol: Record<string, TickerFinancials>): DataProvider {
|
|
141
|
+
const provider = createProvider({}, {});
|
|
142
|
+
provider.getTickerFinancials = async (symbol) => financialsBySymbol[symbol] ?? makeDatedFinancials(symbol, "USD", []);
|
|
143
|
+
provider.getQuote = async (symbol) => financialsBySymbol[symbol]?.quote ?? {
|
|
144
|
+
symbol,
|
|
145
|
+
price: 0,
|
|
146
|
+
currency: "USD",
|
|
147
|
+
change: 0,
|
|
148
|
+
changePercent: 0,
|
|
149
|
+
lastUpdated: Date.now(),
|
|
150
|
+
};
|
|
151
|
+
provider.getPriceHistory = async (symbol) => financialsBySymbol[symbol]?.priceHistory ?? [];
|
|
152
|
+
provider.getPriceHistoryForResolution = async (symbol) => financialsBySymbol[symbol]?.priceHistory ?? [];
|
|
153
|
+
return provider;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function createRangeAwareDatedProvider({
|
|
157
|
+
fullHistoryBySymbol,
|
|
158
|
+
shortHistoryBySymbol,
|
|
159
|
+
}: {
|
|
160
|
+
fullHistoryBySymbol: Record<string, Array<[string, number]>>;
|
|
161
|
+
shortHistoryBySymbol: Record<string, Array<[string, number]>>;
|
|
162
|
+
}): DataProvider {
|
|
163
|
+
const financialsBySymbol = Object.fromEntries(
|
|
164
|
+
Object.entries(shortHistoryBySymbol).map(([symbol, history]) => [
|
|
165
|
+
symbol,
|
|
166
|
+
makeDatedFinancials(symbol, "USD", history),
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
const provider = createDatedProvider(financialsBySymbol);
|
|
170
|
+
const resolveHistory = (symbol: string, range: string) => makeDatedPriceHistory(
|
|
171
|
+
range === "5Y"
|
|
172
|
+
? fullHistoryBySymbol[symbol] ?? shortHistoryBySymbol[symbol] ?? []
|
|
173
|
+
: shortHistoryBySymbol[symbol] ?? [],
|
|
174
|
+
);
|
|
175
|
+
provider.getPriceHistory = async (symbol, _exchange, range) => resolveHistory(symbol, range);
|
|
176
|
+
provider.getPriceHistoryForResolution = async (symbol, _exchange, range) => resolveHistory(symbol, range);
|
|
177
|
+
return provider;
|
|
178
|
+
}
|
|
179
|
+
|
|
104
180
|
function createRegistrySpy(spy: { selected: string[]; focused: string[] }): PluginRegistry {
|
|
105
181
|
return {
|
|
106
182
|
selectTicker: (symbol: string) => { spy.selected.push(symbol); },
|
|
@@ -133,7 +209,13 @@ function createComparisonHarness(
|
|
|
133
209
|
tickers: TickerRecord[],
|
|
134
210
|
financials: Array<[string, TickerFinancials]>,
|
|
135
211
|
runtime: PluginRuntimeAccess,
|
|
212
|
+
options: ComparisonHarnessOptions = {},
|
|
136
213
|
) {
|
|
214
|
+
const {
|
|
215
|
+
dispatch = () => {},
|
|
216
|
+
focused = true,
|
|
217
|
+
focusedPaneId = focused ? TEST_PANE_ID : null,
|
|
218
|
+
} = options;
|
|
137
219
|
const config = createDefaultConfig("/tmp/gloomberb-compare");
|
|
138
220
|
const layout = {
|
|
139
221
|
dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
|
|
@@ -151,7 +233,7 @@ function createComparisonHarness(
|
|
|
151
233
|
layouts: [{ name: "Default", layout: cloneLayout(layout) }],
|
|
152
234
|
};
|
|
153
235
|
const state = createInitialState(nextConfig);
|
|
154
|
-
state.focusedPaneId =
|
|
236
|
+
state.focusedPaneId = focusedPaneId;
|
|
155
237
|
state.tickers = new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker]));
|
|
156
238
|
state.financials = new Map(financials);
|
|
157
239
|
|
|
@@ -164,7 +246,7 @@ function createComparisonHarness(
|
|
|
164
246
|
}) => ReactElement;
|
|
165
247
|
|
|
166
248
|
return (
|
|
167
|
-
<AppContext value={{ state, dispatch
|
|
249
|
+
<AppContext value={{ state, dispatch }}>
|
|
168
250
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
169
251
|
<PluginRenderProvider pluginId="comparison-chart" runtime={runtime}>
|
|
170
252
|
<PaneFooterProvider>
|
|
@@ -173,11 +255,11 @@ function createComparisonHarness(
|
|
|
173
255
|
<ComparisonPane
|
|
174
256
|
paneId={TEST_PANE_ID}
|
|
175
257
|
paneType="comparison-chart"
|
|
176
|
-
focused
|
|
258
|
+
focused={focused}
|
|
177
259
|
width={120}
|
|
178
260
|
height={19}
|
|
179
261
|
/>
|
|
180
|
-
<PaneFooterBar footer={footer} focused width={120} />
|
|
262
|
+
<PaneFooterBar footer={footer} focused={focused} width={120} />
|
|
181
263
|
</Box>
|
|
182
264
|
)}
|
|
183
265
|
</PaneFooterProvider>
|
|
@@ -203,13 +285,14 @@ async function mountComparisonHarness(
|
|
|
203
285
|
tickers: TickerRecord[],
|
|
204
286
|
financials: Array<[string, TickerFinancials]>,
|
|
205
287
|
spy: { selected: string[]; focused: string[]; settings?: string[] } = { selected: [], focused: [] },
|
|
288
|
+
options: ComparisonHarnessOptions = {},
|
|
206
289
|
) {
|
|
207
290
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
208
291
|
testSetup = await createTestRenderer({ width: 120, height: 20 });
|
|
209
292
|
root = createRoot(testSetup.renderer);
|
|
210
293
|
await act(async () => {
|
|
211
294
|
root!.render(
|
|
212
|
-
createComparisonHarness(settings, tickers, financials, createRuntimeSpy(spy)),
|
|
295
|
+
createComparisonHarness(settings, tickers, financials, createRuntimeSpy(spy), options),
|
|
213
296
|
);
|
|
214
297
|
await Promise.resolve();
|
|
215
298
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -314,7 +397,7 @@ describe("comparisonChartPlugin", () => {
|
|
|
314
397
|
expect(frame).not.toContain("side by side");
|
|
315
398
|
});
|
|
316
399
|
|
|
317
|
-
test("updates every
|
|
400
|
+
test("updates every return summary range value from the crosshair position", async () => {
|
|
318
401
|
const provider = createProvider({
|
|
319
402
|
AAPL: [100, 102, 104],
|
|
320
403
|
MSFT: [200, 202, 204],
|
|
@@ -343,8 +426,12 @@ describe("comparisonChartPlugin", () => {
|
|
|
343
426
|
|
|
344
427
|
let frame = testSetup!.captureCharFrame();
|
|
345
428
|
expect(frame).not.toContain("AAPL - 1D");
|
|
346
|
-
expect(frame).toContain("
|
|
347
|
-
expect(frame).toContain("
|
|
429
|
+
expect(frame).toContain("Sym");
|
|
430
|
+
expect(frame).toContain("Rng");
|
|
431
|
+
expect(frame).toContain("1Y");
|
|
432
|
+
expect(frame).toContain("5Y");
|
|
433
|
+
expect(frame).toMatch(/> AAPL\s+\+4\.00%/);
|
|
434
|
+
expect(frame).toMatch(/MSFT\s+\+2\.00%/);
|
|
348
435
|
|
|
349
436
|
await act(async () => {
|
|
350
437
|
await testSetup!.mockMouse.moveTo(2, 4);
|
|
@@ -353,8 +440,124 @@ describe("comparisonChartPlugin", () => {
|
|
|
353
440
|
await flushFrames();
|
|
354
441
|
|
|
355
442
|
frame = testSetup!.captureCharFrame();
|
|
356
|
-
expect(frame).
|
|
357
|
-
expect(frame).
|
|
443
|
+
expect(frame).toMatch(/> AAPL\s+0\.00%/);
|
|
444
|
+
expect(frame).toMatch(/MSFT\s+0\.00%/);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("focuses the pane when an unfocused comparison plot is clicked", async () => {
|
|
448
|
+
const dispatched: AppAction[] = [];
|
|
449
|
+
const provider = createProvider({
|
|
450
|
+
AAPL: [100, 102, 104],
|
|
451
|
+
MSFT: [200, 202, 204],
|
|
452
|
+
}, {
|
|
453
|
+
AAPL: "USD",
|
|
454
|
+
MSFT: "USD",
|
|
455
|
+
});
|
|
456
|
+
setSharedMarketDataForTests(provider);
|
|
457
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
458
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
459
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
460
|
+
|
|
461
|
+
await mountComparisonHarness({
|
|
462
|
+
axisMode: "percent",
|
|
463
|
+
symbols: ["AAPL", "MSFT"],
|
|
464
|
+
symbolsText: "AAPL, MSFT",
|
|
465
|
+
}, [
|
|
466
|
+
makeTicker("AAPL", "USD"),
|
|
467
|
+
makeTicker("MSFT", "USD"),
|
|
468
|
+
], [
|
|
469
|
+
["AAPL", makeFinancials("AAPL", "USD", [100, 102, 104])],
|
|
470
|
+
["MSFT", makeFinancials("MSFT", "USD", [200, 202, 204])],
|
|
471
|
+
], { selected: [], focused: [] }, {
|
|
472
|
+
dispatch: (action) => { dispatched.push(action); },
|
|
473
|
+
focused: false,
|
|
474
|
+
focusedPaneId: "ticker-detail:main",
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await flushFrames();
|
|
478
|
+
await act(async () => {
|
|
479
|
+
await testSetup!.mockMouse.click(20, 8);
|
|
480
|
+
await testSetup!.renderOnce();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(dispatched).toContainEqual({ type: "FOCUS_PANE", paneId: TEST_PANE_ID });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("keeps fixed return horizons independent from the selected chart range", async () => {
|
|
487
|
+
const shortHistory = {
|
|
488
|
+
AAPL: [["2025-01-02", 180], ["2026-01-02", 200]],
|
|
489
|
+
MSFT: [["2025-01-02", 300], ["2026-01-02", 330]],
|
|
490
|
+
} satisfies Record<string, Array<[string, number]>>;
|
|
491
|
+
const fullHistory = {
|
|
492
|
+
AAPL: [["2021-01-02", 100], ["2023-01-02", 150], ["2025-01-02", 180], ["2026-01-02", 200]],
|
|
493
|
+
MSFT: [["2021-01-02", 220], ["2023-01-02", 270], ["2025-01-02", 300], ["2026-01-02", 330]],
|
|
494
|
+
} satisfies Record<string, Array<[string, number]>>;
|
|
495
|
+
const provider = createRangeAwareDatedProvider({
|
|
496
|
+
fullHistoryBySymbol: fullHistory,
|
|
497
|
+
shortHistoryBySymbol: shortHistory,
|
|
498
|
+
});
|
|
499
|
+
setSharedMarketDataForTests(provider);
|
|
500
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
501
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
502
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
503
|
+
|
|
504
|
+
await mountComparisonHarness({
|
|
505
|
+
axisMode: "percent",
|
|
506
|
+
rangePreset: "1Y",
|
|
507
|
+
chartResolution: "1d",
|
|
508
|
+
symbols: ["AAPL", "MSFT"],
|
|
509
|
+
symbolsText: "AAPL, MSFT",
|
|
510
|
+
}, [
|
|
511
|
+
makeTicker("AAPL", "USD"),
|
|
512
|
+
makeTicker("MSFT", "USD"),
|
|
513
|
+
], [
|
|
514
|
+
["AAPL", makeDatedFinancials("AAPL", "USD", shortHistory.AAPL)],
|
|
515
|
+
["MSFT", makeDatedFinancials("MSFT", "USD", shortHistory.MSFT)],
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
await flushFrames(8);
|
|
519
|
+
|
|
520
|
+
const frame = testSetup!.captureCharFrame();
|
|
521
|
+
expect(frame).toContain("5Y");
|
|
522
|
+
expect(frame).toMatch(/> AAPL\s+\+11\.11%[^\n]*\+100\.00%/);
|
|
523
|
+
expect(frame).toMatch(/MSFT\s+\+10\.00%[^\n]*\+50\.00%/);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("sorts comparison summary rows by one-year return", async () => {
|
|
527
|
+
const rows = {
|
|
528
|
+
SLOW: makeDatedFinancials("SLOW", "USD", [["2025-01-02", 100], ["2026-01-02", 105]]),
|
|
529
|
+
FAST: makeDatedFinancials("FAST", "USD", [["2025-01-02", 100], ["2026-01-02", 150]]),
|
|
530
|
+
MID: makeDatedFinancials("MID", "USD", [["2025-01-02", 100], ["2026-01-02", 125]]),
|
|
531
|
+
};
|
|
532
|
+
const provider = createDatedProvider(rows);
|
|
533
|
+
setSharedMarketDataForTests(provider);
|
|
534
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
535
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
536
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
537
|
+
|
|
538
|
+
await mountComparisonHarness({
|
|
539
|
+
axisMode: "percent",
|
|
540
|
+
symbols: ["SLOW", "FAST", "MID"],
|
|
541
|
+
symbolsText: "SLOW, FAST, MID",
|
|
542
|
+
}, [
|
|
543
|
+
makeTicker("SLOW", "USD"),
|
|
544
|
+
makeTicker("FAST", "USD"),
|
|
545
|
+
makeTicker("MID", "USD"),
|
|
546
|
+
], Object.entries(rows));
|
|
547
|
+
|
|
548
|
+
await flushFrames();
|
|
549
|
+
|
|
550
|
+
const frame = testSetup!.captureCharFrame();
|
|
551
|
+
const table = frame.slice(frame.indexOf("Sym"));
|
|
552
|
+
const fastIndex = table.indexOf("FAST");
|
|
553
|
+
const midIndex = table.indexOf("MID");
|
|
554
|
+
const slowIndex = table.indexOf("SLOW");
|
|
555
|
+
|
|
556
|
+
expect(fastIndex).toBeGreaterThanOrEqual(0);
|
|
557
|
+
expect(midIndex).toBeGreaterThanOrEqual(0);
|
|
558
|
+
expect(slowIndex).toBeGreaterThanOrEqual(0);
|
|
559
|
+
expect(fastIndex).toBeLessThan(midIndex);
|
|
560
|
+
expect(midIndex).toBeLessThan(slowIndex);
|
|
358
561
|
});
|
|
359
562
|
|
|
360
563
|
test("extends lagging comparison histories with the latest quote", async () => {
|
|
@@ -406,11 +609,11 @@ describe("comparisonChartPlugin", () => {
|
|
|
406
609
|
await flushFrames();
|
|
407
610
|
|
|
408
611
|
const frame = testSetup!.captureCharFrame();
|
|
409
|
-
expect(frame).
|
|
410
|
-
expect(frame).
|
|
612
|
+
expect(frame).toMatch(/> AAPL\s+\+47\.06%/);
|
|
613
|
+
expect(frame).toMatch(/MSFT\s+\+23\.76%/);
|
|
411
614
|
});
|
|
412
615
|
|
|
413
|
-
test("updates comparison
|
|
616
|
+
test("updates comparison return summary from chart-owned quote streams", async () => {
|
|
414
617
|
let emitQuote: ((symbol: string, price: number) => void) | null = null;
|
|
415
618
|
const provider = createProvider({
|
|
416
619
|
AAPL: [100, 102],
|
|
@@ -464,7 +667,7 @@ describe("comparisonChartPlugin", () => {
|
|
|
464
667
|
await flushFrames();
|
|
465
668
|
|
|
466
669
|
const frame = testSetup!.captureCharFrame();
|
|
467
|
-
expect(frame).
|
|
670
|
+
expect(frame).toMatch(/> AAPL\s+\+51\.96%/);
|
|
468
671
|
});
|
|
469
672
|
|
|
470
673
|
test("does not refetch resolution support when only comparison prices update", async () => {
|