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
|
@@ -16,12 +16,15 @@ import { qualifySharpeCadence } from "./sharpe-cadence";
|
|
|
16
16
|
import { evidenceDay } from "./risk-evidence";
|
|
17
17
|
import { FX_LIVE_RATE_MAX_DEVIATION } from "../../../market-data/coordinator/fx-legs";
|
|
18
18
|
import {
|
|
19
|
+
accountFxPair,
|
|
20
|
+
accountFxReason,
|
|
19
21
|
convertClosesToUsd,
|
|
20
22
|
FX_DISAGREES,
|
|
21
23
|
FX_NO_PAIR,
|
|
22
24
|
FX_UNAVAILABLE,
|
|
23
25
|
riskConversion,
|
|
24
26
|
validateFxHistory,
|
|
27
|
+
type AccountFx,
|
|
25
28
|
type FxCloses,
|
|
26
29
|
type RiskConversion,
|
|
27
30
|
} from "./risk-fx";
|
|
@@ -65,8 +68,10 @@ export interface RiskMarketSnapshot {
|
|
|
65
68
|
fetchedAt: string;
|
|
66
69
|
warnings: string[];
|
|
67
70
|
brokerOptions?: import("./risk-options").PortfolioOptionBook;
|
|
68
|
-
/** USD per unit of each listing or position currency of the holdings, at load time. */
|
|
71
|
+
/** USD per unit of the account currency and of each listing or position currency of the holdings, at load time. */
|
|
69
72
|
fxRates?: Record<string, number>;
|
|
73
|
+
/** For an account not in USD: the daily closes its returns convert at, or why they cannot. */
|
|
74
|
+
accountFx?: AccountFx;
|
|
70
75
|
}
|
|
71
76
|
/**
|
|
72
77
|
* The most holdings one load requests daily history for, largest current USD
|
|
@@ -81,13 +86,14 @@ const RISK_QUOTE_LIMIT = 500;
|
|
|
81
86
|
const HISTORY_CONCURRENCY = 4;
|
|
82
87
|
const MAX_FX_CURRENCIES = 24;
|
|
83
88
|
export const BEYOND_SIZE_REASON = `Beyond the supported size: only the ${RISK_HISTORY_LIMIT} largest holdings by value are modelled`;
|
|
84
|
-
// Listing venues as the quotes report them; MTUM
|
|
89
|
+
// Listing venues as the quotes report them; MTUM and QUAL list on Cboe BZX, not Arca.
|
|
85
90
|
export const RISK_FACTOR_INSTRUMENTS: RiskInstrument[] = [
|
|
86
91
|
["SPY", "ARCA"],
|
|
87
92
|
["IWM", "ARCA"],
|
|
88
93
|
["IWD", "ARCA"],
|
|
89
94
|
["IWF", "ARCA"],
|
|
90
95
|
["MTUM", "BATS"],
|
|
96
|
+
["QUAL", "BATS"],
|
|
91
97
|
["IEF", "ARCA"],
|
|
92
98
|
["HYG", "ARCA"],
|
|
93
99
|
].map(([symbol, exchange]) => ({ symbol: symbol!, exchange: exchange! }));
|
|
@@ -313,7 +319,9 @@ const largestFirst = (
|
|
|
313
319
|
* competes at its value at the current rate and, when requested, also loads
|
|
314
320
|
* its currency's daily FX history once per currency, so its closes convert to
|
|
315
321
|
* USD. Holdings with no FX pair, positions the caller marks unsupported and
|
|
316
|
-
* holdings past the limit get no history request; their entry says why.
|
|
322
|
+
* holdings past the limit get no history request; their entry says why. An
|
|
323
|
+
* account in another currency than USD also loads that currency's current
|
|
324
|
+
* rate and daily FX history, which the model converts every return with. The
|
|
317
325
|
* signal stops the fetch: no request starts once it
|
|
318
326
|
* aborts, the ones in flight are cancelled, and the call rejects instead of
|
|
319
327
|
* returning a partial snapshot nobody is waiting for.
|
|
@@ -323,6 +331,7 @@ export async function fetchPortfolioRiskMarket(
|
|
|
323
331
|
client: RiskCloudClient = apiClient,
|
|
324
332
|
now = new Date(),
|
|
325
333
|
signal?: AbortSignal,
|
|
334
|
+
accountCurrency = "USD",
|
|
326
335
|
): Promise<RiskMarketSnapshot> {
|
|
327
336
|
signal?.throwIfAborted();
|
|
328
337
|
const normalized = (row: RiskInstrument): RiskInstrument => ({
|
|
@@ -417,14 +426,16 @@ export async function fetchPortfolioRiskMarket(
|
|
|
417
426
|
}
|
|
418
427
|
}
|
|
419
428
|
// Current rates value a foreign holding: its rank, its mark, and its size when left out.
|
|
429
|
+
// The account currency's comes first: it values every holding.
|
|
430
|
+
const account = accountCurrency === "USD" ? null : accountCurrency;
|
|
420
431
|
const fxCurrencies = [
|
|
421
432
|
...new Set(
|
|
422
|
-
[...entries.values()].flatMap((row) => {
|
|
433
|
+
[...(account ? [account] : []), ...[...entries.values()].flatMap((row) => {
|
|
423
434
|
const currency = resolveCurrencyUnit(
|
|
424
435
|
listings.get(row.id)?.currency ?? (quotes.has(row.id) ? null : row.request.currency),
|
|
425
436
|
).currency;
|
|
426
437
|
return currency && currency !== "USD" ? [currency] : [];
|
|
427
|
-
}),
|
|
438
|
+
})],
|
|
428
439
|
),
|
|
429
440
|
].slice(0, MAX_FX_CURRENCIES);
|
|
430
441
|
const fx = await Promise.allSettled(
|
|
@@ -539,6 +550,22 @@ export async function fetchPortfolioRiskMarket(
|
|
|
539
550
|
}
|
|
540
551
|
return loading;
|
|
541
552
|
};
|
|
553
|
+
// The account currency's daily closes, shared with any listing that converts from it.
|
|
554
|
+
const accountWork: Promise<AccountFx | undefined> = (async () => {
|
|
555
|
+
if (!account) return undefined;
|
|
556
|
+
const failed = (error: string): AccountFx =>
|
|
557
|
+
({ currency: account, pair: accountFxPair(account), closes: {}, first: null, asOf: null, error });
|
|
558
|
+
const conversion = riskConversion(account);
|
|
559
|
+
if (!conversion || conversion.divisor !== 1) return failed(accountFxReason(account, FX_NO_PAIR));
|
|
560
|
+
try {
|
|
561
|
+
const closes = await fxHistory(conversion);
|
|
562
|
+
return { currency: account, pair: accountFxPair(account), closes: Object.fromEntries(closes.closes),
|
|
563
|
+
first: closes.first, asOf: closes.asOf, error: null };
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (isAccessDenied(error) || signal?.aborted) throw error;
|
|
566
|
+
return failed(accountFxReason(account, message(error)));
|
|
567
|
+
}
|
|
568
|
+
})();
|
|
542
569
|
const pending = instruments.flatMap(([id], index) =>
|
|
543
570
|
requestedIds.has(id) ? [index] : [],
|
|
544
571
|
);
|
|
@@ -596,7 +623,7 @@ export async function fetchPortfolioRiskMarket(
|
|
|
596
623
|
}
|
|
597
624
|
}),
|
|
598
625
|
);
|
|
599
|
-
const [, fred] = await Promise.all([
|
|
626
|
+
const [, fred, accountFx] = await Promise.all([
|
|
600
627
|
historyWork,
|
|
601
628
|
Promise.allSettled(
|
|
602
629
|
["DGS10", "VIXCLS"].map(async (id) =>
|
|
@@ -611,6 +638,7 @@ export async function fetchPortfolioRiskMarket(
|
|
|
611
638
|
),
|
|
612
639
|
),
|
|
613
640
|
),
|
|
641
|
+
accountWork,
|
|
614
642
|
]);
|
|
615
643
|
signal?.throwIfAborted();
|
|
616
644
|
for (const [index, result] of fred.entries()) {
|
|
@@ -628,25 +656,30 @@ export async function fetchPortfolioRiskMarket(
|
|
|
628
656
|
fetchedAt: now.toISOString(),
|
|
629
657
|
warnings,
|
|
630
658
|
fxRates,
|
|
659
|
+
...(accountFx ? { accountFx } : {}),
|
|
631
660
|
};
|
|
632
661
|
}
|
|
633
|
-
/**
|
|
634
|
-
|
|
662
|
+
/**
|
|
663
|
+
* What changes the requests: identity, size, whether a holding can enter the
|
|
664
|
+
* basket, and an account currency other than USD (whose daily FX the load adds).
|
|
665
|
+
*/
|
|
666
|
+
const cacheKey = (holdings: readonly RiskHoldingRequest[], accountCurrency: string) =>
|
|
635
667
|
[
|
|
636
668
|
...new Set(
|
|
637
669
|
holdings.map(
|
|
638
670
|
(row) => `${riskInstrumentId(row)}|${row.quantity ?? ""}|${row.currency ?? ""}|${row.unsupported ? 1 : 0}`,
|
|
639
671
|
),
|
|
640
672
|
),
|
|
641
|
-
].sort().join(",");
|
|
673
|
+
].sort().join(",") + (accountCurrency === "USD" ? "" : `@${accountCurrency}`);
|
|
642
674
|
export async function loadPortfolioRiskMarket(
|
|
643
675
|
holdings: readonly RiskHoldingRequest[],
|
|
644
676
|
force = false,
|
|
677
|
+
accountCurrency = "USD",
|
|
645
678
|
) {
|
|
646
679
|
const { payload, stale, refreshError } = await loadCloudResource(
|
|
647
680
|
portfolioRiskCache,
|
|
648
|
-
cacheKey(holdings),
|
|
649
|
-
() => fetchPortfolioRiskMarket(holdings),
|
|
681
|
+
cacheKey(holdings, accountCurrency),
|
|
682
|
+
() => fetchPortfolioRiskMarket(holdings, undefined, undefined, undefined, accountCurrency),
|
|
650
683
|
{ force },
|
|
651
684
|
);
|
|
652
685
|
return {
|
|
@@ -4,9 +4,26 @@ import { dailyFxCloses, FxHistoryError, type FxCloses } from "../../../market-da
|
|
|
4
4
|
import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
|
|
5
5
|
import type { PricePoint } from "../../../types/financials";
|
|
6
6
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
7
|
+
import type { DatedReturn } from "./metrics";
|
|
7
8
|
|
|
8
9
|
export type { FxCloses };
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The daily closes that restate a non-USD account's returns, as the market
|
|
13
|
+
* load keeps them (plain data, so the cache can hold them), or why there are none.
|
|
14
|
+
*/
|
|
15
|
+
export interface AccountFx {
|
|
16
|
+
currency: string;
|
|
17
|
+
/** "USD/TRY" or "EUR/USD", as the pair trades; null when no daily pair prices the currency. */
|
|
18
|
+
pair: string | null;
|
|
19
|
+
/** USD per unit of the account currency at each completed daily close; empty with an error. */
|
|
20
|
+
closes: Record<string, number>;
|
|
21
|
+
first: string | null;
|
|
22
|
+
asOf: string | null;
|
|
23
|
+
/** Why returns cannot convert into the account currency; every holding is left out with it. */
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/** How a listing quoted in another currency is restated in USD. */
|
|
11
28
|
export interface RiskConversion {
|
|
12
29
|
/** The listing currency as quoted, sub-units included (GBp, ZAc). */
|
|
@@ -35,6 +52,50 @@ const MAX_LOCAL_CLOSED_DAYS = 10;
|
|
|
35
52
|
const BASKET_CALENDAR = "NYSE";
|
|
36
53
|
const DAY_MS = 86_400_000;
|
|
37
54
|
|
|
55
|
+
/** The daily pair of an account currency as dealers quote it ("USD/TRY", "EUR/USD"), or null when none prices it. */
|
|
56
|
+
export function accountFxPair(currency: string): string | null {
|
|
57
|
+
const leg = fxLegForCurrency(currency);
|
|
58
|
+
return !leg ? null : leg.invert ? `USD/${leg.currency}` : `${leg.currency}/USD`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Why the account currency's daily closes cannot convert a holding: the
|
|
63
|
+
* problem a listing's own conversion would report (stale, unavailable,
|
|
64
|
+
* gapped, disagreeing with the current rate), said of the account's pair.
|
|
65
|
+
*/
|
|
66
|
+
export function accountFxReason(currency: string, problem: string): string {
|
|
67
|
+
const pair = accountFxPair(currency);
|
|
68
|
+
if (!pair) return `No daily FX pair converts USD into the account currency ${currency}`;
|
|
69
|
+
const tail = /^Foreign holdings: (daily FX .*)$/.exec(problem)?.[1];
|
|
70
|
+
return `${pair} ${tail ?? "daily FX closes unavailable"}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Restates daily USD returns in the account currency at the FX closes of each
|
|
75
|
+
* return's own start and end dates: the holding's USD value is divided by the
|
|
76
|
+
* USD price of one account unit on both dates, so a return becomes
|
|
77
|
+
* (1 + r) * fx(start) / fx(end) - 1. A listing already converted from its own
|
|
78
|
+
* currency to USD therefore converts at the cross of the two closes of the
|
|
79
|
+
* same date. Returns before the first FX close or after the last are dropped,
|
|
80
|
+
* so the series starts and ends with the FX history; a return inside it with an
|
|
81
|
+
* endpoint that has no FX close rejects the whole series instead of bridging it.
|
|
82
|
+
*/
|
|
83
|
+
export function convertReturnsToAccount(
|
|
84
|
+
returns: readonly DatedReturn[],
|
|
85
|
+
fx: Pick<AccountFx, "currency" | "closes" | "first" | "asOf">,
|
|
86
|
+
): DatedReturn[] {
|
|
87
|
+
if (!fx.first || !fx.asOf) throw new Error(accountFxReason(fx.currency, FX_UNAVAILABLE));
|
|
88
|
+
const converted: DatedReturn[] = [];
|
|
89
|
+
for (const point of returns) {
|
|
90
|
+
if (point.startDateKey < fx.first || point.dateKey > fx.asOf) continue;
|
|
91
|
+
const start = fx.closes[point.startDateKey],
|
|
92
|
+
end = fx.closes[point.dateKey];
|
|
93
|
+
if (!(start! > 0) || !(end! > 0)) throw new Error(accountFxReason(fx.currency, FX_GAP));
|
|
94
|
+
converted.push({ ...point, value: (1 + point.value) * (start! / end!) - 1 });
|
|
95
|
+
}
|
|
96
|
+
return converted;
|
|
97
|
+
}
|
|
98
|
+
|
|
38
99
|
/** The pair that restates a listing currency in USD, or null when there is none. */
|
|
39
100
|
export function riskConversion(listingCurrency: string): RiskConversion | null {
|
|
40
101
|
const unit = resolveCurrencyUnit(listingCurrency);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
RISK_VIEWS,
|
|
16
16
|
riskEvidence,
|
|
17
17
|
riskPercentile,
|
|
18
|
+
riskReturnsCurrency,
|
|
18
19
|
riskValue,
|
|
19
20
|
sinceFallbackNote,
|
|
20
21
|
sinceFiledMethod,
|
|
@@ -142,7 +143,7 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
142
143
|
"Explicit local evidence for account returns and attribution",
|
|
143
144
|
],
|
|
144
145
|
limitations: [
|
|
145
|
-
`Fixed-current-weight
|
|
146
|
+
`Fixed-current-weight equity basket over the holdings that qualify, largest ${RISK_HISTORY_LIMIT} by value, in the account's currency; foreign listings convert to USD at daily FX closes, and a non-USD account's returns then convert into its currency at its own daily FX closes; metadata.coverage states the covered share of market value and each holding left out.`,
|
|
146
147
|
"--since filed buys the 13F's own weights at the filing-date close and holds them (--weights current holds today's weights fixed instead).",
|
|
147
148
|
"No account performance inferred.",
|
|
148
149
|
"ETF price-return factor proxies.",
|
|
@@ -246,6 +247,7 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
246
247
|
ctx.apiClient,
|
|
247
248
|
new Date(),
|
|
248
249
|
ctx.signal,
|
|
250
|
+
local.portfolio.currency,
|
|
249
251
|
),
|
|
250
252
|
localOnly
|
|
251
253
|
? Promise.resolve(undefined)
|
|
@@ -270,7 +272,12 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
270
272
|
// Options for views the report does not print are named, not dropped silently; the fallback from --since opens the report.
|
|
271
273
|
const fallback = sinceFallbackNote(local.portfolio);
|
|
272
274
|
const shifted = ["equity-shift", "rate-shift", "vol-shift"].filter((key) => args.options[key] != null);
|
|
275
|
+
// A non-USD account's returns were converted, and the basket views say into what.
|
|
276
|
+
const returnsCurrency = riskReturnsCurrency(model);
|
|
273
277
|
const notices = [
|
|
278
|
+
...(returnsCurrency && (!requestedView || (BASKET_VIEWS.has(requestedView) && requestedView !== "holdings"))
|
|
279
|
+
? [`${returnsCurrency[0]!.toUpperCase()}${returnsCurrency.slice(1)}.`]
|
|
280
|
+
: []),
|
|
274
281
|
...(model.window.kind === "since-filed" ? [sinceFiledMethod(model.window, model.benchmark)] : []),
|
|
275
282
|
...(args.options.since === "filed" && model.window.kind !== "since-filed" ? [fallback] : []),
|
|
276
283
|
...(requestedView && requestedView !== "stress" && shifted.length ? [`${shifted.map((key) => `--${key}`).join(", ")} change${shifted.length === 1 ? "s" : ""} the stress view only, not --view ${requestedView}`] : []),
|
|
@@ -335,6 +342,8 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
335
342
|
...(basketView && model.coverage
|
|
336
343
|
? { coverage: coverageSummary(model.coverage, COMPACT_LEFT_OUT_LIMIT) }
|
|
337
344
|
: {}),
|
|
345
|
+
// The currency a non-USD account's returns and amounts are in, and the daily pair they converted at.
|
|
346
|
+
...(basketView && model.accountConversion ? { accountConversion: model.accountConversion } : {}),
|
|
338
347
|
asOf: model.fetchedAt,
|
|
339
348
|
...(notices.length ? { notices } : {}),
|
|
340
349
|
},
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type RiskHoldingRequest,
|
|
37
37
|
type RiskMarketSnapshot,
|
|
38
38
|
} from "./risk-client";
|
|
39
|
+
import { accountFxPair, accountFxReason, convertReturnsToAccount, FX_UNAVAILABLE, type AccountFx } from "./risk-fx";
|
|
39
40
|
|
|
40
41
|
interface PortfolioRiskHolding {
|
|
41
42
|
id: string;
|
|
@@ -43,7 +44,10 @@ interface PortfolioRiskHolding {
|
|
|
43
44
|
exchange: string;
|
|
44
45
|
quantity: number;
|
|
45
46
|
currency: string;
|
|
46
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Signed value at the current USD mark, in the account currency at its
|
|
49
|
+
* current rate; only these marks weight the basket.
|
|
50
|
+
*/
|
|
47
51
|
value: number | null;
|
|
48
52
|
/** Weight in the covered basket, null outside it. */
|
|
49
53
|
weight: number | null;
|
|
@@ -59,6 +63,7 @@ interface PortfolioRiskHolding {
|
|
|
59
63
|
/** The listing currency its closes were converted from at daily FX closes; null for a USD listing. */
|
|
60
64
|
convertedFrom: string | null;
|
|
61
65
|
historyAsOf: string | null;
|
|
66
|
+
/** Daily returns in the account currency. */
|
|
62
67
|
returns: DatedReturn[];
|
|
63
68
|
error: string | null;
|
|
64
69
|
/** Why the holding is outside the basket; null when it qualifies. */
|
|
@@ -325,6 +330,24 @@ function outsideValue(
|
|
|
325
330
|
const value = resolvePortfolioMarketValue(metrics, unit != null && Number.isFinite(unit) ? unit : null)?.gross;
|
|
326
331
|
return value != null && Number.isFinite(value) ? value : null;
|
|
327
332
|
}
|
|
333
|
+
/** USD returns in the account currency, or none with the reason its daily closes cannot convert them. */
|
|
334
|
+
function accountReturns(returns: readonly DatedReturn[], account: AccountFx): { returns: DatedReturn[]; error: string | null } {
|
|
335
|
+
if (account.error) return { returns: [], error: account.error };
|
|
336
|
+
try {
|
|
337
|
+
return { returns: convertReturnsToAccount(returns, account), error: null };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return { returns: [], error: error instanceof Error ? error.message : accountFxReason(account.currency, FX_UNAVAILABLE) };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* "returns in TRY at daily USD/TRY closes": the currency the basket views of a
|
|
344
|
+
* non-USD account are in and how they got there; null for a USD account, or
|
|
345
|
+
* when its daily closes were unavailable and nothing converted.
|
|
346
|
+
*/
|
|
347
|
+
export function riskReturnsCurrency(model: Pick<PortfolioRiskModel, "accountConversion"> | null | undefined): string | null {
|
|
348
|
+
const conversion = model?.accountConversion;
|
|
349
|
+
return conversion ? `returns in ${conversion.currency} at daily ${conversion.pair} closes` : null;
|
|
350
|
+
}
|
|
328
351
|
// The epsilon keeps 29 of 100 at 29%, not the 28 that 0.29 * 100 rounds down to.
|
|
329
352
|
const coveragePercent = (share: number) => Math.floor(share * 100 + 1e-9);
|
|
330
353
|
const holdingCount = (count: number) => `${count} holding${count === 1 ? "" : "s"}`;
|
|
@@ -441,7 +464,27 @@ export function buildPortfolioRisk(
|
|
|
441
464
|
const byId = new Map(
|
|
442
465
|
market.histories.map((row) => [riskInstrumentId(row.instrument), row]),
|
|
443
466
|
);
|
|
444
|
-
|
|
467
|
+
// A non-USD account converts every USD return at its currency's daily closes and every USD mark at its current rate.
|
|
468
|
+
const account: AccountFx | null =
|
|
469
|
+
portfolio.currency === "USD"
|
|
470
|
+
? null
|
|
471
|
+
: market.accountFx?.currency === portfolio.currency
|
|
472
|
+
? market.accountFx
|
|
473
|
+
: {
|
|
474
|
+
currency: portfolio.currency,
|
|
475
|
+
pair: accountFxPair(portfolio.currency),
|
|
476
|
+
closes: {},
|
|
477
|
+
first: null,
|
|
478
|
+
asOf: null,
|
|
479
|
+
error: accountFxReason(portfolio.currency, FX_UNAVAILABLE),
|
|
480
|
+
};
|
|
481
|
+
// Without a current rate the latest daily close values the account.
|
|
482
|
+
const latestAccountClose = account?.asOf ? account.closes[account.asOf] : undefined;
|
|
483
|
+
const rates: Readonly<Record<string, number>> =
|
|
484
|
+
account && !((market.fxRates?.[account.currency] ?? 0) > 0) && latestAccountClose
|
|
485
|
+
? { ...market.fxRates, [account.currency]: latestAccountClose }
|
|
486
|
+
: (market.fxRates ?? {});
|
|
487
|
+
const accountRate = account ? rates[account.currency] : undefined;
|
|
445
488
|
const holdings: PortfolioRiskHolding[] = portfolioRiskTickers(
|
|
446
489
|
tickers,
|
|
447
490
|
portfolio.id,
|
|
@@ -471,14 +514,11 @@ export function buildPortfolioRisk(
|
|
|
471
514
|
const listingCurrency =
|
|
472
515
|
quote?.currency ?? close?.currency ?? history?.listing?.currency ?? ticker.metadata.currency;
|
|
473
516
|
const foreign = !convertedFrom && listingCurrency !== "USD";
|
|
474
|
-
const
|
|
517
|
+
const usdError =
|
|
475
518
|
syntheticPositionUnsupportedReason(ticker, "USD", portfolio.id) ??
|
|
476
519
|
(lots.some((row) => !Number.isFinite(row.shares))
|
|
477
520
|
? "Invalid position quantity"
|
|
478
521
|
: null) ??
|
|
479
|
-
(portfolio.currency !== "USD"
|
|
480
|
-
? "Historical account-currency FX is required"
|
|
481
|
-
: null) ??
|
|
482
522
|
(ticker.metadata.currency &&
|
|
483
523
|
(convertedFrom ?? mark?.currency) &&
|
|
484
524
|
ticker.metadata.currency !== (convertedFrom ?? mark?.currency)
|
|
@@ -490,10 +530,13 @@ export function buildPortfolioRisk(
|
|
|
490
530
|
: null) ??
|
|
491
531
|
history?.error ??
|
|
492
532
|
(!history ? "Daily history unavailable" : null);
|
|
533
|
+
// A holding with a USD history converts into the account currency, or is left out with the reason it cannot.
|
|
534
|
+
const inAccount = account && !usdError ? accountReturns(history?.returns ?? [], account) : null;
|
|
535
|
+
const error = usdError ?? inAccount?.error ?? null;
|
|
493
536
|
const priceDate = close ? close.date : regularClose ? regularClose.date : quoteDate(quote?.lastUpdated);
|
|
494
537
|
const marked =
|
|
495
538
|
mark &&
|
|
496
|
-
mark.currency ===
|
|
539
|
+
mark.currency === "USD" &&
|
|
497
540
|
!mark.stale &&
|
|
498
541
|
Number.isFinite(mark.price) &&
|
|
499
542
|
mark.price > 0 &&
|
|
@@ -506,8 +549,9 @@ export function buildPortfolioRisk(
|
|
|
506
549
|
error?.startsWith("Short positions"))
|
|
507
550
|
? quantity * mark.price
|
|
508
551
|
: null;
|
|
509
|
-
const
|
|
510
|
-
const
|
|
552
|
+
const accountValue = marked == null || !account ? marked : accountRate! > 0 ? marked / accountRate! : null;
|
|
553
|
+
const value = accountValue != null && Number.isFinite(accountValue) ? accountValue : null;
|
|
554
|
+
const returns = inAccount?.returns ?? history?.returns ?? [];
|
|
511
555
|
const leftOut =
|
|
512
556
|
error ??
|
|
513
557
|
(value == null || value <= 0
|
|
@@ -529,7 +573,8 @@ export function buildPortfolioRisk(
|
|
|
529
573
|
priceAsOf: priceDate,
|
|
530
574
|
markSource: close ? "close" : regularClose ? "regular-close" : quote ? "quote" : null,
|
|
531
575
|
convertedFrom,
|
|
532
|
-
|
|
576
|
+
// Converted returns end with the account currency's last daily close.
|
|
577
|
+
historyAsOf: (inAccount ? inAccount.returns.at(-1)?.dateKey : null) ?? history?.asOf ?? null,
|
|
533
578
|
returns,
|
|
534
579
|
error,
|
|
535
580
|
leftOut,
|
|
@@ -610,7 +655,14 @@ export function buildPortfolioRisk(
|
|
|
610
655
|
);
|
|
611
656
|
return (instrument && byId.get(riskInstrumentId(instrument))?.returns) || [];
|
|
612
657
|
};
|
|
613
|
-
|
|
658
|
+
// SPY's USD returns are the market factor and the equity stress; the risk
|
|
659
|
+
// rows compare the basket with SPY in the basket's own currency. A session
|
|
660
|
+
// without an account close is a gap in that benchmark, never bridged, so it
|
|
661
|
+
// cannot blank the holdings the closes do cover.
|
|
662
|
+
const spy = proxy(RISK_BENCHMARK);
|
|
663
|
+
const benchmark = account
|
|
664
|
+
? accountReturns(spy.filter((point) => account.closes[point.startDateKey] && account.closes[point.dateKey]), account).returns
|
|
665
|
+
: spy;
|
|
614
666
|
// Since the filing only for a portfolio that has one; any other keeps the trailing window and says why.
|
|
615
667
|
const filed = window.since === "filed" ? portfolio.importedFrom?.filed ?? null : null;
|
|
616
668
|
if (window.since === "filed" && !filed)
|
|
@@ -647,12 +699,13 @@ export function buildPortfolioRisk(
|
|
|
647
699
|
const metrics = filed
|
|
648
700
|
? sinceBasketRisk(sample, filed, RISK_BENCHMARK)
|
|
649
701
|
: rollingBasketRisk(sample, undefined, RISK_BENCHMARK);
|
|
702
|
+
// Factor proxies stay in USD: a beta is the account-currency move per unit of the factor as it trades.
|
|
650
703
|
const factors = [
|
|
651
|
-
{ id: "market", label: "Market (SPY)", series:
|
|
704
|
+
{ id: "market", label: "Market (SPY)", series: spy },
|
|
652
705
|
{
|
|
653
706
|
id: "size",
|
|
654
707
|
label: "Size (IWM - SPY)",
|
|
655
|
-
series: subtractReturns(proxy("IWM"),
|
|
708
|
+
series: subtractReturns(proxy("IWM"), spy),
|
|
656
709
|
},
|
|
657
710
|
{
|
|
658
711
|
id: "value",
|
|
@@ -662,7 +715,12 @@ export function buildPortfolioRisk(
|
|
|
662
715
|
{
|
|
663
716
|
id: "momentum",
|
|
664
717
|
label: "Momentum (MTUM - SPY)",
|
|
665
|
-
series: subtractReturns(proxy("MTUM"),
|
|
718
|
+
series: subtractReturns(proxy("MTUM"), spy),
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: "quality",
|
|
722
|
+
label: "Quality (QUAL - SPY)",
|
|
723
|
+
series: subtractReturns(proxy("QUAL"), spy),
|
|
666
724
|
},
|
|
667
725
|
{ id: "rates", label: "Rates (IEF)", series: proxy("IEF") },
|
|
668
726
|
{
|
|
@@ -690,7 +748,7 @@ export function buildPortfolioRisk(
|
|
|
690
748
|
{
|
|
691
749
|
id: "equity",
|
|
692
750
|
label: `Index ${shifts.equity > 0 ? "+" : ""}${shifts.equity}%`,
|
|
693
|
-
series:
|
|
751
|
+
series: spy,
|
|
694
752
|
shock: shifts.equity / 100,
|
|
695
753
|
source: "SPY price return",
|
|
696
754
|
},
|
|
@@ -772,7 +830,7 @@ export function buildPortfolioRisk(
|
|
|
772
830
|
asOf: row.priceAsOf,
|
|
773
831
|
detail: row.leftOut
|
|
774
832
|
? row.leftOut
|
|
775
|
-
: `${row.quantity} shares; ${formatNumber(row.value ?? undefined)} ${
|
|
833
|
+
: `${row.quantity} shares; ${formatNumber(row.value ?? undefined)} ${portfolio.currency}${row.markSource === "close" || row.markSource === "regular-close" ? ` at ${row.priceAsOf} close` : ""}${row.convertedFrom ? ` from ${row.convertedFrom} at daily FX` : ""}; history ${row.historyAsOf}`,
|
|
776
834
|
...(row.leftOut ? { leftOut: row.leftOut } : {}),
|
|
777
835
|
})),
|
|
778
836
|
correlation: correlation.map((row) => ({
|
|
@@ -947,6 +1005,11 @@ export function buildPortfolioRisk(
|
|
|
947
1005
|
notes,
|
|
948
1006
|
fetchedAt: market.fetchedAt,
|
|
949
1007
|
benchmark: RISK_BENCHMARK,
|
|
1008
|
+
/** The currency a non-USD account's returns converted into, at the daily closes of `pair`; null for USD. */
|
|
1009
|
+
accountConversion:
|
|
1010
|
+
account && !account.error && account.pair
|
|
1011
|
+
? { currency: account.currency, pair: account.pair, asOf: account.asOf }
|
|
1012
|
+
: null,
|
|
950
1013
|
window: filed
|
|
951
1014
|
? {
|
|
952
1015
|
kind: "since-filed" as const,
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
riskPercentile,
|
|
59
59
|
riskAmountBase,
|
|
60
60
|
riskEvidence,
|
|
61
|
+
riskReturnsCurrency,
|
|
61
62
|
riskValue,
|
|
62
63
|
RISK_VIEWS,
|
|
63
64
|
type PortfolioRiskModel,
|
|
@@ -301,7 +302,7 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
|
|
|
301
302
|
const loader = useCallback(
|
|
302
303
|
async (force: boolean) => {
|
|
303
304
|
const [market, brokerOptions] = await Promise.all([
|
|
304
|
-
loadPortfolioRiskMarket(requests, force),
|
|
305
|
+
loadPortfolioRiskMarket(requests, force, portfolio!.currency),
|
|
305
306
|
loadPortfolioOptionBook(localTickers, portfolio!),
|
|
306
307
|
]);
|
|
307
308
|
return { ...market, brokerOptions };
|
|
@@ -466,6 +467,9 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
|
|
|
466
467
|
// What the basket views leave out, unless the body already says why they show nothing.
|
|
467
468
|
const coverageText = model && BASKET_VIEWS.has(view) && !basketBody ? riskCoverageHeadline(model.coverage) : null;
|
|
468
469
|
const asOfDate = model?.rows[view].find((row) => row.asOf)?.asOf?.slice(0, 10) ?? null;
|
|
470
|
+
// The views estimated from returns name the currency a non-USD account's returns were converted into.
|
|
471
|
+
const returnsCurrency = BASKET_VIEWS.has(view) && view !== "holdings" ? riskReturnsCurrency(model) : null;
|
|
472
|
+
const amountBase = rows.map(riskAmountBase).find(Boolean);
|
|
469
473
|
// Dated evidence only when a row has a date; a missing date is not a claim about history.
|
|
470
474
|
const asOfText = !model
|
|
471
475
|
? null
|
|
@@ -473,7 +477,9 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
|
|
|
473
477
|
...(basketBody ? [] : [
|
|
474
478
|
...(asOfDate && sharedEvidence ? [sharedEvidence] : []),
|
|
475
479
|
// The value the tail losses in money are taken on names the currency too.
|
|
476
|
-
|
|
480
|
+
...(amountBase
|
|
481
|
+
? [amountBase, ...(returnsCurrency ? [returnsCurrency] : [])]
|
|
482
|
+
: [returnsCurrency ?? model.portfolio.currency]),
|
|
477
483
|
...(asOfDate ? [asOfDate] : []),
|
|
478
484
|
...(EVIDENCE_VIEWS.has(view) && model.evidence ? [model.evidence.source] : []),
|
|
479
485
|
]),
|
|
@@ -76,6 +76,9 @@ const BROKER_FX_RATES: Readonly<Record<string, number>> = {
|
|
|
76
76
|
HKD: 0.128,
|
|
77
77
|
JPY: 0.0068,
|
|
78
78
|
TWD: 0.031,
|
|
79
|
+
// An account currency, and a listing currency with no daily FX pair.
|
|
80
|
+
TRY: 0.0202,
|
|
81
|
+
SAR: 0.2667,
|
|
79
82
|
};
|
|
80
83
|
export const BROKER_PORTFOLIO: Portfolio = { id: "broker", name: "Broker account", currency: "USD" };
|
|
81
84
|
|
|
@@ -21,18 +21,20 @@ function useTrending(symbol?: string) {
|
|
|
21
21
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
22
22
|
return { resource, session, access };
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Optional profile context. A failure here never hides or blocks the description,
|
|
26
|
+
* and a symbol with no published research hours shows no section at all: the
|
|
27
|
+
* Overview leaves out what it has nothing to say about.
|
|
28
|
+
*/
|
|
25
29
|
export function TrendingSummary({ symbol, onOpen }: { symbol: string; onOpen?: () => void }) {
|
|
26
30
|
const { resource, access } = useTrending(symbol);
|
|
27
31
|
const data = resource.data?.payload;
|
|
28
32
|
const row = data?.rows[0];
|
|
29
|
-
if (!access.signedIn || !data) return null;
|
|
33
|
+
if (!access.signedIn || !data || !row) return null;
|
|
30
34
|
return <Box flexDirection="column">
|
|
31
35
|
<SectionHeading title="Gloom Trending" />
|
|
32
|
-
{row
|
|
33
|
-
|
|
34
|
-
<KeyValueRow label="Latest-hour Z" value={signed(row.zScore)} detail={data.asOf ? date(data.asOf) : undefined} />
|
|
35
|
-
</> : <EmptyState title={data.status === "ready" ? data.entitlement === "preview" ? "Outside the current preview." : "No published research hours today." : emptyAttention(data)} />}
|
|
36
|
+
<KeyValueRow label="Research rank" value={`#${row.rank}`} detail={`Today · ${number(row.researchUnits)} research hours`} />
|
|
37
|
+
<KeyValueRow label="Latest-hour Z" value={signed(row.zScore)} detail={data.asOf ? date(data.asOf) : undefined} />
|
|
36
38
|
{onOpen ? <ActionRow label="Open research attention" onPress={onOpen} /> : null}
|
|
37
39
|
</Box>;
|
|
38
40
|
}
|
|
@@ -2,7 +2,7 @@ import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValu
|
|
|
2
2
|
import type { ChartRequest, InstrumentRef } from "../../../market-data/request-types";
|
|
3
3
|
import type { QueryEntry } from "../../../market-data/result-types";
|
|
4
4
|
import type { DataProvider } from "../../../types/data-provider";
|
|
5
|
-
import type { PricePoint } from "../../../types/financials";
|
|
5
|
+
import type { ListingDelisting, PricePoint } from "../../../types/financials";
|
|
6
6
|
import type { BacktestBar } from "./rules";
|
|
7
7
|
import { isFiniteNumber } from "../../../utils/guards";
|
|
8
8
|
|
|
@@ -27,6 +27,8 @@ export interface BacktestHistory {
|
|
|
27
27
|
skippedDates: string[];
|
|
28
28
|
undatedSkipped: number;
|
|
29
29
|
fetchedAt: number;
|
|
30
|
+
/** The listing no longer trades: its last bar is its last session, and no later one will come. */
|
|
31
|
+
delisting?: ListingDelisting;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -85,5 +87,6 @@ export async function loadBacktestHistory(
|
|
|
85
87
|
options.signal?.throwIfAborted();
|
|
86
88
|
const history = toBacktestBars(resolveEntryValue(entry) ?? [], entry.history?.nonPositiveCloseDates);
|
|
87
89
|
if (!history.bars.length) throw new Error(entry.error?.message ?? "Daily price history unavailable");
|
|
88
|
-
|
|
90
|
+
const delisting = entry.history?.delisting;
|
|
91
|
+
return { symbol, ...history, fetchedAt: entry.fetchedAt ?? Date.now(), ...(delisting ? { delisting } : {}) };
|
|
89
92
|
}
|
|
@@ -2,6 +2,7 @@ import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
|
2
2
|
import { futuresGenericCaption, futuresGenericListing } from "../../../utils/futures-generic";
|
|
3
3
|
import type { BacktestHistory } from "./client";
|
|
4
4
|
import { runBacktest, type BacktestOptions, type BacktestResult } from "./engine";
|
|
5
|
+
import { delistingLabel } from "../../../market-data/quotes/delisting";
|
|
5
6
|
import type { BacktestBar, Rule } from "./rules";
|
|
6
7
|
|
|
7
8
|
/** Which served sessions a run tests. */
|
|
@@ -39,10 +40,12 @@ export interface HistoryBacktestResult extends BacktestResult {
|
|
|
39
40
|
* and a session still forming, which is left out unless asked for so the
|
|
40
41
|
* newest complete session ends the run and a repeat gives the same answer.
|
|
41
42
|
* An end after the newest complete session is warned too: sessions still to
|
|
42
|
-
* come up to it change the run until it has passed.
|
|
43
|
+
* come up to it change the run until it has passed. A listing that no longer
|
|
44
|
+
* trades has none to come: a run that reaches its last session ends there,
|
|
45
|
+
* says so, and is repeatable.
|
|
43
46
|
*/
|
|
44
47
|
export function backtestWindow(
|
|
45
|
-
history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped"
|
|
48
|
+
history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped" | "delisting"> & { symbol?: string },
|
|
46
49
|
options: BacktestWindowOptions = {},
|
|
47
50
|
): { bars: BacktestBar[]; warnings: string[]; repeatable: boolean } {
|
|
48
51
|
const end = options.end?.trim() || null;
|
|
@@ -70,6 +73,16 @@ export function backtestWindow(
|
|
|
70
73
|
+ `${through ? `: the test runs through ${through}, the last complete session` : ""}.`);
|
|
71
74
|
}
|
|
72
75
|
const newestComplete = history.bars.findLast((bar) => !bar.partial)?.date;
|
|
76
|
+
const delisting = history.delisting;
|
|
77
|
+
if (delisting) {
|
|
78
|
+
const listing = `${history.symbol ? `${parsePublicTickerKey(history.symbol).symbol}: ` : ""}${delistingLabel(delisting)}`;
|
|
79
|
+
if (through && through >= delisting.lastTradeDate) {
|
|
80
|
+
warnings.push(`${listing}. The run ends on its last session, ${through}, and a position still held`
|
|
81
|
+
+ " is valued at that close, not at anything holders received after it.");
|
|
82
|
+
return { bars, warnings, repeatable: true };
|
|
83
|
+
}
|
|
84
|
+
warnings.push(`${listing}.`);
|
|
85
|
+
}
|
|
73
86
|
const repeatable = !!end && !!newestComplete && end <= newestComplete;
|
|
74
87
|
if (end && !repeatable && through) {
|
|
75
88
|
warnings.push(`The end date ${end} is after the newest complete session, so the run ends at ${through}`
|
|
@@ -80,7 +93,7 @@ export function backtestWindow(
|
|
|
80
93
|
|
|
81
94
|
/** The run over a loaded history: its window's bars, then the engine, every left-out session warned first. */
|
|
82
95
|
export function runHistoryBacktest(
|
|
83
|
-
history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped"
|
|
96
|
+
history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped" | "delisting"> & { symbol?: string },
|
|
84
97
|
entry: Rule,
|
|
85
98
|
exit: Rule,
|
|
86
99
|
options: BacktestOptions & BacktestWindowOptions,
|