@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,8 +1,9 @@
|
|
|
1
|
-
import type { AlertMetric } from "./metric-model";
|
|
2
|
-
import { validMetricRule } from "./metric-model";
|
|
1
|
+
import type { AlertMetric, RotationQuadrant } from "./metric-model";
|
|
2
|
+
import { ROTATION_QUADRANTS, validMetricRule } from "./metric-model";
|
|
3
3
|
import { readAlertPrice } from "./price";
|
|
4
4
|
import type { AlertCondition } from "./types";
|
|
5
5
|
import type { WizardStep } from "../../../types/plugin";
|
|
6
|
+
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
6
7
|
|
|
7
8
|
export interface MetricAlertInput { symbol: string; condition: AlertCondition; price: number; metric: AlertMetric }
|
|
8
9
|
const direction = (s: string) => /^(above|over|>)$/i.test(s) ? "above" as const : /^(below|under|<)$/i.test(s) ? "below" as const : null;
|
|
@@ -11,10 +12,58 @@ function checked(input: MetricAlertInput): MetricAlertInput {
|
|
|
11
12
|
if (!validMetricRule({ ...input, targetPrice: input.price })) throw new Error("Use a positive multiple, RSI from 0 to 100, and whole MA/RSI periods from 2 to 200 (fast MA shorter than slow MA).");
|
|
12
13
|
return input;
|
|
13
14
|
}
|
|
15
|
+
// RRG's own default benchmark (relative-rotation/model.ts), kept here so the CLI does not load the RRG pane's model.
|
|
16
|
+
const DEFAULT_ROTATION_BENCHMARK = "SPY:NYSEARCA";
|
|
17
|
+
const RELATIVE_USAGE = "Relative strength alerts take the ratio of two listings or an RRG quadrant: alerts add XLK/SPY above 0.25, or alerts add XLE rrg leading [--benchmark SPY].";
|
|
18
|
+
const QUADRANT_USAGE = "An RRG alert names the quadrant it enters: leading, weakening, lagging or improving, such as alerts add XLE rrg leading [--benchmark SPY].";
|
|
19
|
+
/** A benchmark typed as SPY or SPY:NYSEARCA; its listing is confirmed when the alert is added. */
|
|
20
|
+
function benchmarkListing(text: string): { benchmark: string; benchmarkExchange?: string } {
|
|
21
|
+
const parsed = parsePublicTickerKey(text.toUpperCase());
|
|
22
|
+
if (!/^[A-Z0-9.^=-]{1,20}$/.test(parsed.symbol)) throw new Error(`Not a benchmark listing: ${text}.`);
|
|
23
|
+
return { benchmark: parsed.symbol, ...(parsed.exchange ? { benchmarkExchange: parsed.exchange } : {}) };
|
|
24
|
+
}
|
|
25
|
+
const sameListing = (symbol: string, metric: { benchmark: string }) => parsePublicTickerKey(symbol).symbol === metric.benchmark;
|
|
26
|
+
/**
|
|
27
|
+
* `XLK/SPY ratio above 0.25` and `XLE rrg leading [vs SPY]`; undefined for anything else. `XLK/SPY above 0.25`
|
|
28
|
+
* without the word is first a listing (BRK/B, EUR/USD), so `alerts add` reads it as a ratio only once it quotes nothing.
|
|
29
|
+
*/
|
|
30
|
+
function parseRelativeAlert(parts: readonly string[]): MetricAlertInput | undefined {
|
|
31
|
+
const [first = "", second = "", ...rest] = parts;
|
|
32
|
+
const kind = second.toLowerCase();
|
|
33
|
+
if (kind === "rrg") {
|
|
34
|
+
const [quadrant = "", vs, benchmark, ...extra] = rest;
|
|
35
|
+
if (first.includes("/")) throw new Error(`Name the benchmark of an RRG alert after it: alerts add ${first.split("/")[0]!.toUpperCase()} rrg ${quadrant.toLowerCase() || "leading"} --benchmark ${first.split("/")[1]!.toUpperCase()}.`);
|
|
36
|
+
if (!(ROTATION_QUADRANTS as readonly string[]).includes(quadrant.toLowerCase())) throw new Error(QUADRANT_USAGE);
|
|
37
|
+
if (vs !== undefined && (!/^(vs\.?|versus|against)$/i.test(vs) || !benchmark || extra.length > 0)) throw new Error(QUADRANT_USAGE);
|
|
38
|
+
const metric = { kind: "rrg" as const, quadrant: quadrant.toLowerCase() as RotationQuadrant, ...benchmarkListing(benchmark ?? DEFAULT_ROTATION_BENCHMARK) };
|
|
39
|
+
if (sameListing(first, metric)) throw new Error(`An RRG alert compares ${first.toUpperCase()} with another listing: alerts add ${first.toUpperCase()} rrg ${metric.quadrant} --benchmark SPY.`);
|
|
40
|
+
return checked({ symbol: first.toUpperCase(), condition: "crosses", price: 0, metric });
|
|
41
|
+
}
|
|
42
|
+
const pair = /^([^/]+)\/([^/]+)$/.exec(first);
|
|
43
|
+
if (pair && kind === "ratio") {
|
|
44
|
+
const [typed = "", level, ...extra] = rest;
|
|
45
|
+
const condition = direction(typed) ?? (/^(crosses|cross|x)$/i.test(typed) ? "crosses" as const : null);
|
|
46
|
+
const example = `alerts add ${first.toUpperCase()} above 0.25`;
|
|
47
|
+
if (!condition || level === undefined || extra.length > 0) throw new Error(`A ratio alert takes above, below or crosses and a ratio: ${example}.`);
|
|
48
|
+
const metric = { kind: "rs_ratio" as const, ...benchmarkListing(pair[2]!) };
|
|
49
|
+
if (sameListing(pair[1]!, metric)) throw new Error(`A ratio alert divides one listing by another: ${example}.`);
|
|
50
|
+
const price = readAlertPrice(level);
|
|
51
|
+
if (!(price > 0)) throw new Error(`A ratio alert takes a positive ratio: ${example}.`);
|
|
52
|
+
return checked({ symbol: pair[1]!.toUpperCase(), condition, price, metric });
|
|
53
|
+
}
|
|
54
|
+
// `XLE rs above 100`, `XLE vs SPY above 1`, `XLE relative strength leading`: a relative alert typed another way.
|
|
55
|
+
if (/^(rs|rsr|rs-?ratio|rs_ratio|rs-?momentum|rel|relative|rotation|quadrant|ratio|vs\.?|versus)$/i.test(kind)
|
|
56
|
+
&& rest.some((word) => /^(above|below|over|under|crosses|cross|>|<|leading|weakening|lagging|improving|rrg)$/i.test(word))) {
|
|
57
|
+
throw new Error(RELATIVE_USAGE);
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
14
61
|
/** Parses both compact CLI notation and the words traders type, without swallowing malformed metrics as calendar events. */
|
|
15
62
|
export function parseMetricAlert(input: string): MetricAlertInput | null {
|
|
16
63
|
const words = input.trim().replace(/\s+/g, " ");
|
|
17
64
|
const parts = words.split(" ");
|
|
65
|
+
const relative = parseRelativeAlert(parts);
|
|
66
|
+
if (relative) return relative;
|
|
18
67
|
if (!/^(p\/?e|p\/?s|rsi(?:\(|$)|ma(?:\(|$)|\d+-day|closes?$)/i.test(parts[1] ?? "")) return null;
|
|
19
68
|
const ratio = /^(\S+) (p\/?e|p\/?s) (above|below|over|under|>|<) (\S+)$/i.exec(words);
|
|
20
69
|
if (ratio) return checked({ symbol: ratio[1]!.toUpperCase(), metric: { kind: ratio[2]!.toLowerCase().replace("/", "") as "pe" | "ps" }, condition: direction(ratio[3]!)!, price: readAlertPrice(ratio[4]!) });
|
|
@@ -27,8 +76,17 @@ export function parseMetricAlert(input: string): MetricAlertInput | null {
|
|
|
27
76
|
if (close) return checked({ symbol: close[1]!.toUpperCase(), metric: { kind: "close_ma", period: Number(close[3]) }, condition: direction(close[2]!)!, price: 0 });
|
|
28
77
|
throw new Error(USAGE);
|
|
29
78
|
}
|
|
79
|
+
export const benchmarkKey = (metric: { benchmark: string; benchmarkExchange?: string }) => `${metric.benchmark}${metric.benchmarkExchange ? `:${metric.benchmarkExchange}` : ""}`;
|
|
80
|
+
/** A relative alert as `alerts add` takes it, for editing: `XLK/SPY ratio above 0.25`, `XLE rrg leading vs SPY`. */
|
|
81
|
+
export function relativeAlertText(input: { symbol: string; condition: AlertCondition; targetPrice: number; metric?: AlertMetric }): string | null {
|
|
82
|
+
const m = input.metric;
|
|
83
|
+
if (m?.kind === "rs_ratio") return `${input.symbol}/${m.benchmark} ratio ${input.condition} ${input.targetPrice}`;
|
|
84
|
+
return m?.kind === "rrg" ? `${input.symbol} rrg ${m.quadrant} vs ${m.benchmark}` : null;
|
|
85
|
+
}
|
|
30
86
|
export function metricShortcut(input: MetricAlertInput): Record<string, string> {
|
|
31
87
|
const m = input.metric;
|
|
88
|
+
if (m.kind === "rs_ratio") return { symbol: input.symbol, kind: m.kind, "rs_ratio:benchmark": benchmarkKey(m), "rs_ratio:condition": input.condition, "rs_ratio:target": String(input.price) };
|
|
89
|
+
if (m.kind === "rrg") return { symbol: input.symbol, kind: m.kind, "rrg:quadrant": m.quadrant, "rrg:benchmark": benchmarkKey(m) };
|
|
32
90
|
return { symbol: input.symbol, kind: m.kind, [`${m.kind}:condition`]: m.kind === "ma_cross" ? m.direction : input.condition,
|
|
33
91
|
[`${m.kind}:target`]: String(input.price), ...("period" in m ? { [`${m.kind}:period`]: String(m.period) } : {}),
|
|
34
92
|
...(m.kind === "ma_cross" ? { "ma_cross:slowPeriod": String(m.slowPeriod) } : {}) };
|
|
@@ -38,6 +96,11 @@ export function metricFormInput(values: Record<string, string>): MetricAlertInpu
|
|
|
38
96
|
if (!kind || kind === "price") return null;
|
|
39
97
|
const symbol = values.symbol?.trim().toUpperCase();
|
|
40
98
|
if (!symbol) return null;
|
|
99
|
+
if (kind === "rs_ratio") {
|
|
100
|
+
const benchmark = values["rs_ratio:benchmark"]?.trim();
|
|
101
|
+
return benchmark ? parseMetricAlert(`${symbol}/${benchmark} ratio ${values["rs_ratio:condition"] ?? "above"} ${values["rs_ratio:target"] ?? ""}`) : null;
|
|
102
|
+
}
|
|
103
|
+
if (kind === "rrg") return parseMetricAlert(`${symbol} rrg ${values["rrg:quadrant"] ?? "leading"} vs ${values["rrg:benchmark"]?.trim() || DEFAULT_ROTATION_BENCHMARK}`);
|
|
41
104
|
const condition = values[`${kind}:condition`] ?? "below";
|
|
42
105
|
const window = values[`${kind}:period`] ?? (kind === "rsi" ? "14" : kind === "ma_cross" ? "50" : "200");
|
|
43
106
|
const text = kind === "ma_cross" ? `${symbol} ma(${window}) crosses ${condition} ma(${values["ma_cross:slowPeriod"] ?? "200"})`
|
|
@@ -48,8 +111,18 @@ export function metricFormInput(values: Record<string, string>): MetricAlertInpu
|
|
|
48
111
|
export const METRIC_CHOICES = [
|
|
49
112
|
{ value: "price", label: "Price" }, { value: "pe", label: "P/E (TTM)" }, { value: "ps", label: "P/S (TTM)" },
|
|
50
113
|
{ value: "rsi", label: "RSI" }, { value: "ma_cross", label: "MA crossover" }, { value: "close_ma", label: "Close vs MA" },
|
|
114
|
+
{ value: "rs_ratio", label: "Ratio to benchmark" }, { value: "rrg", label: "RRG quadrant" },
|
|
115
|
+
];
|
|
116
|
+
const RELATIVE_FIELDS: WizardStep[] = [
|
|
117
|
+
{ key: "rs_ratio:benchmark", label: "Divided by", type: "text", placeholder: "SPY", dependsOn: { key: "kind", value: "rs_ratio" } },
|
|
118
|
+
{ key: "rs_ratio:condition", label: "Condition", type: "select", defaultValue: "above", dependsOn: { key: "kind", value: "rs_ratio" },
|
|
119
|
+
options: [{ label: "Above", value: "above" }, { label: "Below", value: "below" }, { label: "Crosses", value: "crosses" }] },
|
|
120
|
+
{ key: "rs_ratio:target", label: "Ratio", type: "number", placeholder: "0.25", dependsOn: { key: "kind", value: "rs_ratio" } },
|
|
121
|
+
{ key: "rrg:quadrant", label: "Enters", type: "select", defaultValue: "leading", dependsOn: { key: "kind", value: "rrg" },
|
|
122
|
+
options: ROTATION_QUADRANTS.map((value) => ({ label: `${value[0]!.toUpperCase()}${value.slice(1)}`, value })) },
|
|
123
|
+
{ key: "rrg:benchmark", label: "Benchmark", type: "text", defaultValue: "SPY", placeholder: "SPY", dependsOn: { key: "kind", value: "rrg" } },
|
|
51
124
|
];
|
|
52
|
-
export const metricWizardFields: WizardStep[] = METRIC_CHOICES.filter(x => x.value !== "price").flatMap(({ value: kind }) => {
|
|
125
|
+
export const metricWizardFields: WizardStep[] = [...METRIC_CHOICES.filter(x => x.value !== "price" && x.value !== "rs_ratio" && x.value !== "rrg").flatMap(({ value: kind }) => {
|
|
53
126
|
const dependsOn = { key: "kind", value: kind };
|
|
54
127
|
return [
|
|
55
128
|
...(["rsi", "ma_cross", "close_ma"].includes(kind) ? [{ key: `${kind}:period`, label: kind === "ma_cross" ? "Fast MA (days)" : kind === "rsi" ? "RSI period" : "MA (days)", type: "number" as const, defaultValue: kind === "rsi" ? "14" : kind === "ma_cross" ? "50" : "200", dependsOn }] : []),
|
|
@@ -58,4 +131,4 @@ export const metricWizardFields: WizardStep[] = METRIC_CHOICES.filter(x => x.val
|
|
|
58
131
|
...(kind === "ma_cross" ? [{ key: "ma_cross:slowPeriod", label: "Slow MA (days)", type: "number" as const, defaultValue: "200", dependsOn }]
|
|
59
132
|
: kind === "close_ma" ? [] : [{ key: `${kind}:target`, label: kind === "rsi" ? "RSI level" : "Target multiple", type: "number" as const, placeholder: kind === "rsi" ? "30" : kind === "pe" ? "25" : "8", dependsOn }]),
|
|
60
133
|
];
|
|
61
|
-
});
|
|
134
|
+
}), ...RELATIVE_FIELDS];
|
|
@@ -5,7 +5,9 @@ import { comparablePriceEarnings } from "../../../utils/price-earnings";
|
|
|
5
5
|
import { canonicalExchange } from "../../../utils/exchanges";
|
|
6
6
|
import { getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
|
|
7
7
|
import { barSessionDay } from "../../../market-data/market/period-return";
|
|
8
|
-
import {
|
|
8
|
+
import { finalDailyBars } from "../../../time-series/session-bar";
|
|
9
|
+
import { getPricePointTimestamp } from "../../../utils/price-history";
|
|
10
|
+
import { DAILY_CLOSE_SETTLE_MS, dailyMetricReading, latestSettledClose, DailyMetricCache, isDailyMetric, isRelativeMetric, type MetricReading } from "./metric-model";
|
|
9
11
|
import type { AlertRule } from "./types";
|
|
10
12
|
|
|
11
13
|
// Keep valuation fixtures in step with the delivery evaluator and the research panes.
|
|
@@ -21,12 +23,43 @@ export function valuationAlertValue(kind: "pe" | "ps", data: TickerFinancials):
|
|
|
21
23
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
const US_CALENDAR = "Daily technical alerts need a published US listing calendar; delivery is unavailable for this listing.";
|
|
27
|
+
/** Why a rule's listings cannot be judged on settled closes: a benchmark is held to the same US calendar as the listing. */
|
|
28
|
+
export function metricAvailabilityError(rule: AlertRule, now = Date.now()): string | null {
|
|
25
29
|
if (!rule.metric || !isDailyMetric(rule.metric)) return null;
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const today = new Date(now).toISOString().slice(0, 10);
|
|
31
|
+
if (!getPublishedUsEquitySession(canonicalExchange(rule.exchange ?? ""), today)) return US_CALENDAR;
|
|
32
|
+
return isRelativeMetric(rule.metric) && !getPublishedUsEquitySession(canonicalExchange(rule.metric.benchmarkExchange ?? ""), today)
|
|
33
|
+
? `Relative strength alerts compare two US listings; ${rule.metric.benchmark} has no published US listing calendar here.` : null;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The first close a daily technical rule set at `now` is judged on, and when:
|
|
38
|
+
* the next session to close after it, read once the close has settled
|
|
39
|
+
* (DAILY_CLOSE_SETTLE_MS). `weekly` (an RRG quadrant) waits for the last
|
|
40
|
+
* session of that Monday-Friday week, its weekly close. Null when the listing
|
|
41
|
+
* has no published calendar.
|
|
42
|
+
*/
|
|
43
|
+
export function firstDailyCheck(exchange: string | undefined, now = Date.now(), weekly = false): { day: string; at: number } | null {
|
|
44
|
+
const venue = canonicalExchange(exchange ?? "");
|
|
45
|
+
let first: { day: string; close: number } | null = null;
|
|
46
|
+
for (let offset = 0; offset < 15; offset++) {
|
|
47
|
+
const time = now + offset * 86_400_000;
|
|
48
|
+
const day = new Date(time).toISOString().slice(0, 10);
|
|
49
|
+
// The weekly close is the last session before the weekend after the first close.
|
|
50
|
+
if (first && new Date(time).getUTCDay() === 6) break;
|
|
51
|
+
const session = getPublishedUsEquitySession(venue, day);
|
|
52
|
+
if (!session) return null;
|
|
53
|
+
if (session.kind !== "session" || session.close <= now) continue;
|
|
54
|
+
first = { day, close: session.close };
|
|
55
|
+
if (!weekly) break;
|
|
56
|
+
}
|
|
57
|
+
return first ? { day: first.day, at: first.close + DAILY_CLOSE_SETTLE_MS } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The bar size the daily rules read, the one `history` and the charts ask for. */
|
|
61
|
+
const DAILY_BARS = "1d";
|
|
62
|
+
|
|
30
63
|
const dailyCaches = new WeakMap<AssetDataProvider, DailyMetricCache>();
|
|
31
64
|
|
|
32
65
|
/** A pass shares each listing's reads; there is no second provider or quote subscription. */
|
|
@@ -48,23 +81,31 @@ export function metricReadings(provider: AssetDataProvider, now = Date.now()) {
|
|
|
48
81
|
}
|
|
49
82
|
const exchange = canonicalExchange(rule.exchange ?? "");
|
|
50
83
|
const session = (day: string) => getPublishedUsEquitySession(exchange, day);
|
|
51
|
-
|
|
84
|
+
const unavailable = metricAvailabilityError(rule, now);
|
|
85
|
+
if (unavailable) throw new Error(unavailable);
|
|
52
86
|
const latest = latestSettledClose(now, session);
|
|
53
87
|
const ruleKey = JSON.stringify([rule.id, rule.armedAt ?? rule.createdAt, key, rule.condition, rule.targetPrice, m]);
|
|
54
88
|
return daily.read(ruleKey, latest.day, now, async () => {
|
|
55
|
-
const load = async () => {
|
|
89
|
+
const load = async (symbol: string, venue: string) => {
|
|
56
90
|
// An explicit end at the settled session avoids today's forming-bar quote read.
|
|
57
91
|
const end = new Date(`${latest.day}T23:59:59Z`);
|
|
58
92
|
const start = new Date(Date.parse(latest.day) - 366 * 86_400_000);
|
|
59
|
-
if (provider.getDetailedPriceHistoryWithMetadata) return (await provider.getDetailedPriceHistoryWithMetadata(
|
|
60
|
-
if (provider.getDetailedPriceHistory) return provider.getDetailedPriceHistory(
|
|
93
|
+
if (provider.getDetailedPriceHistoryWithMetadata) return (await provider.getDetailedPriceHistoryWithMetadata(symbol, venue, start, end, DAILY_BARS, { cacheMode: "current" })).points;
|
|
94
|
+
if (provider.getDetailedPriceHistory) return provider.getDetailedPriceHistory(symbol, venue, start, end, DAILY_BARS, { cacheMode: "current" });
|
|
61
95
|
throw new Error("Completed daily history is unavailable from this connection.");
|
|
62
96
|
};
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
const completed = async (symbol: string, venue: string) => {
|
|
98
|
+
const listing = `${symbol}|${venue}`;
|
|
99
|
+
const request = history.get(listing) ?? load(symbol, venue);
|
|
100
|
+
history.set(listing, request);
|
|
101
|
+
// Final as `history` shows it: a close it prints without `partial` is one the rule may judge.
|
|
102
|
+
// Bars read back from the saved cache carry their dates as text.
|
|
103
|
+
return finalDailyBars(await request, { now, venue }).map(point => ({ date: barSessionDay(getPricePointTimestamp(point), venue), close: point.close }));
|
|
104
|
+
};
|
|
105
|
+
const bars = await completed(rule.symbol, exchange);
|
|
106
|
+
// RS ratios and RRG quadrants read the benchmark's own completed bars, never a quote.
|
|
107
|
+
const benchmark = isRelativeMetric(m) ? await completed(m.benchmark, canonicalExchange(m.benchmarkExchange ?? "")) : undefined;
|
|
108
|
+
return dailyMetricReading(rule, bars, now, session, benchmark);
|
|
68
109
|
});
|
|
69
110
|
};
|
|
70
111
|
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// Keep this model identical to the delivery model; the parity fixture checks both copies.
|
|
2
2
|
/** Synced separately from price rules so older clients cannot judge a ratio as a price. */
|
|
3
3
|
export const METRIC_ALERTS_KEY = "metricAlerts";
|
|
4
|
+
/** Relative strength has a list of its own too: a client that reads metricAlerts keeps none of it once it holds a measure it does not know. */
|
|
5
|
+
export const RELATIVE_ALERTS_KEY = "relativeAlerts";
|
|
6
|
+
export const ROTATION_QUADRANTS = ["leading", "weakening", "lagging", "improving"] as const;
|
|
7
|
+
export type RotationQuadrant = typeof ROTATION_QUADRANTS[number];
|
|
4
8
|
export type AlertMetric =
|
|
5
9
|
| { kind: "pe" | "ps" }
|
|
6
10
|
| { kind: "rsi"; period: number }
|
|
7
11
|
| { kind: "close_ma"; period: number }
|
|
8
|
-
| { kind: "ma_cross"; period: number; slowPeriod: number; direction: "above" | "below" }
|
|
12
|
+
| { kind: "ma_cross"; period: number; slowPeriod: number; direction: "above" | "below" }
|
|
13
|
+
/** The listing's settled daily close divided by the benchmark's on the same session. */
|
|
14
|
+
| { kind: "rs_ratio"; benchmark: string; benchmarkExchange?: string }
|
|
15
|
+
/** The weekly RRG quadrant against the benchmark, entered on a published weekly close. */
|
|
16
|
+
| { kind: "rrg"; quadrant: RotationQuadrant; benchmark: string; benchmarkExchange?: string };
|
|
9
17
|
export interface MetricReading {
|
|
10
18
|
value: number;
|
|
11
19
|
target: number;
|
|
@@ -25,6 +33,8 @@ export interface MetricRule {
|
|
|
25
33
|
}
|
|
26
34
|
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
27
35
|
const period = (value: unknown): value is number => finite(value) && Number.isInteger(value) && value >= 2 && value <= 200;
|
|
36
|
+
const listing = (value: unknown): value is string => typeof value === "string" && /^[A-Z0-9.^=-]{1,20}$/.test(value);
|
|
37
|
+
const venue = (value: unknown) => value === undefined || (typeof value === "string" && /^[A-Z0-9._-]{1,24}$/.test(value));
|
|
28
38
|
|
|
29
39
|
export function readAlertMetric(raw: unknown): AlertMetric | null {
|
|
30
40
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -35,13 +45,26 @@ export function readAlertMetric(raw: unknown): AlertMetric | null {
|
|
|
35
45
|
&& (m.direction === "above" || m.direction === "below")) {
|
|
36
46
|
return { kind: m.kind, period: m.period, slowPeriod: m.slowPeriod, direction: m.direction };
|
|
37
47
|
}
|
|
48
|
+
if ((m.kind === "rs_ratio" || m.kind === "rrg") && listing(m.benchmark) && venue(m.benchmarkExchange)) {
|
|
49
|
+
const benchmark = { benchmark: m.benchmark, ...(m.benchmarkExchange ? { benchmarkExchange: m.benchmarkExchange as string } : {}) };
|
|
50
|
+
if (m.kind === "rs_ratio") return { kind: m.kind, ...benchmark };
|
|
51
|
+
if (ROTATION_QUADRANTS.includes(m.quadrant as RotationQuadrant)) return { kind: m.kind, quadrant: m.quadrant as RotationQuadrant, ...benchmark };
|
|
52
|
+
}
|
|
38
53
|
return null;
|
|
39
54
|
}
|
|
55
|
+
export function isRelativeMetric(metric: AlertMetric): metric is Extract<AlertMetric, { benchmark: string }> {
|
|
56
|
+
return metric.kind === "rs_ratio" || metric.kind === "rrg";
|
|
57
|
+
}
|
|
58
|
+
/** The synced list a rule belongs in. */
|
|
59
|
+
export function metricAlertsKey(metric: AlertMetric): string {
|
|
60
|
+
return isRelativeMetric(metric) ? RELATIVE_ALERTS_KEY : METRIC_ALERTS_KEY;
|
|
61
|
+
}
|
|
40
62
|
|
|
41
63
|
export function validMetricRule(rule: Pick<MetricRule, "metric" | "condition" | "targetPrice">): boolean {
|
|
42
64
|
const m = readAlertMetric(rule.metric);
|
|
43
65
|
if (!m || !finite(rule.targetPrice)) return false;
|
|
44
|
-
if (m.kind === "ma_cross") return rule.condition === "crosses" && rule.targetPrice === 0;
|
|
66
|
+
if (m.kind === "ma_cross" || m.kind === "rrg") return rule.condition === "crosses" && rule.targetPrice === 0;
|
|
67
|
+
if (m.kind === "rs_ratio") return ["above", "below", "crosses"].includes(rule.condition) && rule.targetPrice > 0;
|
|
45
68
|
if (rule.condition !== "above" && rule.condition !== "below") return false;
|
|
46
69
|
return m.kind === "close_ma" ? rule.targetPrice === 0
|
|
47
70
|
: m.kind === "rsi" ? rule.targetPrice >= 0 && rule.targetPrice <= 100 : rule.targetPrice > 0;
|
|
@@ -51,18 +74,62 @@ export function metricName(metric: AlertMetric): string {
|
|
|
51
74
|
if (metric.kind === "pe") return "P/E (TTM)";
|
|
52
75
|
if (metric.kind === "ps") return "P/S (TTM)";
|
|
53
76
|
if (metric.kind === "rsi") return `RSI(${metric.period})`;
|
|
77
|
+
if (metric.kind === "rs_ratio") return `Ratio to ${metric.benchmark}`;
|
|
78
|
+
if (metric.kind === "rrg") return `RRG vs ${metric.benchmark}`;
|
|
54
79
|
return metric.kind === "ma_cross" ? `${metric.period}-day MA` : "Close";
|
|
55
80
|
}
|
|
81
|
+
const quadrantName = (quadrant: RotationQuadrant) => `${quadrant[0]!.toUpperCase()}${quadrant.slice(1)}`;
|
|
56
82
|
export function metricDescription(rule: MetricRule & { symbol: string }): string {
|
|
57
83
|
const m = rule.metric!;
|
|
84
|
+
const basis = closeBasis(m);
|
|
85
|
+
const suffix = basis ? ` · on ${basis}` : "";
|
|
86
|
+
if (m.kind === "rs_ratio") return `${rule.symbol} ratio to ${m.benchmark} ${rule.condition} ${rule.targetPrice}${suffix}`;
|
|
87
|
+
if (m.kind === "rrg") return `${rule.symbol} ${metricName(m)} enters ${quadrantName(m.quadrant)}${suffix}`;
|
|
58
88
|
const target = m.kind === "ma_cross" ? `${m.slowPeriod}-day MA`
|
|
59
89
|
: m.kind === "close_ma" ? `${m.period}-day MA` : String(rule.targetPrice);
|
|
60
90
|
const condition = m.kind === "ma_cross" ? `crosses ${m.direction}` : rule.condition;
|
|
61
|
-
return `${rule.symbol} ${metricName(m)} ${condition} ${target}${
|
|
91
|
+
return `${rule.symbol} ${metricName(m)} ${condition} ${target}${suffix}`;
|
|
62
92
|
}
|
|
93
|
+
/** Judged on settled closes only, never a forming session; RRG quadrants on weekly ones. */
|
|
63
94
|
export function isDailyMetric(metric: AlertMetric): boolean {
|
|
64
95
|
return metric.kind !== "pe" && metric.kind !== "ps";
|
|
65
96
|
}
|
|
97
|
+
export function closeBasis(metric: AlertMetric): "daily close" | "weekly close" | null {
|
|
98
|
+
return metric.kind === "rrg" ? "weekly close" : isDailyMetric(metric) ? "daily close" : null;
|
|
99
|
+
}
|
|
100
|
+
/** The reading as alerts state it: 31.2x, 0.2512, or Leading (strength 101.23, momentum 100.40) as RRG words them. */
|
|
101
|
+
export function metricReadingText(metric: AlertMetric, reading: Pick<MetricReading, "value" | "target">): string {
|
|
102
|
+
if (metric.kind === "rrg") {
|
|
103
|
+
return `${rotationQuadrant(reading.value, reading.target) ?? "No quadrant"} (strength ${reading.value.toFixed(2)}, momentum ${reading.target.toFixed(2)})`;
|
|
104
|
+
}
|
|
105
|
+
return `${Number(reading.value.toFixed(4))}${metric.kind === "pe" || metric.kind === "ps" ? "x" : ""}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type RotationQuadrantName = "Leading" | "Weakening" | "Lagging" | "Improving" | "Neutral";
|
|
109
|
+
export function rotationQuadrant(strength: number | null | undefined, momentum: number | null | undefined): RotationQuadrantName | null {
|
|
110
|
+
if (strength == null || momentum == null || !finite(strength) || !finite(momentum)) return null;
|
|
111
|
+
if (Math.abs(strength - 100) < 1e-9 || Math.abs(momentum - 100) < 1e-9) return "Neutral";
|
|
112
|
+
return strength > 100 ? momentum > 100 ? "Leading" : "Weakening" : momentum > 100 ? "Improving" : "Lagging";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Weekly strength (RS-Ratio: 100 x the price ratio over its 13-week mean) and momentum (RS-Momentum: 100 x
|
|
116
|
+
* strength over strength four weeks earlier), as RRG plots them. A missing week breaks both until 13 and 17 contiguous weeks.
|
|
117
|
+
*/
|
|
118
|
+
export function rotationSeries(ratios: readonly (number | null)[]): { strength: number | null; momentum: number | null }[] {
|
|
119
|
+
const strengths = ratios.map((ratio, index) => {
|
|
120
|
+
const window = ratios.slice(Math.max(0, index - 12), index + 1);
|
|
121
|
+
if (ratio == null || window.length !== 13 || window.some((value) => value == null)) return null;
|
|
122
|
+
const average = (window as number[]).reduce((sum, value) => sum + value, 0) / 13;
|
|
123
|
+
return (100 * ratio) / average;
|
|
124
|
+
});
|
|
125
|
+
return strengths.map((strength, index) => {
|
|
126
|
+
const previous = index >= 4 ? (strengths[index - 4] ?? null) : null;
|
|
127
|
+
const contiguous = index >= 4 && strengths.slice(index - 4, index + 1).every((value) => value != null);
|
|
128
|
+
return { strength, momentum: strength != null && previous != null && contiguous ? (100 * strength) / previous : null };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Weeks a quadrant change needs: 17 for this week's momentum, and the week before it. */
|
|
132
|
+
export const ROTATION_ALERT_WEEKS = 18;
|
|
66
133
|
|
|
67
134
|
/** Wilder's initial average of N changes, then (previous * (N - 1) + change) / N. */
|
|
68
135
|
function rsi(closes: readonly number[], window: number): number[] {
|
|
@@ -110,24 +177,96 @@ export class DailyMetricCache {
|
|
|
110
177
|
}
|
|
111
178
|
}
|
|
112
179
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
bars: readonly { date: string; close: number }[],
|
|
116
|
-
now: number,
|
|
117
|
-
session: (day: string) => DailySession | null,
|
|
118
|
-
): MetricReading {
|
|
119
|
-
const m = rule.metric!;
|
|
120
|
-
if (!isDailyMetric(m) || !("period" in m)) throw new Error("Expected a daily technical alert.");
|
|
121
|
-
const latest = latestSettledClose(now, session);
|
|
180
|
+
type DailyBar = { date: string; close: number };
|
|
181
|
+
function closesThrough(bars: readonly DailyBar[], through: string): Map<string, number> {
|
|
122
182
|
const byDay = new Map<string, number>();
|
|
123
183
|
for (const bar of bars) {
|
|
124
184
|
const day = bar.date.slice(0, 10);
|
|
125
|
-
if (day >
|
|
185
|
+
if (day > through) continue;
|
|
126
186
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !finite(bar.close) || bar.close <= 0 || byDay.has(day)) {
|
|
127
187
|
throw new Error("Daily history contains an invalid or duplicate bar.");
|
|
128
188
|
}
|
|
129
189
|
byDay.set(day, bar.close);
|
|
130
190
|
}
|
|
191
|
+
return byDay;
|
|
192
|
+
}
|
|
193
|
+
const isoDay = (time: number) => new Date(time).toISOString().slice(0, 10);
|
|
194
|
+
function sessionBefore(day: string, session: (day: string) => DailySession | null): string {
|
|
195
|
+
for (let offset = 1; offset < 15; offset++) {
|
|
196
|
+
const previous = isoDay(Date.parse(day) - offset * DAY);
|
|
197
|
+
const s = session(previous);
|
|
198
|
+
if (!s) throw new Error("Daily close calendar unavailable for part of the history.");
|
|
199
|
+
if (s.kind === "session") return previous;
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`No session before ${day}.`);
|
|
202
|
+
}
|
|
203
|
+
/** The last session of each Monday-Friday week, oldest first, ending with the newest week whose close has settled. */
|
|
204
|
+
export function settledWeekCloses(now: number, session: (day: string) => DailySession | null, count: number): { day: string; at: number }[] {
|
|
205
|
+
const latest = latestSettledClose(now, session);
|
|
206
|
+
const close = (friday: number) => {
|
|
207
|
+
for (let offset = 0; offset < 5; offset++) {
|
|
208
|
+
const day = isoDay(friday - offset * DAY);
|
|
209
|
+
const s = session(day);
|
|
210
|
+
if (!s) throw new Error("Daily close calendar unavailable for part of the history.");
|
|
211
|
+
if (s.kind === "session") return { day, at: s.close };
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`No session in the week ending ${isoDay(friday)}.`);
|
|
214
|
+
};
|
|
215
|
+
const time = Date.parse(latest.day);
|
|
216
|
+
let friday = time + ((5 - new Date(time).getUTCDay() + 7) % 7) * DAY;
|
|
217
|
+
// A week whose last session is still ahead or forming has no weekly close yet.
|
|
218
|
+
if (close(friday).day !== latest.day) friday -= 7 * DAY;
|
|
219
|
+
return Array.from({ length: count }, (_, index) => close(friday - (count - 1 - index) * 7 * DAY));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Both listings' own closes on the same sessions; a session either lacks is never bridged from another day. */
|
|
223
|
+
function relativeMetricReading(
|
|
224
|
+
rule: MetricRule,
|
|
225
|
+
m: Extract<AlertMetric, { benchmark: string }>,
|
|
226
|
+
bars: readonly DailyBar[],
|
|
227
|
+
benchmarkBars: readonly DailyBar[],
|
|
228
|
+
now: number,
|
|
229
|
+
session: (day: string) => DailySession | null,
|
|
230
|
+
): MetricReading {
|
|
231
|
+
const latest = latestSettledClose(now, session);
|
|
232
|
+
const own = closesThrough(bars, latest.day), reference = closesThrough(benchmarkBars, latest.day);
|
|
233
|
+
const ratio = (day: string) => {
|
|
234
|
+
const close = own.get(day), base = reference.get(day);
|
|
235
|
+
if (close == null || base == null) {
|
|
236
|
+
if (day === latest.day) throw new Error(`Waiting for the ${latest.day} ${m.kind === "rrg" ? "weekly" : "daily"} close${close == null ? "" : ` of ${m.benchmark}`}.`);
|
|
237
|
+
throw new Error(`Daily history is missing ${day}${close == null ? "" : ` for ${m.benchmark}`}.`);
|
|
238
|
+
}
|
|
239
|
+
return close / base;
|
|
240
|
+
};
|
|
241
|
+
if (m.kind === "rs_ratio") {
|
|
242
|
+
const value = ratio(latest.day), previousValue = ratio(sessionBefore(latest.day, session));
|
|
243
|
+
return { value, target: rule.targetPrice, previousValue, previousTarget: rule.targetPrice, day: latest.day, at: latest.at };
|
|
244
|
+
}
|
|
245
|
+
const weeks = settledWeekCloses(now, session, ROTATION_ALERT_WEEKS);
|
|
246
|
+
const [before, last] = rotationSeries(weeks.map((week) => ratio(week.day))).slice(-2);
|
|
247
|
+
if (before?.strength == null || before.momentum == null || last?.strength == null || last.momentum == null) {
|
|
248
|
+
throw new Error(`Need ${ROTATION_ALERT_WEEKS} contiguous weekly closes.`);
|
|
249
|
+
}
|
|
250
|
+
const week = weeks.at(-1)!;
|
|
251
|
+
return { value: last.strength, target: last.momentum, previousValue: before.strength, previousTarget: before.momentum, day: week.day, at: week.at };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** RS ratios and RRG quadrants take the benchmark's completed daily bars as well. */
|
|
255
|
+
export function dailyMetricReading(
|
|
256
|
+
rule: MetricRule,
|
|
257
|
+
bars: readonly DailyBar[],
|
|
258
|
+
now: number,
|
|
259
|
+
session: (day: string) => DailySession | null,
|
|
260
|
+
benchmarkBars?: readonly DailyBar[],
|
|
261
|
+
): MetricReading {
|
|
262
|
+
const m = rule.metric!;
|
|
263
|
+
if (m.kind === "rs_ratio" || m.kind === "rrg") {
|
|
264
|
+
if (!benchmarkBars) throw new Error(`Completed daily history for ${m.benchmark} is required.`);
|
|
265
|
+
return relativeMetricReading(rule, m, bars, benchmarkBars, now, session);
|
|
266
|
+
}
|
|
267
|
+
if (!isDailyMetric(m) || !("period" in m)) throw new Error("Expected a daily technical alert.");
|
|
268
|
+
const latest = latestSettledClose(now, session);
|
|
269
|
+
const byDay = closesThrough(bars, latest.day);
|
|
131
270
|
const days = [...byDay.keys()].sort();
|
|
132
271
|
if (days.at(-1) !== latest.day) throw new Error(`Waiting for the ${latest.day} daily close.`);
|
|
133
272
|
const needed = m.kind === "close_ma" ? m.period : (m.kind === "ma_cross" ? m.slowPeriod : m.period) + 1;
|
|
@@ -165,9 +304,19 @@ export function metricTrigger(rule: MetricRule, reading: MetricReading): "above"
|
|
|
165
304
|
if (isDailyMetric(rule.metric!)) {
|
|
166
305
|
if (!reading.day || reading.day <= (rule.lastDailyClose ?? "") || reading.at <= (rule.armedAt ?? rule.createdAt ?? Infinity)) return null;
|
|
167
306
|
}
|
|
307
|
+
const m = rule.metric!;
|
|
308
|
+
if (m.kind === "rrg") {
|
|
309
|
+
// Fires on entering the quadrant, not on a week already inside it.
|
|
310
|
+
const before = rotationQuadrant(reading.previousValue, reading.previousTarget)?.toLowerCase();
|
|
311
|
+
return before && before !== m.quadrant && rotationQuadrant(reading.value, reading.target)?.toLowerCase() === m.quadrant ? "up" : null;
|
|
312
|
+
}
|
|
168
313
|
if (rule.condition === "above") return reading.value > reading.target ? "above" : null;
|
|
169
314
|
if (rule.condition === "below") return reading.value < reading.target ? "below" : null;
|
|
170
|
-
|
|
315
|
+
if (m.kind === "rs_ratio") {
|
|
316
|
+
if (!finite(reading.previousValue)) return null;
|
|
317
|
+
if (reading.previousValue <= reading.target && reading.value > reading.target) return "up";
|
|
318
|
+
return reading.previousValue >= reading.target && reading.value < reading.target ? "down" : null;
|
|
319
|
+
}
|
|
171
320
|
if (m.kind !== "ma_cross" || !finite(reading.previousValue) || !finite(reading.previousTarget)) return null;
|
|
172
321
|
if (m.direction === "above" && reading.previousValue <= reading.previousTarget && reading.value > reading.target) return "up";
|
|
173
322
|
if (m.direction === "below" && reading.previousValue >= reading.previousTarget && reading.value < reading.target) return "down";
|