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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SyncContributor } from "../../../sync/types";
|
|
2
|
+
import type { NoteFileEntry, NotesFiles, QuickNoteEntry } from "./files";
|
|
3
|
+
|
|
4
|
+
// The server rejects snapshots over 1.5MB, and notes share it with portfolios.
|
|
5
|
+
const MAX_NOTE_CHARS = 100_000;
|
|
6
|
+
const MAX_NOTES_CHARS = 400_000;
|
|
7
|
+
|
|
8
|
+
interface SyncedNote {
|
|
9
|
+
key: string;
|
|
10
|
+
text: string;
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseNotes(value: unknown): SyncedNote[] {
|
|
19
|
+
if (!Array.isArray(value)) return [];
|
|
20
|
+
return value.flatMap((entry) => (
|
|
21
|
+
isPlainObject(entry) && typeof entry.key === "string" && typeof entry.text === "string"
|
|
22
|
+
? [{
|
|
23
|
+
key: entry.key,
|
|
24
|
+
text: entry.text,
|
|
25
|
+
updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)
|
|
26
|
+
? entry.updatedAt
|
|
27
|
+
: 0,
|
|
28
|
+
}]
|
|
29
|
+
: []
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseQuickNotes(value: unknown): QuickNoteEntry[] {
|
|
34
|
+
if (!Array.isArray(value)) return [];
|
|
35
|
+
return value.flatMap((entry) => (
|
|
36
|
+
isPlainObject(entry) && typeof entry.id === "string" && typeof entry.title === "string"
|
|
37
|
+
? [{
|
|
38
|
+
id: entry.id,
|
|
39
|
+
title: entry.title,
|
|
40
|
+
updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined,
|
|
41
|
+
}]
|
|
42
|
+
: []
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withinBudget(entries: NoteFileEntry[]): SyncedNote[] {
|
|
47
|
+
const notes: SyncedNote[] = [];
|
|
48
|
+
let total = 0;
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.text.length > MAX_NOTE_CHARS) continue;
|
|
51
|
+
if (total + entry.text.length > MAX_NOTES_CHARS) break;
|
|
52
|
+
total += entry.text.length;
|
|
53
|
+
notes.push(entry);
|
|
54
|
+
}
|
|
55
|
+
return notes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Notes live in files rather than app state, so they carry their own
|
|
60
|
+
* timestamps and merge per note: newest write wins, identical text is left
|
|
61
|
+
* alone so the two sides converge instead of ping-ponging.
|
|
62
|
+
*
|
|
63
|
+
* ponytail: deletions do not propagate, a note deleted on one device comes
|
|
64
|
+
* back from the cloud. Add tombstones if that turns out to matter.
|
|
65
|
+
*/
|
|
66
|
+
export function createNotesSyncContributor(notesFiles: NotesFiles): SyncContributor {
|
|
67
|
+
return {
|
|
68
|
+
id: "notes",
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
collect: async () => ({
|
|
71
|
+
// list() is newest first, so the budget keeps the notes worked on last.
|
|
72
|
+
notes: withinBudget(await notesFiles.list()),
|
|
73
|
+
quickNotes: await notesFiles.loadQuickNotesIndex(),
|
|
74
|
+
}),
|
|
75
|
+
apply: async (payload, { isCurrent }) => {
|
|
76
|
+
if (!isPlainObject(payload)) return;
|
|
77
|
+
const local = new Map((await notesFiles.list()).map((entry) => [entry.key, entry]));
|
|
78
|
+
|
|
79
|
+
for (const remote of parseNotes(payload.notes)) {
|
|
80
|
+
if (!isCurrent()) return;
|
|
81
|
+
const current = local.get(remote.key);
|
|
82
|
+
if (current && (current.text === remote.text || current.updatedAt >= remote.updatedAt)) continue;
|
|
83
|
+
await notesFiles.save(remote.key, remote.text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isCurrent()) return;
|
|
87
|
+
const merged = new Map((await notesFiles.loadQuickNotesIndex()).map((entry) => [entry.id, entry]));
|
|
88
|
+
let changed = false;
|
|
89
|
+
for (const remote of parseQuickNotes(payload.quickNotes)) {
|
|
90
|
+
const current = merged.get(remote.id);
|
|
91
|
+
if (current && (current.updatedAt ?? 0) >= (remote.updatedAt ?? 0)) continue;
|
|
92
|
+
merged.set(remote.id, remote);
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
95
|
+
if (changed) await notesFiles.saveQuickNotesIndex([...merged.values()]);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, expect, test } from "bun:test";
|
|
2
|
-
import { act } from "react";
|
|
2
|
+
import { act, useState } from "react";
|
|
3
3
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
4
4
|
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
5
5
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
@@ -17,6 +17,7 @@ import { OptionsView } from "./view";
|
|
|
17
17
|
const TEST_PANE_ID = "ticker-detail:options-test";
|
|
18
18
|
|
|
19
19
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
20
|
+
let setOptionsQuotePrice: ((price: number) => void) | null = null;
|
|
20
21
|
|
|
21
22
|
function makeTicker(symbol: string): TickerRecord {
|
|
22
23
|
return {
|
|
@@ -125,6 +126,12 @@ function OptionsHarness({
|
|
|
125
126
|
);
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
function RealtimeOptionsHarness({ ticker }: { ticker: TickerRecord }) {
|
|
130
|
+
const [quotePrice, setQuotePrice] = useState(120.2);
|
|
131
|
+
setOptionsQuotePrice = setQuotePrice;
|
|
132
|
+
return <OptionsHarness ticker={ticker} quotePrice={quotePrice} />;
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
async function renderSettled() {
|
|
129
136
|
for (let i = 0; i < 4; i += 1) {
|
|
130
137
|
await act(async () => {
|
|
@@ -141,6 +148,7 @@ afterEach(async () => {
|
|
|
141
148
|
});
|
|
142
149
|
testSetup = undefined;
|
|
143
150
|
}
|
|
151
|
+
setOptionsQuotePrice = null;
|
|
144
152
|
setSharedMarketDataCoordinator(null);
|
|
145
153
|
});
|
|
146
154
|
|
|
@@ -168,7 +176,7 @@ test("defaults the table around the nearest strike to the current quote", async
|
|
|
168
176
|
expect(frame).not.toContain(" 50 ");
|
|
169
177
|
});
|
|
170
178
|
|
|
171
|
-
test("streams
|
|
179
|
+
test("streams live quotes without resetting manual scroll", async () => {
|
|
172
180
|
const strikes = Array.from({ length: 100 }, (_, index) => 50 + index);
|
|
173
181
|
const subscriptions: QuoteSubscriptionTarget[][] = [];
|
|
174
182
|
let emitQuote:
|
|
@@ -187,7 +195,7 @@ test("streams every visible option row and overlays live contract quotes", async
|
|
|
187
195
|
|
|
188
196
|
await act(async () => {
|
|
189
197
|
testSetup = await testRender(
|
|
190
|
-
<
|
|
198
|
+
<RealtimeOptionsHarness ticker={makeTicker("AAPL")} />,
|
|
191
199
|
{
|
|
192
200
|
width: 124,
|
|
193
201
|
height: 16,
|
|
@@ -238,6 +246,20 @@ test("streams every visible option row and overlays live contract quotes", async
|
|
|
238
246
|
}).data?.mark,
|
|
239
247
|
).toBe(99.99);
|
|
240
248
|
expect(testSetup!.captureCharFrame()).toContain("99.99");
|
|
249
|
+
|
|
250
|
+
const bodyScroll = testSetup!.renderer.root.findDescendantById("options-table-body-scroll") as ScrollBoxRenderable;
|
|
251
|
+
await act(async () => {
|
|
252
|
+
bodyScroll.scrollTo(0);
|
|
253
|
+
await testSetup!.renderOnce();
|
|
254
|
+
});
|
|
255
|
+
await renderSettled();
|
|
256
|
+
|
|
257
|
+
await act(async () => {
|
|
258
|
+
setOptionsQuotePrice?.(120.3);
|
|
259
|
+
await testSetup!.renderOnce();
|
|
260
|
+
});
|
|
261
|
+
await renderSettled();
|
|
262
|
+
expect(bodyScroll.scrollTop).toBe(0);
|
|
241
263
|
});
|
|
242
264
|
|
|
243
265
|
test("lets the expiration tab row use the full available width", async () => {
|
|
@@ -465,6 +465,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
465
465
|
sortDirection="asc"
|
|
466
466
|
onHeaderClick={() => {}}
|
|
467
467
|
onTableMouseDown={enterInteractive}
|
|
468
|
+
onBodyScrollActivity={() => { userSelectedStrikeRef.current = true; }}
|
|
468
469
|
visibleRangeKey={viewportKey}
|
|
469
470
|
onVisibleRangeChange={handleVisibleStrikeRangeChange}
|
|
470
471
|
getItemKey={(row) => String(row.strike)}
|
|
@@ -38,6 +38,28 @@ describe("resolvePortfolioAccountMetrics", () => {
|
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test("converts broker account money into the app base currency", () => {
|
|
42
|
+
const account: BrokerAccount = {
|
|
43
|
+
accountId: "DU12345",
|
|
44
|
+
name: "DU12345",
|
|
45
|
+
currency: "EUR",
|
|
46
|
+
netLiquidation: 10_000,
|
|
47
|
+
grossPositionValue: 8_000,
|
|
48
|
+
dailyPnl: 100,
|
|
49
|
+
unrealizedPnl: 400,
|
|
50
|
+
realizedPnl: -50,
|
|
51
|
+
};
|
|
52
|
+
const toUsd = (value: number) => value * 1.1;
|
|
53
|
+
|
|
54
|
+
const metrics = resolvePortfolioAccountMetrics(createTotals(), account, toUsd);
|
|
55
|
+
expect(metrics.dailyPnl).toBeCloseTo(110);
|
|
56
|
+
expect(metrics.dailyPnlPct).toBeCloseTo(110 / 10_890 * 100);
|
|
57
|
+
expect(metrics.unrealizedPnl).toBeCloseTo(440);
|
|
58
|
+
expect(metrics.unrealizedPnlPct).toBeCloseTo(5.5);
|
|
59
|
+
expect(metrics.realizedPnl).toBeCloseTo(-55);
|
|
60
|
+
expect(resolvePortfolioMarketValue(createTotals(), account, toUsd)).toBeCloseTo(8_800);
|
|
61
|
+
});
|
|
62
|
+
|
|
41
63
|
test("falls back to reconstructed portfolio totals when broker P&L is missing", () => {
|
|
42
64
|
expect(resolvePortfolioAccountMetrics(createTotals(), null)).toEqual({
|
|
43
65
|
dailyPnl: 50,
|
|
@@ -17,9 +17,12 @@ function percentChange(value: number, previousValue: number): number {
|
|
|
17
17
|
return previousValue !== 0 ? (value / previousValue) * 100 : 0;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function resolveBrokerPortfolioMarketValue(
|
|
20
|
+
export function resolveBrokerPortfolioMarketValue(
|
|
21
|
+
account?: BrokerAccount | null,
|
|
22
|
+
convertAccountValue: (value: number) => number = (value) => value,
|
|
23
|
+
): number | null {
|
|
21
24
|
if (finiteNumber(account?.grossPositionValue)) {
|
|
22
|
-
return account.grossPositionValue;
|
|
25
|
+
return convertAccountValue(account.grossPositionValue);
|
|
23
26
|
}
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
@@ -27,24 +30,26 @@ export function resolveBrokerPortfolioMarketValue(account?: BrokerAccount | null
|
|
|
27
30
|
export function resolvePortfolioMarketValue(
|
|
28
31
|
totals: PortfolioSummaryTotals,
|
|
29
32
|
account?: BrokerAccount | null,
|
|
33
|
+
convertAccountValue: (value: number) => number = (value) => value,
|
|
30
34
|
): number {
|
|
31
|
-
return resolveBrokerPortfolioMarketValue(account) ?? totals.totalMktValue;
|
|
35
|
+
return resolveBrokerPortfolioMarketValue(account, convertAccountValue) ?? totals.totalMktValue;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export function resolvePortfolioAccountMetrics(
|
|
35
39
|
totals: PortfolioSummaryTotals,
|
|
36
40
|
account?: BrokerAccount | null,
|
|
41
|
+
convertAccountValue: (value: number) => number = (value) => value,
|
|
37
42
|
): PortfolioAccountMetrics {
|
|
38
|
-
const brokerDailyPnl = finiteNumber(account?.dailyPnl) ? account.dailyPnl : null;
|
|
43
|
+
const brokerDailyPnl = finiteNumber(account?.dailyPnl) ? convertAccountValue(account.dailyPnl) : null;
|
|
39
44
|
const dailyPnl = brokerDailyPnl ?? totals.dailyPnl;
|
|
40
45
|
const previousNetLiquidation = brokerDailyPnl != null && finiteNumber(account?.netLiquidation)
|
|
41
|
-
? account.netLiquidation - dailyPnl
|
|
46
|
+
? convertAccountValue(account.netLiquidation) - dailyPnl
|
|
42
47
|
: null;
|
|
43
48
|
const dailyPnlPct = previousNetLiquidation != null
|
|
44
49
|
? percentChange(dailyPnl, previousNetLiquidation)
|
|
45
50
|
: totals.dailyPnlPct;
|
|
46
51
|
|
|
47
|
-
const brokerUnrealizedPnl = finiteNumber(account?.unrealizedPnl) ? account.unrealizedPnl : null;
|
|
52
|
+
const brokerUnrealizedPnl = finiteNumber(account?.unrealizedPnl) ? convertAccountValue(account.unrealizedPnl) : null;
|
|
48
53
|
const unrealizedPnl = brokerUnrealizedPnl ?? totals.unrealizedPnl;
|
|
49
54
|
const unrealizedPnlPct = totals.totalCostBasis !== 0
|
|
50
55
|
? percentChange(unrealizedPnl, totals.totalCostBasis)
|
|
@@ -55,6 +60,6 @@ export function resolvePortfolioAccountMetrics(
|
|
|
55
60
|
dailyPnlPct,
|
|
56
61
|
unrealizedPnl,
|
|
57
62
|
unrealizedPnlPct,
|
|
58
|
-
realizedPnl: finiteNumber(account?.realizedPnl) ? account.realizedPnl : undefined,
|
|
63
|
+
realizedPnl: finiteNumber(account?.realizedPnl) ? convertAccountValue(account.realizedPnl) : undefined,
|
|
59
64
|
};
|
|
60
65
|
}
|
|
@@ -142,11 +142,13 @@ async function showCollectionWithMarketData(
|
|
|
142
142
|
const multiplier = position.multiplier ?? 1;
|
|
143
143
|
const quoteCurrency = quote?.currency ?? ticker.metadata.currency ?? baseCurrency;
|
|
144
144
|
const positionCurrency = position.currency ?? quoteCurrency;
|
|
145
|
+
const direction = position.side === "short" || position.shares < 0 ? -1 : 1;
|
|
146
|
+
const magnitude = Math.abs(position.shares);
|
|
145
147
|
const currentValueBase = quote
|
|
146
|
-
? await toBase(
|
|
148
|
+
? await toBase(magnitude * quote.price * multiplier, quoteCurrency)
|
|
147
149
|
: null;
|
|
148
|
-
const costBasisBase = await toBase(
|
|
149
|
-
const pnl = currentValueBase != null ? currentValueBase - costBasisBase : null;
|
|
150
|
+
const costBasisBase = await toBase(magnitude * position.avgCost * multiplier, positionCurrency);
|
|
151
|
+
const pnl = currentValueBase != null ? direction * (currentValueBase - costBasisBase) : null;
|
|
150
152
|
if (pnl != null) totalPnl += pnl;
|
|
151
153
|
|
|
152
154
|
rows.push([
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getPortfolioPositionMetrics,
|
|
27
27
|
resolveBrokerFallbackMarketValue,
|
|
28
28
|
resolveBrokerFallbackPnl,
|
|
29
|
+
signedQuoteUnrealizedPnl,
|
|
29
30
|
} from "./position-metrics";
|
|
30
31
|
|
|
31
32
|
export interface ColumnContext {
|
|
@@ -332,7 +333,11 @@ export function getColumnValue(
|
|
|
332
333
|
return { text: "—" };
|
|
333
334
|
case "pnl":
|
|
334
335
|
if (activeQuote && totalPriceUnits !== 0) {
|
|
335
|
-
const pnl =
|
|
336
|
+
const pnl = signedQuoteUnrealizedPnl(
|
|
337
|
+
toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price),
|
|
338
|
+
toBasePosition(totalCost),
|
|
339
|
+
totalPriceUnits,
|
|
340
|
+
);
|
|
336
341
|
return { text: `${pnl >= 0 ? "+" : ""}${formatCompact(pnl)}`, color: priceColor(pnl) };
|
|
337
342
|
}
|
|
338
343
|
if (brokerFallbackPnl != null) {
|
|
@@ -344,7 +349,8 @@ export function getColumnValue(
|
|
|
344
349
|
if (activeQuote && totalCost !== 0) {
|
|
345
350
|
const marketValue = toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price);
|
|
346
351
|
const costBasis = toBasePosition(totalCost);
|
|
347
|
-
const
|
|
352
|
+
const pnl = signedQuoteUnrealizedPnl(marketValue, costBasis, totalPriceUnits);
|
|
353
|
+
const percent = costBasis !== 0 ? (pnl / costBasis) * 100 : 0;
|
|
348
354
|
return { text: formatPercentRaw(percent), color: priceColor(percent) };
|
|
349
355
|
}
|
|
350
356
|
if (brokerFallbackPnl != null && totalCost !== 0) {
|
|
@@ -530,7 +536,11 @@ export function getSortValue(
|
|
|
530
536
|
return null;
|
|
531
537
|
case "pnl":
|
|
532
538
|
if (activeQuote && totalPriceUnits !== 0) {
|
|
533
|
-
return
|
|
539
|
+
return signedQuoteUnrealizedPnl(
|
|
540
|
+
toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price),
|
|
541
|
+
toBasePosition(totalCost),
|
|
542
|
+
totalPriceUnits,
|
|
543
|
+
);
|
|
534
544
|
}
|
|
535
545
|
if (brokerFallbackPnl != null) {
|
|
536
546
|
return toBasePosition(brokerFallbackPnl);
|
|
@@ -540,7 +550,8 @@ export function getSortValue(
|
|
|
540
550
|
if (activeQuote && totalCost !== 0) {
|
|
541
551
|
const marketValue = toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price);
|
|
542
552
|
const costBasis = toBasePosition(totalCost);
|
|
543
|
-
|
|
553
|
+
const pnl = signedQuoteUnrealizedPnl(marketValue, costBasis, totalPriceUnits);
|
|
554
|
+
return costBasis !== 0 ? (pnl / costBasis) * 100 : null;
|
|
544
555
|
}
|
|
545
556
|
if (brokerFallbackPnl != null && totalCost !== 0) {
|
|
546
557
|
const costBasis = toBasePosition(totalCost);
|
|
@@ -7,7 +7,7 @@ import { colors } from "../../../theme/colors";
|
|
|
7
7
|
import type { BrokerConnectionStatus } from "../../../types/broker";
|
|
8
8
|
import type { Portfolio } from "../../../types/ticker";
|
|
9
9
|
import type { BrokerAccount } from "../../../types/trading";
|
|
10
|
-
import { formatCompact, padTo } from "../../../utils/format";
|
|
10
|
+
import { convertCurrency, formatCompact, padTo } from "../../../utils/format";
|
|
11
11
|
import { formatMarketQuantity } from "../../../market-data/market/format";
|
|
12
12
|
import { getBrokerInstance } from "../../../utils/broker-instances";
|
|
13
13
|
import { usePluginBrokerActions } from "../../runtime";
|
|
@@ -90,14 +90,24 @@ export function PortfolioCashMarginDrawer({
|
|
|
90
90
|
onToggle,
|
|
91
91
|
width,
|
|
92
92
|
height,
|
|
93
|
+
baseCurrency,
|
|
94
|
+
exchangeRates,
|
|
93
95
|
}: {
|
|
94
96
|
accountState: ResolvedPortfolioAccountState;
|
|
95
97
|
expanded: boolean;
|
|
96
98
|
onToggle: () => void;
|
|
97
99
|
width: number;
|
|
98
100
|
height: number;
|
|
101
|
+
baseCurrency: string;
|
|
102
|
+
exchangeRates: Map<string, number>;
|
|
99
103
|
}) {
|
|
100
|
-
const
|
|
104
|
+
const convertAccountValue = (value: number) => convertCurrency(
|
|
105
|
+
value,
|
|
106
|
+
accountState.account.currency || baseCurrency,
|
|
107
|
+
baseCurrency,
|
|
108
|
+
exchangeRates,
|
|
109
|
+
);
|
|
110
|
+
const previewText = `${accountState.visibleCashBalances.length} ccy · Cash ${formatCompact(convertAccountValue(accountState.account.totalCashValue ?? 0))} · ${accountState.sourceLabel}`;
|
|
101
111
|
const drawerHeight = Math.max(1, height);
|
|
102
112
|
|
|
103
113
|
if (!expanded) {
|
|
@@ -124,7 +134,7 @@ export function PortfolioCashMarginDrawer({
|
|
|
124
134
|
);
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
const metricSegments = buildDrawerMetricSegments(accountState.account, width);
|
|
137
|
+
const metricSegments = buildDrawerMetricSegments(accountState.account, width, convertAccountValue);
|
|
128
138
|
const currencyRowsHeight = Math.max(1, drawerHeight - 2);
|
|
129
139
|
|
|
130
140
|
return (
|
|
@@ -47,7 +47,15 @@ export const portfolioListModule: PluginModule = {
|
|
|
47
47
|
defaultPosition: "left",
|
|
48
48
|
defaultMode: "floating",
|
|
49
49
|
defaultWidth: "40%",
|
|
50
|
+
tickerSource: true,
|
|
50
51
|
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
52
|
+
portableShare: {
|
|
53
|
+
private: {
|
|
54
|
+
params: ["collectionId"],
|
|
55
|
+
settings: ["collectionId", "visibleCollectionIds"],
|
|
56
|
+
state: ["collectionId", "collectionSorts"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
51
59
|
settings: (context) => withLiveStreamingSetting(
|
|
52
60
|
buildPortfolioPaneSettingsDef(
|
|
53
61
|
context.config,
|
|
@@ -199,7 +199,7 @@ describe("portfolio-metrics", () => {
|
|
|
199
199
|
|
|
200
200
|
expect(totals.totalMktValue).toBeCloseTo(58803.06, 2);
|
|
201
201
|
expect(totals.totalCostBasis).toBeCloseTo(50950.7295, 4);
|
|
202
|
-
expect(totals.unrealizedPnl).toBeCloseTo(7852.
|
|
202
|
+
expect(totals.unrealizedPnl).toBeCloseTo(7852.33, 4);
|
|
203
203
|
expect(totals.hasPositions).toBe(true);
|
|
204
204
|
});
|
|
205
205
|
|
|
@@ -277,6 +277,36 @@ describe("portfolio-metrics", () => {
|
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
test("signs short unrealized P&L from a live quote", () => {
|
|
281
|
+
const ticker = createTicker({
|
|
282
|
+
positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "ibkr", side: "short" }],
|
|
283
|
+
});
|
|
284
|
+
const financials = createFinancials({
|
|
285
|
+
quote: { price: 80, change: -20, changePercent: -20, previousClose: 100 },
|
|
286
|
+
});
|
|
287
|
+
const dayPnlColumn: ColumnConfig = { id: "day_pnl", label: "DAY", width: 10, align: "right", format: "compact" };
|
|
288
|
+
const pnlColumn: ColumnConfig = { id: "pnl", label: "P&L", width: 10, align: "right", format: "compact" };
|
|
289
|
+
|
|
290
|
+
expect(getSortValue(dayPnlColumn, ticker, financials, defaultColumnContext)).toBe(200);
|
|
291
|
+
expect(getSortValue(pnlColumn, ticker, financials, defaultColumnContext)).toBe(200);
|
|
292
|
+
expect(getColumnValue(pnlColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
293
|
+
text: "+200",
|
|
294
|
+
color: expect.any(String),
|
|
295
|
+
});
|
|
296
|
+
expect(calculatePortfolioSummaryTotals(
|
|
297
|
+
[ticker],
|
|
298
|
+
new Map([["AAPL", financials]]),
|
|
299
|
+
"USD",
|
|
300
|
+
new Map([["USD", 1]]),
|
|
301
|
+
true,
|
|
302
|
+
"main",
|
|
303
|
+
)).toMatchObject({
|
|
304
|
+
totalMktValue: 800,
|
|
305
|
+
dailyPnl: 200,
|
|
306
|
+
unrealizedPnl: 200,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
280
310
|
test("formats derived quote and position columns", () => {
|
|
281
311
|
const ticker = createTicker({
|
|
282
312
|
sector: "Technology",
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type DataTableKeyEvent,
|
|
7
7
|
type TickerListVisibleRange,
|
|
8
8
|
} from "../../../../components";
|
|
9
|
-
import {
|
|
9
|
+
import { useTickerSourceActivate } from "../../shared/ticker-source";
|
|
10
10
|
import { useFxRatesMap, useTickerFinancialsMap } from "../../../../market-data/hooks";
|
|
11
11
|
import { useAppActive } from "../../../../state/app/activity";
|
|
12
12
|
import {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { selectEffectiveExchangeRates } from "../../../../utils/exchange-rate-map";
|
|
21
21
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
22
22
|
import type { PaneProps } from "../../../../types/plugin";
|
|
23
|
-
import { TICKER_RESEARCH_PANE_ID } from "../../../../types/config";
|
|
24
23
|
import { calculatePortfolioSummaryTotals, resolveCollectionSortPreference, type ColumnContext } from "../metrics";
|
|
25
24
|
import {
|
|
26
25
|
PortfolioCashMarginDrawer,
|
|
@@ -55,7 +54,7 @@ import { useLiveStreamingSetting } from "../../shared/live-streaming";
|
|
|
55
54
|
import { useThrottledTickerOrder } from "../use-throttled-ticker-order";
|
|
56
55
|
|
|
57
56
|
export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
58
|
-
const
|
|
57
|
+
const activateTicker = useTickerSourceActivate();
|
|
59
58
|
const paneInstance = usePaneInstance();
|
|
60
59
|
const appActive = useAppActive();
|
|
61
60
|
const config = useAppSelector((state) => state.config);
|
|
@@ -238,9 +237,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
238
237
|
setSortPreference({ columnId, direction: "asc" });
|
|
239
238
|
}, [activeSort.columnId, activeSort.direction, setSortPreference]);
|
|
240
239
|
|
|
241
|
-
const openTickerFloating = useCallback((symbol: string) => {
|
|
242
|
-
|
|
243
|
-
}, [
|
|
240
|
+
const openTickerFloating = useCallback((symbol: string, options?: { newPane?: boolean }) => {
|
|
241
|
+
activateTicker(symbol, { floating: true, newPane: options?.newPane });
|
|
242
|
+
}, [activateTicker]);
|
|
244
243
|
|
|
245
244
|
const toggleViewMode = useCallback(() => {
|
|
246
245
|
if (!isPortfolioTab) return;
|
|
@@ -266,7 +265,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
266
265
|
event.stopPropagation?.();
|
|
267
266
|
const ticker = sortedTickers[safeSelectedIdx];
|
|
268
267
|
if (ticker) {
|
|
269
|
-
|
|
268
|
+
flushCursorSymbol(ticker.metadata.ticker);
|
|
269
|
+
openTickerFloating(ticker.metadata.ticker, { newPane: true });
|
|
270
270
|
}
|
|
271
271
|
return true;
|
|
272
272
|
}
|
|
@@ -288,6 +288,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
288
288
|
return false;
|
|
289
289
|
}, [
|
|
290
290
|
cashDrawerExpanded,
|
|
291
|
+
flushCursorSymbol,
|
|
291
292
|
focused,
|
|
292
293
|
isPortfolioTab,
|
|
293
294
|
openTickerFloating,
|
|
@@ -475,6 +476,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
475
476
|
onToggle={() => setCashDrawerExpanded(!cashDrawerExpanded)}
|
|
476
477
|
width={Math.max(0, width - 2)}
|
|
477
478
|
height={drawerHeight}
|
|
479
|
+
baseCurrency={config.baseCurrency}
|
|
480
|
+
exchangeRates={effectiveExchangeRates}
|
|
478
481
|
/>
|
|
479
482
|
</Box>
|
|
480
483
|
)}
|
|
@@ -71,7 +71,12 @@ export function usePortfolioPaneStreaming({
|
|
|
71
71
|
const streamTargets = useMemo(() => (
|
|
72
72
|
sortedTickers
|
|
73
73
|
.map((ticker) => {
|
|
74
|
-
const target = quoteSubscriptionTargetFromTicker(
|
|
74
|
+
const target = quoteSubscriptionTargetFromTicker(
|
|
75
|
+
ticker,
|
|
76
|
+
ticker.metadata.ticker,
|
|
77
|
+
isPortfolioTab && (ticker.metadata.broker_contracts?.length ?? 0) > 0 ? "broker" : "provider",
|
|
78
|
+
instrumentOptions,
|
|
79
|
+
);
|
|
75
80
|
if (!target) return null;
|
|
76
81
|
const selected = ticker.metadata.ticker === cursorSymbol;
|
|
77
82
|
const visible = priorityStreamSymbols.has(ticker.metadata.ticker);
|
|
@@ -27,6 +27,24 @@ function normalizePositionMultiplier(multiplier: number | undefined): number {
|
|
|
27
27
|
: 1;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function signedPositionDirection(position: {
|
|
31
|
+
shares: number;
|
|
32
|
+
side?: "long" | "short";
|
|
33
|
+
}): 1 | -1 {
|
|
34
|
+
if (position.side === "short") return -1;
|
|
35
|
+
if (position.side === "long") return 1;
|
|
36
|
+
return position.shares < 0 ? -1 : 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Quote-path unrealized P&L. Shorts profit when market value falls below cost. */
|
|
40
|
+
export function signedQuoteUnrealizedPnl(
|
|
41
|
+
absMarketValue: number,
|
|
42
|
+
cost: number,
|
|
43
|
+
totalPriceUnits: number,
|
|
44
|
+
): number {
|
|
45
|
+
return (totalPriceUnits < 0 ? -1 : 1) * (absMarketValue - cost);
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
function resolvePositionCostMultiplier(
|
|
31
49
|
position: TickerRecord["metadata"]["positions"][number],
|
|
32
50
|
): number {
|
|
@@ -37,7 +55,7 @@ function resolvePositionCostMultiplier(
|
|
|
37
55
|
return priceMultiplier;
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
const costWithoutMultiplier = position.shares * position.avgCost;
|
|
58
|
+
const costWithoutMultiplier = Math.abs(position.shares) * position.avgCost;
|
|
41
59
|
const costWithMultiplier = costWithoutMultiplier * priceMultiplier;
|
|
42
60
|
const withoutMultiplierError = Math.abs((position.marketValue - costWithoutMultiplier) - position.unrealizedPnl);
|
|
43
61
|
const withMultiplierError = Math.abs((position.marketValue - costWithMultiplier) - position.unrealizedPnl);
|
|
@@ -65,15 +83,16 @@ export function getPortfolioPositionMetrics(
|
|
|
65
83
|
let brokerPnl = 0;
|
|
66
84
|
let hasBrokerPnl = false;
|
|
67
85
|
for (const position of tabPositions) {
|
|
68
|
-
const direction = position
|
|
86
|
+
const direction = signedPositionDirection(position);
|
|
87
|
+
const magnitude = Math.abs(position.shares);
|
|
69
88
|
const priceMultiplier = normalizePositionMultiplier(position.multiplier);
|
|
70
89
|
const costMultiplier = resolvePositionCostMultiplier(position);
|
|
71
90
|
multiplierHint = Math.max(multiplierHint, priceMultiplier, costMultiplier);
|
|
72
91
|
|
|
73
|
-
totalShares +=
|
|
74
|
-
totalCost +=
|
|
75
|
-
totalCostUnits +=
|
|
76
|
-
totalPriceUnits +=
|
|
92
|
+
totalShares += magnitude * direction;
|
|
93
|
+
totalCost += magnitude * position.avgCost * costMultiplier;
|
|
94
|
+
totalCostUnits += magnitude * costMultiplier;
|
|
95
|
+
totalPriceUnits += magnitude * priceMultiplier * direction;
|
|
77
96
|
|
|
78
97
|
if (position.marketValue != null) {
|
|
79
98
|
brokerMktValue += position.marketValue;
|