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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scheduleConfigSave } from "../state/config-save-scheduler";
|
|
2
|
+
import { normalizeStoredBaseCurrency } from "../utils/base-currency";
|
|
2
3
|
import { debugLog } from "../utils/debug-log";
|
|
3
4
|
import { fnv1aHex, stableStringify } from "../utils/hash";
|
|
4
5
|
import type { AppConfig, BrokerInstanceConfig, SavedLayout } from "../types/config";
|
|
@@ -39,6 +40,16 @@ import {
|
|
|
39
40
|
knownBrokerHoldings,
|
|
40
41
|
type KnownBrokerHoldings,
|
|
41
42
|
} from "../brokers/broker-portfolio-sync";
|
|
43
|
+
import {
|
|
44
|
+
isLocalOnlyInstance,
|
|
45
|
+
isLocalOnlyPortfolio,
|
|
46
|
+
localOnlyScope,
|
|
47
|
+
publicConfig,
|
|
48
|
+
publicMarketData,
|
|
49
|
+
publicTickers,
|
|
50
|
+
withLocalOnlyEntries,
|
|
51
|
+
type LocalOnlyScope,
|
|
52
|
+
} from "./local-only";
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
55
|
* What a saved layout mirrors from the live session rather than from the
|
|
@@ -264,6 +275,7 @@ function sanitizePosition(position: TickerPosition): TickerPosition {
|
|
|
264
275
|
avgCost: position.avgCost,
|
|
265
276
|
currency: position.currency,
|
|
266
277
|
dateAcquired: position.dateAcquired,
|
|
278
|
+
...(position.costFxRate != null && position.costFxCurrency ? { costFxRate: position.costFxRate, costFxCurrency: position.costFxCurrency } : {}),
|
|
267
279
|
broker: position.broker,
|
|
268
280
|
side: position.side,
|
|
269
281
|
marketValue: position.marketValue,
|
|
@@ -611,9 +623,11 @@ function collectCoreCollectionsPayload(
|
|
|
611
623
|
financials: Map<string, TickerFinancials>,
|
|
612
624
|
exchangeRates: Map<string, number>,
|
|
613
625
|
brokerAccounts: Record<string, BrokerAccount[]>,
|
|
626
|
+
/** The financials whose quotes may be uploaded: without those a source that keeps its market data local supplied. */
|
|
627
|
+
quoted: Map<string, TickerFinancials> = financials,
|
|
614
628
|
) {
|
|
615
629
|
const records = [...tickers.values()]
|
|
616
|
-
.map((ticker) => sanitizeTickerMetadata(ticker.metadata,
|
|
630
|
+
.map((ticker) => sanitizeTickerMetadata(ticker.metadata, quoted.get(ticker.metadata.ticker)))
|
|
617
631
|
.filter(isSyncableTicker);
|
|
618
632
|
|
|
619
633
|
// Only the USD anchor goes on the wire, as before: readers of the snapshot
|
|
@@ -872,14 +886,38 @@ interface LayoutMergeContext {
|
|
|
872
886
|
pushedAt?: string;
|
|
873
887
|
}
|
|
874
888
|
|
|
889
|
+
/** A pulled config without profiles and portfolios that are local-only here (an older build may have pushed them). */
|
|
890
|
+
function publicConfigPayload(payload: Record<string, unknown>, scope: LocalOnlyScope): Record<string, unknown> {
|
|
891
|
+
const without = (entries: unknown, isLocal: (entry: Record<string, unknown>) => boolean) => (
|
|
892
|
+
Array.isArray(entries) ? entries.filter((entry) => !isRecord(entry) || !isLocal(entry)) : entries
|
|
893
|
+
);
|
|
894
|
+
return {
|
|
895
|
+
...payload,
|
|
896
|
+
...("brokerInstances" in payload ? { brokerInstances: without(payload.brokerInstances, (entry) => isLocalOnlyInstance({ id: String(entry.id) }, scope)) } : {}),
|
|
897
|
+
...("portfolios" in payload ? { portfolios: without(payload.portfolios, (entry) => isLocalOnlyPortfolio(entry as unknown as Portfolio, scope)) } : {}),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
875
901
|
function mergeConfigPayload(
|
|
876
902
|
config: AppConfig,
|
|
877
903
|
payload: unknown,
|
|
878
904
|
baselineConfig: AppConfig = config,
|
|
879
905
|
lastSyncedPayload?: unknown,
|
|
880
906
|
layoutContext: LayoutMergeContext = {},
|
|
907
|
+
scope: LocalOnlyScope | null = null,
|
|
881
908
|
): AppConfig | null {
|
|
882
909
|
if (!isRecord(payload)) return null;
|
|
910
|
+
if (scope) {
|
|
911
|
+
// Merged as if the local-only profiles and portfolios were not here, then put back as they were.
|
|
912
|
+
const local = publicConfig(config, scope);
|
|
913
|
+
const merged = mergeConfigPayload(local, publicConfigPayload(payload, scope), baselineConfig === config ? local : publicConfig(baselineConfig, scope),
|
|
914
|
+
lastSyncedPayload, layoutContext);
|
|
915
|
+
return merged && {
|
|
916
|
+
...merged,
|
|
917
|
+
brokerInstances: withLocalOnlyEntries(merged.brokerInstances, config.brokerInstances, (instance) => isLocalOnlyInstance(instance, scope)),
|
|
918
|
+
portfolios: withLocalOnlyEntries(merged.portfolios, config.portfolios, (portfolio) => isLocalOnlyPortfolio(portfolio, scope)),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
883
921
|
const next: AppConfig = { ...config };
|
|
884
922
|
// Two guards, both needed. baselineConfig catches edits made while this pull
|
|
885
923
|
// was in flight; lastSyncedPayload catches edits made while the app was not
|
|
@@ -899,6 +937,8 @@ function mergeConfigPayload(
|
|
|
899
937
|
};
|
|
900
938
|
|
|
901
939
|
assign("baseCurrency");
|
|
940
|
+
// A device on an older version may still send a name or sign (Canadian dollar, CA$): it applies as its code, as a saved one loads.
|
|
941
|
+
if (typeof next.baseCurrency === "string" && next.baseCurrency.trim()) next.baseCurrency = normalizeStoredBaseCurrency(next.baseCurrency);
|
|
902
942
|
assign("refreshIntervalMinutes");
|
|
903
943
|
if ("portfolios" in payload && canApply("portfolios")) {
|
|
904
944
|
const syncedPortfolios = payload.portfolios;
|
|
@@ -1147,6 +1187,8 @@ interface PulledTickersMerge {
|
|
|
1147
1187
|
/** This device's live broker portfolios, each with the side holding its latest broker sync. */
|
|
1148
1188
|
brokerPortfolios: ReadonlyMap<string, "local" | "pulled">;
|
|
1149
1189
|
known: KnownBrokerHoldings;
|
|
1190
|
+
/** Connections kept on this device: their portfolios' holdings here outlast any pull. */
|
|
1191
|
+
localOnly: LocalOnlyScope | null;
|
|
1150
1192
|
}
|
|
1151
1193
|
|
|
1152
1194
|
/**
|
|
@@ -1223,7 +1265,7 @@ function mergePulledTicker(
|
|
|
1223
1265
|
const portfolios: string[] = [];
|
|
1224
1266
|
for (const portfolioId of portfolioIds) {
|
|
1225
1267
|
const brokerSide = merge.brokerPortfolios.get(portfolioId);
|
|
1226
|
-
const takePulled = brokerSide
|
|
1268
|
+
const takePulled = merge.localOnly?.portfolioIds.has(portfolioId) ? false : brokerSide
|
|
1227
1269
|
? brokerSide === "pulled"
|
|
1228
1270
|
: !comparable || sameAsStored(
|
|
1229
1271
|
holdingIn(localPositions, localLinks, portfolioId),
|
|
@@ -1268,7 +1310,7 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1268
1310
|
id: CORE_CONFIG_CONTRIBUTOR_ID,
|
|
1269
1311
|
schemaVersion: 1,
|
|
1270
1312
|
collect: ({ state, baselinePayload, memo }) => withLayoutProvenance(
|
|
1271
|
-
collectCoreConfigPayload(state.config) as Record<string, unknown>,
|
|
1313
|
+
collectCoreConfigPayload(publicConfig(state.config, localOnlyScope(state.config))) as Record<string, unknown>,
|
|
1272
1314
|
baselinePayload,
|
|
1273
1315
|
memo?.get(),
|
|
1274
1316
|
),
|
|
@@ -1281,7 +1323,7 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1281
1323
|
const nextConfig = mergeConfigPayload(state.config, payload, baselineState.config, baselinePayload, {
|
|
1282
1324
|
memo: memo?.get(),
|
|
1283
1325
|
pushedAt,
|
|
1284
|
-
});
|
|
1326
|
+
}, localOnlyScope(state.config));
|
|
1285
1327
|
if (!nextConfig) return;
|
|
1286
1328
|
const configChanged = !valuesEqual(nextConfig, state.config);
|
|
1287
1329
|
if (configChanged) {
|
|
@@ -1327,15 +1369,24 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1327
1369
|
export const coreCollectionsSyncContributor: SyncContributor = {
|
|
1328
1370
|
id: "core.collections",
|
|
1329
1371
|
schemaVersion: 1,
|
|
1330
|
-
collect: async ({ state }) =>
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
state.
|
|
1334
|
-
|
|
1335
|
-
state.
|
|
1336
|
-
|
|
1372
|
+
collect: async ({ state, isLocalMarketData }) => {
|
|
1373
|
+
// Connections the user keeps on this device stay here, with their portfolios, account values and
|
|
1374
|
+
// positions; so do the quotes and bars of a source that keeps its market data local.
|
|
1375
|
+
const scope = localOnlyScope(state.config);
|
|
1376
|
+
const config = publicConfig(state.config, scope);
|
|
1377
|
+
const tickers = publicTickers(state.tickers, scope);
|
|
1378
|
+
return collectCoreCollectionsPayload(
|
|
1379
|
+
config,
|
|
1380
|
+
tickers,
|
|
1381
|
+
state.financials,
|
|
1382
|
+
await loadHoldingExchangeRates(config, tickers, state.financials),
|
|
1383
|
+
state.brokerAccounts,
|
|
1384
|
+
publicMarketData(state.financials, isLocalMarketData),
|
|
1385
|
+
);
|
|
1386
|
+
},
|
|
1337
1387
|
apply: async (payload, { snapshot, baselinePayload, baselineState, getState, isCurrent, dispatch, tickerRepository }) => {
|
|
1338
1388
|
if (!isRecord(payload)) return;
|
|
1389
|
+
const scope = localOnlyScope(getState().config);
|
|
1339
1390
|
hydrateProfileAnalytics(payload);
|
|
1340
1391
|
if (!isCurrent()) return;
|
|
1341
1392
|
// The config contributor applies first, so this is the config this pull left.
|
|
@@ -1355,6 +1406,7 @@ export const coreCollectionsSyncContributor: SyncContributor = {
|
|
|
1355
1406
|
portfolios: [pulledPortfolios, pulledConfig?.portfolios].flatMap(idEntries),
|
|
1356
1407
|
brokerInstances: idEntries(pulledConfig?.brokerInstances),
|
|
1357
1408
|
}),
|
|
1409
|
+
localOnly: scope,
|
|
1358
1410
|
};
|
|
1359
1411
|
// Tickers here and pulled ones drop the ids of any watchlist that is gone
|
|
1360
1412
|
// (removed elsewhere, or deleted by an older build that left its id on the
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AppConfig, BrokerInstanceConfig } from "../types/config";
|
|
2
|
+
import type { Quote, TickerFinancials } from "../types/financials";
|
|
3
|
+
import type { BrokerContractRef } from "../types/instrument";
|
|
4
|
+
import type { Portfolio, TickerPosition, TickerRecord } from "../types/ticker";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Broker connections the user keeps on this device (`BrokerInstanceConfig.localOnly`), and the
|
|
8
|
+
* portfolios of their accounts. Cloud sync leaves the connections, those portfolios with their
|
|
9
|
+
* account values, and the positions held in them out of every push, and keeps them through every
|
|
10
|
+
* pull, so nothing of them reaches Gloom Cloud and a pull never removes them here.
|
|
11
|
+
*/
|
|
12
|
+
export interface LocalOnlyScope {
|
|
13
|
+
instanceIds: ReadonlySet<string>;
|
|
14
|
+
portfolioIds: ReadonlySet<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a source keeps the quotes and bars it supplies on this device (`BrokerAdapter.localMarketData`),
|
|
19
|
+
* by its provider id: a broker's is its broker id.
|
|
20
|
+
*/
|
|
21
|
+
export type LocalMarketDataLookup = (providerId: string) => boolean;
|
|
22
|
+
|
|
23
|
+
/** The connections the user keeps on this device, or null when there are none. */
|
|
24
|
+
export function localOnlyScope(config: Pick<AppConfig, "brokerInstances" | "portfolios">): LocalOnlyScope | null {
|
|
25
|
+
const instanceIds = new Set(config.brokerInstances.flatMap((instance) => instance.localOnly === true ? [instance.id] : []));
|
|
26
|
+
if (instanceIds.size === 0) return null;
|
|
27
|
+
const scope = { instanceIds, portfolioIds: new Set<string>() };
|
|
28
|
+
return { instanceIds, portfolioIds: new Set(config.portfolios.flatMap((portfolio) => isLocalOnlyPortfolio(portfolio, scope) ? [portfolio.id] : [])) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isLocalOnlyInstance(instance: Pick<BrokerInstanceConfig, "id">, scope: LocalOnlyScope): boolean {
|
|
32
|
+
return scope.instanceIds.has(instance.id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isLocalOnlyPortfolio(portfolio: Pick<Portfolio, "id" | "brokerInstanceId">, scope: LocalOnlyScope): boolean {
|
|
36
|
+
return scope.portfolioIds.has(portfolio.id) || !!portfolio.brokerInstanceId && scope.instanceIds.has(portfolio.brokerInstanceId)
|
|
37
|
+
// A pulled portfolio has no profile id; its own id names the profile.
|
|
38
|
+
|| [...scope.instanceIds].some((id) => portfolio.id.startsWith(`broker:${id}:`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isLocalOnlyPosition(position: Pick<TickerPosition, "portfolio" | "brokerInstanceId">, scope: LocalOnlyScope): boolean {
|
|
42
|
+
return scope.portfolioIds.has(position.portfolio) || !!position.brokerInstanceId && scope.instanceIds.has(position.brokerInstanceId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isLocalOnlyContract(contract: Pick<BrokerContractRef, "brokerInstanceId">, scope: LocalOnlyScope): boolean {
|
|
46
|
+
return !!contract.brokerInstanceId && scope.instanceIds.has(contract.brokerInstanceId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The config as Cloud sync may see it: without the connections kept on this device and their portfolios. */
|
|
50
|
+
export function publicConfig<T extends Pick<AppConfig, "brokerInstances" | "portfolios">>(config: T, scope: LocalOnlyScope | null): T {
|
|
51
|
+
if (!scope) return config;
|
|
52
|
+
const brokerInstances = config.brokerInstances.filter((instance) => !isLocalOnlyInstance(instance, scope));
|
|
53
|
+
const portfolios = config.portfolios.filter((portfolio) => !isLocalOnlyPortfolio(portfolio, scope));
|
|
54
|
+
if (brokerInstances.length === config.brokerInstances.length && portfolios.length === config.portfolios.length) return config;
|
|
55
|
+
return { ...config, brokerInstances, portfolios };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The ticker as Cloud sync may see it: without positions, portfolios or contracts of connections kept on this device. */
|
|
59
|
+
function publicTicker(ticker: TickerRecord, scope: LocalOnlyScope | null): TickerRecord {
|
|
60
|
+
if (!scope) return ticker;
|
|
61
|
+
const { metadata } = ticker;
|
|
62
|
+
const positions = metadata.positions.filter((position) => !isLocalOnlyPosition(position, scope));
|
|
63
|
+
const portfolios = metadata.portfolios.filter((id) => !scope.portfolioIds.has(id));
|
|
64
|
+
const contracts = metadata.broker_contracts?.filter((contract) => !isLocalOnlyContract(contract, scope));
|
|
65
|
+
if (positions.length === metadata.positions.length && portfolios.length === metadata.portfolios.length
|
|
66
|
+
&& contracts?.length === metadata.broker_contracts?.length) return ticker;
|
|
67
|
+
return { ...ticker, metadata: { ...metadata, positions, portfolios, ...(contracts ? { broker_contracts: contracts } : {}) } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function publicTickers(tickers: Map<string, TickerRecord>, scope: LocalOnlyScope | null): Map<string, TickerRecord> {
|
|
71
|
+
if (!scope) return tickers;
|
|
72
|
+
return new Map([...tickers].map(([symbol, ticker]) => [symbol, publicTicker(ticker, scope)]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The quote fields sync uploads, any of which a source that keeps its market data local may have supplied. */
|
|
76
|
+
const SYNCED_QUOTE_FIELDS = ["price", "change", "changePercent", "previousClose"] as const;
|
|
77
|
+
|
|
78
|
+
function isLocalQuote(quote: Quote, isLocal: LocalMarketDataLookup): boolean {
|
|
79
|
+
const provenance = quote.provenance;
|
|
80
|
+
const providers = [quote.providerId, provenance?.price?.providerId, ...SYNCED_QUOTE_FIELDS.map((field) => provenance?.fields?.[field]?.providerId)];
|
|
81
|
+
return providers.some((providerId) => !!providerId && isLocal(providerId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The provider of stored bars, from their source key: `provider:<id>` or `broker:<broker id>:<profile>`. */
|
|
85
|
+
function historyProvider(sourceKey: string | undefined): string | null {
|
|
86
|
+
const [kind, id] = sourceKey?.split(":") ?? [];
|
|
87
|
+
return (kind === "provider" || kind === "broker") && id ? id : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Financials as Cloud sync may upload them: without a quote or bars a source that keeps its market
|
|
92
|
+
* data local supplied. Sync reads only the quote and the week's move from them.
|
|
93
|
+
*/
|
|
94
|
+
export function publicMarketData(financials: Map<string, TickerFinancials>, isLocal?: LocalMarketDataLookup): Map<string, TickerFinancials> {
|
|
95
|
+
if (!isLocal) return financials;
|
|
96
|
+
const output = new Map<string, TickerFinancials>();
|
|
97
|
+
for (const [symbol, entry] of financials) {
|
|
98
|
+
const localQuote = !!entry.quote && isLocalQuote(entry.quote, isLocal);
|
|
99
|
+
const provider = historyProvider(entry.priceHistorySourceKey);
|
|
100
|
+
const localBars = !!provider && isLocal(provider);
|
|
101
|
+
output.set(symbol, localQuote || localBars ? { ...entry, ...(localQuote ? { quote: undefined } : {}), ...(localBars ? { priceHistory: [] } : {}) } : entry);
|
|
102
|
+
}
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `merged` (a list merged without the local-only entries) with this device's local-only entries
|
|
108
|
+
* of `local` back where they were: each stays at its place among the others, so an unchanged list
|
|
109
|
+
* comes back equal to `local`. An entry the pull brought that is local-only here is left out.
|
|
110
|
+
*/
|
|
111
|
+
export function withLocalOnlyEntries<T>(merged: readonly T[], local: readonly T[], isLocalOnly: (entry: T) => boolean): T[] {
|
|
112
|
+
const incoming = merged.filter((entry) => !isLocalOnly(entry));
|
|
113
|
+
const output: T[] = [];
|
|
114
|
+
let next = 0;
|
|
115
|
+
for (const entry of local) {
|
|
116
|
+
if (isLocalOnly(entry)) output.push(entry);
|
|
117
|
+
else if (next < incoming.length) output.push(incoming[next++]!);
|
|
118
|
+
}
|
|
119
|
+
output.push(...incoming.slice(next));
|
|
120
|
+
return output;
|
|
121
|
+
}
|
package/src/sync/react.ts
CHANGED
|
@@ -41,6 +41,12 @@ export function useCloudSyncRuntime({
|
|
|
41
41
|
baselineStore,
|
|
42
42
|
getContributors: () => pluginRegistry.getEnabledSyncContributors(),
|
|
43
43
|
getTransport: () => pluginRegistry.getActiveSyncTransport(),
|
|
44
|
+
isLocalMarketData: (providerId) => {
|
|
45
|
+
const broker = pluginRegistry.brokers.get(providerId);
|
|
46
|
+
if (broker) return broker.localMarketData === true;
|
|
47
|
+
// A broker whose plugin is off may still have its quotes about from before; they stay here too.
|
|
48
|
+
return getState().config.brokerInstances.some((instance) => instance.brokerType === providerId);
|
|
49
|
+
},
|
|
44
50
|
});
|
|
45
51
|
}, [baselineStore, dispatch, getState, pluginRegistry, tickerRepository]);
|
|
46
52
|
|
package/src/sync/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Dispatch } from "react";
|
|
2
2
|
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
3
|
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
4
|
+
import type { LocalMarketDataLookup } from "./local-only";
|
|
4
5
|
|
|
5
6
|
export const SYNC_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
6
7
|
|
|
@@ -31,6 +32,8 @@ interface SyncContributorMemo {
|
|
|
31
32
|
|
|
32
33
|
interface SyncCollectContext {
|
|
33
34
|
state: AppState;
|
|
35
|
+
/** Sources whose quotes and bars never leave this device (`BrokerAdapter.localMarketData`); none when absent. */
|
|
36
|
+
isLocalMarketData?: LocalMarketDataLookup;
|
|
34
37
|
/** The payload this device last pushed for this contributor, or null when it never synced. */
|
|
35
38
|
baselinePayload?: unknown;
|
|
36
39
|
memo?: SyncContributorMemo;
|
package/src/team/collections.ts
CHANGED
|
@@ -174,6 +174,8 @@ export function applyMembership(
|
|
|
174
174
|
currency: position?.currency ?? currency,
|
|
175
175
|
broker: "manual",
|
|
176
176
|
...(!flipped && position?.dateAcquired ? { dateAcquired: position.dateAcquired } : {}),
|
|
177
|
+
// So does the rate its cost was bought at, with its cost.
|
|
178
|
+
...(!flipped && position?.costFxRate != null && position.costFxCurrency ? { costFxRate: position.costFxRate, costFxCurrency: position.costFxCurrency } : {}),
|
|
177
179
|
// A future's contract size stays with the holding when the team changes its count.
|
|
178
180
|
...(position?.multiplier ? { multiplier: position.multiplier } : {}),
|
|
179
181
|
});
|
|
@@ -19,6 +19,7 @@ import { isQuoteBeforeLatestSession, isQuoteStaleForCurrentSession } from "../ma
|
|
|
19
19
|
import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
|
|
20
20
|
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey, resolveExchangeTimeZone } from "../utils/exchanges";
|
|
21
21
|
import { zonedDateKey } from "../utils/zoned-date-time";
|
|
22
|
+
import { delistingLabel } from "../market-data/quotes/delisting";
|
|
22
23
|
|
|
23
24
|
/*
|
|
24
25
|
* What became of a symbol, read the way `quote` reads it: the listing's own
|
|
@@ -33,8 +34,10 @@ const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
|
|
|
33
34
|
/**
|
|
34
35
|
* The exchange-local date a listing last traded, when its quote is stale and
|
|
35
36
|
* that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
|
|
37
|
+
* A quote that says its listing no longer trades gives its last session.
|
|
36
38
|
*/
|
|
37
39
|
function endedTradingDate(quote: Quote, now = Date.now()): string | null {
|
|
40
|
+
if (quote.delisting) return quote.delisting.lastTradeDate;
|
|
38
41
|
const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
|
|
39
42
|
if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
|
|
40
43
|
return quoteDateKey(quote, last);
|
|
@@ -60,12 +63,18 @@ export function tradingListing(quote: Quote | null | undefined, now = Date.now()
|
|
|
60
63
|
return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote
|
|
68
|
+
* that stopped long ago; "TWTR: Delisted Oct 27, 2022 (last price $53.86)"
|
|
69
|
+
* when the quote says the listing no longer trades.
|
|
70
|
+
*/
|
|
64
71
|
export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
|
|
65
72
|
const date = endedTradingDate(quote, now);
|
|
66
73
|
if (!date) return null;
|
|
67
74
|
const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
|
|
68
|
-
return
|
|
75
|
+
return quote.delisting
|
|
76
|
+
? `${label}: ${delistingLabel(quote.delisting)} (last price ${price})`
|
|
77
|
+
: `${label}: no longer trading since ${date} (last price ${price})`;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -81,11 +90,17 @@ export interface ListingFate {
|
|
|
81
90
|
/**
|
|
82
91
|
* The line a command with nothing to serve for the symbol says: `quote`'s
|
|
83
92
|
* change line when its company left it ("FB now lists ...; Meta Platforms
|
|
84
|
-
* trades as META (changed 2022-06-09)"), else, for a listing
|
|
85
|
-
* "TWTR
|
|
86
|
-
*
|
|
93
|
+
* trades as META (changed 2022-06-09)"), else, for a listing the service
|
|
94
|
+
* says no longer trades, "TWTR: Delisted Oct 27, 2022", and for another
|
|
95
|
+
* that stopped, "XYZ stopped trading 2022-10-27; no history or options
|
|
96
|
+
* served". Null for a symbol that trades as it always has.
|
|
87
97
|
*/
|
|
88
98
|
note: string | null;
|
|
99
|
+
/**
|
|
100
|
+
* "TWTR: Delisted Oct 27, 2022" when the listing's quote says it no longer
|
|
101
|
+
* trades: its history is served, ending on that session, and says so.
|
|
102
|
+
*/
|
|
103
|
+
delisted?: string;
|
|
89
104
|
/**
|
|
90
105
|
* The change when the symbol's company trades under another symbol now
|
|
91
106
|
* and its own quote shows nothing new listed under it (`movedAwayChange`):
|
|
@@ -156,9 +171,10 @@ export function describeListingFate(
|
|
|
156
171
|
const abroad = !!quote && !!bareListingNote({ symbol, request: { exchange: listing.exchange ?? "" } }, quote);
|
|
157
172
|
const current = tradingListing(quote, now);
|
|
158
173
|
const change = abroad ? null : describeTickerChange(symbol, changes, current);
|
|
159
|
-
const
|
|
174
|
+
const delisted = quote?.delisting && !abroad ? `${symbol}: ${delistingLabel(quote.delisting)}` : undefined;
|
|
175
|
+
const note = tickerChangeLine(change) ?? delisted ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
|
|
160
176
|
const formerIssuer = abroad ? null : describeFormerIssuer(symbol, changes, current);
|
|
161
|
-
return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer };
|
|
177
|
+
return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer, ...(delisted ? { delisted } : {}) };
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
/**
|
|
@@ -200,7 +216,8 @@ export async function loadListingFate(
|
|
|
200
216
|
|
|
201
217
|
/**
|
|
202
218
|
* The lines a command says about a symbol's fate beside what it served. With
|
|
203
|
-
* nothing served, `fate.note`. With data, `
|
|
219
|
+
* nothing served, `fate.note`. With data, `fate.delisted` for a listing that
|
|
220
|
+
* no longer trades ("TWTR: Delisted Oct 27, 2022"), `quote`'s change line when the
|
|
204
221
|
* symbol's company left it (FB's bars are the fund listed there now), and, for
|
|
205
222
|
* bars from before the symbol's holder took it (META's from 2021), that they
|
|
206
223
|
* are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
|
|
@@ -211,6 +228,8 @@ export function listingFateNotes(fate: ListingFate, served: { served: boolean; f
|
|
|
211
228
|
const first = served.firstDate?.slice(0, 10);
|
|
212
229
|
const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
|
|
213
230
|
return [
|
|
231
|
+
// What it served ends on the listing's last session, and says so.
|
|
232
|
+
fate.delisted ?? null,
|
|
214
233
|
tickerChangeLine(change),
|
|
215
234
|
earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
|
|
216
235
|
].filter((line): line is string => !!line);
|
package/src/tickers/metadata.ts
CHANGED
|
@@ -9,6 +9,15 @@ const DEFAULT_TICKER_METADATA: Omit<TickerMetadata, "ticker" | "exchange" | "cur
|
|
|
9
9
|
tags: [],
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** A lot's cost FX rate, kept only whole: a positive rate and the currency it converts into. */
|
|
13
|
+
function hydrateCostFx(raw: Record<string, unknown>): Pick<TickerPosition, "costFxRate" | "costFxCurrency"> {
|
|
14
|
+
const rate = raw.costFxRate ?? raw.cost_fx_rate;
|
|
15
|
+
const currency = raw.costFxCurrency ?? raw.cost_fx_currency;
|
|
16
|
+
return typeof rate === "number" && Number.isFinite(rate) && rate > 0 && typeof currency === "string" && currency.trim()
|
|
17
|
+
? { costFxRate: rate, costFxCurrency: currency.trim() }
|
|
18
|
+
: {};
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function hydratePosition(raw: Record<string, unknown>): TickerPosition {
|
|
13
22
|
const avgCost = Object.hasOwn(raw, "avgCost") ? raw.avgCost : raw.avg_cost;
|
|
14
23
|
return {
|
|
@@ -18,6 +27,7 @@ function hydratePosition(raw: Record<string, unknown>): TickerPosition {
|
|
|
18
27
|
priceBasis: raw.priceBasis === "per-unit" || raw.priceBasis === "percent-of-par" ? raw.priceBasis : undefined,
|
|
19
28
|
currency: (raw.currency as string) ?? undefined,
|
|
20
29
|
dateAcquired: (raw.dateAcquired ?? raw.date_acquired) as string | undefined,
|
|
30
|
+
...hydrateCostFx(raw),
|
|
21
31
|
broker: (raw.broker as string) ?? "manual",
|
|
22
32
|
side: (raw.side as "long" | "short") ?? undefined,
|
|
23
33
|
marketValue: (raw.marketValue ?? raw.market_value) as number | undefined,
|
|
@@ -38,6 +38,7 @@ import { readTerminalListingCode } from "../terminal-codes";
|
|
|
38
38
|
import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
|
|
39
39
|
import { settleWithin } from "../../utils/async-deadline";
|
|
40
40
|
import { companyNameExtraWords } from "../company-name";
|
|
41
|
+
import { delistingLabel } from "../../market-data/quotes/delisting";
|
|
41
42
|
export type {
|
|
42
43
|
ResolvedTickerSearch,
|
|
43
44
|
TickerSearchCandidate,
|
|
@@ -58,6 +59,17 @@ const MAX_NAMED_LISTINGS = 6;
|
|
|
58
59
|
*/
|
|
59
60
|
export const LISTING_SEARCH_LIMIT = 30;
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A listing's name as a choice between listings gives it: a listing that no
|
|
64
|
+
* longer trades says so and when ("First Republic Bank · Last traded Apr 28,
|
|
65
|
+
* 2023"), and drops a name that only repeats its symbol.
|
|
66
|
+
*/
|
|
67
|
+
function listingChoiceName(result: InstrumentSearchResult): string {
|
|
68
|
+
if (!result.delisting) return result.name;
|
|
69
|
+
const name = result.name && normalizeTickerSymbol(result.name) !== normalizeTickerSymbol(result.symbol) ? result.name : "";
|
|
70
|
+
return [name, delistingLabel(result.delisting)].filter(Boolean).join(" · ");
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export class AmbiguousTickerError extends Error {
|
|
62
74
|
constructor(readonly query: string, readonly listings: readonly string[], readonly listingNames: Readonly<Record<string, string>> = {}) {
|
|
63
75
|
// Every listing is a choice: a list cut short could leave out the one meant.
|
|
@@ -467,7 +479,7 @@ async function resolveTickerSymbol(
|
|
|
467
479
|
}
|
|
468
480
|
const listings = new Set(matches.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
|
|
469
481
|
const ambiguity = (query: string) => new AmbiguousTickerError(query, [...listings], Object.fromEntries(matches.map((item) => [
|
|
470
|
-
publicTickerKey(item.symbol, listingExchange(item.result!)), item.result
|
|
482
|
+
publicTickerKey(item.symbol, listingExchange(item.result!)), listingChoiceName(item.result!),
|
|
471
483
|
])));
|
|
472
484
|
if (pickerQuery && listings.size > 1) throw ambiguity(pickerQuery);
|
|
473
485
|
// A code that names several venues (TSE) the symbol trades on more than one of is a choice.
|
|
@@ -485,7 +497,12 @@ async function resolveTickerSymbol(
|
|
|
485
497
|
});
|
|
486
498
|
const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
487
499
|
const currency = resolveCurrencyUnit(quote.currency).currency;
|
|
488
|
-
|
|
500
|
+
const ended = quote.delisting;
|
|
501
|
+
if (ended) {
|
|
502
|
+
// The symbol's own quote is a US listing that no longer trades (a bare FRC, whose
|
|
503
|
+
// venue is not on record): it is the run search offers with the same last session.
|
|
504
|
+
verified = matches.filter((item) => item.result?.delisting?.lastTradeDate === ended.lastTradeDate);
|
|
505
|
+
} else if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
|
|
489
506
|
&& Number.isFinite(quote.lastUpdated) && quote.lastUpdated > 0) {
|
|
490
507
|
const sameSecurity = matches.filter((item) => {
|
|
491
508
|
const result = item.result!;
|
|
@@ -663,7 +680,11 @@ async function searchProviderResults(
|
|
|
663
680
|
|
|
664
681
|
// A sector word ("defense", which the server answers with rows tagged with it) lists its small companies by
|
|
665
682
|
// name; the server orders "defense ETF" by size, which a name search does not. Those funds lead the list.
|
|
666
|
-
|
|
683
|
+
// So do they when the funds are asked for by name: "defense etf" searches "defense", whose answer holds
|
|
684
|
+
// only the few funds tagged with it (not ITA), and "gold miners etf" a phrase no symbol is.
|
|
685
|
+
const fundsAsked = assetClass?.code === "ETF" || assetClass?.code === "FUND";
|
|
686
|
+
const theme = withThemeFunds && (!assetClass || fundsAsked)
|
|
687
|
+
&& (themeQueryWord([...byKey.values()], query) || (fundsAsked && /\s/.test(query.trim())));
|
|
667
688
|
if (theme) {
|
|
668
689
|
try {
|
|
669
690
|
const { onPartial: _ignored, ...context } = searchContext ?? {};
|
|
@@ -123,6 +123,41 @@ export function resolvePriceComparison(
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The bars a compared leg pans and zooms over: inside the view only its shared
|
|
128
|
+
* dates, as the chart shows them, and every loaded bar past either edge of the
|
|
129
|
+
* view. Clipping the whole buffer to the view left a pan or a zoom out nothing
|
|
130
|
+
* to move into until the next fetch, and none at all toward newer bars.
|
|
131
|
+
*/
|
|
132
|
+
export function comparedBufferPoints(
|
|
133
|
+
points: readonly TimeSeriesPoint[],
|
|
134
|
+
shared: { start: number | null; end: number | null },
|
|
135
|
+
view: { start: number | null; end: number | null },
|
|
136
|
+
): TimeSeriesPoint[] {
|
|
137
|
+
const { start, end } = shared;
|
|
138
|
+
if (start === null || end === null) return [];
|
|
139
|
+
const before = view.start === null ? Number.NEGATIVE_INFINITY : Math.min(start, view.start);
|
|
140
|
+
const after = view.end === null ? Number.POSITIVE_INFINITY : Math.max(end, view.end);
|
|
141
|
+
return points.filter((point) => {
|
|
142
|
+
const time = point.date.getTime();
|
|
143
|
+
return time < before || time > after || (time >= start && time <= end);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A compared leg's pan buffer around `view`; see comparedBufferPoints. */
|
|
148
|
+
export function bufferPriceComparison(
|
|
149
|
+
series: ResolvedSeries,
|
|
150
|
+
comparison: PriceComparison | null,
|
|
151
|
+
view: { start: number | null; end: number | null },
|
|
152
|
+
): ResolvedSeries {
|
|
153
|
+
if (!comparison?.seriesIds.includes(series.id)) return series;
|
|
154
|
+
return {
|
|
155
|
+
...series,
|
|
156
|
+
latestChangePercent: undefined,
|
|
157
|
+
points: comparedBufferPoints(series.points, priceComparisonBoundsForSeries(series, comparison)!, view),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
/** Clip only compared legs; raw observations and study inputs stay available in the source cache. */
|
|
127
162
|
export function clipPriceComparison(series: ResolvedSeries, comparison: PriceComparison | null): ResolvedSeries {
|
|
128
163
|
if (!comparison?.seriesIds.includes(series.id)) return series;
|