gloomberb 0.18.0 → 0.18.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/package.json +1 -1
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +31 -7
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/onboarding/wizard-positions.ts +8 -0
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +57 -22
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +63 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/ticker.ts +9 -0
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +41 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
import { formatCompact, formatPercentRaw } from "../../../../utils/format";
|
|
13
13
|
import { currencyPriceMinDigits, formatMarketCostWithCurrency, formatMarketPrice, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions } from "../../../../market-data/market/format";
|
|
14
14
|
import { resolvePriceBasis } from "../../../../market-data/market/price-basis";
|
|
15
|
-
import {
|
|
15
|
+
import { combinedChangeBasisLabel, quoteChangeBasis, type QuoteChangeBasis } from "../../../../market-data/market/change-basis";
|
|
16
|
+
import { getPortfolioPositionMetrics, lotCostAtTodaysRate, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../../../portfolio/position-metrics";
|
|
16
17
|
import {
|
|
17
18
|
getPortfolioValuationDisplay,
|
|
18
19
|
getPortfolioValuationTime,
|
|
@@ -56,6 +57,7 @@ import { loadPerpetualMarket } from "../../../../sources/gloomberb-cloud/perpetu
|
|
|
56
57
|
import { listedMarketCapitalization, selectMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
57
58
|
import { chunked, mapWithConcurrency } from "../../../../utils/concurrency";
|
|
58
59
|
import { liveMarketCapitalization } from "../../../../market-data/quotes/live-valuation";
|
|
60
|
+
import { delistingLabel } from "../../../../market-data/quotes/delisting";
|
|
59
61
|
import { instrumentFromTicker } from "../../../../market-data/request-types";
|
|
60
62
|
import { toMarketDataContext } from "../../../../market-data/selectors";
|
|
61
63
|
import { cliFreshnessFooter } from "../../../../cli/result";
|
|
@@ -282,6 +284,12 @@ interface PortfolioPositionValue {
|
|
|
282
284
|
row: Record<string, unknown>;
|
|
283
285
|
/** A future's contract size and settlement from its terms; null for any other holding. */
|
|
284
286
|
futures: PortfolioFuturesTerms | null;
|
|
287
|
+
/**
|
|
288
|
+
* Only on a lot held in another currency than the portfolio's: the rate its
|
|
289
|
+
* cost converted at (one unit of its currency in the portfolio's, when
|
|
290
|
+
* bought), or null when it converted at today's rate.
|
|
291
|
+
*/
|
|
292
|
+
costFx?: { rate: number; currency: string } | null;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
/** What a row's value is priced at: exported after every other field, so existing columns keep their places. */
|
|
@@ -309,6 +317,8 @@ export interface PortfolioValuation {
|
|
|
309
317
|
unknownContractSize: Set<string>;
|
|
310
318
|
/** Futures valued at the close without a confirmed settlement to value them at. */
|
|
311
319
|
unsettled: Set<string>;
|
|
320
|
+
/** Holdings in another currency whose cost converts at today's rate: no purchase date or cost FX rate was entered. */
|
|
321
|
+
costAtTodaysRate: Set<string>;
|
|
312
322
|
/** Long and short market value of the priced lots, and their net and gross, in the portfolio's currency. */
|
|
313
323
|
exposure: PortfolioExposure;
|
|
314
324
|
}
|
|
@@ -333,6 +343,15 @@ function portfolioExposure(values: Iterable<number | null | undefined>): Portfol
|
|
|
333
343
|
return { long, short, net: long + short, gross: long - short };
|
|
334
344
|
}
|
|
335
345
|
|
|
346
|
+
/**
|
|
347
|
+
* What a book says of holdings in another currency whose cost converts at
|
|
348
|
+
* today's rate, which leaves the currency's move since purchase out of their
|
|
349
|
+
* P&L; `named` lists them as the table names its rows.
|
|
350
|
+
*/
|
|
351
|
+
export function costAtTodaysRateNote(named: string): string {
|
|
352
|
+
return `Cost converted at today's rate for ${named}; give portfolio position set a --date or --cost-fx to use the rate when bought.`;
|
|
353
|
+
}
|
|
354
|
+
|
|
336
355
|
/**
|
|
337
356
|
* Values every position of a portfolio in the portfolio's currency, as
|
|
338
357
|
* `portfolio show` prints it: cost basis, market value and unrealized P&L
|
|
@@ -368,6 +387,7 @@ export async function valuePortfolioPositions({
|
|
|
368
387
|
const brokerPnlSymbols = new Set<string>();
|
|
369
388
|
const unknownContractSize = new Set<string>();
|
|
370
389
|
const unsettled = new Set<string>();
|
|
390
|
+
const costAtTodaysRate = new Set<string>();
|
|
371
391
|
const values: PortfolioPositionValue[] = [];
|
|
372
392
|
const holdings: AllocationHolding[] = [];
|
|
373
393
|
const known = (value: number | null | undefined): number | null => value != null && Number.isFinite(value) ? value : null;
|
|
@@ -409,11 +429,17 @@ export async function valuePortfolioPositions({
|
|
|
409
429
|
const valuationQuote = getPortfolioValuationDisplay(metrics, quote, basis, settlement);
|
|
410
430
|
const atSettlement = valuesAtSettlement(metrics, basis);
|
|
411
431
|
const positionCurrency = metrics.positionCurrency;
|
|
412
|
-
const costBasisBase = positionCurrency ? await toBase(metrics.totalCost, positionCurrency) : Number.NaN;
|
|
413
|
-
|
|
414
432
|
const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
|
|
433
|
+
// The cost converts at the rate the lot was bought at when one was entered, else at today's, as its value does.
|
|
415
434
|
const baseMetrics = getPortfolioPositionMetrics({ ...ticker, metadata: { ...ticker.metadata, positions: [position] } }, id, quoteCurrency,
|
|
416
435
|
{ currency, convert: value => value * positionRate }, quote);
|
|
436
|
+
const boughtRate = positionCurrency ? lotCostFxRate(position, positionCurrency, currency) : null;
|
|
437
|
+
const costBasisBase = !positionCurrency ? Number.NaN
|
|
438
|
+
: boughtRate !== null ? baseMetrics.totalCost : await toBase(metrics.totalCost, positionCurrency);
|
|
439
|
+
// Only a lot entered by hand can be given the rate it was bought at; a broker's lot reads as it always has.
|
|
440
|
+
const atTodaysRate = position.broker === "manual" && !!positionCurrency && lotCostAtTodaysRate(position, positionCurrency, currency);
|
|
441
|
+
if (atTodaysRate && metrics.hasCostBasis) costAtTodaysRate.add(ticker.metadata.ticker);
|
|
442
|
+
const costFx = boughtRate !== null ? { rate: boughtRate, currency } : atTodaysRate ? null : undefined;
|
|
417
443
|
const currentValueBase = resolvePortfolioMarketValue(baseMetrics,
|
|
418
444
|
valuationQuote ? await toBase(valuationQuote.price, quoteCurrency) : null)?.gross ?? null;
|
|
419
445
|
const selectedPnl = resolvePortfolioPositionPnl(baseMetrics,
|
|
@@ -436,7 +462,7 @@ export async function valuePortfolioPositions({
|
|
|
436
462
|
? { change: await toBase(metrics.totalPriceUnits * change, quoteCurrency),
|
|
437
463
|
previousValue: await toBase(metrics.grossPriceUnits * (valuationQuote.price - change), quoteCurrency) }
|
|
438
464
|
: null;
|
|
439
|
-
values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl, futures: futuresTerms,
|
|
465
|
+
values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl, futures: futuresTerms, ...(costFx !== undefined ? { costFx } : {}),
|
|
440
466
|
dayMove: dayMove && Number.isFinite(dayMove.change) && Number.isFinite(dayMove.previousValue) ? dayMove : null,
|
|
441
467
|
valuation: { valuationBasis: basis, valuationPrice: known(valuationQuote?.price),
|
|
442
468
|
valuationAsOf: valuationTimeIso(getPortfolioValuationTime(quote, basis, metrics, settlement)),
|
|
@@ -455,35 +481,52 @@ export async function valuePortfolioPositions({
|
|
|
455
481
|
|
|
456
482
|
const exposure = portfolioExposure(values.map((entry) => entry.position ? entry.row.marketValue as number | null : null));
|
|
457
483
|
return { basis, positions: values, holdings, totalPnl, unavailablePnl, unavailableCost, unavailableMarketValue, brokerPnlSymbols,
|
|
458
|
-
unknownContractSize, unsettled, exposure };
|
|
484
|
+
unknownContractSize, unsettled, costAtTodaysRate, exposure };
|
|
459
485
|
}
|
|
460
486
|
|
|
461
487
|
/**
|
|
462
488
|
* The holdings' move over the day of the prices they are valued at, in the
|
|
463
489
|
* portfolio's currency, and that move against their value at the close
|
|
464
|
-
* before
|
|
490
|
+
* before, with what it is measured from as the pane's Day says it ("vs Fri 16
|
|
491
|
+
* Oct close", "vs mixed closes"). Null when any priced holding's move is
|
|
492
|
+
* unknown, as the pane's Day is.
|
|
465
493
|
*/
|
|
466
|
-
export function portfolioDayMove(
|
|
494
|
+
export function portfolioDayMove(
|
|
495
|
+
valuation: Pick<PortfolioValuation, "positions">,
|
|
496
|
+
now = Date.now(),
|
|
497
|
+
): { change: number; changePercent: number | null; basis: string | null } | null {
|
|
467
498
|
let change = 0;
|
|
468
499
|
let previousValue = 0;
|
|
500
|
+
const bases: Array<QuoteChangeBasis | null> = [];
|
|
469
501
|
for (const entry of valuation.positions) {
|
|
470
502
|
if (!entry.position) continue;
|
|
471
503
|
if (!entry.dayMove) return null;
|
|
472
504
|
change += entry.dayMove.change;
|
|
473
505
|
previousValue += entry.dayMove.previousValue;
|
|
506
|
+
bases.push(quoteChangeBasis(entry.quote, { terms: entry.futures }));
|
|
474
507
|
}
|
|
475
|
-
return {
|
|
508
|
+
return {
|
|
509
|
+
change,
|
|
510
|
+
changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null,
|
|
511
|
+
basis: combinedChangeBasisLabel(bases, now),
|
|
512
|
+
};
|
|
476
513
|
}
|
|
477
514
|
|
|
478
|
-
/**
|
|
515
|
+
/**
|
|
516
|
+
* `Day's move: +$1,165.49 (+0.51%) vs Fri 16 Oct close`, the line a book
|
|
517
|
+
* valued at the close carries; `color` tints the figures by sign, `muted` the
|
|
518
|
+
* basis.
|
|
519
|
+
*/
|
|
479
520
|
export function dayMoveLine(
|
|
480
521
|
move: ReturnType<typeof portfolioDayMove>,
|
|
481
522
|
currency: string,
|
|
482
523
|
color: (text: string, value: number) => string = (text) => text,
|
|
524
|
+
muted: (text: string) => string = (text) => text,
|
|
483
525
|
): string {
|
|
484
526
|
if (!move) return "Day's move: unavailable";
|
|
485
527
|
const percent = move.changePercent != null ? ` (${formatPercentRaw(move.changePercent)})` : "";
|
|
486
|
-
|
|
528
|
+
const basis = move.basis ? ` ${muted(move.basis)}` : "";
|
|
529
|
+
return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}${basis}`;
|
|
487
530
|
}
|
|
488
531
|
|
|
489
532
|
/**
|
|
@@ -725,6 +768,7 @@ async function showCollectionWithMarketData(
|
|
|
725
768
|
brokerPnlSymbols,
|
|
726
769
|
unknownContractSize,
|
|
727
770
|
unsettled,
|
|
771
|
+
costAtTodaysRate,
|
|
728
772
|
exposure,
|
|
729
773
|
} = valuation;
|
|
730
774
|
const { allocation, cash } = await valuePortfolioAllocation({ valuation, portfolio: matchedPortfolio, account, toBase });
|
|
@@ -782,6 +826,8 @@ async function showCollectionWithMarketData(
|
|
|
782
826
|
contractMultiplier: entry.position.multiplier ?? null,
|
|
783
827
|
contractSize: entry.futures.size?.size ?? null,
|
|
784
828
|
} : {}),
|
|
829
|
+
// Only on a lot held in another currency: the rate its cost converted at when bought, null when at today's.
|
|
830
|
+
...(entry.costFx !== undefined ? { costFxRate: entry.costFx?.rate ?? null, costFxCurrency: entry.costFx?.currency ?? null } : {}),
|
|
785
831
|
};
|
|
786
832
|
});
|
|
787
833
|
const totalPnlValue = unavailablePnl.size > 0 ? null : totalPnl;
|
|
@@ -871,7 +917,7 @@ async function showCollectionWithMarketData(
|
|
|
871
917
|
total: allocation.total, cash: cashExport, targetSum: allocation.targetSum,
|
|
872
918
|
// Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
|
|
873
919
|
...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
|
|
874
|
-
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
|
|
920
|
+
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
|
|
875
921
|
unpricedCount: allocation.unpriced.length, unpricedSymbols: allocation.unpriced,
|
|
876
922
|
totalUnrealizedPnl: totalPnlValue,
|
|
877
923
|
annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
|
|
@@ -884,6 +930,8 @@ async function showCollectionWithMarketData(
|
|
|
884
930
|
exposure,
|
|
885
931
|
...(unknownContractSize.size > 0 ? { unknownContractSizeSymbols: [...unknownContractSize] } : {}),
|
|
886
932
|
...(unsettled.size > 0 ? { unsettledSymbols: [...unsettled] } : {}),
|
|
933
|
+
// Holdings in another currency with no purchase date or cost FX rate: their cost converts at today's rate.
|
|
934
|
+
...(costAtTodaysRate.size > 0 ? { costAtTodaysRateSymbols: [...costAtTodaysRate] } : {}),
|
|
887
935
|
accountingBasis, ...(manualAccounting ? { manualAccounting } : {}),
|
|
888
936
|
...(matchedPortfolio.importedFrom ? { importedFrom: matchedPortfolio.importedFrom } : {}),
|
|
889
937
|
...(explain ? { glossary: glossaryExport(explained) } : {}),
|
|
@@ -1014,7 +1062,7 @@ async function showCollectionWithMarketData(
|
|
|
1014
1062
|
rows,
|
|
1015
1063
|
));
|
|
1016
1064
|
console.log("");
|
|
1017
|
-
if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign));
|
|
1065
|
+
if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign, cliStyles.muted));
|
|
1018
1066
|
if (valued.some((entry) => entry.position)) console.log(exposureLine(exposure, currency));
|
|
1019
1067
|
const notes: string[] = [];
|
|
1020
1068
|
const weightNote = weightBasisLine(allocation, currency);
|
|
@@ -1038,6 +1086,7 @@ async function showCollectionWithMarketData(
|
|
|
1038
1086
|
const targetNote = describeTargetSum(allocation.targetSum, allocation.weightBasis);
|
|
1039
1087
|
if (targetNote) notes.push(cliStyles.muted(targetNote));
|
|
1040
1088
|
if (brokerPnlSymbols.size > 0) notes.push(cliStyles.muted(`P&L for ${named(brokerPnlSymbols)} is the broker's last snapshot.`));
|
|
1089
|
+
if (costAtTodaysRate.size > 0) notes.push(cliStyles.muted(costAtTodaysRateNote(named(costAtTodaysRate))));
|
|
1041
1090
|
if (unknownContractSize.size > 0) {
|
|
1042
1091
|
notes.push(cliStyles.warning(`No contract size for ${named(unknownContractSize)}, so ${unknownContractSize.size === 1 ? "its" : "their"} value, cost and P&L are not shown.`));
|
|
1043
1092
|
}
|
|
@@ -1106,13 +1155,15 @@ async function showCollectionWithMarketData(
|
|
|
1106
1155
|
const priceText = quote && headline
|
|
1107
1156
|
? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
|
|
1108
1157
|
: "—";
|
|
1109
|
-
|
|
1158
|
+
// A listing that no longer trades has no day's move: its last session's is years old.
|
|
1159
|
+
const changeText = quote?.delisting ? "—" : headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
|
|
1160
|
+
const staleText = quote?.delisting ? delistingLabel(quote.delisting) : lastTrade ? `STALE ${lastTrade}` : "STALE";
|
|
1110
1161
|
return [
|
|
1111
1162
|
listingLabel(ticker),
|
|
1112
1163
|
venue ?? "—",
|
|
1113
1164
|
priceText,
|
|
1114
1165
|
changeText,
|
|
1115
|
-
...stale.length > 0 ? [rowStale ? cliStyles.warning(
|
|
1166
|
+
...stale.length > 0 ? [rowStale ? cliStyles.warning(staleText) : ""] : [],
|
|
1116
1167
|
...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
|
|
1117
1168
|
...showFunding ? [funding == null ? "—" : colorBySign(formatPerpetualRate(funding, 4), funding)] : [],
|
|
1118
1169
|
marketCap == null ? "—" : `${formatCompact(marketCap)} ${currency}`,
|
|
@@ -1138,7 +1189,8 @@ async function showCollectionWithMarketData(
|
|
|
1138
1189
|
if (notes.length > 0) console.log(["", ...notes.map((note) => cliStyles.muted(note))].join("\n"));
|
|
1139
1190
|
// The closing line counts the stale rows; the line under it names them.
|
|
1140
1191
|
if (stale.length > 0) {
|
|
1141
|
-
staleNote = `Stale: ${namedList(stale.map(({ ticker, lastTrade }) => `${listingLabel(ticker)}${
|
|
1192
|
+
staleNote = `Stale: ${namedList(stale.map(({ ticker, quote, lastTrade }) => `${listingLabel(ticker)}${quote?.delisting ? ` (${delistingLabel(quote.delisting)})`
|
|
1193
|
+
: lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
|
|
1142
1194
|
}
|
|
1143
1195
|
}
|
|
1144
1196
|
if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}${staleNote ? `\n${cliStyles.muted(staleNote)}` : ""}`);
|
|
@@ -1274,6 +1326,8 @@ async function valueWatchlistRows({
|
|
|
1274
1326
|
// A stale quote (as the source line counts it) and the exchange-local date it last traded.
|
|
1275
1327
|
stale,
|
|
1276
1328
|
lastTradeDate: lastTrade,
|
|
1329
|
+
// A listing that no longer trades: its status and last session.
|
|
1330
|
+
...quote?.delisting ? { delisting: quote.delisting } : {},
|
|
1277
1331
|
},
|
|
1278
1332
|
});
|
|
1279
1333
|
}
|
|
@@ -125,7 +125,7 @@ async function addToWatchlistCommand(args: string[], file: string | undefined, e
|
|
|
125
125
|
dryRun,
|
|
126
126
|
add: (ticker) => addTickerToWatchlist(ticker, watchlist.id),
|
|
127
127
|
});
|
|
128
|
-
printSymbolRows(ctx, rows, { verb: "add", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated,
|
|
128
|
+
printSymbolRows(ctx, rows, { verb: "add", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, notes: list.notes, retry, dryRun });
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -158,7 +158,7 @@ async function removeFromWatchlistCommand(args: string[], file: string | undefin
|
|
|
158
158
|
store: context.store,
|
|
159
159
|
remove: (ticker) => removeWatchlistMembership(ticker, watchlist.id),
|
|
160
160
|
});
|
|
161
|
-
printSymbolRows(ctx, rows, { verb: "remove", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated,
|
|
161
|
+
printSymbolRows(ctx, rows, { verb: "remove", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, notes: list.notes });
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
valuationTimeText,
|
|
25
25
|
valuePortfolioAllocation,
|
|
26
26
|
valuePortfolioPositions,
|
|
27
|
+
costAtTodaysRateNote,
|
|
27
28
|
type CollectionMatch,
|
|
28
29
|
type PortfolioValuation,
|
|
29
30
|
} from "./cli/render";
|
|
@@ -401,6 +402,7 @@ async function portfolioHoldings(
|
|
|
401
402
|
: []),
|
|
402
403
|
...(targetNote ? [targetNote] : []),
|
|
403
404
|
...(target.portfolio.brokerInstanceId && !resolved.account ? [BROKER_ACCOUNT_MISSING_NOTE] : []),
|
|
405
|
+
...(valuation.costAtTodaysRate.size > 0 ? [costAtTodaysRateNote([...valuation.costAtTodaysRate].join(", "))] : []),
|
|
404
406
|
];
|
|
405
407
|
|
|
406
408
|
const positionColumns = allocation.weightBasis === "gross"
|
|
@@ -442,7 +444,7 @@ async function portfolioHoldings(
|
|
|
442
444
|
cash: allocation.cash ? finite(allocation.cash.value) : null,
|
|
443
445
|
total: allocation.total,
|
|
444
446
|
// At the regular close, the holdings' move from the close before.
|
|
445
|
-
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
|
|
447
|
+
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
|
|
446
448
|
},
|
|
447
449
|
// Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
|
|
448
450
|
...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isStablecoinCurrency } from "../../../../utils/currency-units";
|
|
1
|
+
import { heldCurrencyUnit, isStablecoinCurrency } from "../../../../utils/currency-units";
|
|
2
|
+
import { isIndexLevel } from "../../../../market-data/market/index-level";
|
|
2
3
|
import { createRowValueCache } from "../../../../components/ui/row-value-cache";
|
|
3
4
|
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../../components/price-sparkline/view";
|
|
4
5
|
import type { AppConfig, ColumnConfig } from "../../../../types/config";
|
|
@@ -196,6 +197,27 @@ export function buildTrackedCurrencies(
|
|
|
196
197
|
return [...currencies].filter((currency) => !isStablecoinCurrency(currency));
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Whether a row is priced in another currency than the collection's totals
|
|
202
|
+
* (RY in CAD beside JNJ in USD, or a US stock in a CAD portfolio), so its
|
|
203
|
+
* prices need their currency beside them. Pence and cents count as their
|
|
204
|
+
* currency, whose sign the price already carries; an index has none.
|
|
205
|
+
*/
|
|
206
|
+
export function holdsOtherCurrencies(
|
|
207
|
+
tickers: readonly TickerRecord[],
|
|
208
|
+
financialsMap: ReadonlyMap<string, TickerFinancials>,
|
|
209
|
+
totalsCurrency: string,
|
|
210
|
+
): boolean {
|
|
211
|
+
const totals = heldCurrencyUnit(totalsCurrency).currency;
|
|
212
|
+
return tickers.some((ticker) => {
|
|
213
|
+
const quote = financialsMap.get(ticker.metadata.ticker)?.quote;
|
|
214
|
+
if (isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return false;
|
|
215
|
+
const currency = heldCurrencyUnit(ticker.metadata.positions.find((position) => position.currency)?.currency
|
|
216
|
+
|| quote?.currency || ticker.metadata.currency).currency;
|
|
217
|
+
return !!currency && currency !== totals;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
199
221
|
export function getCollectionTypeFromConfig(config: AppConfig, collectionId: string | null): "portfolio" | "watchlist" | null {
|
|
200
222
|
if (!collectionId) return null;
|
|
201
223
|
if (config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Box } from "../../../../ui";
|
|
1
|
+
import { Box, useUiCapabilities } from "../../../../ui";
|
|
2
|
+
import { brokerPortfolioLogo } from "../../../../brokers/logos";
|
|
3
|
+
import { LogoMark } from "../../../../components/ui/logo-mark";
|
|
2
4
|
import { describeFundamentalMarketCap } from "../../../../utils/market-capitalization";
|
|
3
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
6
|
import {
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
renderSummarySegments,
|
|
54
56
|
} from "../summary";
|
|
55
57
|
import { usePortfolioAccountState } from "../summary/live-accounts";
|
|
58
|
+
import { useOfflineBrokerFooter } from "../../shared/broker-reconnect";
|
|
56
59
|
import {
|
|
57
60
|
getCollectionEntries,
|
|
58
61
|
getPortfolioPaneSettings,
|
|
@@ -68,7 +71,7 @@ import { PortfolioGrid } from "../grid";
|
|
|
68
71
|
import { useThrottledCursorSymbol } from "../use-throttled-cursor-symbol";
|
|
69
72
|
import { useCursorNeighborPrefetch } from "../use-cursor-neighbor-prefetch";
|
|
70
73
|
import { getSharedRegistry } from "../../../registry";
|
|
71
|
-
import { usePluginAppActions } from "../../../runtime";
|
|
74
|
+
import { usePluginAppActions, usePluginBrokerActions } from "../../../runtime";
|
|
72
75
|
import { isManualPortfolio } from "../../../../portfolio/mutations";
|
|
73
76
|
import {
|
|
74
77
|
cursorAfterRemoval,
|
|
@@ -77,8 +80,10 @@ import {
|
|
|
77
80
|
tickerRemovalSummary,
|
|
78
81
|
} from "./remove-ticker";
|
|
79
82
|
import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
|
|
83
|
+
import { delistingLabel } from "../../../../market-data/quotes/delisting";
|
|
80
84
|
import {
|
|
81
85
|
buildTrackedCurrencies,
|
|
86
|
+
holdsOtherCurrencies,
|
|
82
87
|
getCollectionTickersFromConfig,
|
|
83
88
|
getCollectionTypeFromConfig,
|
|
84
89
|
resolveVisibleWarmupRequirements,
|
|
@@ -190,12 +195,21 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
190
195
|
const flashSymbols = useQuoteFlashMap(financialsMap, valueFlashingEnabled);
|
|
191
196
|
|
|
192
197
|
const accountStateInput = useMemo(() => ({ brokerAccounts, config }), [brokerAccounts, config]);
|
|
193
|
-
const { accountState, accountsError,
|
|
198
|
+
const { accountState, accountsError, offline } = usePortfolioAccountState(currentPortfolio, accountStateInput);
|
|
194
199
|
const brokerPortfolio = isPortfolioTab && !!currentPortfolio?.brokerInstanceId;
|
|
200
|
+
// The positions stay as last synced while the broker is away, dated, with one key to bring it back.
|
|
201
|
+
const offlineBrokerFooter = useOfflineBrokerFooter(brokerPortfolio ? offline : null);
|
|
195
202
|
const portfolioTargets = currentPortfolio?.targetWeights;
|
|
203
|
+
// Totals, money columns and account values are in the portfolio's currency.
|
|
204
|
+
const totalsCurrency = useMemo(
|
|
205
|
+
() => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
|
|
206
|
+
[config.baseCurrency, currentPortfolio],
|
|
207
|
+
);
|
|
208
|
+
// A boolean, so a quote tick does not rebuild the columns.
|
|
209
|
+
const otherCurrencies = holdsOtherCurrencies(tickers, financialsMap, totalsCurrency);
|
|
196
210
|
const columns = useMemo(
|
|
197
|
-
() => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio) }),
|
|
198
|
-
[currentPortfolio, isPortfolioTab, paneSettings.columnIds],
|
|
211
|
+
() => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio), otherCurrencies }),
|
|
212
|
+
[currentPortfolio, isPortfolioTab, otherCurrencies, paneSettings.columnIds],
|
|
199
213
|
);
|
|
200
214
|
const supplementalData = usePortfolioSupplementalData(tickers, columns, appActive, financialsMap);
|
|
201
215
|
// One lookup for the whole list, only while a perpetual column is on screen.
|
|
@@ -207,11 +221,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
207
221
|
[columns],
|
|
208
222
|
);
|
|
209
223
|
|
|
210
|
-
// Totals, money columns and account values are in the portfolio's currency.
|
|
211
|
-
const totalsCurrency = useMemo(
|
|
212
|
-
() => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
|
|
213
|
-
[config.baseCurrency, currentPortfolio],
|
|
214
|
-
);
|
|
215
224
|
// The broker account's cash when it reports one, else the cash entered by hand.
|
|
216
225
|
const cashLine = useMemo(
|
|
217
226
|
() => isPortfolioTab ? resolvePortfolioCash(currentPortfolio, accountState?.account) : null,
|
|
@@ -417,10 +426,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
417
426
|
cancelPendingCursorSymbol();
|
|
418
427
|
setCurrentCollectionId(collectionId);
|
|
419
428
|
}, [cancelPendingCursorSymbol, setCurrentCollectionId]);
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
429
|
+
const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
|
|
430
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
431
|
+
const collectionTabs = useMemo(() => visibleCollections.map((collection) => {
|
|
432
|
+
const portfolio = desktop && collection.kind === "portfolio"
|
|
433
|
+
? config.portfolios.find((entry) => entry.id === collection.id)
|
|
434
|
+
: undefined;
|
|
435
|
+
// A broker account's tab leads with its broker's logo on the desktop and the web.
|
|
436
|
+
const logo = portfolio ? brokerPortfolioLogo(portfolio, config.brokerInstances, getBrokerAdapter, getBrokerLogo) : null;
|
|
437
|
+
return {
|
|
438
|
+
label: collection.name,
|
|
439
|
+
value: collection.id,
|
|
440
|
+
...(logo ? { leading: <LogoMark src={logo.src} name={logo.name} /> } : {}),
|
|
441
|
+
};
|
|
442
|
+
}), [config.brokerInstances, config.portfolios, desktop, getBrokerAdapter, getBrokerLogo, visibleCollections]);
|
|
424
443
|
const { strip: tabStrip, rows: headerHeight } = usePaneTabs(showCollectionTabs
|
|
425
444
|
? {
|
|
426
445
|
tabs: collectionTabs,
|
|
@@ -548,7 +567,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
548
567
|
accountState: summaryAccountState,
|
|
549
568
|
accountStatusText: accountsError
|
|
550
569
|
? `Accounts unavailable: ${accountsError}`
|
|
551
|
-
: brokerPortfolio
|
|
570
|
+
: brokerPortfolio && !accountState ? "Acct missing" : undefined,
|
|
571
|
+
brokerOffline: brokerPortfolio && offline ? { dated: offline.syncedAt != null } : undefined,
|
|
552
572
|
financialsMap,
|
|
553
573
|
isPortfolioTab,
|
|
554
574
|
refreshingSize,
|
|
@@ -557,9 +577,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
557
577
|
}), [financialsMap, sortedTickers], [
|
|
558
578
|
accountState,
|
|
559
579
|
accountsError,
|
|
560
|
-
brokerError,
|
|
561
580
|
brokerPortfolio,
|
|
562
581
|
isPortfolioTab,
|
|
582
|
+
offline,
|
|
563
583
|
portfolioSummaryTotals,
|
|
564
584
|
refreshingSize,
|
|
565
585
|
summaryAccountState,
|
|
@@ -637,11 +657,14 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
637
657
|
|
|
638
658
|
usePaneFooter("portfolio-list", () => ({
|
|
639
659
|
info: [
|
|
660
|
+
...offlineBrokerFooter.asOf,
|
|
640
661
|
...summaryFooterInfo,
|
|
662
|
+
...offlineBrokerFooter.status,
|
|
641
663
|
...(fxStatusText && !fxWarning ? [{ id: "fx", parts: [{ text: `FX ${fxStatusText}`, tone: "muted" as const }] }] : []),
|
|
642
664
|
...(showsPerpetuals && perpetuals.stale ? [{ id: "perpetuals", parts: [{ text: "perpetuals stale", tone: "warning" as const }] }] : []),
|
|
643
665
|
],
|
|
644
666
|
hints: [
|
|
667
|
+
...offlineBrokerFooter.hints,
|
|
645
668
|
...(removalCollection
|
|
646
669
|
? [{
|
|
647
670
|
id: "delete",
|
|
@@ -676,6 +699,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
676
699
|
fxStatusText,
|
|
677
700
|
fxWarning,
|
|
678
701
|
isPortfolioTab,
|
|
702
|
+
offlineBrokerFooter,
|
|
679
703
|
perpetuals.stale,
|
|
680
704
|
removalCollection,
|
|
681
705
|
removeSelectedTicker,
|
|
@@ -708,9 +732,12 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
708
732
|
? `${cursorSymbol} market cap: ${describeFundamentalMarketCap(selectedCap.provenance)}.` : undefined;
|
|
709
733
|
const multiplierNotice = isPortfolioTab && selectedTicker && hasUnknownOptionMultiplier(selectedTicker, activeCollectionId)
|
|
710
734
|
? `${cursorSymbol} has no contract multiplier; its value assumes 1x.` : undefined;
|
|
735
|
+
// The row's change cells say DELISTED; the notice gives its last session.
|
|
736
|
+
const selectedDelisting = selectedFinancials?.quote?.delisting;
|
|
737
|
+
const delistedNotice = selectedDelisting ? `${cursorSymbol}: ${delistingLabel(selectedDelisting)}.` : undefined;
|
|
711
738
|
usePaneNoticeFooter({
|
|
712
739
|
registrationId: "portfolio-list-notices",
|
|
713
|
-
notices: [...summaryNotices, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
|
|
740
|
+
notices: [...summaryNotices, delistedNotice, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
|
|
714
741
|
focused: focused && !quickAddFocused,
|
|
715
742
|
});
|
|
716
743
|
const contentHeight = Math.max(1, height - headerHeight - summaryHeight - drawerHeight - quickAddHeight);
|
|
@@ -195,6 +195,7 @@ function describeColumnOption(column: ColumnConfig): string {
|
|
|
195
195
|
if (column.id === "perp_premium") return "Premium of the stock's perpetual to its last price.";
|
|
196
196
|
if (column.id === "perp_funding") return "Funding of the stock's perpetual, per 8 hours.";
|
|
197
197
|
if (column.id === "as_of") return "When each row's price is from: its last print, or the regular close.";
|
|
198
|
+
if (column.id === "currency") return "The currency each row is priced in; shown by default once a row is in another currency than the totals.";
|
|
198
199
|
return PORTFOLIO_ONLY_COLUMN_IDS.has(column.id)
|
|
199
200
|
? "Visible only when this pane is showing a portfolio."
|
|
200
201
|
: "Visible for watchlists and portfolios.";
|
|
@@ -275,12 +276,29 @@ export function resolvePortfolioPaneCollectionId(
|
|
|
275
276
|
return resolveActiveCollectionId(currentCollectionId, visibleCollections);
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
/**
|
|
280
|
+
* A collection holding listings in another currency than its totals shows
|
|
281
|
+
* CCY after LAST, so RY's 278.08 reads as Canadian dollars beside JNJ's US
|
|
282
|
+
* ones, unless the pane's columns already place CCY.
|
|
283
|
+
*/
|
|
284
|
+
function withCurrencyColumn(columnIds: string[]): string[] {
|
|
285
|
+
if (columnIds.includes("currency")) return columnIds;
|
|
286
|
+
const price = columnIds.indexOf("price");
|
|
287
|
+
const at = price >= 0 ? price + 1 : Math.min(1, columnIds.length);
|
|
288
|
+
return [...columnIds.slice(0, at), "currency", ...columnIds.slice(at)];
|
|
289
|
+
}
|
|
290
|
+
|
|
278
291
|
export function resolveVisibleColumns(
|
|
279
292
|
columnIds: string[],
|
|
280
293
|
isPortfolioTab: boolean,
|
|
281
|
-
{ targetWeights = false
|
|
294
|
+
{ targetWeights = false, otherCurrencies = false }: {
|
|
295
|
+
targetWeights?: boolean;
|
|
296
|
+
/** A row is in another currency than the totals (`holdsOtherCurrencies`): CCY shows. */
|
|
297
|
+
otherCurrencies?: boolean;
|
|
298
|
+
} = {},
|
|
282
299
|
): ColumnConfig[] {
|
|
283
|
-
const
|
|
300
|
+
const withTargets = isPortfolioTab && targetWeights ? withTargetColumns(columnIds) : columnIds;
|
|
301
|
+
const resolved = (otherCurrencies ? withCurrencyColumn(withTargets) : withTargets)
|
|
284
302
|
.map((columnId) => PORTFOLIO_COLUMNS_BY_ID.get(columnId))
|
|
285
303
|
.filter((column): column is ColumnConfig => column != null)
|
|
286
304
|
.filter((column) => isPortfolioTab || !PORTFOLIO_ONLY_COLUMN_IDS.has(column.id));
|
|
@@ -224,6 +224,15 @@ export function resolvePortfolioAccountState(
|
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* What the day's move is measured from, after it, as every change line says
|
|
229
|
+
* it ("vs Fri 16 Oct close", "vs mixed closes"), so a narrow row never shows
|
|
230
|
+
* the move without it.
|
|
231
|
+
*/
|
|
232
|
+
function dayBasisPart(totals: Pick<PortfolioSummaryTotals, "dayChangeBasis">): PortfolioSummarySegment["parts"] {
|
|
233
|
+
return totals.dayChangeBasis ? [{ text: totals.dayChangeBasis, tone: "muted" }] : [];
|
|
234
|
+
}
|
|
235
|
+
|
|
227
236
|
/** Headline numbers in priority order, so the narrowest header row keeps the most important ones. */
|
|
228
237
|
export function buildPortfolioSummarySegments({
|
|
229
238
|
totals,
|
|
@@ -258,6 +267,7 @@ export function buildPortfolioSummarySegments({
|
|
|
258
267
|
? [createSummarySegment("avg-day", [
|
|
259
268
|
{ text: "Avg Day", tone: "label" },
|
|
260
269
|
{ text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: priceColor(totals.avgWatchlistChange), bold: true },
|
|
270
|
+
...dayBasisPart(totals),
|
|
261
271
|
])]
|
|
262
272
|
: [];
|
|
263
273
|
}
|
|
@@ -332,6 +342,8 @@ export function buildPortfolioSummarySegments({
|
|
|
332
342
|
{ text: "Day", tone: "label" },
|
|
333
343
|
{ text: money(accountMetrics.dailyPnl, true), tone: "value", color: priceColor(accountMetrics.dailyPnl), bold: true },
|
|
334
344
|
{ text: `(${formatPercentRaw(accountMetrics.dailyPnlPct)})`, tone: "muted", color: priceColor(accountMetrics.dailyPnlPct) },
|
|
345
|
+
// The broker's own day is from its account's close, not the quotes': it names none.
|
|
346
|
+
...(accountMetrics.brokerDay || !Number.isFinite(accountMetrics.dailyPnl) ? [] : dayBasisPart(totals)),
|
|
335
347
|
]));
|
|
336
348
|
candidates.push(createSummarySegment("pnl", [
|
|
337
349
|
{ text: !Number.isFinite(account?.unrealizedPnl)
|
|
@@ -406,6 +418,7 @@ export function layoutPortfolioSummaryHeader(
|
|
|
406
418
|
export function buildPortfolioFooterSegments({
|
|
407
419
|
accountState,
|
|
408
420
|
accountStatusText,
|
|
421
|
+
brokerOffline,
|
|
409
422
|
financialsMap,
|
|
410
423
|
isPortfolioTab,
|
|
411
424
|
refreshingSize,
|
|
@@ -414,17 +427,25 @@ export function buildPortfolioFooterSegments({
|
|
|
414
427
|
}: {
|
|
415
428
|
accountState: PortfolioSummaryAccountState | null;
|
|
416
429
|
accountStatusText?: string;
|
|
430
|
+
/**
|
|
431
|
+
* The portfolio's broker is offline. The pane says why, which stands in for
|
|
432
|
+
* the missing account and the quote time, and when it knows, when the
|
|
433
|
+
* positions were synced, which also dates the account.
|
|
434
|
+
*/
|
|
435
|
+
brokerOffline?: { dated: boolean };
|
|
417
436
|
financialsMap: Map<string, TickerFinancials>;
|
|
418
437
|
isPortfolioTab: boolean;
|
|
419
438
|
refreshingSize: number;
|
|
420
439
|
sortedTickers: TickerRecord[];
|
|
421
440
|
totals: PortfolioSummaryTotals;
|
|
422
441
|
}): PaneFooterSegment[] {
|
|
442
|
+
const source: PaneFooterSegment[] = accountState ? [{ id: "source", parts: [{ text: accountState.sourceLabel, tone: "muted" }] }] : [];
|
|
443
|
+
if (isPortfolioTab && brokerOffline) return brokerOffline.dated ? [] : source;
|
|
423
444
|
const accountStatus: PaneFooterSegment[] = isPortfolioTab && accountStatusText
|
|
424
445
|
? [{ id: "account-status", parts: [{ text: accountStatusText, tone: "muted" }] }]
|
|
425
446
|
: [];
|
|
426
447
|
// Cached account numbers stay on screen when a live refresh fails, so the failure sits beside their date.
|
|
427
|
-
if (accountState) return [
|
|
448
|
+
if (accountState) return [...source, ...accountStatus];
|
|
428
449
|
if (isPortfolioTab ? !totals.hasPositions && !accountStatusText : totals.watchlistCount === 0) return [];
|
|
429
450
|
|
|
430
451
|
const lastRefreshTimestamp = getMostRecentQuoteUpdate(
|
|
@@ -6,6 +6,9 @@ import type { Portfolio } from "../../../../types/ticker";
|
|
|
6
6
|
import type { BrokerAccount } from "../../../../types/trading";
|
|
7
7
|
import { getBrokerInstance } from "../../../../utils/broker-instances";
|
|
8
8
|
import { usePluginBrokerActions } from "../../../runtime";
|
|
9
|
+
import type { OfflineBroker } from "../../shared/broker-reconnect";
|
|
10
|
+
import { tf } from "../../../../i18n";
|
|
11
|
+
import { isSignedInBrokerProfile, signedInBrokerId } from "../../../../brokers/signed-in/profile";
|
|
9
12
|
import { resolvePortfolioAccountState, type ResolvedPortfolioAccountState } from "./index";
|
|
10
13
|
|
|
11
14
|
const EMPTY_ACCOUNTS = { instanceId: null, accounts: [] as BrokerAccount[], error: null };
|
|
@@ -85,11 +88,40 @@ export interface PortfolioAccountStateResult {
|
|
|
85
88
|
/** Set when the broker refused to list accounts, so "no cash" is not mistaken for a clean empty. */
|
|
86
89
|
accountsError: string | null;
|
|
87
90
|
/**
|
|
88
|
-
* The
|
|
89
|
-
* sign-in lapsed
|
|
90
|
-
* is the only word of why the
|
|
91
|
+
* The portfolio's broker while it cannot be reached, such as after its
|
|
92
|
+
* sign-in lapsed: the positions on screen are its last sync. Accounts are
|
|
93
|
+
* listed only while it is connected, so this is the only word of why the
|
|
94
|
+
* account is missing or stale.
|
|
91
95
|
*/
|
|
92
|
-
|
|
96
|
+
offline: OfflineBroker | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The broker behind a portfolio while Gloom cannot reach it: disconnected,
|
|
101
|
+
* failing, or without the plugin that connects it. Null while it is connected
|
|
102
|
+
* or connecting, before it reports a status, for a profile turned off, and for
|
|
103
|
+
* one that only syncs on demand (IBKR Flex), which is never connected.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveOfflineBroker(
|
|
106
|
+
portfolio: Portfolio | null,
|
|
107
|
+
brokerInstance: BrokerInstanceConfig | null | undefined,
|
|
108
|
+
broker: BrokerAdapter | null | undefined,
|
|
109
|
+
status: BrokerConnectionStatus | null,
|
|
110
|
+
): OfflineBroker | null {
|
|
111
|
+
if (!portfolio || !brokerInstance || brokerInstance.enabled === false) return null;
|
|
112
|
+
const syncedAt = portfolio.lastSyncedAt ?? brokerInstance.lastSyncedAt ?? null;
|
|
113
|
+
if (!broker) {
|
|
114
|
+
// A profile signed in through Gloom names its broker, not the adapter it needs.
|
|
115
|
+
const brokerName = (isSignedInBrokerProfile(brokerInstance) ? signedInBrokerId(brokerInstance) : brokerInstance.brokerType).toUpperCase();
|
|
116
|
+
return { instance: brokerInstance, brokerName, syncedAt, reason: tf("{broker} unavailable", { broker: brokerName }), reconnectable: false };
|
|
117
|
+
}
|
|
118
|
+
if (!status || status.state === "connected" || status.state === "connecting") return null;
|
|
119
|
+
if (status.mode?.toLowerCase() === "flex") return null;
|
|
120
|
+
const brokerName = broker.describeInstance?.(brokerInstance).brokerName ?? broker.name;
|
|
121
|
+
const reason = status.state === "error"
|
|
122
|
+
? status.message?.trim() || tf("{broker} unavailable", { broker: brokerName })
|
|
123
|
+
: tf("{broker} disconnected", { broker: brokerName });
|
|
124
|
+
return { instance: brokerInstance, brokerName, syncedAt, reason, reconnectable: true };
|
|
93
125
|
}
|
|
94
126
|
|
|
95
127
|
/**
|
|
@@ -117,9 +149,12 @@ export function usePortfolioAccountState(
|
|
|
117
149
|
() => resolvePortfolioAccountState(portfolio, state, snapshot),
|
|
118
150
|
[portfolio, snapshot, state.brokerAccounts, state.config],
|
|
119
151
|
);
|
|
120
|
-
const
|
|
152
|
+
const offline = useMemo(
|
|
153
|
+
() => resolveOfflineBroker(portfolio, brokerInstance, broker, live.status),
|
|
154
|
+
[broker, brokerInstance, live.status, portfolio],
|
|
155
|
+
);
|
|
121
156
|
return useMemo(
|
|
122
|
-
() => ({ accountState, accountsError: live.error,
|
|
123
|
-
[accountState,
|
|
157
|
+
() => ({ accountState, accountsError: live.error, offline }),
|
|
158
|
+
[accountState, live.error, offline],
|
|
124
159
|
);
|
|
125
160
|
}
|