gloomberb 0.12.0 → 0.13.2
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 +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -19,8 +19,8 @@ export function AccountChooserPanel({
|
|
|
19
19
|
const choices = useMemo<ListViewItem[]>(() => [
|
|
20
20
|
{
|
|
21
21
|
id: "qr",
|
|
22
|
-
label: t("
|
|
23
|
-
description: t("
|
|
22
|
+
label: t("Continue in browser"),
|
|
23
|
+
description: t("Sign in securely, or scan with your phone"),
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
id: "signup",
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { Box
|
|
3
|
-
import { colors } from "../../../theme/colors";
|
|
4
|
-
import { t } from "../../../i18n";
|
|
2
|
+
import { Box } from "../../../ui";
|
|
5
3
|
import { useAppLanguage } from "../../../i18n/react";
|
|
6
4
|
import { useShortcut } from "../../../react/input";
|
|
7
5
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
@@ -56,13 +54,7 @@ export function AccountQrPanel({
|
|
|
56
54
|
|
|
57
55
|
return (
|
|
58
56
|
<Box flexDirection="column" paddingX={2}>
|
|
59
|
-
<
|
|
60
|
-
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
|
|
61
|
-
{t("Scan with the Gloom app to sign in")}
|
|
62
|
-
</Text>
|
|
63
|
-
</Box>
|
|
64
|
-
<Box height={1} />
|
|
65
|
-
<DeviceSignInPanel snapshot={snapshot} height={Math.max(4, height - 2)} />
|
|
57
|
+
<DeviceSignInPanel snapshot={snapshot} height={height} />
|
|
66
58
|
</Box>
|
|
67
59
|
);
|
|
68
60
|
}
|
|
@@ -63,6 +63,7 @@ function createPluginRegistry(options: {
|
|
|
63
63
|
tickerRepository: options.tickerRepository ?? createTickerRepository(),
|
|
64
64
|
persistence: { resources: undefined },
|
|
65
65
|
openCommandBar: () => {},
|
|
66
|
+
navigateTicker: () => {},
|
|
66
67
|
} as unknown as PluginRegistry;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -265,7 +266,7 @@ describe("OnboardingWizard", () => {
|
|
|
265
266
|
expect(capturedConfig?.onboardingProgress?.stage).toBe("add-ticker");
|
|
266
267
|
});
|
|
267
268
|
|
|
268
|
-
test("
|
|
269
|
+
test("opens research before Cloud after the expected AP save", async () => {
|
|
269
270
|
tempDataDir = await mkdtemp(join(tmpdir(), "gloomberb-onboarding-actions-"));
|
|
270
271
|
const pluginRegistry = createPluginRegistry();
|
|
271
272
|
const config = {
|
|
@@ -300,12 +301,12 @@ describe("OnboardingWizard", () => {
|
|
|
300
301
|
await Bun.sleep(0);
|
|
301
302
|
await testSetup!.renderOnce();
|
|
302
303
|
});
|
|
303
|
-
const frame = await waitForFrame("
|
|
304
|
-
expect(frame).toContain("
|
|
305
|
-
expect(capturedConfig?.onboardingProgress?.stage).toBe("
|
|
304
|
+
const frame = await waitForFrame("Connect free Cloud");
|
|
305
|
+
expect(frame).toContain("Connect free Cloud");
|
|
306
|
+
expect(capturedConfig?.onboardingProgress?.stage).toBe("research");
|
|
306
307
|
});
|
|
307
308
|
|
|
308
|
-
test("imports a broker portfolio before
|
|
309
|
+
test("imports a broker portfolio before opening research", async () => {
|
|
309
310
|
tempDataDir = await mkdtemp(join(tmpdir(), "gloomberb-onboarding-broker-"));
|
|
310
311
|
const tickerRepository = createTickerRepository();
|
|
311
312
|
const broker: BrokerAdapter = {
|
|
@@ -348,10 +349,10 @@ describe("OnboardingWizard", () => {
|
|
|
348
349
|
await waitForFrame("Connect Demo Broker");
|
|
349
350
|
await emitKeypress({ name: "return", sequence: "\r" });
|
|
350
351
|
|
|
351
|
-
const frame = await waitForFrame("
|
|
352
|
-
expect(frame).toContain("
|
|
352
|
+
const frame = await waitForFrame("Connect free Cloud");
|
|
353
|
+
expect(frame).toContain("Connect free Cloud");
|
|
353
354
|
expect(capturedConfig?.onboardingProgress).toMatchObject({
|
|
354
|
-
stage: "
|
|
355
|
+
stage: "research",
|
|
355
356
|
path: "broker",
|
|
356
357
|
tickerSymbol: "AAPL",
|
|
357
358
|
brokerName: "Demo Broker",
|
|
@@ -511,9 +512,9 @@ describe("OnboardingWizard", () => {
|
|
|
511
512
|
await testSetup!.renderOnce();
|
|
512
513
|
});
|
|
513
514
|
|
|
514
|
-
await waitForFrame("
|
|
515
|
+
await waitForFrame("Connect free Cloud");
|
|
515
516
|
expect((await tickerRepository.loadAllTickers()).map((ticker) => ticker.metadata.ticker).sort()).toEqual(["AAPL", "MSFT"]);
|
|
516
|
-
expect(capturedConfig?.onboardingProgress?.stage).toBe("
|
|
517
|
+
expect(capturedConfig?.onboardingProgress?.stage).toBe("research");
|
|
517
518
|
});
|
|
518
519
|
|
|
519
520
|
test("keeps Skip locked until broker onboarding finalization completes", async () => {
|
|
@@ -590,9 +591,34 @@ describe("OnboardingWizard", () => {
|
|
|
590
591
|
await testSetup!.renderOnce();
|
|
591
592
|
});
|
|
592
593
|
|
|
593
|
-
await waitForFrame("
|
|
594
|
+
await waitForFrame("Connect free Cloud");
|
|
594
595
|
expect(completionCount).toBe(0);
|
|
595
|
-
expect(capturedConfig?.onboardingProgress?.stage).toBe("
|
|
596
|
+
expect(capturedConfig?.onboardingProgress?.stage).toBe("research");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("waits for email verification before offering Pro", async () => {
|
|
600
|
+
tempDataDir = await mkdtemp(join(tmpdir(), "gloomberb-onboarding-verify-"));
|
|
601
|
+
const originalGetSession = apiClient.getSession;
|
|
602
|
+
let resolveSession!: (user: Awaited<ReturnType<typeof apiClient.getSession>>) => void;
|
|
603
|
+
apiClient.getSession = () => new Promise((resolve) => { resolveSession = resolve; });
|
|
604
|
+
try {
|
|
605
|
+
testSetup = await testRender(<WizardHarness config={{
|
|
606
|
+
...createDefaultConfig(tempDataDir),
|
|
607
|
+
onboardingProgress: { version: 1, stage: "verify", accountStatus: "signed-in" },
|
|
608
|
+
}} pluginRegistry={createPluginRegistry()} />, { width: 100, height: 30 });
|
|
609
|
+
await testSetup.renderOnce();
|
|
610
|
+
expect(testSetup.captureCharFrame()).toContain("Check your email");
|
|
611
|
+
expect(capturedConfig?.onboardingProgress?.stage).toBe("verify");
|
|
612
|
+
apiClient.setSessionToken("verification-test-session");
|
|
613
|
+
apiClient.restoreCachedUser({ id: "verified-user", email: "research@example.com", emailVerified: true, plan: "free" });
|
|
614
|
+
await act(async () => {
|
|
615
|
+
resolveSession(apiClient.getCurrentUser());
|
|
616
|
+
await Bun.sleep(0);
|
|
617
|
+
await testSetup!.renderOnce();
|
|
618
|
+
});
|
|
619
|
+
await waitForFrame("Start 7-day free trial");
|
|
620
|
+
expect(capturedConfig?.onboardingProgress?.stage).toBe("upgrade");
|
|
621
|
+
} finally { apiClient.getSession = originalGetSession; }
|
|
596
622
|
});
|
|
597
623
|
|
|
598
624
|
test("loads the current Cloud price on the Pro step", async () => {
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
useRef,
|
|
6
6
|
useState,
|
|
7
7
|
} from "react";
|
|
8
|
+
import { CloudVerificationPanel } from "../../plugins/builtin/cloud/verification-panel";
|
|
9
|
+
import { recordResearchActivity } from "../../api-client/research-activity";
|
|
8
10
|
import { apiClient, type CloudPricing } from "../../api-client";
|
|
9
11
|
import type { AppBrokerImportRuntime } from "../../app/runtime/broker-import";
|
|
10
12
|
import type { SyncBrokerInstanceResult } from "../../brokers/sync-broker-instance";
|
|
@@ -72,6 +74,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
72
74
|
const stage = progress.stage;
|
|
73
75
|
const [persistenceError, setPersistenceError] = useState<string | null>(null);
|
|
74
76
|
const [isFinishing, setIsFinishing] = useState(false);
|
|
77
|
+
const researchOpenedRef = useRef<string | null>(null);
|
|
75
78
|
const [pricing, setPricing] = useState<CloudPricing | null>(null);
|
|
76
79
|
|
|
77
80
|
const [portfolioSub, setPortfolioSub] = useState<PortfolioSub>("choose");
|
|
@@ -92,7 +95,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
92
95
|
const portfolioChoices = useMemo<ListViewItem[]>(() => [
|
|
93
96
|
{
|
|
94
97
|
id: "manual",
|
|
95
|
-
label: t("
|
|
98
|
+
label: t("Research a company"),
|
|
96
99
|
description: t("Add one company with the command bar"),
|
|
97
100
|
detail: t("Recommended"),
|
|
98
101
|
},
|
|
@@ -148,7 +151,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
148
151
|
|
|
149
152
|
const account = useOnboardingAccount({
|
|
150
153
|
nextStep: () => {
|
|
151
|
-
saveProgressInBackground({ stage: "upgrade", accountStatus: "signed-in" });
|
|
154
|
+
saveProgressInBackground({ stage: apiClient.getCurrentUser()?.emailVerified ? "upgrade" : "verify", accountStatus: "signed-in" });
|
|
152
155
|
},
|
|
153
156
|
setEditingField,
|
|
154
157
|
});
|
|
@@ -163,7 +166,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
163
166
|
?? result.updatedTickers[0]?.metadata.ticker;
|
|
164
167
|
const brokerName = brokerOptions.find((option) => option.id === selectedBrokerId)?.name;
|
|
165
168
|
const nextConfig = withOnboardingProgress(syncedConfig, {
|
|
166
|
-
stage: tickerSymbol ? "
|
|
169
|
+
stage: tickerSymbol ? "research" : "add-ticker",
|
|
167
170
|
path: "broker",
|
|
168
171
|
portfolioId: tickerSymbol ? (result.portfolioIds[0] ?? "main") : "main",
|
|
169
172
|
tickerSymbol,
|
|
@@ -248,7 +251,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
248
251
|
const current = getOnboardingProgress(stateRef.current.config);
|
|
249
252
|
if (current.stage !== "add-ticker" || portfolioId !== current.portfolioId) return;
|
|
250
253
|
saveProgressInBackground({
|
|
251
|
-
stage: "
|
|
254
|
+
stage: "research",
|
|
252
255
|
path: current.path,
|
|
253
256
|
portfolioId,
|
|
254
257
|
tickerSymbol: symbol,
|
|
@@ -256,6 +259,13 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
256
259
|
},
|
|
257
260
|
), [pluginRegistry.events, saveProgressInBackground, stateRef]);
|
|
258
261
|
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (stage !== "research" || !progress.tickerSymbol || researchOpenedRef.current === progress.tickerSymbol) return;
|
|
264
|
+
researchOpenedRef.current = progress.tickerSymbol;
|
|
265
|
+
pluginRegistry.navigateTicker(progress.tickerSymbol, { sourcePaneId: "ticker-detail:main" });
|
|
266
|
+
recordResearchActivity("ticker_saved");
|
|
267
|
+
}, [stage, progress.tickerSymbol, pluginRegistry]);
|
|
268
|
+
|
|
259
269
|
useEffect(() => {
|
|
260
270
|
if (stage !== "upgrade" || pricing) return;
|
|
261
271
|
void apiClient.getCloudPricing().then(setPricing).catch(() => {});
|
|
@@ -439,7 +449,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
439
449
|
// The QR panel owns enter (retry after a denial); approval advances itself.
|
|
440
450
|
if (account.accountSub === "qr") return;
|
|
441
451
|
if (account.accountSub === "signed-in") {
|
|
442
|
-
saveProgressInBackground({ stage: "upgrade", accountStatus: "signed-in" });
|
|
452
|
+
saveProgressInBackground({ stage: apiClient.getCurrentUser()?.emailVerified ? "upgrade" : "verify", accountStatus: "signed-in" });
|
|
443
453
|
return;
|
|
444
454
|
}
|
|
445
455
|
if (account.accountSubmitError?.kind === "switch-to-login") {
|
|
@@ -584,6 +594,11 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
584
594
|
}
|
|
585
595
|
return;
|
|
586
596
|
}
|
|
597
|
+
if (stage === "research" && enter) {
|
|
598
|
+
consume();
|
|
599
|
+
saveProgressInBackground({ stage: "account" });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
587
602
|
if (stage === "account") {
|
|
588
603
|
if (enter) {
|
|
589
604
|
consume();
|
|
@@ -630,6 +645,26 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
630
645
|
return null;
|
|
631
646
|
}
|
|
632
647
|
|
|
648
|
+
if (stage === "research") {
|
|
649
|
+
return <OnboardingCoach step={t("YOUR FIRST RESEARCH WORKSPACE")}
|
|
650
|
+
title={tf("Explore {ticker}", { ticker: progress.tickerSymbol ?? "your company" })}
|
|
651
|
+
actions={<><OnboardingButton label="Keep exploring" variant="ghost" onPress={() => { void finish(); }} />
|
|
652
|
+
<OnboardingButton label="Connect free Cloud" variant="primary" onPress={() => saveProgressInBackground({ stage: "account" })} /></>}>
|
|
653
|
+
<Text fg={colors.textDim} wrapText>{t("Your company is saved. Explore the chart and financials now. Connect Cloud when you're ready for synced layouts, news and Pro research.")}</Text>
|
|
654
|
+
</OnboardingCoach>;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (stage === "verify") {
|
|
658
|
+
return <OnboardingModal width={66} height={14}>
|
|
659
|
+
<OnboardingTitle step={t("CONNECT CLOUD")} title={t("Check your email")}
|
|
660
|
+
description={apiClient.getCurrentUser()?.email ?? account.accountEmail} />
|
|
661
|
+
<CloudVerificationPanel onVerified={() => {
|
|
662
|
+
void chatController.refreshSession();
|
|
663
|
+
saveProgressInBackground({ stage: "upgrade", accountStatus: "signed-in" });
|
|
664
|
+
}} onContinueFree={() => { void finish(); }} />
|
|
665
|
+
</OnboardingModal>;
|
|
666
|
+
}
|
|
667
|
+
|
|
633
668
|
if (stage === "add-ticker") {
|
|
634
669
|
return (
|
|
635
670
|
<OnboardingCoach
|
|
@@ -664,7 +699,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
664
699
|
<OnboardingTitle
|
|
665
700
|
step={t("WELCOME")}
|
|
666
701
|
title={t("Make Gloomberb yours")}
|
|
667
|
-
description={t("
|
|
702
|
+
description={t("Pick a company. Explore its chart and financials. Keep your workspace free, or add Pro research when you need it.")}
|
|
668
703
|
/>
|
|
669
704
|
{persistenceError ? (
|
|
670
705
|
<Text fg={colors.negative} wrapText style={desktop ? { marginTop: 10 } : undefined}>
|
|
@@ -673,7 +708,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
673
708
|
) : null}
|
|
674
709
|
<OnboardingActions>
|
|
675
710
|
<OnboardingButton
|
|
676
|
-
label="
|
|
711
|
+
label="Research my first company"
|
|
677
712
|
variant="primary"
|
|
678
713
|
onPress={() => saveProgressInBackground({ stage: "portfolio" })}
|
|
679
714
|
/>
|
|
@@ -771,14 +806,14 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
771
806
|
: account.accountSub === "login"
|
|
772
807
|
? t("Log in")
|
|
773
808
|
: account.accountSub === "qr"
|
|
774
|
-
? t("
|
|
809
|
+
? t("Continue in browser")
|
|
775
810
|
: account.accountSub === "signed-in"
|
|
776
811
|
? t("Connected")
|
|
777
812
|
: t("Sync across apps");
|
|
778
813
|
const accountDescription = account.accountSub === "choose"
|
|
779
|
-
? t("
|
|
814
|
+
? t("Sync your layouts. Search calls, news and filings.")
|
|
780
815
|
: account.accountSub === "qr"
|
|
781
|
-
? t("Open the
|
|
816
|
+
? t("Open the sign-in link, or scan the code with your phone.")
|
|
782
817
|
: account.accountSub === "signup"
|
|
783
818
|
? t("Create the free account now. Cloud features unlock after you verify your email.")
|
|
784
819
|
: account.accountSub === "login"
|
|
@@ -791,7 +826,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
791
826
|
// The QR grid is the tallest thing this wizard ever shows; DeviceSignInPanel
|
|
792
827
|
// degrades to the code plus URL when the terminal cannot give it these rows.
|
|
793
828
|
const accountModalHeight = account.accountSub === "choose"
|
|
794
|
-
?
|
|
829
|
+
? 21
|
|
795
830
|
: account.accountSub === "qr"
|
|
796
831
|
? 32
|
|
797
832
|
: account.accountSub === "signed-in"
|
|
@@ -887,7 +922,7 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
887
922
|
) : undefined}
|
|
888
923
|
description={planAccess.hasProAccess
|
|
889
924
|
? t("This account already has real-time Cloud data.")
|
|
890
|
-
: t("
|
|
925
|
+
: t("7 days free, then the price above. Card required. Cancel anytime.")}
|
|
891
926
|
/>
|
|
892
927
|
<Box flexDirection="column" style={desktop ? { marginTop: 14, gap: 8 } : undefined}>
|
|
893
928
|
<OnboardingFeature
|
|
@@ -899,8 +934,8 @@ export function OnboardingWizard({ pluginRegistry, importBrokerPositions, onComp
|
|
|
899
934
|
description={t("Free: 12h delayed. Pro: real-time.")}
|
|
900
935
|
/>
|
|
901
936
|
<OnboardingFeature
|
|
902
|
-
title={t("
|
|
903
|
-
description={t("
|
|
937
|
+
title={t("Earnings transcripts and instant search")}
|
|
938
|
+
description={t("Read calls. Search transcripts, news and filings. Ask Gloomberb AI coming soon.")}
|
|
904
939
|
/>
|
|
905
940
|
</Box>
|
|
906
941
|
{persistenceError ? (
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
findPaneInstance,
|
|
12
12
|
} from "../../../types/config";
|
|
13
13
|
import { getDockedPaneIds } from "../../../plugins/pane-manager";
|
|
14
|
+
import { EXTRACTED_PLUGINS, seedExtractedPlugins } from "../../../plugins/seed";
|
|
14
15
|
|
|
15
16
|
const tempDirs: string[] = [];
|
|
16
17
|
|
|
@@ -48,6 +49,28 @@ async function writeConfigJson(dataDir: string, config: Record<string, unknown>)
|
|
|
48
49
|
await writeFile(join(dataDir, "config.json"), JSON.stringify(config), "utf-8");
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
test("fresh installs skip plugin restoration across config reloads", async () => {
|
|
53
|
+
const dataDir = await createTempConfigDir();
|
|
54
|
+
const fresh = await loadConfig(dataDir);
|
|
55
|
+
await saveConfig(fresh);
|
|
56
|
+
const reloaded = await loadConfig(dataDir);
|
|
57
|
+
const installed: string[] = [];
|
|
58
|
+
await seedExtractedPlugins(reloaded, async (ref) => { installed.push(ref); }, dataDir);
|
|
59
|
+
expect(installed).toEqual([]);
|
|
60
|
+
expect(reloaded.seededPlugins).toEqual(EXTRACTED_PLUGINS.map((plugin) => plugin.id));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("existing users retain pending and completed plugin migrations", async () => {
|
|
64
|
+
const dataDir = await createTempConfigDir();
|
|
65
|
+
await writeConfigJson(dataDir, createSavedConfig());
|
|
66
|
+
expect((await loadConfig(dataDir)).seededPlugins).toEqual([]);
|
|
67
|
+
await writeConfigJson(dataDir, createSavedConfig({ seededPlugins: ["substack", "substack", 42] }));
|
|
68
|
+
const loaded = await loadConfig(dataDir);
|
|
69
|
+
expect(loaded.seededPlugins).toEqual(["substack"]);
|
|
70
|
+
await saveConfig(loaded);
|
|
71
|
+
expect((await loadConfig(dataDir)).seededPlugins).toEqual(["substack"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
51
74
|
describe("sanitizeLayout", () => {
|
|
52
75
|
test("preserves an intentionally blank layout", () => {
|
|
53
76
|
expect(sanitizeLayout({
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from "path";
|
|
|
4
4
|
import type { AppConfig } from "../../../types/config";
|
|
5
5
|
import { createDefaultConfig } from "../../../types/config";
|
|
6
6
|
import { debugLog } from "../../../utils/debug-log";
|
|
7
|
+
import { EXTRACTED_PLUGINS } from "../../../plugins/seed";
|
|
7
8
|
import {
|
|
8
9
|
normalizeConfigForSave,
|
|
9
10
|
normalizeLoadedConfig,
|
|
@@ -54,7 +55,12 @@ async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; ne
|
|
|
54
55
|
const saved = JSON.parse(raw) as Record<string, unknown>;
|
|
55
56
|
return normalizeLoadedConfig(saved, dataDir);
|
|
56
57
|
} catch {
|
|
57
|
-
|
|
58
|
+
// Fresh installs have no former built-in plugins to restore. Record the
|
|
59
|
+
// current baseline so onboarding never waits for plugin network installs.
|
|
60
|
+
return {
|
|
61
|
+
config: { ...createDefaultConfig(dataDir), seededPlugins: EXTRACTED_PLUGINS.map((plugin) => plugin.id) },
|
|
62
|
+
needsSave: true,
|
|
63
|
+
};
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -52,6 +52,7 @@ export function normalizeLoadedConfig(saved: Record<string, unknown>, dataDir: s
|
|
|
52
52
|
activeLayoutIndex,
|
|
53
53
|
brokerInstances: sanitizeBrokerInstances(candidate.brokerInstances),
|
|
54
54
|
disabledPlugins,
|
|
55
|
+
seededPlugins: sanitizeUniqueStringList(candidate.seededPlugins),
|
|
55
56
|
disabledSources: sanitizeUniqueStringList(candidate.disabledSources ?? defaults.disabledSources),
|
|
56
57
|
pluginConfig: sanitizePluginConfig(candidate.pluginConfig),
|
|
57
58
|
theme: typeof candidate.theme === "string" ? candidate.theme : defaults.theme,
|
|
@@ -108,6 +109,7 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
|
|
|
108
109
|
activeLayoutIndex,
|
|
109
110
|
brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
|
|
110
111
|
disabledPlugins: sanitizeUniqueStringList(config.disabledPlugins),
|
|
112
|
+
seededPlugins: sanitizeUniqueStringList(config.seededPlugins),
|
|
111
113
|
disabledSources: sanitizeUniqueStringList(config.disabledSources),
|
|
112
114
|
pluginConfig: sanitizePluginConfig(config.pluginConfig),
|
|
113
115
|
chartPreferences: sanitizeChartPreferences(config.chartPreferences, defaults.chartPreferences),
|
|
@@ -130,6 +132,8 @@ const ONBOARDING_STAGES = new Set<OnboardingProgress["stage"]>([
|
|
|
130
132
|
"welcome",
|
|
131
133
|
"portfolio",
|
|
132
134
|
"add-ticker",
|
|
135
|
+
"research",
|
|
136
|
+
"verify",
|
|
133
137
|
"account",
|
|
134
138
|
"upgrade",
|
|
135
139
|
"ready",
|
package/src/news/types.ts
CHANGED
|
@@ -41,6 +41,26 @@ describe("loadCdsActivity", () => {
|
|
|
41
41
|
expect(activity.issuer).toBe("Oracle Corporation");
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test("prefers the primary US common stock when exchanges reuse a ticker", async () => {
|
|
45
|
+
const cedear = {
|
|
46
|
+
...result("ORCL", "Oracle Corp. - CEDEAR"),
|
|
47
|
+
exchange: "BYMA",
|
|
48
|
+
primaryExchange: "BYMA",
|
|
49
|
+
type: "Depositary Receipt",
|
|
50
|
+
currency: "ARS",
|
|
51
|
+
};
|
|
52
|
+
const primary = {
|
|
53
|
+
...result("ORCL", "Oracle Corporation"),
|
|
54
|
+
currency: "USD",
|
|
55
|
+
primaryExchange: "NYSE",
|
|
56
|
+
};
|
|
57
|
+
const calls = spy([cedear, primary]);
|
|
58
|
+
|
|
59
|
+
await loadCdsActivity("ORCL", calls.fetchCds, calls.searchInstruments);
|
|
60
|
+
|
|
61
|
+
expect(calls.requested).toEqual(["Oracle Corporation"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
44
64
|
test("falls back to the first result when no symbol matches exactly", async () => {
|
|
45
65
|
const calls = spy([result("AVGO", "Broadcom Inc."), result("AVGOP", "Broadcom Preferred")]);
|
|
46
66
|
|
|
@@ -16,14 +16,23 @@ export interface CdsActivity {
|
|
|
16
16
|
|
|
17
17
|
export type CdsActivityLoader = (issuer: string | null) => Promise<CdsActivity>;
|
|
18
18
|
|
|
19
|
-
type CdsFetch = (params: { issuer?: string; days?: number; limit?: number }) => Promise<CloudCdsResponse>;
|
|
20
|
-
type InstrumentSearch = (query: string, limit?: number) => Promise<InstrumentSearchResult[]>;
|
|
19
|
+
export type CdsFetch = (params: { issuer?: string; days?: number; limit?: number }) => Promise<CloudCdsResponse>;
|
|
20
|
+
export type InstrumentSearch = (query: string, limit?: number) => Promise<InstrumentSearchResult[]>;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* A bare ticker with normal symbol punctuation and no spaces. Anything longer or
|
|
24
24
|
* containing a space is already a company name and is sent to the backend as-is.
|
|
25
25
|
*/
|
|
26
26
|
const TICKER_LIKE = /^[A-Za-z0-9][A-Za-z0-9.^:-]{0,11}$/;
|
|
27
|
+
const US_PRIMARY_EXCHANGES = new Set(["AMEX", "NASDAQ", "NYSE", "NYSEAMERICAN", "NYSEARCA"]);
|
|
28
|
+
|
|
29
|
+
function isPrimaryUsCommonStock(result: InstrumentSearchResult): boolean {
|
|
30
|
+
const exchange = (result.primaryExchange || result.exchange).toUpperCase().replace(/[^A-Z]/g, "");
|
|
31
|
+
const type = result.type.toLowerCase();
|
|
32
|
+
return result.currency === "USD"
|
|
33
|
+
&& US_PRIMARY_EXCHANGES.has(exchange)
|
|
34
|
+
&& (type === "stk" || type === "equity" || type.includes("common stock"));
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* The backend matches DTCC issuer names, so a ticker has to become a company
|
|
@@ -40,7 +49,8 @@ export async function resolveIssuerName(
|
|
|
40
49
|
try {
|
|
41
50
|
const results = await searchInstruments(issuer);
|
|
42
51
|
const symbol = issuer.toUpperCase();
|
|
43
|
-
const
|
|
52
|
+
const exactMatches = results.filter((result) => result.symbol.toUpperCase() === symbol);
|
|
53
|
+
const match = exactMatches.find(isPrimaryUsCommonStock) ?? exactMatches[0] ?? results[0];
|
|
44
54
|
return match?.name?.trim() || issuer;
|
|
45
55
|
} catch {
|
|
46
56
|
return issuer;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
3
|
+
import { createCdsHeadless } from "./headless";
|
|
4
|
+
import type { CdsActivity } from "./client";
|
|
5
|
+
|
|
6
|
+
const activity: CdsActivity = {
|
|
7
|
+
source: "DTCC",
|
|
8
|
+
asOf: "2026-09-04T12:00:00Z",
|
|
9
|
+
issuer: "Oracle Corporation",
|
|
10
|
+
trades: [{
|
|
11
|
+
id: "1", issuer: "Oracle Corporation", issuerKey: "oracle", eventAt: 100,
|
|
12
|
+
maturity: "2031-06-20", notional: 5_000_000, notionalCapped: false,
|
|
13
|
+
currency: "USD", couponBp: 100, spreadBp: 45, upfront: null, upfrontCurrency: null,
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function args(argument: string | null): HeadlessPaneLoadArgs {
|
|
18
|
+
return { rawArgument: argument ?? "", argument, symbols: argument ? [argument] : [], options: {} };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("CDS headless model", () => {
|
|
22
|
+
test("switches from issuer summaries to trade rows for a ticker argument", async () => {
|
|
23
|
+
const headless = createCdsHeadless({ load: async () => activity });
|
|
24
|
+
const market = await headless.load(args(null), {} as HeadlessPaneContext);
|
|
25
|
+
const ticker = await headless.load(args("ORCL"), {} as HeadlessPaneContext);
|
|
26
|
+
|
|
27
|
+
expect(market.rows).toEqual([expect.objectContaining({ issuer: "Oracle Corporation", trades: 1 })]);
|
|
28
|
+
expect(ticker.rows).toEqual([expect.objectContaining({ id: "1", spreadBp: 45 })]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneDefinition,
|
|
3
|
+
HeadlessPaneLoadArgs,
|
|
4
|
+
HeadlessRowsResult,
|
|
5
|
+
} from "../../../types/plugin";
|
|
6
|
+
import {
|
|
7
|
+
loadCdsActivity,
|
|
8
|
+
type CdsActivity,
|
|
9
|
+
type CdsFetch,
|
|
10
|
+
type InstrumentSearch,
|
|
11
|
+
} from "./client";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_ISSUER_SORT,
|
|
14
|
+
DEFAULT_TRADE_SORT,
|
|
15
|
+
formatBp,
|
|
16
|
+
formatEventTime,
|
|
17
|
+
formatNotional,
|
|
18
|
+
formatUpfront,
|
|
19
|
+
sortIssuers,
|
|
20
|
+
sortTrades,
|
|
21
|
+
summarizeIssuers,
|
|
22
|
+
type CdsTrade,
|
|
23
|
+
} from "./model";
|
|
24
|
+
|
|
25
|
+
const ISSUER_COLUMNS = [
|
|
26
|
+
{ key: "issuer", header: "Issuer" },
|
|
27
|
+
{ key: "trades", header: "Trades", align: "right" as const },
|
|
28
|
+
{ key: "lastTradeAt", header: "Last UTC", format: (value: unknown) => formatEventTime(Number(value)) },
|
|
29
|
+
{ key: "latestSpreadBp", header: "Spread", align: "right" as const, format: (value: unknown) => formatBp(value as number | null) },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function tradeForFormatting(row: Record<string, unknown>): CdsTrade {
|
|
33
|
+
return row as unknown as CdsTrade;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TRADE_COLUMNS = [
|
|
37
|
+
{ key: "eventAt", header: "Time UTC", format: (value: unknown) => formatEventTime(Number(value)) },
|
|
38
|
+
{ key: "issuer", header: "Issuer" },
|
|
39
|
+
{ key: "maturity", header: "Maturity" },
|
|
40
|
+
{
|
|
41
|
+
key: "notional",
|
|
42
|
+
header: "Notional",
|
|
43
|
+
align: "right" as const,
|
|
44
|
+
format: (_value: unknown, row: Record<string, unknown>) => formatNotional(tradeForFormatting(row)),
|
|
45
|
+
},
|
|
46
|
+
{ key: "currency", header: "CCY" },
|
|
47
|
+
{ key: "couponBp", header: "Coupon", align: "right" as const, format: (value: unknown) => formatBp(value as number | null) },
|
|
48
|
+
{ key: "spreadBp", header: "Spread", align: "right" as const, format: (value: unknown) => formatBp(value as number | null) },
|
|
49
|
+
{
|
|
50
|
+
key: "upfront",
|
|
51
|
+
header: "Upfront",
|
|
52
|
+
align: "right" as const,
|
|
53
|
+
format: (_value: unknown, row: Record<string, unknown>) => formatUpfront(tradeForFormatting(row)),
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function projectCdsHeadless(activity: CdsActivity, issuer: string | null): HeadlessRowsResult {
|
|
58
|
+
if (issuer) {
|
|
59
|
+
return {
|
|
60
|
+
columns: TRADE_COLUMNS,
|
|
61
|
+
rows: sortTrades(activity.trades, DEFAULT_TRADE_SORT).map((trade) => ({ ...trade })),
|
|
62
|
+
metadata: { source: activity.source, asOf: activity.asOf, issuer: activity.issuer },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
columns: ISSUER_COLUMNS,
|
|
67
|
+
rows: sortIssuers(summarizeIssuers(activity.trades), DEFAULT_ISSUER_SORT).map((issuerRow) => ({ ...issuerRow })),
|
|
68
|
+
metadata: { source: activity.source, asOf: activity.asOf, issuer: null },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CdsHeadlessDependencies {
|
|
73
|
+
load(
|
|
74
|
+
args: HeadlessPaneLoadArgs,
|
|
75
|
+
issuer: string | null,
|
|
76
|
+
fetchCds: CdsFetch,
|
|
77
|
+
searchInstruments: InstrumentSearch,
|
|
78
|
+
): Promise<CdsActivity>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const defaultDependencies: CdsHeadlessDependencies = {
|
|
82
|
+
load: (_args, issuer, fetchCds, searchInstruments) => (
|
|
83
|
+
loadCdsActivity(issuer, fetchCds, searchInstruments)
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function createCdsHeadless(
|
|
88
|
+
dependencies: CdsHeadlessDependencies = defaultDependencies,
|
|
89
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
90
|
+
return {
|
|
91
|
+
shape: "rows",
|
|
92
|
+
argument: {
|
|
93
|
+
kind: "free-text",
|
|
94
|
+
placeholder: "ticker or issuer",
|
|
95
|
+
description: "Optional ticker or issuer. Omit for the most-active issuer view.",
|
|
96
|
+
optional: true,
|
|
97
|
+
},
|
|
98
|
+
options: [],
|
|
99
|
+
describe: (args) => typeof args.argument === "string" ? `CDS | ${args.argument}` : "Single-Name CDS",
|
|
100
|
+
async load(args, ctx) {
|
|
101
|
+
const issuer = typeof args.argument === "string" ? args.argument : null;
|
|
102
|
+
const activity = await dependencies.load(
|
|
103
|
+
args,
|
|
104
|
+
issuer,
|
|
105
|
+
(params) => ctx.apiClient.getCloudCds(params),
|
|
106
|
+
(query, limit) => ctx.apiClient.searchInstruments(query, limit),
|
|
107
|
+
);
|
|
108
|
+
return projectCdsHeadless(activity, issuer);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const cdsHeadless = createCdsHeadless();
|
|
@@ -2,6 +2,9 @@ import type { PaneTemplateCreateOptions } from "../../../types/plugin";
|
|
|
2
2
|
import type { PluginModule } from "../plugin-module";
|
|
3
3
|
import { CDS_PANE_ID } from "./model";
|
|
4
4
|
import { CdsPane } from "./pane";
|
|
5
|
+
import { cdsHeadless } from "./headless";
|
|
6
|
+
|
|
7
|
+
export { cdsHeadless } from "./headless";
|
|
5
8
|
|
|
6
9
|
/** Only an explicit argument binds a ticker; bare `CDS` stays market-wide. */
|
|
7
10
|
function explicitSymbol(options?: PaneTemplateCreateOptions): string | null {
|
|
@@ -26,6 +29,7 @@ export const cdsModule: PluginModule = {
|
|
|
26
29
|
description: "Single-name corporate CDS trade activity from DTCC public dissemination.",
|
|
27
30
|
keywords: ["cds", "credit", "default", "swap", "single name", "issuer", "dtcc", "protection"],
|
|
28
31
|
shortcut: { prefix: "CDS", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
|
|
32
|
+
headless: cdsHeadless,
|
|
29
33
|
createInstance: (_context, options) => {
|
|
30
34
|
const symbol = explicitSymbol(options);
|
|
31
35
|
return symbol
|
|
@@ -45,7 +45,10 @@ export function applyChartComposerCapabilityOptions(
|
|
|
45
45
|
options: NormalizedPaneFunctionOptions,
|
|
46
46
|
): ChartSpec {
|
|
47
47
|
let next = spec;
|
|
48
|
-
const
|
|
48
|
+
const selectedRange = chartRange(options);
|
|
49
|
+
const range = capabilityId === "intraday-price-chart" && typeof options.session === "string"
|
|
50
|
+
? "1D"
|
|
51
|
+
: selectedRange;
|
|
49
52
|
const resolution = chartResolution(options);
|
|
50
53
|
if (range || resolution) {
|
|
51
54
|
next = {
|
|
@@ -279,7 +279,7 @@ const chartComposerTemplates: PaneTemplateDef[] = [
|
|
|
279
279
|
id: "graph-intraday-price-pane",
|
|
280
280
|
prefix: "GIP",
|
|
281
281
|
label: "Intraday Price Graph",
|
|
282
|
-
description: "Open
|
|
282
|
+
description: "Open an intraday price chart for one or five sessions.",
|
|
283
283
|
argKind: "ticker",
|
|
284
284
|
minimumSymbols: 1,
|
|
285
285
|
build: (symbols) => buildIntradayPriceChartPreset(symbols[0]!),
|