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
|
@@ -59,6 +59,9 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
59
59
|
const lastSavedTextRef = useRef<Map<string, string>>(new Map());
|
|
60
60
|
const loadedTabIdRef = useRef<string | null>(null);
|
|
61
61
|
const loadedRef = useRef(false);
|
|
62
|
+
// Owners whose index exists but could not be read: nothing is saved over it.
|
|
63
|
+
const unreadableIndexRef = useRef(new Set<string>());
|
|
64
|
+
const [indexError, setIndexError] = useState<{ message: string; owners: NoteOwner[] } | null>(null);
|
|
62
65
|
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
|
63
66
|
|
|
64
67
|
const saveQuickNotesIndex = useCallback((entries: OwnedQuickNote[]) => {
|
|
@@ -73,7 +76,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
73
76
|
for (const owner of owners) {
|
|
74
77
|
const bucket = byOwner.get(noteOwnerKey(owner));
|
|
75
78
|
const store = storeFor(owner);
|
|
76
|
-
if (store.readOnly) continue;
|
|
79
|
+
if (store.readOnly || unreadableIndexRef.current.has(noteOwnerKey(owner))) continue;
|
|
77
80
|
store.saveQuickNotesIndex(bucket?.entries ?? []).catch((error) => {
|
|
78
81
|
notesLog.error("Failed to save notes index", { error: error instanceof Error ? error.message : String(error) });
|
|
79
82
|
notify({ body: error instanceof Error ? error.message : "Failed to save notes index.", type: "error" });
|
|
@@ -143,17 +146,24 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
143
146
|
useEffect(() => {
|
|
144
147
|
// Mine first, then each team's notes; reloads when the team list changes.
|
|
145
148
|
let cancelled = false;
|
|
149
|
+
const unreadable = new Map<string, { owner: NoteOwner; message: string }>();
|
|
146
150
|
void Promise.all(owners.map(async (owner) => {
|
|
147
151
|
try {
|
|
148
|
-
return (await storeFor(owner).loadQuickNotesIndex()).map((entry) => ({ ...entry, owner }));
|
|
152
|
+
return (await storeFor(owner).loadQuickNotesIndex({ strict: true })).map((entry) => ({ ...entry, owner }));
|
|
149
153
|
} catch (error) {
|
|
154
|
+
unreadable.set(noteOwnerKey(owner), { owner, message: error instanceof Error ? error.message : String(error) });
|
|
150
155
|
return logFailure("notes.quick-notes", "Load a quick notes index failed", [] as OwnedQuickNote[])(error);
|
|
151
156
|
}
|
|
152
157
|
})).then((lists) => {
|
|
153
158
|
if (cancelled) return;
|
|
159
|
+
unreadableIndexRef.current = new Set(unreadable.keys());
|
|
160
|
+
const failed = [...unreadable.values()];
|
|
161
|
+
setIndexError(failed.length > 0 ? { message: failed[0]!.message, owners: failed.map((entry) => entry.owner) } : null);
|
|
154
162
|
const entries = lists.flat();
|
|
155
163
|
if (entries.length === 0) {
|
|
156
|
-
|
|
164
|
+
// Only a first run, with no index yet, gets a first tab; an index
|
|
165
|
+
// that failed to load must not have a fresh one saved over it.
|
|
166
|
+
if (loadedRef.current || unreadable.size > 0) return;
|
|
157
167
|
const id = generateNoteId();
|
|
158
168
|
const initial: OwnedQuickNote[] = [{ id, title: "New", owner: { kind: "user" } }];
|
|
159
169
|
lastSavedTextRef.current.set(id, "");
|
|
@@ -224,6 +234,10 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
224
234
|
notify({ body: `${ownerLabel(owner, teams)} notes are read-only right now.`, type: "info" });
|
|
225
235
|
return;
|
|
226
236
|
}
|
|
237
|
+
if (unreadableIndexRef.current.has(noteOwnerKey(owner))) {
|
|
238
|
+
notify({ body: `${ownerLabel(owner, teams)} quick notes could not be read, so a new one is not added yet.`, type: "info" });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
227
241
|
const id = generateNoteId();
|
|
228
242
|
const entry: OwnedQuickNote = { id, title: "New", owner };
|
|
229
243
|
lastSavedTextRef.current.set(id, "");
|
|
@@ -243,6 +257,14 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
243
257
|
const owner = tabOwner(id, tabsRef.current);
|
|
244
258
|
setTabs((prev) => {
|
|
245
259
|
const next = prev.filter((t) => t.id !== id);
|
|
260
|
+
// With my index unreadable, a fresh tab of mine could not be saved in it.
|
|
261
|
+
if (next.length === 0 && unreadableIndexRef.current.has(noteOwnerKey({ kind: "user" }))) {
|
|
262
|
+
saveQuickNotesIndex(next);
|
|
263
|
+
setActiveTabId(null);
|
|
264
|
+
setNoteText("");
|
|
265
|
+
prevTabRef.current = null;
|
|
266
|
+
return next;
|
|
267
|
+
}
|
|
246
268
|
if (next.length === 0) {
|
|
247
269
|
const newId = generateNoteId();
|
|
248
270
|
const fresh: OwnedQuickNote[] = [{ id: newId, title: "New", owner: { kind: "user" } }];
|
|
@@ -332,6 +354,13 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
332
354
|
if (activeTabId) void requestRemoveTab(activeTabId);
|
|
333
355
|
}, [activeTabId, requestRemoveTab]);
|
|
334
356
|
|
|
357
|
+
// With no tab to show, an index that failed to load is the pane's state;
|
|
358
|
+
// beside other owners' tabs it is a footer warning.
|
|
359
|
+
const indexUnreadable = tabs.length === 0 && !!indexError;
|
|
360
|
+
const readError = loadError ?? (indexUnreadable ? indexError!.message : null);
|
|
361
|
+
const footerWarning = readError
|
|
362
|
+
?? (indexError ? `${indexError.owners.map((owner) => ownerLabel(owner, teams)).join(", ")} quick notes could not be read` : null);
|
|
363
|
+
|
|
335
364
|
// n, t and d are footer hints, which bind them without modifiers, so the
|
|
336
365
|
// shell's Ctrl+W or Cmd+N never adds or deletes a note.
|
|
337
366
|
useShortcut((event) => {
|
|
@@ -351,7 +380,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
351
380
|
}
|
|
352
381
|
|
|
353
382
|
// A note that failed to load stays read-only, as it does for a click.
|
|
354
|
-
if (isPlainKey(event, "enter", "return") && !editing && !
|
|
383
|
+
if (isPlainKey(event, "enter", "return") && !editing && !readError) {
|
|
355
384
|
event.preventDefault();
|
|
356
385
|
setEditing(true);
|
|
357
386
|
return;
|
|
@@ -375,11 +404,11 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
375
404
|
|
|
376
405
|
// Deleting the only note when it is empty just swaps in another empty one,
|
|
377
406
|
// and a note that failed to load reads as empty, so it would go unconfirmed.
|
|
378
|
-
const canDelete = !!activeTabId && !
|
|
407
|
+
const canDelete = !!activeTabId && !readError && (tabs.length > 1 || noteText.trim().length > 0);
|
|
379
408
|
|
|
380
409
|
usePaneFooter("quick-notes", () => ({
|
|
381
|
-
info:
|
|
382
|
-
? [{ id: "load-error", parts: [{ text:
|
|
410
|
+
info: footerWarning
|
|
411
|
+
? [{ id: "load-error", parts: [{ text: footerWarning, tone: "warning" as const }] }]
|
|
383
412
|
: [
|
|
384
413
|
{ id: "edited", parts: [{ text: editing || renaming ? "editing" : formatLastEdited(activeTab?.updatedAt), tone: "muted" as const }] },
|
|
385
414
|
],
|
|
@@ -391,7 +420,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
391
420
|
{ id: "title", key: "t", label: "itle", title: "Rename Note", onPress: startRename, disabled: !activeTabId },
|
|
392
421
|
{ id: "delete", key: "d", label: "elete", title: "Delete Note", onPress: deleteActiveNote, disabled: !canDelete },
|
|
393
422
|
],
|
|
394
|
-
}), [activeTab, activeTabId, addTab, canDelete, deleteActiveNote, editing,
|
|
423
|
+
}), [activeTab, activeTabId, addTab, canDelete, deleteActiveNote, editing, footerWarning, renaming, startRename]);
|
|
395
424
|
|
|
396
425
|
const noteTabs = tabs.map((tab) => ({
|
|
397
426
|
label: tab.owner.kind === "team" ? `${ownerLabel(tab.owner, teams)} ${tab.title}` : tab.title,
|
|
@@ -437,13 +466,19 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
437
466
|
/>
|
|
438
467
|
</Box>
|
|
439
468
|
)}
|
|
440
|
-
<Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!editing && !renaming && !
|
|
469
|
+
<Box flexGrow={1} minHeight={0} paddingX={1} onMouseDown={() => { if (!editing && !renaming && !readError) setEditing(true); }}>
|
|
441
470
|
{loadError ? (
|
|
442
471
|
<EmptyState
|
|
443
472
|
title="This note could not be read."
|
|
444
473
|
message={loadError}
|
|
445
474
|
hint="Editing is disabled so the saved note is not overwritten. Fix the file, then reopen the pane."
|
|
446
475
|
/>
|
|
476
|
+
) : indexUnreadable ? (
|
|
477
|
+
<EmptyState
|
|
478
|
+
title="Quick notes could not be read."
|
|
479
|
+
message={indexError!.message}
|
|
480
|
+
hint="Nothing is saved over them until they load. Reopen the pane to try again."
|
|
481
|
+
/>
|
|
447
482
|
) : editing && !renaming ? (
|
|
448
483
|
<MarkdownEditor
|
|
449
484
|
textareaKey="editing"
|
|
@@ -458,7 +493,7 @@ export function createQuickNotesPane(registry: NotesStoreRegistry) {
|
|
|
458
493
|
text={noteText}
|
|
459
494
|
width={width}
|
|
460
495
|
placeholder="Write notes..."
|
|
461
|
-
onActivate={() => { if (!renaming && !
|
|
496
|
+
onActivate={() => { if (!renaming && !readError) setEditing(true); }}
|
|
462
497
|
/>
|
|
463
498
|
)}
|
|
464
499
|
</Box>
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type NoteKind,
|
|
8
8
|
} from "../../../api-client";
|
|
9
9
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
10
|
-
import type { NoteFileEntry, QuickNoteEntry } from "./files";
|
|
10
|
+
import type { NoteFileEntry, NotesListOptions, QuickNoteEntry } from "./files";
|
|
11
11
|
|
|
12
12
|
export { NoteConflictError };
|
|
13
13
|
|
|
@@ -27,8 +27,18 @@ export interface NotesStore {
|
|
|
27
27
|
load(key: string): Promise<string>;
|
|
28
28
|
save(key: string, text: string, options?: { title?: string | null }): Promise<void>;
|
|
29
29
|
delete(key: string): Promise<void>;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Every note, newest first. A note that cannot be read is left out and
|
|
32
|
+
* handed to `skipped`; with `strict`, notes that cannot be listed at all
|
|
33
|
+
* reject instead of coming back as none.
|
|
34
|
+
*/
|
|
35
|
+
list(options?: NotesListOptions): Promise<NoteFileEntry[]>;
|
|
36
|
+
/**
|
|
37
|
+
* The quick notes' ids and titles. With `strict`, an index that exists but
|
|
38
|
+
* cannot be read rejects instead of reading as empty, for a caller that
|
|
39
|
+
* would otherwise save a replacement over it.
|
|
40
|
+
*/
|
|
41
|
+
loadQuickNotesIndex(options?: { strict?: boolean }): Promise<QuickNoteEntry[]>;
|
|
32
42
|
saveQuickNotesIndex(entries: QuickNoteEntry[]): Promise<void>;
|
|
33
43
|
quickNoteKey(id: string): string;
|
|
34
44
|
/** Called after a server-side change so the next load sees it. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment, PaneHint } from "../../../components";
|
|
3
|
+
import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
|
|
3
4
|
import { t, tf } from "../../../i18n";
|
|
4
5
|
import type { OptionsChain } from "../../../types/financials";
|
|
5
6
|
import { formatRelativeTime } from "../../../utils/datetime-format";
|
|
@@ -9,18 +10,18 @@ import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
|
|
|
9
10
|
import type { OptionQuoteCoverage, OptionQuoteCoverageStatus } from "./live-quotes";
|
|
10
11
|
import { formatOptionPrice, optionSpread, type OptionMarketReference } from "./market-reference";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (status === "live") return { text: t("real-time options"), tone: "positive" };
|
|
20
|
-
if (status === "mixed") return {
|
|
13
|
+
/**
|
|
14
|
+
* Stream coverage the pane reports once the account is entitled to real-time
|
|
15
|
+
* options. Delayed quotes, all or some, are the footer's clock, its detail on
|
|
16
|
+
* hover.
|
|
17
|
+
*/
|
|
18
|
+
export function optionsCoverageSegment(status: OptionQuoteCoverageStatus, delayLabel?: string): PaneFooterSegment | null {
|
|
19
|
+
const id = "options-access";
|
|
20
|
+
if (status === "live") return { id, parts: [{ text: t("real-time options"), tone: "positive" }] };
|
|
21
|
+
if (status === "mixed") return delayedFooterSegment({ id, detail: t("mixed real-time and delayed options") });
|
|
21
22
|
// The stream connects in the background; nothing to report until it answers.
|
|
22
23
|
if (status === "connecting") return null;
|
|
23
|
-
return {
|
|
24
|
+
return delayedFooterSegment({ id, detail: t("Delayed options quotes, real-time stream unavailable"), delay: delayLabel });
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -82,6 +83,7 @@ export function resolveOptionsDelayLabel(chain: OptionsChain | null | undefined)
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export function useOptionsAccessFooter({
|
|
86
|
+
broker = false,
|
|
85
87
|
chain,
|
|
86
88
|
error,
|
|
87
89
|
focused,
|
|
@@ -91,6 +93,8 @@ export function useOptionsAccessFooter({
|
|
|
91
93
|
reference,
|
|
92
94
|
spreadColumnVisible,
|
|
93
95
|
}: {
|
|
96
|
+
/** The chain is a broker profile's, quoted by that broker: it has no Gloom Cloud delay to report or upgrade from. */
|
|
97
|
+
broker?: boolean;
|
|
94
98
|
chain: OptionsChain | null | undefined;
|
|
95
99
|
error?: string | null;
|
|
96
100
|
focused: boolean;
|
|
@@ -102,24 +106,25 @@ export function useOptionsAccessFooter({
|
|
|
102
106
|
/** Set while the chain prints a SPRD column, so the status bar stops repeating it. */
|
|
103
107
|
spreadColumnVisible?: boolean;
|
|
104
108
|
}): void {
|
|
109
|
+
const delayLabel = resolveOptionsDelayLabel(chain);
|
|
105
110
|
const { access, hint: upgradeHint, segment } = useCloudAccessFooter({
|
|
106
|
-
delayLabel
|
|
111
|
+
delayLabel,
|
|
112
|
+
degraded: !broker,
|
|
107
113
|
focused,
|
|
108
114
|
segmentId: "options-access",
|
|
109
115
|
placement: "omon-footer",
|
|
110
116
|
shortcutScope: "options:upgrade",
|
|
111
117
|
});
|
|
112
|
-
// Trial accounts stream real-time too, but the countdown is the status worth the row.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Trial accounts stream real-time too, but the countdown is the status worth the row. A broker's
|
|
119
|
+
// chain streams from the broker whatever the plan, so its coverage is the status, and its delay
|
|
120
|
+
// is the chain's own, not Gloom Cloud's.
|
|
121
|
+
const proCoverage = broker || (access.hasProAccess && !access.isTrialActive);
|
|
122
|
+
const coverageDelay = broker && !(chain?.delayMinutes && chain.delayMinutes > 0) ? undefined : delayLabel;
|
|
117
123
|
const accessInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
124
|
+
const coverage = proCoverage ? optionsCoverageSegment(quoteCoverage.status, coverageDelay) : null;
|
|
125
|
+
if (coverage) return [coverage];
|
|
121
126
|
return segment ? [segment] : [];
|
|
122
|
-
}, [
|
|
127
|
+
}, [coverageDelay, proCoverage, quoteCoverage.status, segment]);
|
|
123
128
|
|
|
124
129
|
// Rebuilt every render so the quote and trade ages keep counting with the
|
|
125
130
|
// stream; the registration compares by value, so only a changed label
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
2
2
|
import type { OptionContract, OptionsChain, Quote } from "../../../types/financials";
|
|
3
|
-
import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
|
|
3
|
+
import { buildQuoteKey, instrumentFromQuoteTarget, resolveEntryData } from "../../../market-data/selectors";
|
|
4
4
|
import type { QueryEntry } from "../../../market-data/result-types";
|
|
5
5
|
import type { OptionTableRow } from "./types";
|
|
6
6
|
import { isFiniteNumber } from "../../../utils/guards";
|
|
@@ -75,9 +75,20 @@ function normalizeVisibleRange(
|
|
|
75
75
|
return { start, end };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The broker profile a chain came from. Its contracts' quotes are asked of the
|
|
80
|
+
* same profile, so a broker that streams options (and no cloud stream) keeps
|
|
81
|
+
* the chain it served current, rather than leaving it to a slower source.
|
|
82
|
+
*/
|
|
83
|
+
export interface OptionQuoteBroker {
|
|
84
|
+
brokerId?: string;
|
|
85
|
+
brokerInstanceId?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
export function buildOptionQuoteTargets(
|
|
79
89
|
rows: readonly OptionTableRow[],
|
|
80
90
|
window: OptionQuoteTargetWindow,
|
|
91
|
+
broker?: OptionQuoteBroker,
|
|
81
92
|
): QuoteSubscriptionTarget[] {
|
|
82
93
|
if (rows.length === 0) return [];
|
|
83
94
|
const visibleRange = normalizeVisibleRange(rows.length, window);
|
|
@@ -101,6 +112,7 @@ export function buildOptionQuoteTargets(
|
|
|
101
112
|
targets.set(symbol, {
|
|
102
113
|
symbol,
|
|
103
114
|
exchange: OPTIONS_QUOTE_EXCHANGE,
|
|
115
|
+
...(broker ? { context: { brokerId: broker.brokerId, brokerInstanceId: broker.brokerInstanceId } } : {}),
|
|
104
116
|
surface: "options",
|
|
105
117
|
visible,
|
|
106
118
|
selected,
|
|
@@ -119,10 +131,12 @@ export function buildOptionQuoteTargets(
|
|
|
119
131
|
return [...targets.values()];
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
export function buildOptionQuoteKey(contractSymbol: string): string {
|
|
134
|
+
export function buildOptionQuoteKey(contractSymbol: string, broker?: OptionQuoteBroker): string {
|
|
123
135
|
return buildQuoteKey({
|
|
124
136
|
symbol: contractSymbol,
|
|
125
137
|
exchange: OPTIONS_QUOTE_EXCHANGE,
|
|
138
|
+
brokerId: broker?.brokerId,
|
|
139
|
+
brokerInstanceId: broker?.brokerInstanceId,
|
|
126
140
|
});
|
|
127
141
|
}
|
|
128
142
|
|
|
@@ -179,17 +193,20 @@ export interface LiveOptionsChain {
|
|
|
179
193
|
}
|
|
180
194
|
|
|
181
195
|
const EMPTY_STRIKES: ReadonlySet<number> = new Set();
|
|
182
|
-
const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, number[]
|
|
196
|
+
const contractKeyIndexes = new WeakMap<readonly OptionContract[], Map<string, Map<string, number[]>>>();
|
|
183
197
|
|
|
184
|
-
function contractKeyIndex(contracts: readonly OptionContract[]): Map<string, number[]> {
|
|
185
|
-
|
|
198
|
+
function contractKeyIndex(contracts: readonly OptionContract[], broker?: OptionQuoteBroker): Map<string, number[]> {
|
|
199
|
+
const byBroker = contractKeyIndexes.get(contracts) ?? new Map<string, Map<string, number[]>>();
|
|
200
|
+
contractKeyIndexes.set(contracts, byBroker);
|
|
201
|
+
const brokerKey = `${broker?.brokerId ?? ""}|${broker?.brokerInstanceId ?? ""}`;
|
|
202
|
+
let index = byBroker.get(brokerKey);
|
|
186
203
|
if (!index) {
|
|
187
204
|
index = new Map();
|
|
188
205
|
contracts.forEach((contract, position) => {
|
|
189
|
-
const key = buildOptionQuoteKey(contract.contractSymbol);
|
|
206
|
+
const key = buildOptionQuoteKey(contract.contractSymbol, broker);
|
|
190
207
|
index!.set(key, [...(index!.get(key) ?? []), position]);
|
|
191
208
|
});
|
|
192
|
-
|
|
209
|
+
byBroker.set(brokerKey, index);
|
|
193
210
|
}
|
|
194
211
|
return index;
|
|
195
212
|
}
|
|
@@ -204,12 +221,13 @@ export function overlayOptionChainQuotes(
|
|
|
204
221
|
chain: OptionsChain,
|
|
205
222
|
quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
|
|
206
223
|
freshness: OptionsQuoteFreshness,
|
|
224
|
+
broker?: OptionQuoteBroker,
|
|
207
225
|
): LiveOptionsChain {
|
|
208
226
|
if (quoteEntries.size === 0) return { chain, streamedStrikes: EMPTY_STRIKES, quotedAt: null };
|
|
209
227
|
const streamedStrikes = new Set<number>();
|
|
210
228
|
let quotedAt: number | null = null;
|
|
211
229
|
const overlay = (contracts: OptionContract[]): OptionContract[] => {
|
|
212
|
-
const index = contractKeyIndex(contracts);
|
|
230
|
+
const index = contractKeyIndex(contracts, broker);
|
|
213
231
|
let next: OptionContract[] | null = null;
|
|
214
232
|
for (const [key, entry] of quoteEntries) {
|
|
215
233
|
const positions = index.get(key);
|
|
@@ -242,19 +260,16 @@ export function resolveOptionQuoteCoverage(
|
|
|
242
260
|
quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
|
|
243
261
|
freshness: OptionsQuoteFreshness,
|
|
244
262
|
): OptionQuoteCoverage {
|
|
245
|
-
|
|
263
|
+
// Each target's own key: a chain's broker profile is part of it.
|
|
264
|
+
const visibleKeys = new Set(
|
|
246
265
|
targets
|
|
247
|
-
.filter((target) => target.visible === true)
|
|
248
|
-
.map((target) =>
|
|
249
|
-
.filter(Boolean),
|
|
266
|
+
.filter((target) => target.visible === true && target.symbol.trim())
|
|
267
|
+
.map((target) => buildQuoteKey(instrumentFromQuoteTarget(target))),
|
|
250
268
|
);
|
|
251
269
|
let liveCount = 0;
|
|
252
270
|
let fallbackCount = 0;
|
|
253
|
-
for (const
|
|
254
|
-
const quote = freshOptionQuote(
|
|
255
|
-
quoteEntries.get(buildOptionQuoteKey(symbol)),
|
|
256
|
-
freshness,
|
|
257
|
-
);
|
|
271
|
+
for (const key of visibleKeys) {
|
|
272
|
+
const quote = freshOptionQuote(quoteEntries.get(key), freshness);
|
|
258
273
|
if (
|
|
259
274
|
quote?.dataSource === "live"
|
|
260
275
|
&& quote.delivery === "stream"
|
|
@@ -266,7 +281,7 @@ export function resolveOptionQuoteCoverage(
|
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
283
|
|
|
269
|
-
const totalCount =
|
|
284
|
+
const totalCount = visibleKeys.size;
|
|
270
285
|
const status: OptionQuoteCoverageStatus = totalCount > 0 && liveCount === totalCount
|
|
271
286
|
? "live"
|
|
272
287
|
: liveCount > 0
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
3
|
-
import { usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
|
|
3
|
+
import { usePaneAppConfig, usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
|
|
4
4
|
import { colors, priceColor } from "../../../theme/colors";
|
|
5
5
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
6
6
|
import { formatCompact, formatPercentRaw } from "../../../utils/format";
|
|
7
7
|
import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
|
|
8
|
-
import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
|
|
8
|
+
import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
|
|
9
9
|
import {
|
|
10
10
|
expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
|
|
11
11
|
} from "../../../utils/option-expiry";
|
|
@@ -34,7 +34,9 @@ import { useLiveQuoteEntries, useQuoteUpdates } from "../../../state/hooks/quote
|
|
|
34
34
|
import { buildChartKey } from "../../../market-data/selectors";
|
|
35
35
|
import type { ChartRequest } from "../../../market-data/request-types";
|
|
36
36
|
import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
|
|
37
|
-
import { usePluginAppActions } from "../../runtime";
|
|
37
|
+
import { usePluginAppActions, usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
|
|
38
|
+
import { brokerCanTrade, openTradeTicket } from "../broker-trading/command";
|
|
39
|
+
import type { OptionContract } from "../../../types/financials";
|
|
38
40
|
import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
|
|
39
41
|
import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
|
|
40
42
|
import { useOptionsCatalogue } from "./expiry-catalogue";
|
|
@@ -66,7 +68,7 @@ import { signedPositionDirection } from "../../../portfolio/position-metrics";
|
|
|
66
68
|
import { optionMarketReference } from "./market-reference";
|
|
67
69
|
import { useOptionsEnrichment } from "./enrichment";
|
|
68
70
|
import type { OptionsEnrichmentSnapshot } from "./enrichment-model";
|
|
69
|
-
import { optionMid, type OptionSide } from "../shared/volatility";
|
|
71
|
+
import { daysToExpiryFrom, optionMid, type OptionSide } from "../shared/volatility";
|
|
70
72
|
import type { TickerRecord } from "../../../types/ticker";
|
|
71
73
|
import type { TickerFinancials } from "../../../types/financials";
|
|
72
74
|
import type { IvStats } from "../iv-history/client";
|
|
@@ -176,6 +178,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
176
178
|
const ticker = useMemo(() => savedTicker ?? (boundSymbol ? transientTicker(boundSymbol, fallbackExchange, fallbackCurrency) : null),
|
|
177
179
|
[savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
|
|
178
180
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
181
|
+
const { pinTicker } = usePluginTickerActions();
|
|
182
|
+
const { getBrokerAdapter } = usePluginBrokerActions();
|
|
183
|
+
const appConfig = usePaneAppConfig();
|
|
179
184
|
const liveStreaming = useLiveStreamingSetting();
|
|
180
185
|
// A number from the app, or the date or seconds `fn OMON --expiration` took as text.
|
|
181
186
|
const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
|
|
@@ -238,13 +243,20 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
238
243
|
const strikeWindowText = strikeWindowValue(strikeWindow);
|
|
239
244
|
const initialChainEntry = useOptionsQuery(baseRequest);
|
|
240
245
|
const initialChain = useResolvedEntryValue(initialChainEntry);
|
|
241
|
-
|
|
246
|
+
// A listing can still carry a date whose 16:00 ET close has passed. The chain
|
|
247
|
+
// never opens there, nor on a saved date that has since passed: it takes the nearest live one.
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const liveExpiration = (expiration: number) => daysToExpiryFrom(expiration, now) > 0;
|
|
250
|
+
const listedExpirations = initialChain?.expirationDates ?? [];
|
|
251
|
+
const openingExpirations = listedExpirations.some(liveExpiration) ? listedExpirations.filter(liveExpiration) : listedExpirations;
|
|
252
|
+
const initialExpiration = openingExpirations.length === 0 ? undefined : openingExpirations.reduce((best, expiration) => (
|
|
242
253
|
parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
|
|
243
|
-
),
|
|
254
|
+
), openingExpirations[0]!);
|
|
244
255
|
// A typed date names a day; the catalogue's stamp for that day is the expiry.
|
|
245
256
|
const seededDate = readOptionExpiration(seededSetting);
|
|
246
|
-
const
|
|
247
|
-
|
|
257
|
+
const seededListed = seededDate == null ? undefined : findListedExpiry(seededDate, listedExpirations) ?? seededDate;
|
|
258
|
+
const seededExpiration = seededListed != null && (liveExpiration(seededListed) || !openingExpirations.some(liveExpiration))
|
|
259
|
+
? seededListed : undefined;
|
|
248
260
|
const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
|
|
249
261
|
? seededExpiration ?? initialExpiration : initialExpiration;
|
|
250
262
|
const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
|
|
@@ -281,8 +293,11 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
281
293
|
?? (matchesSelectedExpiration(initialChain) ? initialChain : null);
|
|
282
294
|
const strikesLoading = strikeChain === null && !expirationUnavailable;
|
|
283
295
|
const expirationDates = useMemo(() => {
|
|
284
|
-
|
|
296
|
+
const listed = selectedExpirationMissing
|
|
285
297
|
? [...availableExpirations, selectedExpiration!].sort((left, right) => left - right) : availableExpirations;
|
|
298
|
+
// A date that has ended has no contracts left to show; the strip leaves it out, unless every date has.
|
|
299
|
+
const current = (expiration: number) => daysToExpiry(expiration, Date.now()) >= 0;
|
|
300
|
+
return listed.some(current) ? listed.filter((expiration) => expiration === selectedExpiration || current(expiration)) : listed;
|
|
286
301
|
}, [availableExpirations, selectedExpiration, selectedExpirationMissing]);
|
|
287
302
|
// Keep the date strip stable while a mouse press focuses this pane. A new
|
|
288
303
|
// tab list asks the web host to reveal the active tab and can move the date
|
|
@@ -468,13 +483,17 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
468
483
|
: { key: viewportKey, range }
|
|
469
484
|
));
|
|
470
485
|
}, [viewportKey]);
|
|
486
|
+
// A chain read from a broker profile has its contracts quoted by that profile too.
|
|
487
|
+
const quoteBrokerId = instrument?.brokerId, quoteBrokerInstanceId = instrument?.brokerInstanceId;
|
|
488
|
+
const optionQuoteBroker = useMemo(() => quoteBrokerId || quoteBrokerInstanceId
|
|
489
|
+
? { brokerId: quoteBrokerId, brokerInstanceId: quoteBrokerInstanceId } : undefined, [quoteBrokerId, quoteBrokerInstanceId]);
|
|
471
490
|
const optionQuoteTargets = useMemo(
|
|
472
491
|
() => buildOptionQuoteTargets(quoteRows, {
|
|
473
492
|
fallbackHeight: height,
|
|
474
493
|
selectedIndex: selectedStrikeIdx,
|
|
475
494
|
visibleRange: visibleStrikeRange,
|
|
476
|
-
}),
|
|
477
|
-
[height, quoteRows, selectedStrikeIdx, visibleStrikeRange],
|
|
495
|
+
}, optionQuoteBroker),
|
|
496
|
+
[height, quoteRows, selectedStrikeIdx, visibleStrikeRange, optionQuoteBroker],
|
|
478
497
|
);
|
|
479
498
|
const {
|
|
480
499
|
entries: optionQuoteEntries,
|
|
@@ -497,8 +516,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
497
516
|
// The snapshot object itself comes back while nothing streamed, so the
|
|
498
517
|
// memos below only recompute when a quote actually landed.
|
|
499
518
|
const liveChain = useMemo(
|
|
500
|
-
() => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness).chain : null,
|
|
501
|
-
[optionQuoteEntries, optionQuoteFreshness, strikeChain],
|
|
519
|
+
() => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness, optionQuoteBroker).chain : null,
|
|
520
|
+
[optionQuoteEntries, optionQuoteFreshness, strikeChain, optionQuoteBroker],
|
|
502
521
|
);
|
|
503
522
|
const callsByStrike = useMemo(
|
|
504
523
|
() => new Map(liveChain?.calls.map((c) => [c.strike, c]) ?? []),
|
|
@@ -672,6 +691,28 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
672
691
|
}, [scenarioContract, scenarioAvailable, scenarioMid, scenarioVolatility, createPaneFromTemplate, dividendYield, effectiveTicker,
|
|
673
692
|
effectiveExchange, selectedSide, spot, underlying?.quote?.lastUpdated]);
|
|
674
693
|
|
|
694
|
+
// A contract opens as its own listing: quote, chart and Trade. It stays on
|
|
695
|
+
// the chain's broker while that profile is on, else it opens by its symbol.
|
|
696
|
+
const contractListing = useCallback((contract: OptionContract) => {
|
|
697
|
+
const symbol = contract.contractSymbol.trim().toUpperCase();
|
|
698
|
+
const currency = contract.currency || underlying?.quote?.currency || "USD";
|
|
699
|
+
const brokerOn = !!instrument && appConfig.brokerInstances.some((profile) => profile.id === instrument.brokerInstanceId && profile.enabled !== false);
|
|
700
|
+
return { symbol, selection: { instrument: brokerOn ? chainOptionContract(symbol, instrument, currency) : null,
|
|
701
|
+
listing: { name: optionContractName(symbol) ?? symbol, exchange: "", currency, type: "OPT" } } };
|
|
702
|
+
}, [appConfig.brokerInstances, instrument, underlying?.quote?.currency]);
|
|
703
|
+
const openContract = useCallback((contract: OptionContract | null | undefined) => {
|
|
704
|
+
if (!contract) return;
|
|
705
|
+
const { symbol, selection } = contractListing(contract);
|
|
706
|
+
pinTicker(symbol, selection);
|
|
707
|
+
}, [contractListing, pinTicker]);
|
|
708
|
+
const tradable = !!scenarioContract && brokerCanTrade(appConfig.brokerInstances, getBrokerAdapter, scenarioContract.contractSymbol.trim().toUpperCase(), "OPT");
|
|
709
|
+
const tradeContract = useCallback(() => {
|
|
710
|
+
if (!scenarioContract || !tradable) return;
|
|
711
|
+
const { symbol, selection } = contractListing(scenarioContract);
|
|
712
|
+
// A new contract's ticket starts on Buy; b and s choose the side in it.
|
|
713
|
+
openTradeTicket({ pinTicker }, symbol, { action: "BUY" }, selection);
|
|
714
|
+
}, [contractListing, pinTicker, scenarioContract, tradable]);
|
|
715
|
+
|
|
675
716
|
const openSurface = useCallback(() => {
|
|
676
717
|
if (!ticker || selectedExpiration == null) return;
|
|
677
718
|
createPaneFromTemplate("vol-surface-pane", { symbol: ticker.metadata.ticker, ticker, instrument,
|
|
@@ -703,12 +744,14 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
703
744
|
setCalcSide(sideTarget);
|
|
704
745
|
}, [selectContract, selectedRow, selectedStrikeIdx, sideTarget]);
|
|
705
746
|
|
|
747
|
+
// With a broker that trades the contract, [t]rade opens its ticket on Buy, and Sell is chosen there.
|
|
706
748
|
const footerHints = useMemo(() => [
|
|
707
749
|
...(sideTarget ? [{ id: "side", key: SIDE_KEY, label: ` ${sideTarget}`, title: sideTarget === "put" ? "Select Put" : "Select Call", onPress: switchSide }] : []),
|
|
750
|
+
...(tradable ? [{ id: "trade-contract", key: "t", label: "rade", title: "Trade Contract", onPress: tradeContract }] : []),
|
|
708
751
|
...(calcParams ? [{ id: "calc", key: "c", label: "alc", onPress: openCalculator }] : []),
|
|
709
752
|
...(scenarioAvailable ? [{ id: "scenario", key: "a", label: "dd to OSA", onPress: openScenario }] : []),
|
|
710
753
|
...(ticker && selectedExpiration != null ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
|
|
711
|
-
], [sideTarget, switchSide, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
|
|
754
|
+
], [sideTarget, switchSide, tradable, tradeContract, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
|
|
712
755
|
|
|
713
756
|
// Back to where the chain opens: the held contract's strike, else the money.
|
|
714
757
|
const defaultStrikeIndex = strikes.length === 0 ? -1 : (() => {
|
|
@@ -732,8 +775,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
732
775
|
{ id: "default-strike", label: parsed ? "Go to Held Strike" : "Go to ATM Strike",
|
|
733
776
|
enabled: defaultStrikeIndex >= 0, onSelect: goToDefaultStrike },
|
|
734
777
|
...(enrichment?.asOf && dialog ? [{ id: "analytics-as-of", label: "Analytics As Of…", onSelect: showAnalyticsAsOf }] : []),
|
|
778
|
+
{ id: "open-contract", label: "Open Contract", accelerator: "Enter", enabled: !!scenarioContract, onSelect: () => openContract(scenarioContract) },
|
|
779
|
+
...(tradable ? [{ id: "trade-contract", label: "Trade Contract", accelerator: "t", onSelect: tradeContract }] : []),
|
|
735
780
|
], [defaultStrikeIndex, dialog, enrichment?.asOf, expirationDates.length, goToDefaultStrike, parsed,
|
|
736
|
-
selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf]);
|
|
781
|
+
selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf, scenarioContract, openContract, tradable, tradeContract]);
|
|
737
782
|
|
|
738
783
|
const renderCell = useCallback((
|
|
739
784
|
row: OptionTableRow,
|
|
@@ -756,6 +801,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
756
801
|
}, [selectContract, selectedSide]);
|
|
757
802
|
|
|
758
803
|
useOptionsAccessFooter({
|
|
804
|
+
broker: !!optionQuoteBroker,
|
|
759
805
|
chain,
|
|
760
806
|
error: [error, underlyingStale ? "Underlying quote stale: Greeks and calculator unavailable" : null,
|
|
761
807
|
selectedContract && strikeChain && !selectedContractAvailable
|
|
@@ -789,7 +835,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
789
835
|
// the arrows for it; in a research tab those stay with the tab strip.
|
|
790
836
|
useShortcut((event) => {
|
|
791
837
|
if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
|
|
792
|
-
if (event.ctrl || event.meta || event.alt
|
|
838
|
+
if (event.ctrl || event.meta || event.alt) return;
|
|
839
|
+
const surface = isPlainKey(event, "s");
|
|
840
|
+
if (surface && ticker && selectedExpiration != null) {
|
|
841
|
+
event.preventDefault();
|
|
842
|
+
event.stopPropagation();
|
|
843
|
+
openSurface();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (event.shift) return;
|
|
847
|
+
if (tradable && isPlainKey(event, "t") && !event.repeat) {
|
|
848
|
+
event.preventDefault();
|
|
849
|
+
event.stopPropagation();
|
|
850
|
+
tradeContract();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
793
853
|
|
|
794
854
|
const expiryStep = isPlainKey(event, EXPIRY_PREVIOUS_KEY) || (!nestedInTabs && isPlainKey(event, "h", "left")) ? -1
|
|
795
855
|
: isPlainKey(event, EXPIRY_NEXT_KEY) || (!nestedInTabs && isPlainKey(event, "l", "right")) ? 1 : 0;
|
|
@@ -799,12 +859,6 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
799
859
|
selectAdjacentExpiration(expiryStep);
|
|
800
860
|
return;
|
|
801
861
|
}
|
|
802
|
-
if (isPlainKey(event, "s") && ticker && selectedExpiration != null) {
|
|
803
|
-
event.preventDefault();
|
|
804
|
-
event.stopPropagation();
|
|
805
|
-
openSurface();
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
862
|
if (isPlainKey(event, "c") && calcParams) {
|
|
809
863
|
event.preventDefault();
|
|
810
864
|
event.stopPropagation();
|
|
@@ -925,6 +979,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
925
979
|
onVisibleRangeChange={handleVisibleStrikeRangeChange}
|
|
926
980
|
getItemKey={(row) => String(row.strike)}
|
|
927
981
|
renderCell={renderCell}
|
|
982
|
+
onActivate={(row) => openContract(selectedSide === "put" ? row.put : selectedSide === "call" ? row.call : null)}
|
|
928
983
|
selectedTextOverridesCellColor
|
|
929
984
|
emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
|
|
930
985
|
: strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
2
|
+
import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
3
3
|
import { CompositeChart } from "../../../components/chart/composite/composite-chart";
|
|
4
4
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
5
5
|
import { useAsyncResource, useInputCapture } from "../../../public/react";
|
|
@@ -279,7 +279,9 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
279
279
|
...(surfaceSource && surfaceResource.loading ? [{ id: "loading", parts: [{ text: "loading surface", tone: "muted" as const }] }] : []),
|
|
280
280
|
...(surfaceSource && surface?.asOf ? [{ id: "surface-asof", parts: [{ text: `surface · ${surface.asOf}`, tone: "muted" as const }] }] : []),
|
|
281
281
|
...(effectiveSteps && effectiveSteps !== (draft.steps ?? 400) ? [{ id: "refined", parts: [{ text: `tree refined to ${effectiveSteps} steps`, tone: "muted" as const }] }] : []),
|
|
282
|
-
...(liveInputs ? [
|
|
282
|
+
...(liveInputs ? [liveInputs.delayed
|
|
283
|
+
? delayedFooterSegment({ id: "market", detail: "Delayed market inputs" })
|
|
284
|
+
: { id: "market", parts: [{ text: "real-time market", tone: "muted" as const }] }] : []),
|
|
283
285
|
],
|
|
284
286
|
hints: activeFieldId ? [] : [
|
|
285
287
|
// Names the cell Enter or `e` edits, which the grid does not mark.
|