gloomberb 0.11.0 → 0.11.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 +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -7,7 +7,7 @@ import type { BrokerConnectionStatus } from "../../../../types/broker";
|
|
|
7
7
|
import type { TickerFinancials } from "../../../../types/financials";
|
|
8
8
|
import type { Portfolio, TickerRecord } from "../../../../types/ticker";
|
|
9
9
|
import type { BrokerAccount, BrokerCashBalance } from "../../../../types/trading";
|
|
10
|
-
import { displayWidth, formatCompact, formatPercentRaw } from "../../../../utils/format";
|
|
10
|
+
import { convertCurrency, displayWidth, formatCompact, formatPercentRaw } from "../../../../utils/format";
|
|
11
11
|
import { getBrokerInstance } from "../../../../utils/broker-instances";
|
|
12
12
|
import { resolvePortfolioAccountMetrics, resolvePortfolioMarketValue } from "../account-metrics";
|
|
13
13
|
import { calculatePortfolioSummaryTotals, type PortfolioSummaryTotals } from "./totals";
|
|
@@ -188,21 +188,23 @@ export function buildPortfolioSummarySegments({
|
|
|
188
188
|
accountStatusText,
|
|
189
189
|
widthBudget,
|
|
190
190
|
refreshText,
|
|
191
|
+
convertAccountValue = (value) => value,
|
|
191
192
|
}: {
|
|
192
193
|
totals: PortfolioSummaryTotals;
|
|
193
194
|
accountState: PortfolioSummaryAccountState | null;
|
|
194
195
|
accountStatusText?: string;
|
|
195
196
|
widthBudget: number;
|
|
196
197
|
refreshText?: string;
|
|
198
|
+
convertAccountValue?: (value: number) => number;
|
|
197
199
|
}): PortfolioSummarySegment[] {
|
|
198
200
|
const candidates: PortfolioSummarySegment[] = [];
|
|
199
|
-
const accountMetrics = resolvePortfolioAccountMetrics(totals, accountState?.account);
|
|
200
|
-
const totalMarketValue = resolvePortfolioMarketValue(totals, accountState?.account);
|
|
201
|
+
const accountMetrics = resolvePortfolioAccountMetrics(totals, accountState?.account, convertAccountValue);
|
|
202
|
+
const totalMarketValue = resolvePortfolioMarketValue(totals, accountState?.account, convertAccountValue);
|
|
201
203
|
|
|
202
204
|
if (accountState?.account.netLiquidation != null) {
|
|
203
205
|
candidates.push(createSummarySegment("netliq", [
|
|
204
206
|
{ text: "Net Liq", tone: "label" },
|
|
205
|
-
{ text: formatCompact(accountState.account.netLiquidation), tone: "value", bold: true },
|
|
207
|
+
{ text: formatCompact(convertAccountValue(accountState.account.netLiquidation)), tone: "value", bold: true },
|
|
206
208
|
]));
|
|
207
209
|
}
|
|
208
210
|
|
|
@@ -214,7 +216,7 @@ export function buildPortfolioSummarySegments({
|
|
|
214
216
|
if (accountState?.account.totalCashValue != null) {
|
|
215
217
|
candidates.push(createSummarySegment("cash", [
|
|
216
218
|
{ text: "Cash", tone: "label" },
|
|
217
|
-
{ text: formatCompact(accountState.account.totalCashValue), tone: "value", bold: true },
|
|
219
|
+
{ text: formatCompact(convertAccountValue(accountState.account.totalCashValue)), tone: "value", bold: true },
|
|
218
220
|
]));
|
|
219
221
|
}
|
|
220
222
|
|
|
@@ -241,25 +243,25 @@ export function buildPortfolioSummarySegments({
|
|
|
241
243
|
account.settledCash != null
|
|
242
244
|
? createSummarySegment("settled", [
|
|
243
245
|
{ text: "Settled", tone: "label" },
|
|
244
|
-
{ text: formatCompact(account.settledCash), tone: "value", bold: true },
|
|
246
|
+
{ text: formatCompact(convertAccountValue(account.settledCash)), tone: "value", bold: true },
|
|
245
247
|
])
|
|
246
248
|
: null,
|
|
247
249
|
account.availableFunds != null
|
|
248
250
|
? createSummarySegment("avail", [
|
|
249
251
|
{ text: "Avail", tone: "label" },
|
|
250
|
-
{ text: formatCompact(account.availableFunds), tone: "value", bold: true },
|
|
252
|
+
{ text: formatCompact(convertAccountValue(account.availableFunds)), tone: "value", bold: true },
|
|
251
253
|
])
|
|
252
254
|
: null,
|
|
253
255
|
account.excessLiquidity != null
|
|
254
256
|
? createSummarySegment("excess", [
|
|
255
257
|
{ text: "Excess", tone: "label" },
|
|
256
|
-
{ text: formatCompact(account.excessLiquidity), tone: "value", bold: true },
|
|
258
|
+
{ text: formatCompact(convertAccountValue(account.excessLiquidity)), tone: "value", bold: true },
|
|
257
259
|
])
|
|
258
260
|
: null,
|
|
259
261
|
account.buyingPower != null
|
|
260
262
|
? createSummarySegment("bp", [
|
|
261
263
|
{ text: "BP", tone: "label" },
|
|
262
|
-
{ text: formatCompact(account.buyingPower), tone: "value", bold: true },
|
|
264
|
+
{ text: formatCompact(convertAccountValue(account.buyingPower)), tone: "value", bold: true },
|
|
263
265
|
])
|
|
264
266
|
: null,
|
|
265
267
|
createSummarySegment("source", [
|
|
@@ -348,12 +350,19 @@ export function buildPortfolioFooterSegments({
|
|
|
348
350
|
|
|
349
351
|
// An account error must still reach the footer, otherwise a failed load looks like an empty portfolio.
|
|
350
352
|
if (!totals.hasPositions && !accountState && !accountStatusText) return [];
|
|
353
|
+
const convertAccountValue = (value: number) => convertCurrency(
|
|
354
|
+
value,
|
|
355
|
+
accountState?.account.currency || baseCurrency,
|
|
356
|
+
baseCurrency,
|
|
357
|
+
exchangeRates,
|
|
358
|
+
);
|
|
351
359
|
return buildPortfolioSummarySegments({
|
|
352
360
|
totals,
|
|
353
361
|
accountState,
|
|
354
362
|
accountStatusText,
|
|
355
363
|
widthBudget: Math.max(16, width - 14),
|
|
356
364
|
refreshText,
|
|
365
|
+
convertAccountValue,
|
|
357
366
|
}).map((segment) => ({
|
|
358
367
|
id: segment.id,
|
|
359
368
|
parts: segment.parts,
|
|
@@ -384,40 +393,45 @@ export function renderSummarySegments(segments: PortfolioSummarySegment[], width
|
|
|
384
393
|
);
|
|
385
394
|
}
|
|
386
395
|
|
|
387
|
-
export function buildDrawerMetricSegments(
|
|
396
|
+
export function buildDrawerMetricSegments(
|
|
397
|
+
account: BrokerAccount,
|
|
398
|
+
widthBudget: number,
|
|
399
|
+
convertAccountValue: (value: number) => number = (value) => value,
|
|
400
|
+
): PortfolioSummarySegment[] {
|
|
401
|
+
const money = (value: number) => convertAccountValue(value);
|
|
388
402
|
const candidates = [
|
|
389
403
|
account.dailyPnl != null
|
|
390
|
-
? createSummarySegment("day", [{ text: "Day", tone: "label" }, { text: formatSignedCompact(account.dailyPnl), tone: "value", color: priceColor(account.dailyPnl), bold: true }])
|
|
404
|
+
? createSummarySegment("day", [{ text: "Day", tone: "label" }, { text: formatSignedCompact(money(account.dailyPnl)), tone: "value", color: priceColor(money(account.dailyPnl)), bold: true }])
|
|
391
405
|
: null,
|
|
392
406
|
account.unrealizedPnl != null
|
|
393
|
-
? createSummarySegment("unreal", [{ text: "Unreal", tone: "label" }, { text: formatSignedCompact(account.unrealizedPnl), tone: "value", color: priceColor(account.unrealizedPnl), bold: true }])
|
|
407
|
+
? createSummarySegment("unreal", [{ text: "Unreal", tone: "label" }, { text: formatSignedCompact(money(account.unrealizedPnl)), tone: "value", color: priceColor(money(account.unrealizedPnl)), bold: true }])
|
|
394
408
|
: null,
|
|
395
409
|
account.realizedPnl != null
|
|
396
|
-
? createSummarySegment("realized", [{ text: "Realized", tone: "label" }, { text: formatSignedCompact(account.realizedPnl), tone: "value", color: priceColor(account.realizedPnl), bold: true }])
|
|
410
|
+
? createSummarySegment("realized", [{ text: "Realized", tone: "label" }, { text: formatSignedCompact(money(account.realizedPnl)), tone: "value", color: priceColor(money(account.realizedPnl)), bold: true }])
|
|
397
411
|
: null,
|
|
398
412
|
account.totalCashValue != null
|
|
399
|
-
? createSummarySegment("cash", [{ text: "Cash", tone: "label" }, { text: formatCompact(account.totalCashValue), tone: "value", bold: true }])
|
|
413
|
+
? createSummarySegment("cash", [{ text: "Cash", tone: "label" }, { text: formatCompact(money(account.totalCashValue)), tone: "value", bold: true }])
|
|
400
414
|
: null,
|
|
401
415
|
account.settledCash != null
|
|
402
|
-
? createSummarySegment("settled", [{ text: "Settled", tone: "label" }, { text: formatCompact(account.settledCash), tone: "value", bold: true }])
|
|
416
|
+
? createSummarySegment("settled", [{ text: "Settled", tone: "label" }, { text: formatCompact(money(account.settledCash)), tone: "value", bold: true }])
|
|
403
417
|
: null,
|
|
404
418
|
account.netLiquidation != null
|
|
405
|
-
? createSummarySegment("netliq", [{ text: "Net Liq", tone: "label" }, { text: formatCompact(account.netLiquidation), tone: "value", bold: true }])
|
|
419
|
+
? createSummarySegment("netliq", [{ text: "Net Liq", tone: "label" }, { text: formatCompact(money(account.netLiquidation)), tone: "value", bold: true }])
|
|
406
420
|
: null,
|
|
407
421
|
account.availableFunds != null
|
|
408
|
-
? createSummarySegment("avail", [{ text: "Avail", tone: "label" }, { text: formatCompact(account.availableFunds), tone: "value", bold: true }])
|
|
422
|
+
? createSummarySegment("avail", [{ text: "Avail", tone: "label" }, { text: formatCompact(money(account.availableFunds)), tone: "value", bold: true }])
|
|
409
423
|
: null,
|
|
410
424
|
account.excessLiquidity != null
|
|
411
|
-
? createSummarySegment("excess", [{ text: "Excess", tone: "label" }, { text: formatCompact(account.excessLiquidity), tone: "value", bold: true }])
|
|
425
|
+
? createSummarySegment("excess", [{ text: "Excess", tone: "label" }, { text: formatCompact(money(account.excessLiquidity)), tone: "value", bold: true }])
|
|
412
426
|
: null,
|
|
413
427
|
account.buyingPower != null
|
|
414
|
-
? createSummarySegment("bp", [{ text: "BP", tone: "label" }, { text: formatCompact(account.buyingPower), tone: "value", bold: true }])
|
|
428
|
+
? createSummarySegment("bp", [{ text: "BP", tone: "label" }, { text: formatCompact(money(account.buyingPower)), tone: "value", bold: true }])
|
|
415
429
|
: null,
|
|
416
430
|
account.initMarginReq != null
|
|
417
|
-
? createSummarySegment("init", [{ text: "Init", tone: "label" }, { text: formatCompact(account.initMarginReq), tone: "value", bold: true }])
|
|
431
|
+
? createSummarySegment("init", [{ text: "Init", tone: "label" }, { text: formatCompact(money(account.initMarginReq)), tone: "value", bold: true }])
|
|
418
432
|
: null,
|
|
419
433
|
account.maintMarginReq != null
|
|
420
|
-
? createSummarySegment("maint", [{ text: "Maint", tone: "label" }, { text: formatCompact(account.maintMarginReq), tone: "value", bold: true }])
|
|
434
|
+
? createSummarySegment("maint", [{ text: "Maint", tone: "label" }, { text: formatCompact(money(account.maintMarginReq)), tone: "value", bold: true }])
|
|
421
435
|
: null,
|
|
422
436
|
].filter((segment): segment is PortfolioSummarySegment => segment != null);
|
|
423
437
|
|
|
@@ -2,7 +2,12 @@ import type { TickerFinancials } from "../../../../types/financials";
|
|
|
2
2
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
3
3
|
import { convertCurrency } from "../../../../utils/format";
|
|
4
4
|
import { getActiveQuoteDisplay } from "../../../../market-data/market/status";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getPortfolioPositionMetrics,
|
|
7
|
+
resolveBrokerFallbackMarketValue,
|
|
8
|
+
resolveBrokerFallbackPnl,
|
|
9
|
+
signedQuoteUnrealizedPnl,
|
|
10
|
+
} from "../position-metrics";
|
|
6
11
|
|
|
7
12
|
export interface PortfolioSummaryTotals {
|
|
8
13
|
totalMktValue: number;
|
|
@@ -27,6 +32,8 @@ export function calculatePortfolioSummaryTotals(
|
|
|
27
32
|
let totalMktValue = 0;
|
|
28
33
|
let totalPrevValue = 0;
|
|
29
34
|
let totalCostBasis = 0;
|
|
35
|
+
let signedDailyPnl = 0;
|
|
36
|
+
let signedUnrealizedPnl = 0;
|
|
30
37
|
let hasPositions = false;
|
|
31
38
|
let watchlistChangeSum = 0;
|
|
32
39
|
let watchlistCount = 0;
|
|
@@ -54,21 +61,28 @@ export function calculatePortfolioSummaryTotals(
|
|
|
54
61
|
if (quote && activeQuote && totalPriceUnits !== 0) {
|
|
55
62
|
hasPositions = true;
|
|
56
63
|
const marketValue = Math.abs(totalPriceUnits) * activeQuote.price;
|
|
57
|
-
totalMktValue += toBaseQuote(marketValue);
|
|
58
64
|
const previousClose = quote.previousClose || (activeQuote.price - activeQuote.change);
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const previousValue = Math.abs(totalPriceUnits) * previousClose;
|
|
66
|
+
const convertedMarket = toBaseQuote(marketValue);
|
|
67
|
+
const convertedCost = toBasePosition(totalCost);
|
|
68
|
+
totalMktValue += convertedMarket;
|
|
69
|
+
totalPrevValue += toBaseQuote(previousValue);
|
|
70
|
+
totalCostBasis += convertedCost;
|
|
71
|
+
signedDailyPnl += toBaseQuote(totalPriceUnits * (activeQuote.price - previousClose));
|
|
72
|
+
signedUnrealizedPnl += signedQuoteUnrealizedPnl(convertedMarket, convertedCost, totalPriceUnits);
|
|
61
73
|
} else if (brokerFallbackMktValue != null) {
|
|
62
74
|
hasPositions = true;
|
|
63
75
|
totalMktValue += toBasePosition(brokerFallbackMktValue);
|
|
64
76
|
totalCostBasis += toBasePosition(totalCost);
|
|
65
77
|
totalPrevValue += toBasePosition(brokerFallbackMktValue);
|
|
78
|
+
const brokerPnl = resolveBrokerFallbackPnl(positionMetrics, brokerFallbackMktValue);
|
|
79
|
+
if (brokerPnl != null) signedUnrealizedPnl += toBasePosition(brokerPnl);
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
const dailyPnl =
|
|
83
|
+
const dailyPnl = signedDailyPnl;
|
|
70
84
|
const dailyPnlPct = totalPrevValue !== 0 ? (dailyPnl / totalPrevValue) * 100 : 0;
|
|
71
|
-
const unrealizedPnl =
|
|
85
|
+
const unrealizedPnl = signedUnrealizedPnl;
|
|
72
86
|
const unrealizedPnlPct = totalCostBasis !== 0 ? (unrealizedPnl / totalCostBasis) * 100 : 0;
|
|
73
87
|
const avgWatchlistChange = watchlistCount > 0 ? watchlistChangeSum / watchlistCount : 0;
|
|
74
88
|
|
|
@@ -23,6 +23,7 @@ import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from
|
|
|
23
23
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
24
24
|
import { usePaneTicker } from "../../../state/app/context";
|
|
25
25
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
26
|
+
import { computeTTM } from "../ticker-detail/financials/aggregation";
|
|
26
27
|
import { useAssetData } from "../../runtime";
|
|
27
28
|
import { handleRefreshKey, loadingErrorFooterInfo } from "../shared/table-pane";
|
|
28
29
|
import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
|
|
@@ -241,16 +242,8 @@ function estimateValue(pair: EstimatePair): string {
|
|
|
241
242
|
|
|
242
243
|
function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow | null {
|
|
243
244
|
const latestFour = quarterlyStatements.slice(-4);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (statement.totalRevenue == null) return total;
|
|
247
|
-
return (total ?? 0) + statement.totalRevenue;
|
|
248
|
-
}, undefined);
|
|
249
|
-
const fiscalEps = latestFour.reduce<number | undefined>((total, statement) => {
|
|
250
|
-
if (statement.eps == null) return total;
|
|
251
|
-
return (total ?? 0) + statement.eps;
|
|
252
|
-
}, undefined);
|
|
253
|
-
if (fiscalRevenue == null && fiscalEps == null) return null;
|
|
245
|
+
const ttm = computeTTM([...quarterlyStatements]);
|
|
246
|
+
if (!ttm || (ttm.totalRevenue == null && ttm.eps == null)) return null;
|
|
254
247
|
const latest = latestFour.at(-1);
|
|
255
248
|
return {
|
|
256
249
|
id: `ttm:${latest?.date ?? ""}`,
|
|
@@ -258,8 +251,8 @@ function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow |
|
|
|
258
251
|
status: "TTM",
|
|
259
252
|
period: "4 qtrs",
|
|
260
253
|
detail: "sum",
|
|
261
|
-
annualEps:
|
|
262
|
-
annualRevenue:
|
|
254
|
+
annualEps: ttm.eps,
|
|
255
|
+
annualRevenue: ttm.totalRevenue,
|
|
263
256
|
value: "-",
|
|
264
257
|
tone: "muted",
|
|
265
258
|
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { apiClient, setCloudApiFetchTransport } from "../../../api-client";
|
|
4
|
+
import type { CloudEquityDiagnosticResponse } from "../../../api-client";
|
|
5
|
+
import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane/footer";
|
|
6
|
+
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
7
|
+
import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app/context";
|
|
8
|
+
import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
|
|
9
|
+
import { cloneLayout, createDefaultConfig } from "../../../types/config";
|
|
10
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
11
|
+
import { Box } from "../../../ui";
|
|
12
|
+
import { PluginRenderProvider } from "../../runtime";
|
|
13
|
+
import { EquityDiagnosticView } from "./equity-diagnostic-pane";
|
|
14
|
+
|
|
15
|
+
const TEST_PANE_ID = "equity-diagnostic:AAPL";
|
|
16
|
+
const WIDTH = 96;
|
|
17
|
+
/** Tall enough that a full report fits without scrolling the assertions away. */
|
|
18
|
+
const HEIGHT = 44;
|
|
19
|
+
|
|
20
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
21
|
+
|
|
22
|
+
function makeTicker(symbol: string): TickerRecord {
|
|
23
|
+
return {
|
|
24
|
+
metadata: {
|
|
25
|
+
ticker: symbol,
|
|
26
|
+
exchange: "NASDAQ",
|
|
27
|
+
currency: "USD",
|
|
28
|
+
name: symbol,
|
|
29
|
+
portfolios: [],
|
|
30
|
+
watchlists: [],
|
|
31
|
+
positions: [],
|
|
32
|
+
custom: {},
|
|
33
|
+
tags: [],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signIn(plan: "free" | "pro"): void {
|
|
39
|
+
apiClient.setSessionToken("equity-diagnostic-test-token");
|
|
40
|
+
apiClient.restoreCachedUser({
|
|
41
|
+
id: "user-1",
|
|
42
|
+
name: "Tester",
|
|
43
|
+
email: "tester@example.com",
|
|
44
|
+
username: "tester",
|
|
45
|
+
emailVerified: true,
|
|
46
|
+
plan,
|
|
47
|
+
} as never);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Answers the diagnostic endpoint locally and records the request bodies. */
|
|
51
|
+
function mockDiagnosticTransport(
|
|
52
|
+
respond: (body: { symbol: string; exchange?: string; mode?: string }) => Response,
|
|
53
|
+
): Array<{ symbol: string; exchange?: string; mode?: string }> {
|
|
54
|
+
const requests: Array<{ symbol: string; exchange?: string; mode?: string }> = [];
|
|
55
|
+
setCloudApiFetchTransport(async (url, init) => {
|
|
56
|
+
if (!url.endsWith("/research/equity-diagnostic")) {
|
|
57
|
+
throw new Error(`unexpected cloud request: ${url}`);
|
|
58
|
+
}
|
|
59
|
+
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
60
|
+
requests.push(body);
|
|
61
|
+
return respond(body);
|
|
62
|
+
});
|
|
63
|
+
return requests;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
67
|
+
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeReport(overrides: Partial<CloudEquityDiagnosticResponse> = {}): CloudEquityDiagnosticResponse {
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
access: "full",
|
|
74
|
+
symbol: "AAPL",
|
|
75
|
+
exchange: "NASDAQ",
|
|
76
|
+
companyName: "Apple Inc.",
|
|
77
|
+
status: "partial",
|
|
78
|
+
verdict: "risk_skewed",
|
|
79
|
+
summary: "Margins slipped while inventory built up.",
|
|
80
|
+
confidence: 0.72,
|
|
81
|
+
findings: [
|
|
82
|
+
{
|
|
83
|
+
id: "f-low",
|
|
84
|
+
kind: "red_flag",
|
|
85
|
+
severity: 1,
|
|
86
|
+
confidence: 0.4,
|
|
87
|
+
title: "Receivables grew faster than sales",
|
|
88
|
+
observation: "Receivables rose 18 percent against 4 percent revenue growth.",
|
|
89
|
+
interpretation: "Collection may be slowing.",
|
|
90
|
+
evidenceIds: [],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "f-high",
|
|
94
|
+
kind: "red_flag",
|
|
95
|
+
severity: 3,
|
|
96
|
+
confidence: 0.81,
|
|
97
|
+
title: "Gross margin fell for three quarters",
|
|
98
|
+
observation: "Gross margin fell to 38.2 percent from 44.6 percent.",
|
|
99
|
+
interpretation: "Pricing pressure is outpacing mix improvement.",
|
|
100
|
+
evidenceIds: ["ev-10k", "ev-missing"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "f-green",
|
|
104
|
+
kind: "green_flag",
|
|
105
|
+
severity: 2,
|
|
106
|
+
confidence: 0.6,
|
|
107
|
+
title: "Buyback pace accelerated",
|
|
108
|
+
observation: "Shares outstanding fell 3 percent.",
|
|
109
|
+
interpretation: "Capital return is intact.",
|
|
110
|
+
evidenceIds: ["ev-news"],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
watchItems: ["Next quarter gross margin guidance"],
|
|
114
|
+
coverage: [
|
|
115
|
+
{ dataset: "financials", status: "available", asOf: "2026-02-01", provider: "Twelve Data" },
|
|
116
|
+
{ dataset: "insider", status: "no_data" },
|
|
117
|
+
],
|
|
118
|
+
evidence: [
|
|
119
|
+
{ id: "ev-10k", dataset: "filings", label: "10-K", asOf: "2026-02-01", url: "https://sec.gov/aapl-10k" },
|
|
120
|
+
{ id: "ev-news", dataset: "news", label: "Reuters" },
|
|
121
|
+
],
|
|
122
|
+
generatedAt: new Date(Date.now() - 4 * 60_000).toISOString(),
|
|
123
|
+
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
|
|
124
|
+
refreshAllowedAt: new Date(Date.now() + 600_000).toISOString(),
|
|
125
|
+
cached: true,
|
|
126
|
+
stale: true,
|
|
127
|
+
promptVersion: 1,
|
|
128
|
+
model: "gpt-5.6-luna",
|
|
129
|
+
...overrides,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function DiagnosticHarness() {
|
|
134
|
+
const config = createDefaultConfig("/tmp/gloomberb-equity-diagnostic-test");
|
|
135
|
+
config.layout = {
|
|
136
|
+
dockRoot: { kind: "pane", instanceId: TEST_PANE_ID },
|
|
137
|
+
instances: [{
|
|
138
|
+
instanceId: TEST_PANE_ID,
|
|
139
|
+
paneId: "equity-diagnostic",
|
|
140
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
141
|
+
}],
|
|
142
|
+
floating: [],
|
|
143
|
+
detached: [],
|
|
144
|
+
};
|
|
145
|
+
config.layouts = [{ name: "Default", layout: cloneLayout(config.layout) }];
|
|
146
|
+
|
|
147
|
+
const state = createInitialState(config);
|
|
148
|
+
state.focusedPaneId = TEST_PANE_ID;
|
|
149
|
+
state.tickers = new Map([["AAPL", makeTicker("AAPL")]]);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
153
|
+
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
154
|
+
<PluginRenderProvider pluginId="ticker-research" runtime={createTestPluginRuntime()}>
|
|
155
|
+
<PaneFooterProvider>
|
|
156
|
+
{(footer) => (
|
|
157
|
+
<Box flexDirection="column" width={WIDTH} height={HEIGHT}>
|
|
158
|
+
<EquityDiagnosticView width={WIDTH} height={HEIGHT - 1} focused />
|
|
159
|
+
<PaneFooterBar footer={footer} focused width={WIDTH} />
|
|
160
|
+
</Box>
|
|
161
|
+
)}
|
|
162
|
+
</PaneFooterProvider>
|
|
163
|
+
</PluginRenderProvider>
|
|
164
|
+
</PaneInstanceProvider>
|
|
165
|
+
</AppContext>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function renderHarness(): Promise<void> {
|
|
170
|
+
await act(async () => {
|
|
171
|
+
testSetup = await testRender(<DiagnosticHarness />, { width: WIDTH, height: HEIGHT });
|
|
172
|
+
});
|
|
173
|
+
await settle();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function settle(): Promise<void> {
|
|
177
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
178
|
+
await act(async () => {
|
|
179
|
+
await Bun.sleep(1);
|
|
180
|
+
await testSetup!.renderOnce();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function pressKey(name: string): Promise<void> {
|
|
186
|
+
await act(async () => {
|
|
187
|
+
testSetup!.renderer.keyInput.emit("keypress", {
|
|
188
|
+
name,
|
|
189
|
+
sequence: name,
|
|
190
|
+
ctrl: false,
|
|
191
|
+
meta: false,
|
|
192
|
+
option: false,
|
|
193
|
+
shift: false,
|
|
194
|
+
eventType: "press",
|
|
195
|
+
repeated: false,
|
|
196
|
+
stopPropagation: () => {},
|
|
197
|
+
preventDefault: () => {},
|
|
198
|
+
} as never);
|
|
199
|
+
await testSetup!.renderOnce();
|
|
200
|
+
});
|
|
201
|
+
await settle();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
afterEach(async () => {
|
|
205
|
+
if (testSetup) {
|
|
206
|
+
await act(async () => testSetup!.renderer.destroy());
|
|
207
|
+
testSetup = undefined;
|
|
208
|
+
}
|
|
209
|
+
setCloudApiFetchTransport(null);
|
|
210
|
+
apiClient.setSessionToken(null);
|
|
211
|
+
apiClient.restoreCachedUser(null);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("shows a cited preview to free accounts and gates the rest", async () => {
|
|
215
|
+
signIn("free");
|
|
216
|
+
const fullReport = makeReport();
|
|
217
|
+
const requests = mockDiagnosticTransport(() => jsonResponse(makeReport({
|
|
218
|
+
access: "preview",
|
|
219
|
+
verdict: "unclear",
|
|
220
|
+
summary: "",
|
|
221
|
+
confidence: 0.81,
|
|
222
|
+
findings: [fullReport.findings[1]!],
|
|
223
|
+
watchItems: [],
|
|
224
|
+
evidence: [fullReport.evidence[0]!],
|
|
225
|
+
})));
|
|
226
|
+
|
|
227
|
+
await renderHarness();
|
|
228
|
+
|
|
229
|
+
const frame = testSetup!.captureCharFrame();
|
|
230
|
+
expect(requests).toEqual([{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" }]);
|
|
231
|
+
expect(frame).toContain("Free preview");
|
|
232
|
+
expect(frame).toContain("Gross margin fell for three quarters");
|
|
233
|
+
expect(frame).not.toContain("Receivables grew faster than sales");
|
|
234
|
+
expect(frame).not.toContain("Risk skewed");
|
|
235
|
+
expect(frame).toContain("UNLOCK THE FULL DIAGNOSTIC");
|
|
236
|
+
expect(frame).toContain("Upgrade to Pro");
|
|
237
|
+
expect(frame).toContain("Gloom Cloud");
|
|
238
|
+
expect(frame).not.toContain("Twelve Data");
|
|
239
|
+
expect(frame).not.toContain("efresh");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("renders a stale partial report with severity order, split observation, and citations", async () => {
|
|
243
|
+
signIn("pro");
|
|
244
|
+
mockDiagnosticTransport(() => jsonResponse(makeReport()));
|
|
245
|
+
|
|
246
|
+
await renderHarness();
|
|
247
|
+
|
|
248
|
+
const frame = testSetup!.captureCharFrame();
|
|
249
|
+
expect(frame).toContain("Risk skewed");
|
|
250
|
+
expect(frame).toContain("Apple Inc.");
|
|
251
|
+
expect(frame).toContain("4m ago");
|
|
252
|
+
expect(frame).toContain("confidence 72%");
|
|
253
|
+
expect(frame).toContain("Margins slipped");
|
|
254
|
+
|
|
255
|
+
// Severity sorted inside a kind, and split into observation versus reading.
|
|
256
|
+
expect(frame.indexOf("Gross margin fell for three quarters"))
|
|
257
|
+
.toBeLessThan(frame.indexOf("Receivables grew faster than sales"));
|
|
258
|
+
expect(frame).toContain("Observed");
|
|
259
|
+
expect(frame).toContain("Reading");
|
|
260
|
+
expect(frame).toContain("Pricing pressure is outpacing mix improvement.");
|
|
261
|
+
|
|
262
|
+
// Citations resolve against the server catalog; unknown ids are dropped.
|
|
263
|
+
expect(frame).toContain("10-K 2026-02-01");
|
|
264
|
+
expect(frame).toContain("Reuters");
|
|
265
|
+
expect(frame).not.toContain("ev-missing");
|
|
266
|
+
|
|
267
|
+
expect(frame).toContain("WATCH ITEMS");
|
|
268
|
+
expect(frame).toContain("COVERAGE");
|
|
269
|
+
expect(frame).toContain("no data");
|
|
270
|
+
|
|
271
|
+
// Footer carries changing state only, and the model stays an implementation detail.
|
|
272
|
+
expect(frame).toContain("partial");
|
|
273
|
+
expect(frame).toContain("stale");
|
|
274
|
+
expect(frame).toContain("efresh");
|
|
275
|
+
expect(frame).not.toContain("luna");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("adds data sources line by line while the diagnostic generates", async () => {
|
|
279
|
+
signIn("pro");
|
|
280
|
+
mockDiagnosticTransport(() => jsonResponse({ status: "generating", retryAfterMs: 5_000 }, 202));
|
|
281
|
+
|
|
282
|
+
await renderHarness();
|
|
283
|
+
|
|
284
|
+
const frame = testSetup!.captureCharFrame();
|
|
285
|
+
expect(frame).toContain("Gloom Cloud market data");
|
|
286
|
+
expect(frame).toContain("SEC EDGAR filings");
|
|
287
|
+
expect(frame).not.toContain("FINRA short interest");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("polls an uncached diagnostic until the background report is ready", async () => {
|
|
291
|
+
signIn("pro");
|
|
292
|
+
let attempts = 0;
|
|
293
|
+
const requests = mockDiagnosticTransport(() => {
|
|
294
|
+
attempts += 1;
|
|
295
|
+
return attempts === 1
|
|
296
|
+
? jsonResponse({ status: "generating", retryAfterMs: 10 }, 202)
|
|
297
|
+
: jsonResponse(makeReport());
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await renderHarness();
|
|
301
|
+
await Bun.sleep(20);
|
|
302
|
+
await settle();
|
|
303
|
+
|
|
304
|
+
expect(requests).toEqual([
|
|
305
|
+
{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" },
|
|
306
|
+
{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" },
|
|
307
|
+
]);
|
|
308
|
+
expect(testSetup!.captureCharFrame()).toContain("Gross margin fell for three quarters");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("asks for a refresh on r and keeps the last report when the retry is rate limited", async () => {
|
|
312
|
+
signIn("pro");
|
|
313
|
+
const requests = mockDiagnosticTransport((body) => (
|
|
314
|
+
body.mode === "refresh"
|
|
315
|
+
? jsonResponse({ message: "Too many requests" }, 429)
|
|
316
|
+
: jsonResponse(makeReport())
|
|
317
|
+
));
|
|
318
|
+
|
|
319
|
+
await renderHarness();
|
|
320
|
+
expect(requests).toEqual([{ symbol: "AAPL", exchange: "NASDAQ", mode: "cache-first" }]);
|
|
321
|
+
|
|
322
|
+
await pressKey("r");
|
|
323
|
+
|
|
324
|
+
expect(requests.at(-1)?.mode).toBe("refresh");
|
|
325
|
+
const frame = testSetup!.captureCharFrame();
|
|
326
|
+
expect(frame).toContain("Rate limited");
|
|
327
|
+
expect(frame).toContain("Gross margin fell for three quarters");
|
|
328
|
+
expect(frame).toContain("Retry");
|
|
329
|
+
});
|