@gloom-sh/terminal 0.18.5 → 0.19.0
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/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +24 -6
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +27 -3
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/financials.ts +32 -1
- package/src/types/headless.ts +21 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { METRIC_ALERTS_KEY,
|
|
1
|
+
import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, closeBasis, metricName, metricReadingText } from "./metric-model";
|
|
2
2
|
import { metricAvailabilityError } from "./metric-data";
|
|
3
|
-
import {
|
|
3
|
+
import { relativeAlertText } from "./metric-command";
|
|
4
|
+
import { levelAlertStoreKey, readLevelAlertStores } from "./storage";
|
|
4
5
|
import { Box, type ScrollBoxRenderable } from "../../../ui";
|
|
5
6
|
import { Button, DetailScrollBody, KeyValueRow, Notice, PaneStatusBody, PaneFooterScope, usePaneTabs } from "../../../components";
|
|
6
|
-
import { usePluginPaneState } from "../../runtime";
|
|
7
|
+
import { usePluginPaneState, usePluginState } from "../../runtime";
|
|
7
8
|
import { EventAlertsPane } from "./events-pane";
|
|
8
9
|
import { AlertHistoryPane } from "./history-pane";
|
|
9
10
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from "react";
|
|
@@ -30,16 +31,21 @@ import {
|
|
|
30
31
|
readAlertsStoreError,
|
|
31
32
|
serializeAlerts,
|
|
32
33
|
} from "./alert-engine";
|
|
33
|
-
import {
|
|
34
|
+
import { parseAlertEdit } from "./command";
|
|
34
35
|
import { ALERT_TABS, ALERTS_KEY } from "./constants";
|
|
35
36
|
import {
|
|
37
|
+
alertListing,
|
|
36
38
|
conditionLabel,
|
|
37
39
|
formatAlertDistance,
|
|
38
40
|
formatAlertTargetPrice,
|
|
39
41
|
formatCurrentPrice,
|
|
40
42
|
formatQuoteChecked,
|
|
43
|
+
isoUtc,
|
|
44
|
+
listingWithCurrency,
|
|
41
45
|
relativeTime,
|
|
42
46
|
} from "./format";
|
|
47
|
+
import { FIRED_LOG_KEY } from "./fired-log";
|
|
48
|
+
import { withAlertChange } from "./alert-log";
|
|
43
49
|
import type { AlertRule } from "./types";
|
|
44
50
|
import { useQuoteEntries } from "../../../market-data/hooks";
|
|
45
51
|
import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
|
|
@@ -57,7 +63,8 @@ type AlertColumnId =
|
|
|
57
63
|
| "away"
|
|
58
64
|
| "condition"
|
|
59
65
|
| "quote"
|
|
60
|
-
| "triggered"
|
|
66
|
+
| "triggered"
|
|
67
|
+
| "created";
|
|
61
68
|
|
|
62
69
|
type AlertColumn = DataTableColumn & { id: AlertColumnId };
|
|
63
70
|
|
|
@@ -69,8 +76,11 @@ const ALERT_COLUMNS: AlertColumn[] = [
|
|
|
69
76
|
{ id: "target", label: "Target", width: 11, align: "right" },
|
|
70
77
|
{ id: "away", label: "Away", width: 8, align: "right" },
|
|
71
78
|
{ id: "condition", label: "Trigger", width: 12, align: "left" },
|
|
72
|
-
|
|
79
|
+
// Wide enough to say "not checked yet" for an alert no check has read.
|
|
80
|
+
{ id: "quote", label: "Quote", width: 15, align: "left" },
|
|
73
81
|
{ id: "triggered", label: "Alerted", width: 8, align: "left" },
|
|
82
|
+
// The day the alert was made (UTC), as Events lists its rules; the detail gives the instant.
|
|
83
|
+
{ id: "created", label: "Since", width: 10, align: "left" },
|
|
74
84
|
];
|
|
75
85
|
|
|
76
86
|
const DETAIL_ROW = { labelWidth: 10, emphasis: false } as const;
|
|
@@ -89,20 +99,28 @@ function AlertDetail({ alert, scrollRef }: { alert: AlertRule; scrollRef: RefObj
|
|
|
89
99
|
const reason = alert.lastCheckError
|
|
90
100
|
? alert.lastCheckError.includes(alert.symbol) ? alert.lastCheckError : `${alert.symbol}: ${alert.lastCheckError}`
|
|
91
101
|
: null;
|
|
102
|
+
const basis = alert.metric ? closeBasis(alert.metric) : null;
|
|
92
103
|
return (
|
|
93
104
|
<DetailScrollBody ref={scrollRef} resetScrollKey={alert.id}>
|
|
94
105
|
<Box flexDirection="column">
|
|
95
106
|
{reason ? <Box paddingBottom={1}><Notice tone="warning">{reason}</Notice></Box> : null}
|
|
96
107
|
<KeyValueRow {...DETAIL_ROW} label="State" value={alert.status === "triggered" ? "Triggered" : "Active"} tone={alert.status === "triggered" ? "positive" : "muted"} />
|
|
108
|
+
<KeyValueRow {...DETAIL_ROW} label="Listing" value={listingWithCurrency(alertListing(alert), alert.lastQuoteCurrency)} />
|
|
97
109
|
{/* With no quote the reason above says why; the price and its distance wait for one. */}
|
|
98
|
-
{
|
|
110
|
+
{basis ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? `Awaiting ${basis}`} /> : null}
|
|
99
111
|
{reason ? null : <KeyValueRow {...DETAIL_ROW} label="Current" value={formatCurrentPrice(alert, 24)} />}
|
|
112
|
+
{/* The two readings that place the quadrant, as RRG states them. */}
|
|
113
|
+
{!reason && alert.metric?.kind === "rrg" && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null ? <>
|
|
114
|
+
<KeyValueRow {...DETAIL_ROW} label="Strength" value={alert.lastCheckedPrice.toFixed(2)} />
|
|
115
|
+
<KeyValueRow {...DETAIL_ROW} label="Momentum" value={alert.lastCheckedTarget.toFixed(2)} />
|
|
116
|
+
</> : null}
|
|
100
117
|
{reason ? null : <KeyValueRow {...DETAIL_ROW} label="Away" value={formatAlertDistance(alert)} />}
|
|
101
118
|
{reason
|
|
102
119
|
? alert.lastCheckedAt ? <KeyValueRow {...DETAIL_ROW} label="Checked" value={relativeTime(alert.lastCheckedAt)} tone="muted" /> : null
|
|
103
120
|
: <KeyValueRow {...DETAIL_ROW} label="Quote" value={formatQuoteChecked(alert)} tone="muted" />}
|
|
104
121
|
{alert.triggeredAt ? <KeyValueRow {...DETAIL_ROW} label="Alerted" value={relativeTime(alert.triggeredAt)} /> : null}
|
|
105
122
|
<KeyValueRow {...DETAIL_ROW} label="Armed" value={relativeTime(alert.armedAt ?? alert.createdAt)} tone="muted" />
|
|
123
|
+
<KeyValueRow {...DETAIL_ROW} label="Created" value={isoUtc(alert.createdAt)} tone="muted" />
|
|
106
124
|
</Box>
|
|
107
125
|
</DetailScrollBody>
|
|
108
126
|
);
|
|
@@ -144,7 +162,10 @@ export function AlertsPane(props: PaneProps) {
|
|
|
144
162
|
function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
145
163
|
const [alertsJson, setAlertsJson] = usePluginConfigState<unknown>(ALERTS_KEY, "[]");
|
|
146
164
|
const [metricsJson, setMetricsJson] = usePluginConfigState<unknown>(METRIC_ALERTS_KEY, "[]");
|
|
165
|
+
const [relativeJson, setRelativeJson] = usePluginConfigState<unknown>(RELATIVE_ALERTS_KEY, "[]");
|
|
147
166
|
const { openPluginCommandWorkflow, notify } = usePluginAppActions();
|
|
167
|
+
// Deleting or re-arming a rule goes in this device's alert log, beside the alerts that fired.
|
|
168
|
+
const [, setAlertLog] = usePluginState<unknown>(FIRED_LOG_KEY, []);
|
|
148
169
|
const dialog = useDialog();
|
|
149
170
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
150
171
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
@@ -152,9 +173,11 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
152
173
|
const detailAlertId = useRef<string | null>(null);
|
|
153
174
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
154
175
|
// Keep the rule and daily-close basis visible in a narrow pane; the detail keeps all figures.
|
|
155
|
-
const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered"
|
|
176
|
+
const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered" && column.id !== "created")
|
|
177
|
+
: width < 118 ? ALERT_COLUMNS.filter(column => column.id !== "created") : ALERT_COLUMNS;
|
|
156
178
|
const showHorizontalScrollbar = columns.reduce((sum, column) => sum + column.width + 1, 2) > width;
|
|
157
|
-
const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : metricsJson
|
|
179
|
+
const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : key === METRIC_ALERTS_KEY ? metricsJson : relativeJson),
|
|
180
|
+
[alertsJson, metricsJson, relativeJson]);
|
|
158
181
|
const storeError = stores.error;
|
|
159
182
|
|
|
160
183
|
const { alerts, rows } = useMemo(() => {
|
|
@@ -205,16 +228,17 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
205
228
|
}), [liveEntries, rows]);
|
|
206
229
|
|
|
207
230
|
const savePaneAlerts = useCallback((id: string, next: (current: AlertRule[]) => AlertRule[]): string | null => {
|
|
208
|
-
const
|
|
231
|
+
const found = alerts.find(alert => alert.id === id);
|
|
232
|
+
const key = found ? levelAlertStoreKey(found) : ALERTS_KEY;
|
|
209
233
|
if (stores.errors[key]) return stores.errors[key];
|
|
210
234
|
let error: string | null = null;
|
|
211
|
-
const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : setAlertsJson;
|
|
235
|
+
const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : key === RELATIVE_ALERTS_KEY ? setRelativeJson : setAlertsJson;
|
|
212
236
|
setJson((currentJson: unknown) => {
|
|
213
237
|
error = readAlertsStoreError(currentJson);
|
|
214
238
|
return error ? currentJson : serializeAlerts(next(deserializeAlerts(currentJson)));
|
|
215
239
|
});
|
|
216
240
|
return error;
|
|
217
|
-
}, [alerts, setAlertsJson, setMetricsJson, stores]);
|
|
241
|
+
}, [alerts, setAlertsJson, setMetricsJson, setRelativeJson, stores]);
|
|
218
242
|
|
|
219
243
|
const deleteAlert = useCallback(async (id: string) => {
|
|
220
244
|
const alert = alerts.find((a) => a.id === id);
|
|
@@ -230,15 +254,24 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
230
254
|
notify({ body: error, type: "error" });
|
|
231
255
|
return;
|
|
232
256
|
}
|
|
257
|
+
setAlertLog((saved: unknown) => withAlertChange(saved, "deleted", alert));
|
|
233
258
|
setSelectedIdx((prev) => Math.max(0, Math.min(prev, rows.length - 2)));
|
|
234
|
-
}, [alerts, dialog, notify, rows.length, savePaneAlerts]);
|
|
259
|
+
}, [alerts, dialog, notify, rows.length, savePaneAlerts, setAlertLog]);
|
|
235
260
|
|
|
236
261
|
const rearmAlert = useCallback((id: string) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
// Only an alert that went off is re-armed, as the store reads at the write: one still waiting is left, and not logged.
|
|
263
|
+
const rearmed: AlertRule[] = [];
|
|
264
|
+
const error = savePaneAlerts(id, (current) => current.map((a) => {
|
|
265
|
+
if (a.id !== id || a.status !== "triggered") return a;
|
|
266
|
+
rearmed.push(a);
|
|
267
|
+
return rebuildAlert(a);
|
|
268
|
+
}));
|
|
269
|
+
if (error) {
|
|
270
|
+
notify({ body: error, type: "error" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (const alert of rearmed) setAlertLog((saved: unknown) => withAlertChange(saved, "rearmed", alert));
|
|
274
|
+
}, [notify, savePaneAlerts, setAlertLog]);
|
|
242
275
|
|
|
243
276
|
const startAddAlert = useCallback(() => {
|
|
244
277
|
openPluginCommandWorkflow("set-alert");
|
|
@@ -276,11 +309,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
276
309
|
description: "SYMBOL above|below|crosses PRICE",
|
|
277
310
|
placeholder: "AAPL above 200",
|
|
278
311
|
}}
|
|
279
|
-
currentValue={selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
|
|
312
|
+
currentValue={relativeAlertText(selected) ?? (selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`)}
|
|
280
313
|
onApply={async (value) => {
|
|
281
|
-
const parsed =
|
|
282
|
-
if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
|
|
283
|
-
if (!!parsed.metric !== !!selected.metric) throw new Error("Add a new alert to change between price and a valuation or technical measure.");
|
|
314
|
+
const parsed = parseAlertEdit(selected, value);
|
|
284
315
|
const error = savePaneAlerts(selected.id, (current) => current.map((alert) => (
|
|
285
316
|
alert.id === selected.id
|
|
286
317
|
? { ...editAlert(alert, parsed.symbol, parsed.condition, parsed.price), ...(parsed.metric ? { metric: parsed.metric } : {}) }
|
|
@@ -382,6 +413,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
382
413
|
case "current":
|
|
383
414
|
return {
|
|
384
415
|
text: formatCurrentPrice(alert, column.width),
|
|
416
|
+
// The quadrant word exports with the readings that place it.
|
|
417
|
+
...(alert.metric?.kind === "rrg" && !alert.lastCheckError && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null
|
|
418
|
+
? { value: metricReadingText(alert.metric, { value: alert.lastCheckedPrice, target: alert.lastCheckedTarget }) } : {}),
|
|
385
419
|
color: alert.lastCheckError ? colors.negative : colors.text,
|
|
386
420
|
};
|
|
387
421
|
case "target":
|
|
@@ -392,10 +426,12 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
392
426
|
color: colors.textDim,
|
|
393
427
|
};
|
|
394
428
|
case "condition":
|
|
395
|
-
return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : conditionLabel(alert.condition) };
|
|
429
|
+
return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : alert.metric?.kind === "rrg" ? "Enters" : conditionLabel(alert.condition) };
|
|
396
430
|
case "quote":
|
|
397
431
|
return {
|
|
398
432
|
text: formatQuoteChecked(alert),
|
|
433
|
+
// A settled close exports as its session date.
|
|
434
|
+
...(alert.metric && closeBasis(alert.metric) && alert.lastDailyClose && !alert.lastCheckError ? { value: alert.lastDailyClose } : {}),
|
|
399
435
|
color: alert.lastCheckError ? colors.negative : colors.textDim,
|
|
400
436
|
};
|
|
401
437
|
case "triggered":
|
|
@@ -403,6 +439,8 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
403
439
|
text: alert.triggeredAt ? relativeTime(alert.triggeredAt) : "-",
|
|
404
440
|
color: colors.textDim,
|
|
405
441
|
};
|
|
442
|
+
case "created":
|
|
443
|
+
return { text: isoUtc(alert.createdAt).slice(0, 10), color: colors.textDim };
|
|
406
444
|
}
|
|
407
445
|
}, []);
|
|
408
446
|
|
|
@@ -2,6 +2,7 @@ import type { DataProvider } from "../../../types/data-provider";
|
|
|
2
2
|
import type { Quote } from "../../../types/financials";
|
|
3
3
|
import { getActiveQuoteDisplay } from "../../../market-data/market/status";
|
|
4
4
|
import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
|
|
5
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
5
6
|
import type { AlertRule } from "./types";
|
|
6
7
|
|
|
7
8
|
export function normalizeAlertSymbol(value: string | null | undefined): string {
|
|
@@ -46,13 +47,18 @@ export function quoteAlertFields(
|
|
|
46
47
|
| "lastQuoteUpdatedAt"
|
|
47
48
|
| "lastQuoteSource"
|
|
48
49
|
| "lastQuoteProviderId"
|
|
50
|
+
| "lastQuoteCurrency"
|
|
51
|
+
| "lastQuoteExchange"
|
|
49
52
|
> & { lastCheckError?: undefined } {
|
|
53
|
+
const exchange = quote.listingExchangeName || quote.exchangeName;
|
|
50
54
|
return {
|
|
51
55
|
lastCheckedPrice: quote.price,
|
|
52
56
|
lastCheckedAt: checkedAt,
|
|
53
57
|
lastQuoteUpdatedAt: quote.lastUpdated,
|
|
54
58
|
lastQuoteSource: quote.dataSource,
|
|
55
59
|
lastQuoteProviderId: quote.providerId,
|
|
60
|
+
...(quote.currency ? { lastQuoteCurrency: quote.currency } : {}),
|
|
61
|
+
...(exchange ? { lastQuoteExchange: canonicalExchange(exchange) } : {}),
|
|
56
62
|
lastCheckError: undefined,
|
|
57
63
|
};
|
|
58
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METRIC_ALERTS_KEY } from "./metric-model";
|
|
1
|
+
import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, metricAlertsKey } from "./metric-model";
|
|
2
2
|
import type { AppConfig } from "../../../types/config";
|
|
3
3
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
4
4
|
import {
|
|
@@ -21,8 +21,9 @@ export async function saveAlerts(
|
|
|
21
21
|
scope: "all" | "price" | "metric" = "all",
|
|
22
22
|
): Promise<void> {
|
|
23
23
|
const stores = readLevelAlertStores((key) => ctx.configState.get<unknown>(key));
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// An unreadable list is never written over; one that has nothing to write does not hold up the others.
|
|
25
|
+
const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key, value]) => (scope === "all"
|
|
26
|
+
|| (scope === "price" ? key === ALERTS_KEY : key !== ALERTS_KEY)) && !(stores.errors[key] && value === "[]"));
|
|
26
27
|
for (const [key] of entries) if (stores.errors[key]) throw new Error(stores.errors[key]!);
|
|
27
28
|
for (const [key, value] of entries) {
|
|
28
29
|
if (ctx.configState.get<unknown>(key) !== value) await ctx.configState.set(key, value);
|
|
@@ -46,17 +47,20 @@ export function readAlertStores(config: AppConfig) {
|
|
|
46
47
|
export function readLevelAlertStores(get: (key: string) => unknown): { alerts: AlertRule[]; error: string | null; errors: Record<string, string | null> } {
|
|
47
48
|
const alerts: AlertRule[] = [];
|
|
48
49
|
const errors: Record<string, string | null> = {};
|
|
49
|
-
for (const key of
|
|
50
|
+
for (const key of LEVEL_ALERT_KEYS) {
|
|
50
51
|
const raw = get(key);
|
|
51
52
|
let error = readAlertsStoreError(raw);
|
|
52
53
|
const rows = error ? [] : deserializeAlerts(raw);
|
|
53
|
-
if (rows.some(a =>
|
|
54
|
-
errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
|
|
54
|
+
if (rows.some(a => levelAlertStoreKey(a) !== key)) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
|
|
55
|
+
errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
|
|
56
|
+
: key === RELATIVE_ALERTS_KEY ? error?.replaceAll("price alerts", "relative strength alerts") ?? null : error;
|
|
55
57
|
if (!error) alerts.push(...rows);
|
|
56
58
|
}
|
|
57
59
|
return { alerts, errors, error: Object.values(errors).filter(Boolean).join(" ") || null };
|
|
58
60
|
}
|
|
59
61
|
export function writeLevelAlertStores(alerts: AlertRule[]): Record<string, string> {
|
|
60
|
-
return
|
|
61
|
-
[METRIC_ALERTS_KEY]: serializeAlerts(alerts.filter(a => a.metric)) };
|
|
62
|
+
return Object.fromEntries(LEVEL_ALERT_KEYS.map(key => [key, serializeAlerts(alerts.filter(a => levelAlertStoreKey(a) === key))]));
|
|
62
63
|
}
|
|
64
|
+
/** Price, valuation/technical and relative strength rules each keep their own synced list. */
|
|
65
|
+
const LEVEL_ALERT_KEYS = [ALERTS_KEY, METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY] as const;
|
|
66
|
+
export const levelAlertStoreKey = (alert: Pick<AlertRule, "metric">): string => alert.metric ? metricAlertsKey(alert.metric) : ALERTS_KEY;
|
|
@@ -23,5 +23,9 @@ export interface AlertRule {
|
|
|
23
23
|
lastQuoteUpdatedAt?: number;
|
|
24
24
|
lastQuoteSource?: QuoteDataSource;
|
|
25
25
|
lastQuoteProviderId?: string;
|
|
26
|
+
/** The currency of the quote the alert was last checked on (USD): its listing's, which lists show beside it. */
|
|
27
|
+
lastQuoteCurrency?: string;
|
|
28
|
+
/** The venue that quote was from (ARCA), which names the listing of a rule set by symbol alone. */
|
|
29
|
+
lastQuoteExchange?: string;
|
|
26
30
|
message?: string;
|
|
27
31
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { usePaneSettingValue } from "../../../public/react";
|
|
2
2
|
import { PortfolioRiskPane } from "./risk-pane";
|
|
3
3
|
import { portfolioRiskHeadless } from "./risk-headless";
|
|
4
|
+
import { NO_HOLDINGS_HINT } from "./risk-model";
|
|
4
5
|
import { portfolioRiskCache } from "./risk-client";
|
|
5
6
|
import { Box, Text } from "../../../ui";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -398,7 +399,7 @@ function LegacyPortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
|
|
|
398
399
|
message={accountsError ?? undefined}
|
|
399
400
|
hint={accountsError
|
|
400
401
|
? "Reconnect the broker in the Brokers pane (BR), then refresh."
|
|
401
|
-
:
|
|
402
|
+
: NO_HOLDINGS_HINT}
|
|
402
403
|
/>
|
|
403
404
|
</Box>
|
|
404
405
|
) : historyOnly ? (
|
|
@@ -58,7 +58,12 @@ interface RiskMarketHistory {
|
|
|
58
58
|
* Set when the listing quotes in another currency and its closes were
|
|
59
59
|
* restated in USD at same-date daily FX closes; quote and closeMark are then in USD.
|
|
60
60
|
*/
|
|
61
|
-
converted?: {
|
|
61
|
+
converted?: {
|
|
62
|
+
currency: string;
|
|
63
|
+
fxAsOf: string;
|
|
64
|
+
/** US sessions converted at the FX close before them, the pair having none that day (bridgedFxClose). */
|
|
65
|
+
fxBridged?: string[];
|
|
66
|
+
} | null;
|
|
62
67
|
error: string | null;
|
|
63
68
|
}
|
|
64
69
|
export interface RiskMarketSnapshot {
|
|
@@ -162,6 +167,8 @@ export function validateRiskHistory(
|
|
|
162
167
|
closeMark: { price: number; date: string; currency: string } | null;
|
|
163
168
|
/** Latest completed close in the returns' currency, whatever marks the holding. */
|
|
164
169
|
latestClose: { price: number; date: string };
|
|
170
|
+
/** With a conversion, the US sessions converted at the FX close before them (bridgedFxClose). */
|
|
171
|
+
fxBridged?: string[];
|
|
165
172
|
} {
|
|
166
173
|
if (
|
|
167
174
|
response.status !== "success" ||
|
|
@@ -244,13 +251,14 @@ export function validateRiskHistory(
|
|
|
244
251
|
if (local.integrity)
|
|
245
252
|
throw new Error("Daily history has inconsistent OHLC observations");
|
|
246
253
|
// A converted series is on the NYSE calendar the basket and SPY share.
|
|
247
|
-
const
|
|
254
|
+
const usd = conversion
|
|
248
255
|
? convertClosesToUsd(
|
|
249
256
|
points.map((point) => ({ date: new Date(point.date).toISOString().slice(0, 10), close: point.close })),
|
|
250
257
|
conversion.fx,
|
|
251
258
|
conversion.divisor,
|
|
252
259
|
)
|
|
253
|
-
:
|
|
260
|
+
: null;
|
|
261
|
+
const series = usd ? usd.points : prices;
|
|
254
262
|
const returns = conversion ? resolveDatedReturns(series).returns : local.returns;
|
|
255
263
|
const cadence = qualifySharpeCadence(returns, [
|
|
256
264
|
{ symbol: instrument.symbol, exchange: conversion ? "NYSE" : exchange, history: series },
|
|
@@ -264,6 +272,7 @@ export function validateRiskHistory(
|
|
|
264
272
|
};
|
|
265
273
|
return {
|
|
266
274
|
returns,
|
|
275
|
+
...(usd?.bridged.length ? { fxBridged: usd.bridged } : {}),
|
|
267
276
|
asOf: latestClose.date,
|
|
268
277
|
// A missing or stale quote is not a current mark; the latest dated close is.
|
|
269
278
|
closeMark:
|
|
@@ -591,7 +600,7 @@ export async function fetchPortfolioRiskMarket(
|
|
|
591
600
|
historyParams,
|
|
592
601
|
signal ? { signal } : undefined,
|
|
593
602
|
);
|
|
594
|
-
const { latestClose, ...resolved } = validateRiskHistory(
|
|
603
|
+
const { latestClose, fxBridged, ...resolved } = validateRiskHistory(
|
|
595
604
|
response,
|
|
596
605
|
instrument,
|
|
597
606
|
quote,
|
|
@@ -610,7 +619,9 @@ export async function fetchPortfolioRiskMarket(
|
|
|
610
619
|
quote: current,
|
|
611
620
|
currency: "USD",
|
|
612
621
|
closeMark: current ? null : { ...latestClose, currency: "USD" },
|
|
613
|
-
converted: {
|
|
622
|
+
converted: {
|
|
623
|
+
currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf, ...(fxBridged?.length ? { fxBridged } : {}),
|
|
624
|
+
},
|
|
614
625
|
error: null,
|
|
615
626
|
};
|
|
616
627
|
continue;
|
|
@@ -38,10 +38,18 @@ export interface RiskConversion {
|
|
|
38
38
|
export const FX_UNAVAILABLE = "Foreign holdings: daily FX closes unavailable";
|
|
39
39
|
export const FX_NO_PAIR = "Foreign holdings: no daily FX pair for this currency";
|
|
40
40
|
export const FX_STALE = "Foreign holdings: daily FX history is stale";
|
|
41
|
-
export const FX_GAP = "Foreign holdings: daily FX history has gaps on the holding's dates";
|
|
41
|
+
export const FX_GAP = "Foreign holdings: daily FX history has gaps longer than three days on the holding's dates";
|
|
42
42
|
export const FX_DISAGREES = "Foreign holdings: daily FX closes disagree with the current rate";
|
|
43
43
|
export const LOCAL_GAP = "Foreign holdings: local daily history has a gap longer than a market holiday";
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Most FX trading days in a row (weekdays) a pair may go without a daily
|
|
47
|
+
* close and still be bridged with the last close before them: Good Friday and
|
|
48
|
+
* Easter Monday, 25 December, 1 January, which the daily source skips while
|
|
49
|
+
* US sessions continue. A longer run is missing data and rejects the series.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_BRIDGED_FX_DAYS = 3;
|
|
52
|
+
|
|
45
53
|
/**
|
|
46
54
|
* Longest run of calendar days a listing may go without a close while US
|
|
47
55
|
* sessions continue (Golden Week, Lunar New Year, national holidays). A longer
|
|
@@ -70,6 +78,42 @@ export function accountFxReason(currency: string, problem: string): string {
|
|
|
70
78
|
return `${pair} ${tail ?? "daily FX closes unavailable"}`;
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The FX close a date converts at: the pair's own close that day, or the last
|
|
83
|
+
* close before it across at most MAX_BRIDGED_FX_DAYS weekdays without one
|
|
84
|
+
* (`bridged`). Null when the gap is longer, or no close comes before it.
|
|
85
|
+
*/
|
|
86
|
+
export function bridgedFxClose(rateOn: (date: string) => number | undefined, date: string): { rate: number; bridged: boolean } | null {
|
|
87
|
+
let missing = 0;
|
|
88
|
+
for (let time = Date.parse(`${date}T00:00:00Z`), step = 0; step <= 7 + MAX_BRIDGED_FX_DAYS; step++, time -= DAY_MS) {
|
|
89
|
+
const day = new Date(time).toISOString().slice(0, 10);
|
|
90
|
+
const rate = rateOn(day);
|
|
91
|
+
if (rate != null && rate > 0) return { rate, bridged: day !== date };
|
|
92
|
+
const weekday = new Date(time).getUTCDay();
|
|
93
|
+
if (weekday !== 0 && weekday !== 6 && ++missing > MAX_BRIDGED_FX_DAYS) return null;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* "GBP/USD has no daily close on 2025-04-18 and 2025-04-21; the returns across
|
|
100
|
+
* them use the close before": what a conversion bridged (bridgedFxClose), said
|
|
101
|
+
* once per pair. Null when it bridged nothing.
|
|
102
|
+
*/
|
|
103
|
+
export function fxBridgeNote(pair: string, dates: Iterable<string>): string | null {
|
|
104
|
+
const days = [...new Set(dates)].sort();
|
|
105
|
+
if (!days.length) return null;
|
|
106
|
+
const shown = days.length > 4 ? [...days.slice(0, 3), `${days.length - 3} more days`] : days;
|
|
107
|
+
const list = shown.length === 1 ? shown[0]! : `${shown.slice(0, -1).join(", ")} and ${shown.at(-1)!}`;
|
|
108
|
+
return `${pair} has no daily close on ${list}; the returns across ${days.length === 1 ? "it use the close before it" : "them use the close before each"}.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Account returns, and the dates whose FX close was the last one before them (bridgedFxClose). */
|
|
112
|
+
export interface AccountReturns {
|
|
113
|
+
returns: DatedReturn[];
|
|
114
|
+
bridged: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
73
117
|
/**
|
|
74
118
|
* Restates daily USD returns in the account currency at the FX closes of each
|
|
75
119
|
* return's own start and end dates: the holding's USD value is divided by the
|
|
@@ -77,23 +121,29 @@ export function accountFxReason(currency: string, problem: string): string {
|
|
|
77
121
|
* (1 + r) * fx(start) / fx(end) - 1. A listing already converted from its own
|
|
78
122
|
* currency to USD therefore converts at the cross of the two closes of the
|
|
79
123
|
* same date. Returns before the first FX close or after the last are dropped,
|
|
80
|
-
* so the series starts and ends with the FX history
|
|
81
|
-
*
|
|
124
|
+
* so the series starts and ends with the FX history. An endpoint without an FX
|
|
125
|
+
* close converts at the last close before it across a gap of up to
|
|
126
|
+
* MAX_BRIDGED_FX_DAYS FX trading days (Good Friday and Easter Monday), and
|
|
127
|
+
* says so in `bridged`; a longer gap rejects the whole series.
|
|
82
128
|
*/
|
|
83
129
|
export function convertReturnsToAccount(
|
|
84
130
|
returns: readonly DatedReturn[],
|
|
85
131
|
fx: Pick<AccountFx, "currency" | "closes" | "first" | "asOf">,
|
|
86
|
-
):
|
|
132
|
+
): AccountReturns {
|
|
87
133
|
if (!fx.first || !fx.asOf) throw new Error(accountFxReason(fx.currency, FX_UNAVAILABLE));
|
|
88
134
|
const converted: DatedReturn[] = [];
|
|
135
|
+
const bridged = new Set<string>();
|
|
136
|
+
const rateOn = (date: string) => fx.closes[date];
|
|
89
137
|
for (const point of returns) {
|
|
90
138
|
if (point.startDateKey < fx.first || point.dateKey > fx.asOf) continue;
|
|
91
|
-
const start =
|
|
92
|
-
end =
|
|
93
|
-
if (!
|
|
94
|
-
|
|
139
|
+
const start = bridgedFxClose(rateOn, point.startDateKey),
|
|
140
|
+
end = bridgedFxClose(rateOn, point.dateKey);
|
|
141
|
+
if (!start || !end) throw new Error(accountFxReason(fx.currency, FX_GAP));
|
|
142
|
+
if (start.bridged) bridged.add(point.startDateKey);
|
|
143
|
+
if (end.bridged) bridged.add(point.dateKey);
|
|
144
|
+
converted.push({ ...point, value: (1 + point.value) * (start.rate / end.rate) - 1 });
|
|
95
145
|
}
|
|
96
|
-
return converted;
|
|
146
|
+
return { returns: converted, bridged: [...bridged].sort() };
|
|
97
147
|
}
|
|
98
148
|
|
|
99
149
|
/** The pair that restates a listing currency in USD, or null when there is none. */
|
|
@@ -127,7 +177,9 @@ export function validateFxHistory(
|
|
|
127
177
|
* On a US session the listing did not trade (its own holiday), the holding is
|
|
128
178
|
* worth its last local close at that day's FX close: its USD value moves with
|
|
129
179
|
* the currency alone, as it would in the account. A US session with no FX
|
|
130
|
-
* close
|
|
180
|
+
* close converts at the last close before it across a gap of up to
|
|
181
|
+
* MAX_BRIDGED_FX_DAYS FX trading days, listed in `bridged`; a longer FX gap,
|
|
182
|
+
* or a local silence longer than any market holiday, rejects the whole
|
|
131
183
|
* series instead of bridging it. The series ends at the earlier of the last
|
|
132
184
|
* local close and the last FX close, so no close is carried past the data.
|
|
133
185
|
*/
|
|
@@ -135,7 +187,7 @@ export function convertClosesToUsd(
|
|
|
135
187
|
local: readonly { date: string; close: number }[],
|
|
136
188
|
fx: FxCloses,
|
|
137
189
|
divisor: number,
|
|
138
|
-
): PricePoint[] {
|
|
190
|
+
): { points: PricePoint[]; bridged: string[] } {
|
|
139
191
|
const points = [...local]
|
|
140
192
|
.map((point) => ({ date: point.date.slice(0, 10), close: point.close }))
|
|
141
193
|
.sort((left, right) => left.date.localeCompare(right.date));
|
|
@@ -143,6 +195,8 @@ export function convertClosesToUsd(
|
|
|
143
195
|
const start = points[0]!.date > fx.first ? points[0]!.date : fx.first;
|
|
144
196
|
const end = points.at(-1)!.date < fx.asOf ? points.at(-1)!.date : fx.asOf;
|
|
145
197
|
const converted: PricePoint[] = [];
|
|
198
|
+
const bridged: string[] = [];
|
|
199
|
+
const rateOn = (date: string) => fx.closes.get(date);
|
|
146
200
|
let cursor = 0;
|
|
147
201
|
for (let time = Date.parse(`${start}T00:00:00Z`); time <= Date.parse(`${end}T00:00:00Z`); time += DAY_MS) {
|
|
148
202
|
const date = new Date(time).toISOString().slice(0, 10);
|
|
@@ -153,11 +207,12 @@ export function convertClosesToUsd(
|
|
|
153
207
|
const last = points[cursor]!;
|
|
154
208
|
if (last.date > date) continue;
|
|
155
209
|
if (time - Date.parse(`${last.date}T00:00:00Z`) > MAX_LOCAL_CLOSED_DAYS * DAY_MS) throw new Error(LOCAL_GAP);
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
158
|
-
|
|
210
|
+
const fxClose = bridgedFxClose(rateOn, date);
|
|
211
|
+
if (!fxClose) throw new Error(FX_GAP);
|
|
212
|
+
if (fxClose.bridged) bridged.push(date);
|
|
213
|
+
const close = (last.close * fxClose.rate) / divisor;
|
|
159
214
|
converted.push({ date: new Date(time), open: close, high: close, low: close, close });
|
|
160
215
|
}
|
|
161
216
|
if (converted.length < 2) throw new Error("Daily history unavailable");
|
|
162
|
-
return converted;
|
|
217
|
+
return { points: converted, bridged };
|
|
163
218
|
}
|
|
@@ -3,6 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
3
3
|
import { fetchPortfolioRiskMarket, RISK_HISTORY_LIMIT } from "./risk-client";
|
|
4
4
|
import { parsePortfolioRiskEvidence } from "./risk-evidence";
|
|
5
5
|
import { describeCollections } from "../../../portfolio/collection-name";
|
|
6
|
+
import { shellWord } from "../../../utils/shell-word";
|
|
6
7
|
import { formatShocks, parseAssetShifts, parseFxShifts, riskCashLines } from "./risk-book";
|
|
7
8
|
import {
|
|
8
9
|
BASKET_VIEWS,
|
|
@@ -48,6 +49,22 @@ function reportPortfolioName(metadata: Record<string, unknown> | undefined): str
|
|
|
48
49
|
return typeof name === "string" ? name : undefined;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* What a portfolio with nothing to estimate says, as one line: the commands
|
|
54
|
+
* that fill it. Risk is read from lots, so a symbol listed without shares
|
|
55
|
+
* (`portfolio add`) is no holding yet; `portfolio position set` gives it
|
|
56
|
+
* shares, and adds the symbol too when it is not listed.
|
|
57
|
+
*/
|
|
58
|
+
function emptyPortfolioHint(name: string, listed: readonly string[]): string {
|
|
59
|
+
const portfolio = shellWord(name);
|
|
60
|
+
if (listed.length > 0) {
|
|
61
|
+
return `${name} lists ${listed.slice(0, 3).join(", ")}${listed.length > 3 ? ` and ${listed.length - 3} more` : ""} but holds no shares: `
|
|
62
|
+
+ `gloom portfolio position set ${portfolio} ${shellWord(listed[0]!)} 10 150.`;
|
|
63
|
+
}
|
|
64
|
+
return `No holdings in ${name}. Add symbols with gloom portfolio add ${portfolio} AAPL,MSFT, `
|
|
65
|
+
+ `then their shares with gloom portfolio position set ${portfolio} AAPL 10 150.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/** Rows of one view in the compact result; the pane and reports keep every row. */
|
|
52
69
|
const COMPACT_ROW_LIMIT = 100;
|
|
53
70
|
/** The correlation view is every pair of holdings; the compact result keeps the strongest. */
|
|
@@ -260,6 +277,16 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
260
277
|
throw new Error(
|
|
261
278
|
"Imported evidence belongs to a different portfolio or currency",
|
|
262
279
|
);
|
|
280
|
+
// A portfolio with no holdings and no cash has nothing to estimate: say how to fill it, not a table of dashes.
|
|
281
|
+
if (tickers.length === 0 && cash.length === 0 && !evidence) {
|
|
282
|
+
const listed = local.tickers.filter((ticker) => ticker.metadata.portfolios.includes(id)).map((ticker) => ticker.metadata.ticker);
|
|
283
|
+
return {
|
|
284
|
+
sections: [],
|
|
285
|
+
complete: false,
|
|
286
|
+
empty: emptyPortfolioHint(local.portfolio.name, listed),
|
|
287
|
+
metadata: { portfolio: { id, name: local.portfolio.name, currency: local.portfolio.currency } },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
263
290
|
const requestedView = isRiskView(args.options.view) ? args.options.view : null;
|
|
264
291
|
const view: string = requestedView ?? "risk";
|
|
265
292
|
const localOnly =
|