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
|
@@ -2,9 +2,29 @@ import { THEME_PERIODS, type ThemeAggregate, type ThemeMember, type ThemePeriod,
|
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
3
|
import { dropColumnsToFit, getTableWidth } from "../../../components/ui/table-layout";
|
|
4
4
|
import { extendedSessionStateLabel, type ExtendedSession } from "../../../market-data/market/status";
|
|
5
|
+
import { yearToDateBaseLabel } from "../../../market-data/market/year-to-date";
|
|
5
6
|
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const PERIOD_LABELS: Record<ThemePeriod, string> = { changePercent: "1D", return1WPercent: "1W", return1MPercent: "1M", return3MPercent: "3M", returnYtdPercent: "YTD" };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The close a board's YTD is measured from, `31 Dec`: NYSE's last session of
|
|
12
|
+
* the year before the session the board describes, as the server measures it.
|
|
13
|
+
*/
|
|
14
|
+
export function boardYtdBase(payload: { asOf?: unknown; regularSessionDate?: unknown; regularSessionClosedAt?: unknown }, now = Date.now()): string {
|
|
15
|
+
const day = boardClose(payload)?.date ?? (typeof payload.asOf === "string" && /^\d{4}-\d{2}-\d{2}/.test(payload.asOf) ? payload.asOf.slice(0, 10) : new Date(now).toISOString().slice(0, 10));
|
|
16
|
+
return yearToDateBaseLabel(day, "NYSE", now);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A period's header. YTD names its base close, as long as `room` allows:
|
|
21
|
+
* `YTD vs 31 Dec close`, `YTD vs 31 Dec`, then `YTD 31 Dec`.
|
|
22
|
+
*/
|
|
23
|
+
export function periodLabel(period: ThemePeriod, ytdBase: string | null, room = Infinity): string {
|
|
24
|
+
if (period !== "returnYtdPercent" || !ytdBase) return PERIOD_LABELS[period];
|
|
25
|
+
const labels = [`YTD vs ${ytdBase} close`, `YTD vs ${ytdBase}`, `YTD ${ytdBase}`];
|
|
26
|
+
return labels.find((label) => label.length <= room) ?? labels.at(-1)!;
|
|
27
|
+
}
|
|
8
28
|
export interface ThemeSort { columnId: string; direction: SortDirection }
|
|
9
29
|
export const DEFAULT_SORT: ThemeSort = { columnId: "changePercent", direction: "desc" };
|
|
10
30
|
export function percent(value: number | null): string {
|
|
@@ -110,17 +130,27 @@ export function matchTheme(rows: readonly ThemeSummary[], input: string): ThemeS
|
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
const column = (id: string, label: string, width: number, align: "left" | "right" = "right"): DataTableColumn => ({ id, label, width, align });
|
|
113
|
-
/**
|
|
114
|
-
|
|
133
|
+
/** Room a header has in its column: the sort indicator takes two cells. */
|
|
134
|
+
const HEADER_ROOM = 2;
|
|
135
|
+
/**
|
|
136
|
+
* Drop leaders/laggards first, then longer windows; keep the day's move and
|
|
137
|
+
* breadth. YTD names `ytdBase`, the close it is measured from, as fully as
|
|
138
|
+
* its column allows.
|
|
139
|
+
*/
|
|
140
|
+
export function themeColumns(width: number, rows: readonly ThemeSummary[], ytdBase: string | null = null): DataTableColumn[] {
|
|
115
141
|
const suffixWidth = (period: ThemePeriod) => coverageWidth(rows.map((row) => row.returns[period]));
|
|
116
142
|
const metricWidth = (period: ThemePeriod) => Math.max(8, ...rows.map((row) => percent(row.returns[period].value).length)) + suffixWidth(period);
|
|
143
|
+
// The same trailing space is reserved in every cell and its header. A note
|
|
144
|
+
// occupies that space without moving the number away from its label.
|
|
145
|
+
const label = (period: ThemePeriod, room?: number) => periodLabel(period, ytdBase, room) + " ".repeat(suffixWidth(period));
|
|
117
146
|
const columns = [column("name", "Theme", 26, "left"), column("memberCount", "N", 3),
|
|
118
|
-
|
|
119
|
-
// occupies that space without moving the number away from its label.
|
|
120
|
-
...THEME_PERIODS.map((period) => column(period, PERIOD_LABELS[period] + " ".repeat(suffixWidth(period)), metricWidth(period))),
|
|
147
|
+
...THEME_PERIODS.map((period) => column(period, label(period, 0), Math.max(metricWidth(period), label(period, 0).length + HEADER_ROOM))),
|
|
121
148
|
column("breadth", "Breadth" + " ".repeat(coverageWidth(rows.map((row) => row.breadth))), 7 + coverageWidth(rows.map((row) => row.breadth))),
|
|
122
149
|
column("best", "Best 1D", 15), column("worst", "Worst 1D", 15)];
|
|
123
|
-
|
|
150
|
+
const fitted = fitColumns(columns, width, ["worst", "best", "return3MPercent", "returnYtdPercent", "return1MPercent", "return1WPercent", "memberCount"]);
|
|
151
|
+
const ytd = fitted.find((col) => col.id === "returnYtdPercent");
|
|
152
|
+
if (ytd) ytd.label = label("returnYtdPercent", ytd.width - HEADER_ROOM - suffixWidth("returnYtdPercent"));
|
|
153
|
+
return fitted;
|
|
124
154
|
}
|
|
125
155
|
/**
|
|
126
156
|
* The member table at `width`. Price and 1D are the regular session's; a
|
|
@@ -128,12 +158,15 @@ export function themeColumns(width: number, rows: readonly ThemeSummary[]): Data
|
|
|
128
158
|
* each session in `extendedSessions`. A narrow pane gives up the longer
|
|
129
159
|
* windows first, then that column, then the name.
|
|
130
160
|
*/
|
|
131
|
-
export function memberColumns(width: number, extendedSessions: readonly ExtendedSession[] = []): DataTableColumn[] {
|
|
161
|
+
export function memberColumns(width: number, extendedSessions: readonly ExtendedSession[] = [], ytdBase: string | null = null): DataTableColumn[] {
|
|
132
162
|
const extended = extendedSessions.map((session) => column(session === "PRE" ? "preMarket" : "afterHours", extendedSessionStateLabel(session), 9));
|
|
133
|
-
const [day, ...windows] = THEME_PERIODS.map((period) => column(period,
|
|
134
|
-
|
|
163
|
+
const [day, ...windows] = THEME_PERIODS.map((period) => column(period, periodLabel(period, ytdBase, 0), Math.max(9, periodLabel(period, ytdBase, 0).length + HEADER_ROOM)));
|
|
164
|
+
const fitted = fitColumns([column("symbol", "Ticker", 7, "left"), column("name", "Name", 24, "left"), column("price", "Price $", 10),
|
|
135
165
|
day!, ...extended, ...windows], width,
|
|
136
166
|
["return3MPercent", "returnYtdPercent", "return1MPercent", "return1WPercent", "preMarket", "afterHours", "name", "price"]);
|
|
167
|
+
const ytd = fitted.find((col) => col.id === "returnYtdPercent");
|
|
168
|
+
if (ytd) ytd.label = periodLabel("returnYtdPercent", ytdBase, ytd.width - HEADER_ROOM);
|
|
169
|
+
return fitted;
|
|
137
170
|
}
|
|
138
171
|
function fitColumns(columns: DataTableColumn[], width: number, drop: string[]): DataTableColumn[] {
|
|
139
172
|
columns = dropColumnsToFit(columns, width, drop);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, type ReactNode } from "react";
|
|
2
2
|
import type { ThemeMember, ThemePeriod, ThemeSummary } from "../../../api-client/themes";
|
|
3
|
-
import { DataTableStackView, DataTableView, PaneStatusBody, usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn } from "../../../components";
|
|
3
|
+
import { splitDelayedStatus, DataTableStackView, DataTableView, PaneStatusBody, usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn } from "../../../components";
|
|
4
4
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
5
5
|
import type { ExtendedSession } from "../../../market-data/market/status";
|
|
6
6
|
import { useAsyncResource, useAutoRefresh, usePluginPaneState, usePluginTickerActions, useUpdatedAgo } from "../../../public/react";
|
|
@@ -12,7 +12,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
|
|
|
12
12
|
import { useMembersMenu } from "../shared/members-menu";
|
|
13
13
|
import { cachedMembers, cachedThemes, loadMembers, loadThemes } from "./client";
|
|
14
14
|
import {
|
|
15
|
-
aggregateText, coverageWidth, DEFAULT_SORT, extendedMove, matchTheme, memberColumns, memberExtendedSessions, memberPrice, percent,
|
|
15
|
+
aggregateText, boardYtdBase, coverageWidth, DEFAULT_SORT, extendedMove, matchTheme, memberColumns, memberExtendedSessions, memberPrice, percent,
|
|
16
16
|
SNAPSHOT_STALE_MS, snapshotFooterText, sortMembers, sortThemes, themeColumns, type ThemeSort,
|
|
17
17
|
} from "./model";
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ function useSnapshotFooter(id: string, resource: { loading: boolean; error: stri
|
|
|
24
24
|
const payload = data?.payload;
|
|
25
25
|
// Outside the regular session the figures are a close, so the footer names it, not the snapshot's time.
|
|
26
26
|
const text = age && payload ? snapshotFooterText(payload, age) : null;
|
|
27
|
-
const info = useMemo(() => text ?
|
|
27
|
+
const info = useMemo(() => text ? splitDelayedStatus({ id: "snapshot", parts: [{ text, tone: "muted" }] }) : [], [text]);
|
|
28
28
|
// The server flag includes retained historical returns; it does not mean the daily board is old.
|
|
29
29
|
const snapshotStale = !!data && Date.now() - Date.parse(data.payload.asOf) > SNAPSHOT_STALE_MS;
|
|
30
30
|
usePaneStatusFooter({ registrationId: id, enabled, loading: resource.loading && !!data,
|
|
@@ -45,8 +45,10 @@ function Members({ id, width, height, focused }: { id: string; width: number; he
|
|
|
45
45
|
const rows = useMemo(() => sortMembers(data?.members ?? [], sort), [data, sort]);
|
|
46
46
|
// Price and 1D are the regular session's; a pre-market or after-hours print gets its own column when a member has one.
|
|
47
47
|
const extendedSessionsKey = useMemo(() => memberExtendedSessions(data?.members ?? []).join(","), [data]);
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// YTD names the close it is measured from, `31 Dec`.
|
|
49
|
+
const ytdBase = data ? boardYtdBase(data) : null;
|
|
50
|
+
const columns = useMemo(() => memberColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], ytdBase),
|
|
51
|
+
[extendedSessionsKey, width, ytdBase]);
|
|
50
52
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
51
53
|
usePaneRefreshKey(() => void resource.reload(), { focused });
|
|
52
54
|
useSnapshotFooter("themes:members", resource, focused);
|
|
@@ -103,7 +105,7 @@ export function ThemesBoard({ width, height, focused, tabStrip, tabRows = 0 }: T
|
|
|
103
105
|
const resource = useAsyncResource(loadThemes, { initialData: cachedThemes });
|
|
104
106
|
const data = resource.data?.payload;
|
|
105
107
|
const rows = useMemo(() => sortThemes(data?.themes ?? [], sort), [data, sort]);
|
|
106
|
-
const columns = useMemo(() => themeColumns(width, data?.themes ?? []), [width, data]);
|
|
108
|
+
const columns = useMemo(() => themeColumns(width, data?.themes ?? [], data ? boardYtdBase(data) : null), [width, data]);
|
|
107
109
|
const theme = opened && data ? matchTheme(data.themes, opened) : null;
|
|
108
110
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
109
111
|
usePaneRefreshKey(() => void resource.reload(), { focused, enabled: !opened });
|
|
@@ -13,6 +13,7 @@ import { apiClient } from "../../../api-client";
|
|
|
13
13
|
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
14
14
|
import { httpFetch } from "../../../utils/http-transport";
|
|
15
15
|
import { asRecord, finiteOrNull } from "../../../utils/guards";
|
|
16
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
16
17
|
|
|
17
18
|
const FORMS_13F_BASE_URL = "https://forms13f.com/api/v1";
|
|
18
19
|
const FORM_PAGE_LIMIT = 100;
|
|
@@ -169,7 +170,9 @@ async function fetchForms13F<T>(
|
|
|
169
170
|
slot.promise = promise;
|
|
170
171
|
activeRequests.set(key, promise);
|
|
171
172
|
if (cached != null && !options.forceRefresh) {
|
|
172
|
-
void promise.catch(() => {
|
|
173
|
+
void promise.catch((error: unknown) => {
|
|
174
|
+
if (!options.signal?.aborted) logFailure("thirteenf.api", "Refresh cached filing data failed")(error);
|
|
175
|
+
});
|
|
173
176
|
return cached.value;
|
|
174
177
|
}
|
|
175
178
|
return promise;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./api";
|
|
14
14
|
import { isoDateDaysAgo, isoDateToday, isoDateYearsAgo } from "../../../utils/calendar-date";
|
|
15
15
|
import { abortable, withDeadline } from "../../../utils/async-deadline";
|
|
16
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
16
17
|
import {
|
|
17
18
|
buildBrowserRows,
|
|
18
19
|
filingGapFor,
|
|
@@ -239,7 +240,10 @@ async function loadBrowserPage(
|
|
|
239
240
|
}
|
|
240
241
|
const found = await searchThirteenFFunds(trimmed, browserLimit, signal, pageApiOptions);
|
|
241
242
|
const funds = oneFundPerCik(found);
|
|
242
|
-
const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, apiOptions).catch((): ThirteenFTopFund[] =>
|
|
243
|
+
const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, apiOptions).catch((error: unknown): ThirteenFTopFund[] => {
|
|
244
|
+
if (!signal?.aborted) logFailure("thirteenf.data", "Load performance ranks for fund search failed")(error);
|
|
245
|
+
return [];
|
|
246
|
+
});
|
|
243
247
|
const topByCik = new Map(topFunds.map((fund) => [fund.cik, fund]));
|
|
244
248
|
const matchedTopFunds = funds
|
|
245
249
|
.map((fund) => topByCik.get(fund.cik))
|
|
@@ -647,7 +647,9 @@ export function createThirteenFHeadless(
|
|
|
647
647
|
if (ctx.signal?.aborted) throw error;
|
|
648
648
|
return [];
|
|
649
649
|
});
|
|
650
|
-
shadowed.catch(() => {
|
|
650
|
+
shadowed.catch(() => {
|
|
651
|
+
// Intentional ignore: only cancellation reaches this guard; the awaited promise still rejects.
|
|
652
|
+
});
|
|
651
653
|
}
|
|
652
654
|
try {
|
|
653
655
|
holdings = await loadTickerHoldings(ticker, Number(args.options.offset ?? 0), ctx.signal);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../../../ui";
|
|
18
18
|
import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
|
|
19
19
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
20
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
20
21
|
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
|
|
21
22
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
22
23
|
import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
@@ -118,7 +119,7 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
118
119
|
let alive = true;
|
|
119
120
|
void Promise.all(browser.pages.map((page) => page.reportsLoaded ?? Promise.resolve())).then(() => {
|
|
120
121
|
if (alive) setReportPaint((value) => value + 1);
|
|
121
|
-
}, (
|
|
122
|
+
}, logFailure("thirteenf.pane", "Load filing metadata for fund rows failed"));
|
|
122
123
|
return () => { alive = false; };
|
|
123
124
|
}, [browser.pages]);
|
|
124
125
|
const error = browser.error?.message ?? null;
|
|
@@ -193,7 +194,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
193
194
|
// Every request behind a fund detail is cached per path, so warming it
|
|
194
195
|
// while the cursor rests on the row makes Enter read from cache.
|
|
195
196
|
const prefetchDetail = useCallback((row: FundBrowserRow) => {
|
|
196
|
-
void loadFundDetail(row.cik, row.name).catch(() => {
|
|
197
|
+
void loadFundDetail(row.cik, row.name).catch(() => {
|
|
198
|
+
// Intentional ignore: speculative prefetch is retried and failures are shown when the fund opens.
|
|
199
|
+
});
|
|
197
200
|
}, []);
|
|
198
201
|
|
|
199
202
|
const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
|
|
@@ -376,7 +379,9 @@ export function FundDetailView({
|
|
|
376
379
|
: Promise.resolve(new Map<string, number | null>())
|
|
377
380
|
).then((factors) => {
|
|
378
381
|
if (abortRef.current === controller) setSplitFactors(factors);
|
|
379
|
-
}, () => {
|
|
382
|
+
}, () => {
|
|
383
|
+
// Intentional ignore: split lookups reject only on cancellation; individual failures remain unconfirmed.
|
|
384
|
+
});
|
|
380
385
|
})
|
|
381
386
|
.catch((loadError) => {
|
|
382
387
|
if (abortRef.current !== controller) return;
|
|
@@ -2,7 +2,9 @@ import { formatPriceEarnings } from "../../../../utils/price-earnings";
|
|
|
2
2
|
import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
3
3
|
import { priceColor } from "../../../../theme/colors";
|
|
4
4
|
import type { CompanyProfile, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
|
-
import type { TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
5
|
+
import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
6
|
+
import type { BrokerInstanceConfig } from "../../../../types/config";
|
|
7
|
+
import type { BrokerAccount } from "../../../../types/trading";
|
|
6
8
|
import {
|
|
7
9
|
formatCompact,
|
|
8
10
|
formatCompactCurrency,
|
|
@@ -65,15 +67,45 @@ function daysAhead(days: number, prefix?: string): string[] {
|
|
|
65
67
|
return days === 0 ? [`${lead}today`] : [`${lead}in ${days}d`, `${lead}${days}d`];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
/** What the Account column reads names from: the portfolios, the brokers' accounts and their profiles. */
|
|
71
|
+
export interface PositionAccountNames {
|
|
72
|
+
portfolios: readonly Pick<Portfolio, "id" | "name" | "brokerAccountId" | "brokerInstanceId">[];
|
|
73
|
+
/** Accounts each broker profile reported, by profile id. */
|
|
74
|
+
brokerAccounts?: Readonly<Record<string, readonly Pick<BrokerAccount, "accountId" | "name">[]>>;
|
|
75
|
+
brokerInstances?: readonly Pick<BrokerInstanceConfig, "id" | "label">[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const NO_ACCOUNT_NAMES: PositionAccountNames = { portfolios: [] };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The account a position is held in, by the name the Portfolio pane gives its
|
|
82
|
+
* portfolio ("Aries individual (simulation)"), never the broker's raw account
|
|
83
|
+
* id: a portfolio still named by its id takes the name the broker reports for
|
|
84
|
+
* the account, then the broker profile's.
|
|
85
|
+
*/
|
|
86
|
+
function positionAccountName(position: TickerPosition, names: PositionAccountNames): string {
|
|
87
|
+
const portfolio = names.portfolios.find((entry) => entry.id === position.portfolio);
|
|
88
|
+
const brokerPortfolio = position.portfolio.startsWith("broker:");
|
|
89
|
+
const accountId = position.brokerAccountId || portfolio?.brokerAccountId
|
|
90
|
+
|| (brokerPortfolio ? position.portfolio.split(":").slice(2).join(":") : "");
|
|
91
|
+
const named = (name: string | undefined) => {
|
|
92
|
+
const text = name?.trim();
|
|
93
|
+
return text && text !== accountId && text !== position.portfolio ? text : null;
|
|
94
|
+
};
|
|
95
|
+
const portfolioName = named(portfolio?.name);
|
|
96
|
+
if (portfolioName) return portfolioName;
|
|
97
|
+
const instanceId = position.brokerInstanceId || portfolio?.brokerInstanceId;
|
|
98
|
+
if (!accountId && !brokerPortfolio && !instanceId) return portfolio?.name.trim() || position.portfolio;
|
|
99
|
+
const accounts = instanceId ? names.brokerAccounts?.[instanceId] ?? [] : [];
|
|
100
|
+
const accountName = named(accounts.find((account) => account.accountId === accountId)?.name);
|
|
101
|
+
if (accountName) return accountName;
|
|
102
|
+
const profileName = named(names.brokerInstances?.find((instance) => instance.id === instanceId)?.label);
|
|
103
|
+
return profileName ?? position.broker;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function positionAccountLabel(position: TickerPosition, names: PositionAccountNames): string {
|
|
75
107
|
const suffix = signedPositionDirection(position) < 0 ? " SHORT" : "";
|
|
76
|
-
return `${
|
|
108
|
+
return `${positionAccountName(position, names)}${suffix}`;
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
export function buildOverviewStats({
|
|
@@ -294,12 +326,14 @@ export function buildPositionRows({
|
|
|
294
326
|
quoteCurrency,
|
|
295
327
|
baseCurrency,
|
|
296
328
|
toBase,
|
|
329
|
+
accountNames = NO_ACCOUNT_NAMES,
|
|
297
330
|
}: {
|
|
298
331
|
ticker: TickerRecord;
|
|
299
332
|
quote: Quote | undefined;
|
|
300
333
|
quoteCurrency: string;
|
|
301
334
|
baseCurrency: string;
|
|
302
335
|
toBase: CurrencyConverter;
|
|
336
|
+
accountNames?: PositionAccountNames;
|
|
303
337
|
}): PositionTableRow[] {
|
|
304
338
|
return ticker.metadata.positions.filter((position) => position.shares !== 0).map((position) => {
|
|
305
339
|
const positionCurrency = getPortfolioPositionMetrics(
|
|
@@ -328,7 +362,7 @@ export function buildPositionRows({
|
|
|
328
362
|
const unit = metrics.priceBasis === "percent-of-par" ? "" : ticker.metadata.assetCategory === "BOND" ? " units" : metrics.multiplierHint > 1 ? " ct" : " sh";
|
|
329
363
|
|
|
330
364
|
return {
|
|
331
|
-
account:
|
|
365
|
+
account: positionAccountLabel(position, accountNames),
|
|
332
366
|
qty: `${formatMarketQuantity(metrics.totalShares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency, maxWidth: metrics.priceBasis === "percent-of-par" ? 11 : undefined })}${unit}`,
|
|
333
367
|
quantityUnit: metrics.priceBasis === "percent-of-par" ? "face" : undefined,
|
|
334
368
|
// Avg and Mark sit side by side, so both keep the currency's minor unit ($118.40 beside $224.36).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useOptionsQuery, useResolvedEntryValue } from "../../../../market-data/hooks";
|
|
3
|
+
import { formatMarketPriceWithCurrency } from "../../../../market-data/market/format";
|
|
4
|
+
import type { OptionContract } from "../../../../types/financials";
|
|
5
|
+
import type { TickerRecord } from "../../../../types/ticker";
|
|
6
|
+
import { formatCompact, formatNumber } from "../../../../utils/format";
|
|
7
|
+
import { expiryIsoDate, formatDaysToExpiry } from "../../../../utils/option-expiry";
|
|
8
|
+
import { resolveOptionsTarget, type ResolvedOptionsTarget } from "../../../../utils/options";
|
|
9
|
+
import type { StatField } from "./types";
|
|
10
|
+
|
|
11
|
+
const contractKey = (symbol: string) => symbol.replace(/\s+/g, "").toUpperCase();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An option contract's own row of its chain, the same query OMON reads: it
|
|
15
|
+
* carries the open interest and implied volatility a contract quote leaves out.
|
|
16
|
+
* Null for anything that is not an option, and until the chain answers.
|
|
17
|
+
*/
|
|
18
|
+
export function useOptionContractRow(ticker: TickerRecord | null | undefined): OptionContract | null {
|
|
19
|
+
const target = resolveOptionsTarget(ticker);
|
|
20
|
+
const option = target?.isOptionTicker && target.parsedOption ? target : null;
|
|
21
|
+
const entry = useOptionsQuery(option ? {
|
|
22
|
+
instrument: {
|
|
23
|
+
symbol: option.effectiveTicker, exchange: option.effectiveExchange,
|
|
24
|
+
brokerId: option.instrument?.brokerId, brokerInstanceId: option.instrument?.brokerInstanceId, instrument: option.instrument,
|
|
25
|
+
},
|
|
26
|
+
expirationDate: option.parsedOption!.expTs,
|
|
27
|
+
} : null);
|
|
28
|
+
const chain = useResolvedEntryValue(entry);
|
|
29
|
+
const symbol = ticker?.metadata.ticker;
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
if (!chain || !symbol || !option) return null;
|
|
32
|
+
const key = contractKey(symbol);
|
|
33
|
+
return [...chain.calls, ...chain.puts].find((contract) => contractKey(contract.contractSymbol) === key) ?? null;
|
|
34
|
+
}, [chain, option !== null, symbol]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a contract is, then what its market says about it: underlying, side,
|
|
39
|
+
* strike, expiry and deliverable, with the open interest, implied volatility
|
|
40
|
+
* and volume the broker's chain gives. A figure the broker leaves out is left out.
|
|
41
|
+
*/
|
|
42
|
+
export function optionContractFields(
|
|
43
|
+
target: Pick<ResolvedOptionsTarget, "parsedOption" | "effectiveTicker" | "instrument"> | null,
|
|
44
|
+
row: OptionContract | null,
|
|
45
|
+
currency: string,
|
|
46
|
+
now = Date.now(),
|
|
47
|
+
): StatField[] {
|
|
48
|
+
const parsed = target?.parsedOption;
|
|
49
|
+
if (!target || !parsed) return [];
|
|
50
|
+
const fields: StatField[] = [
|
|
51
|
+
{ label: "Underlying", value: target.effectiveTicker },
|
|
52
|
+
{ label: "Type", value: parsed.side === "C" ? "Call" : "Put" },
|
|
53
|
+
{ label: "Strike", value: formatMarketPriceWithCurrency(parsed.strike, currency) },
|
|
54
|
+
{ label: "Expiry", value: expiryIsoDate(parsed.expTs), detail: formatDaysToExpiry(parsed.expTs, now) },
|
|
55
|
+
];
|
|
56
|
+
const multiplier = Number(target.instrument?.multiplier);
|
|
57
|
+
if (Number.isFinite(multiplier) && multiplier > 0) fields.push({ label: "Multiplier", value: formatNumber(multiplier, 0) });
|
|
58
|
+
if (row?.openInterest != null && Number.isFinite(row.openInterest)) fields.push({ label: "Open Int", value: formatCompact(row.openInterest) });
|
|
59
|
+
if (Number.isFinite(row?.impliedVolatility) && row!.impliedVolatility > 0) {
|
|
60
|
+
fields.push({ label: "Implied Vol", value: `${(row!.impliedVolatility * 100).toFixed(1)}%` });
|
|
61
|
+
}
|
|
62
|
+
if (row?.volume != null && Number.isFinite(row.volume)) fields.push({ label: "Volume", value: formatCompact(row.volume) });
|
|
63
|
+
return fields;
|
|
64
|
+
}
|
|
@@ -3,7 +3,9 @@ import { fundFactOverviewFields } from "../sec/fund-facts";
|
|
|
3
3
|
import { useFundFacts } from "../sec/fund-facts-view";
|
|
4
4
|
import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
|
|
5
5
|
import { mayBeUsEquityOrFundTicker } from "../../../utils/sec";
|
|
6
|
-
import
|
|
6
|
+
import { ResearchTradeActions, researchTradeMatches, useResearchTrading } from "../shared/research-trading";
|
|
7
|
+
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
8
|
+
import { useMemo, type ReactNode } from "react";
|
|
7
9
|
import { EmptyState, PaneLinkMenu, SectionHeading, usePaneNoticeFooter } from "../../../components";
|
|
8
10
|
import { CompositeChart, pricePointsToResolvedSeries } from "../../../components/chart/composite";
|
|
9
11
|
import { CompanyLogo, resolveCompanyLogoSrc } from "../../../components/company-logo";
|
|
@@ -22,7 +24,11 @@ import {
|
|
|
22
24
|
quoteSessionLabel,
|
|
23
25
|
} from "../../../market-data/market/status";
|
|
24
26
|
import { appendQuoteToPriceReturnHistory, buildPriceReturnFields } from "../../../market-data/performance";
|
|
25
|
-
import {
|
|
27
|
+
import { changeBasisLabel, changeBasisNote, quoteChangeBasis } from "../../../market-data/market/change-basis";
|
|
28
|
+
import {
|
|
29
|
+
fitFuturesContractLines, futuresContractLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer,
|
|
30
|
+
type FuturesContractLine,
|
|
31
|
+
} from "../../../market-data/futures-contract";
|
|
26
32
|
import { useViewport } from "../../../react/input";
|
|
27
33
|
import { useAppSelector } from "../../../state/app/context";
|
|
28
34
|
import { colors, priceColor } from "../../../theme/colors";
|
|
@@ -47,6 +53,8 @@ import {
|
|
|
47
53
|
} from "./overview/components";
|
|
48
54
|
import { useFuturesContractTerms } from "./overview/futures-contract";
|
|
49
55
|
import type { OverviewDividendState } from "./overview/dividend";
|
|
56
|
+
import { optionContractFields, useOptionContractRow } from "./overview/option-contract";
|
|
57
|
+
import { resolveOptionsTarget } from "../../../utils/options";
|
|
50
58
|
import { buildOverviewStats, buildPositionRows, buildProfileFields } from "./overview/model";
|
|
51
59
|
import type { OverviewFunctionLink, StatField } from "./overview/types";
|
|
52
60
|
import { describeFundamentalMarketCap } from "../../../utils/market-capitalization";
|
|
@@ -99,7 +107,15 @@ export function OverviewTab(props: OverviewTabProps) {
|
|
|
99
107
|
const FUND_FACTS_LINK: OverviewFunctionLink = { name: "Fund Facts", templateId: "sec-etf-pane" };
|
|
100
108
|
|
|
101
109
|
function ResolvedOverviewTab({ width, height, focused = false, ticker, financials, onOpenChart, onOpenFunction, perpetuals, dividend }: OverviewTabProps & { ticker: TickerRecord }) {
|
|
110
|
+
const trade = useResearchTrading();
|
|
102
111
|
const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
|
|
112
|
+
const portfolios = useAppSelector((state) => state.config.portfolios);
|
|
113
|
+
const brokerInstances = useAppSelector((state) => state.config.brokerInstances);
|
|
114
|
+
const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
|
|
115
|
+
const accountNames = useMemo(
|
|
116
|
+
() => ({ portfolios, brokerInstances, brokerAccounts }),
|
|
117
|
+
[brokerAccounts, brokerInstances, portfolios],
|
|
118
|
+
);
|
|
103
119
|
const { width: termWidth } = useViewport();
|
|
104
120
|
const { fractionalViewport = false, nativePaneChrome, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
|
|
105
121
|
const figureCells = useFigureCells();
|
|
@@ -114,6 +130,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
114
130
|
// A futures symbol's settlement and contract terms load beside the quote, never in its way.
|
|
115
131
|
const futuresSymbol = quote?.symbol || ticker.metadata.ticker;
|
|
116
132
|
const contractTerms = useFuturesContractTerms(isFuturesInstrument(futuresSymbol, instrumentType) ? futuresSymbol : null);
|
|
133
|
+
const optionRow = useOptionContractRow(ticker);
|
|
117
134
|
const exchangeRates = useFxRatesMap([
|
|
118
135
|
baseCurrency,
|
|
119
136
|
ticker.metadata.currency,
|
|
@@ -179,7 +196,9 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
179
196
|
],
|
|
180
197
|
focused,
|
|
181
198
|
});
|
|
182
|
-
|
|
199
|
+
// A listing that no longer trades has no book: its quote is its last session's close.
|
|
200
|
+
const delisted = !!quote?.delisting;
|
|
201
|
+
const hasBidAsk = !delisted && (quote?.bid != null || quote?.ask != null);
|
|
183
202
|
const quoteBookInline = hasBidAsk && contentWidth >= 68;
|
|
184
203
|
const quoteBookWidth = quoteBookInline ? Math.min(32, Math.max(24, Math.floor(contentWidth * 0.3))) : Math.min(contentWidth, 32);
|
|
185
204
|
const quoteSummaryWidth = quoteBookInline ? Math.max(20, contentWidth - quoteBookWidth - 2) : contentWidth;
|
|
@@ -188,9 +207,16 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
188
207
|
// The headline is the regular session, which stops at the close; extended trading is the line below it.
|
|
189
208
|
const sessionQuote = getRegularSessionDisplay(quote);
|
|
190
209
|
const extendedQuote = getExtendedSessionDisplay(quote);
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
210
|
+
// A price in cents says what it is per (314.95¢/lb), and the move what it is measured from (vs Fri 16 Oct close).
|
|
211
|
+
const quotePriceText = sessionQuote
|
|
212
|
+
? `${formatMarketPriceWithCurrency(sessionQuote.price, quote?.currency, moneyOptions)}${minorUnitPricePer(contractTerms, quote?.currency)}` : "";
|
|
213
|
+
// Nor a move: the close it ended on is where it stays.
|
|
214
|
+
const quoteChangeText = sessionQuote && !delisted ? formatSignedMarketPrice(sessionQuote.change, moneyOptions) : "";
|
|
215
|
+
const quotePercentText = sessionQuote && !delisted ? `(${formatPercentRaw(sessionQuote.changePercent)})` : "";
|
|
216
|
+
const changeBasis = delisted ? null : quoteChangeBasis(quote, { terms: contractTerms });
|
|
217
|
+
const changeBasisText = changeBasis ? changeBasisLabel(changeBasis) : "";
|
|
218
|
+
// The days the venue was closed since that close, under the move: "HKEX closed Mon 19 Oct, holiday".
|
|
219
|
+
const changeBasisNoteText = changeBasisNote(changeBasis);
|
|
194
220
|
// The pane title already names the ticker, so the line leads with the company and drops a name that only repeats it.
|
|
195
221
|
const companyName = [ticker.metadata.name, quote?.name].find((name) => name && name !== ticker.metadata.ticker) ?? "";
|
|
196
222
|
const venueText = listingVenue ? (companyName ? ` (${listingVenue})` : listingVenue) : "";
|
|
@@ -217,15 +243,25 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
217
243
|
? truncateToDisplayWidth(futuresSettleText(contractTerms, quote),
|
|
218
244
|
Math.max(1, quoteTextWidth - displayWidth(settleLabel) - 1))
|
|
219
245
|
: "";
|
|
220
|
-
const quoteChangeCells = figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
|
|
246
|
+
const quoteChangeCells = delisted ? 0 : figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
|
|
221
247
|
const stackQuoteChange = quoteChangeCells > quoteTextWidth;
|
|
222
248
|
const stackQuoteSummary = figureCells(quotePriceText) + 2 + quoteChangeCells > quoteTextWidth;
|
|
249
|
+
// What the move is from follows it on its line when there is room; otherwise it takes the line
|
|
250
|
+
// under the headline, so a narrow pane never splits the move itself to fit the words.
|
|
251
|
+
const changeBasisInline = !!changeBasisText && (stackQuoteSummary ? 0 : figureCells(quotePriceText) + 2)
|
|
252
|
+
+ quoteChangeCells + 1 + displayWidth(changeBasisText) <= quoteTextWidth;
|
|
223
253
|
const companyNameWidth = Math.max(8, quoteTextWidth
|
|
224
254
|
- displayWidth(venueText)
|
|
225
255
|
- (marketStateText ? displayWidth(marketStateText) + 1 : 0));
|
|
256
|
+
// A listing that no longer trades keeps its delisting date whole on a narrow pane: the venue gives way, then the name.
|
|
257
|
+
const delistedRoom = quote?.delisting && marketStateText ? quoteTextWidth - displayWidth(marketStateText) - 1 : null;
|
|
258
|
+
const identityVenue = delistedRoom !== null && delistedRoom - displayWidth(venueText) < 8 ? "" : venueText;
|
|
259
|
+
const identityNameWidth = delistedRoom !== null ? delistedRoom - displayWidth(identityVenue) : companyNameWidth;
|
|
260
|
+
const identityName = identityNameWidth >= 4 ? companyName : "";
|
|
226
261
|
const hasDayRange = quote?.low != null && quote?.high != null && quote.high > quote.low;
|
|
227
262
|
const yearRange = liveFiftyTwoWeekRange(quote);
|
|
228
|
-
|
|
263
|
+
// A listing that no longer trades: its range is its last session's, years ago perhaps, never today's.
|
|
264
|
+
const dayRangeLabel = delisted ? t("Last Session") : t("Day Range");
|
|
229
265
|
const yearRangeLabel = t("52W Range");
|
|
230
266
|
const rangeLabelWidth = Math.max(
|
|
231
267
|
hasDayRange ? displayWidth(dayRangeLabel) : 0,
|
|
@@ -243,7 +279,12 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
243
279
|
const contractLines = contractTerms ? futuresContractLines(contractTerms, futuresSymbol) : [];
|
|
244
280
|
// Columns that keep every term whole; a narrow pane stacks them in one.
|
|
245
281
|
const contractColumns = textGridColumns(contractLines.map(contractField), contentWidth);
|
|
246
|
-
const
|
|
282
|
+
const futuresFields = fitFuturesContractLines(contractLines, contractColumns * contractRowBudget(height)).map(contractField);
|
|
283
|
+
// An option contract states its own terms, and the chain's open interest and IV, where a stock has a profile.
|
|
284
|
+
const optionTarget = resolveOptionsTarget(ticker);
|
|
285
|
+
const optionFields = optionTarget?.isOptionTicker ? optionContractFields(optionTarget, optionRow, quoteCurrency) : [];
|
|
286
|
+
const contractFields = futuresFields.length > 0 ? futuresFields : optionFields;
|
|
287
|
+
const contractGridColumns = futuresFields.length > 0 ? contractColumns : textGridColumns(optionFields, contentWidth);
|
|
247
288
|
const stats = buildOverviewStats({
|
|
248
289
|
quote,
|
|
249
290
|
fundamentals,
|
|
@@ -260,7 +301,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
260
301
|
bond: fundFacts?.bond ?? null,
|
|
261
302
|
});
|
|
262
303
|
const profileFields = buildProfileFields({
|
|
263
|
-
|
|
304
|
+
// The contract section names an option's type.
|
|
305
|
+
instrumentType: optionFields.length > 0 ? undefined : instrumentTypeLabel(instrumentType, quote?.name || ticker.metadata.name),
|
|
264
306
|
sector,
|
|
265
307
|
industry,
|
|
266
308
|
profile,
|
|
@@ -276,6 +318,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
276
318
|
quoteCurrency,
|
|
277
319
|
baseCurrency,
|
|
278
320
|
toBase,
|
|
321
|
+
accountNames,
|
|
279
322
|
});
|
|
280
323
|
|
|
281
324
|
return (
|
|
@@ -315,13 +358,13 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
315
358
|
<Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
316
359
|
{hasIdentityLine && (
|
|
317
360
|
<Box flexDirection="row" minWidth={0} overflow="hidden">
|
|
318
|
-
{
|
|
319
|
-
<ClippedText fg={colors.textDim} text={
|
|
361
|
+
{identityName && (
|
|
362
|
+
<ClippedText fg={colors.textDim} text={identityName} width={identityNameWidth} />
|
|
320
363
|
)}
|
|
321
|
-
{
|
|
322
|
-
{
|
|
364
|
+
{identityVenue && <Text flexShrink={0} fg={colors.textDim}>{identityVenue}</Text>}
|
|
365
|
+
{marketStateText && (
|
|
323
366
|
<Text flexShrink={0} fg={quoteSessionColor(quote)}>
|
|
324
|
-
{
|
|
367
|
+
{identityName || identityVenue ? " " : ""}{marketStateText}
|
|
325
368
|
</Text>
|
|
326
369
|
)}
|
|
327
370
|
</Box>
|
|
@@ -334,12 +377,26 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
334
377
|
alignItems={stackQuoteSummary ? undefined : "baseline"}
|
|
335
378
|
>
|
|
336
379
|
<FigureText>{quotePriceText}</FigureText>
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
{!delisted && (
|
|
381
|
+
<Box flexDirection={stackQuoteChange ? "column" : "row"} gap={stackQuoteChange ? 0 : 1}
|
|
382
|
+
alignItems={changeBasisInline ? "baseline" : undefined}>
|
|
383
|
+
<FigureText part="sub" change={sessionQuote.change}>{quoteChangeText}</FigureText>
|
|
384
|
+
<FigureText part="sub" change={sessionQuote.change}>{quotePercentText}</FigureText>
|
|
385
|
+
{changeBasisInline ? <Text flexShrink={0} fg={colors.textDim}>{changeBasisText}</Text> : null}
|
|
386
|
+
</Box>
|
|
387
|
+
)}
|
|
341
388
|
</Box>
|
|
342
389
|
)}
|
|
390
|
+
{sessionQuote && changeBasisText && !changeBasisInline ? (
|
|
391
|
+
<Box minWidth={0} overflow="hidden">
|
|
392
|
+
<ClippedText fg={colors.textDim} text={changeBasisText} width={quoteTextWidth} />
|
|
393
|
+
</Box>
|
|
394
|
+
) : null}
|
|
395
|
+
{sessionQuote && changeBasisNoteText ? (
|
|
396
|
+
<Box minWidth={0} overflow="hidden">
|
|
397
|
+
<ClippedText fg={colors.textDim} text={changeBasisNoteText} width={quoteTextWidth} />
|
|
398
|
+
</Box>
|
|
399
|
+
) : null}
|
|
343
400
|
{extendedQuote && (
|
|
344
401
|
<Box flexDirection="row" gap={2}>
|
|
345
402
|
<Text fg={colors.textDim}>{extendedSessionLineLabel(extendedQuote, t)}:</Text>
|
|
@@ -384,7 +441,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
384
441
|
low={quote.low!}
|
|
385
442
|
high={quote.high!}
|
|
386
443
|
label={dayRangeLabel}
|
|
387
|
-
shortLabel={t("Day")}
|
|
444
|
+
shortLabel={delisted ? t("Last") : t("Day")}
|
|
388
445
|
labelWidth={rangeLabelWidth}
|
|
389
446
|
endpointWidth={rangeEndpointCells}
|
|
390
447
|
width={rangeWidth}
|
|
@@ -414,10 +471,11 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
414
471
|
{contractFields.length > 0 && (
|
|
415
472
|
<Box flexDirection="column">
|
|
416
473
|
<SectionHeading title={t("Contract")} />
|
|
417
|
-
<FundamentalsGrid fields={contractFields} width={contentWidth} columns={
|
|
474
|
+
<FundamentalsGrid fields={contractFields} width={contentWidth} columns={contractGridColumns} />
|
|
418
475
|
</Box>
|
|
419
476
|
)}
|
|
420
477
|
|
|
478
|
+
<ResearchTradeActions focused={focused} />
|
|
421
479
|
{hasHistory && (
|
|
422
480
|
<Box
|
|
423
481
|
onMouseDown={onOpenChart
|
|
@@ -437,6 +495,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
437
495
|
interactive={false}
|
|
438
496
|
series={[priceSeries]}
|
|
439
497
|
panels={[{ id: "price" }]}
|
|
498
|
+
levels={trade && researchTradeMatches(trade, instrumentFromTicker(ticker)) ? { seriesId: priceSeries.id, items: trade.levels } : undefined}
|
|
440
499
|
axisWidth={8}
|
|
441
500
|
showLegend={false}
|
|
442
501
|
/>
|
|
@@ -475,6 +534,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
475
534
|
</Box>
|
|
476
535
|
)}
|
|
477
536
|
|
|
537
|
+
{trade?.orders?.(contentWidth)}
|
|
538
|
+
|
|
478
539
|
{profileFields.length > 0 && (
|
|
479
540
|
<Box flexDirection="column">
|
|
480
541
|
<SectionHeading title={t("Profile")} />
|