gloomberb 0.18.4 → 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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +169 -5
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +31 -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 +171 -11
- package/src/api-client/live-scan.ts +91 -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 +13 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +6 -12
- package/src/api-client/types.ts +74 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +42 -27
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +1 -1
- 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/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +138 -46
- package/src/cli/commands/options-chain.ts +107 -10
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +176 -39
- package/src/cli/commands/search.ts +39 -3
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +57 -35
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +10 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +73 -1
- package/src/cli/history-rows.ts +87 -16
- package/src/cli/index.ts +2 -0
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +195 -56
- 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 +111 -16
- package/src/cli/pane-functions/function-help.ts +23 -13
- package/src/cli/pane-functions/headless.ts +56 -12
- package/src/cli/pane-functions/index.ts +58 -16
- package/src/cli/pane-functions/market-session.ts +81 -9
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +35 -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 +30 -13
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +26 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +61 -5
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +46 -7
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- 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/types.ts +4 -0
- 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/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
- 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/display.tsx +5 -1
- 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/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/saved-copies.ts +55 -16
- 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 +427 -216
- package/src/i18n/es.ts +19 -3
- package/src/i18n/ja.ts +19 -3
- package/src/i18n/ko.ts +19 -3
- package/src/i18n/zh-cn.ts +19 -3
- package/src/i18n/zh-tw.ts +19 -3
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -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 +48 -7
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +141 -4
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- 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/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +380 -82
- package/src/plugins/builtin/alerts/command.ts +31 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +113 -8
- package/src/plugins/builtin/alerts/format.ts +80 -4
- package/src/plugins/builtin/alerts/headless.ts +30 -7
- package/src/plugins/builtin/alerts/history-rows.ts +7 -3
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +135 -22
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +134 -0
- package/src/plugins/builtin/alerts/metric-data.ts +111 -0
- package/src/plugins/builtin/alerts/metric-model.ts +324 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +90 -39
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +37 -10
- package/src/plugins/builtin/alerts/types.ts +7 -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/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +10 -5
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- 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 +7 -3
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
- package/src/plugins/builtin/chart-composer/headless.ts +19 -3
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -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/cloud/plugin.tsx +1 -1
- 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/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +4 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
- package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- 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 +70 -15
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- 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 +11 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
- package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -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/keybindings-editor.tsx +18 -14
- 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 +36 -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/pane.tsx +7 -5
- 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/footer.ts +2 -2
- 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 +82 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- 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 +54 -6
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +6 -9
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +42 -12
- package/src/plugins/builtin/options-positioning/client.ts +30 -0
- package/src/plugins/builtin/options-positioning/headless.ts +74 -21
- package/src/plugins/builtin/options-positioning/index.tsx +4 -4
- package/src/plugins/builtin/options-positioning/model.ts +122 -14
- package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
- 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/freshness.ts +12 -10
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +32 -31
- 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 +12 -7
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- 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/summary/index.tsx +11 -8
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- 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 +11 -9
- 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/feed.ts +2 -2
- 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 +40 -6
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +138 -0
- package/src/plugins/builtin/scanner/live-model.ts +362 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- 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 +3 -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/macro-releases.ts +65 -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/scanner-capture.ts +3 -0
- 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 +10 -3
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +17 -8
- package/src/plugins/builtin/themes/pane.tsx +7 -7
- 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-footer.ts +6 -1
- 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/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +9 -17
- package/src/plugins/builtin/vol-surface/headless.ts +63 -14
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +4 -4
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +5 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/remote/resources.ts +2 -1
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- 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 +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
- 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/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +30 -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/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -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/renderers/opentui/toast-host.tsx +71 -43
- 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/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +149 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +70 -19
- package/src/sync/core-contributors.ts +90 -31
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- 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/telemetry/usage-counts.ts +0 -18
- package/src/test-support/preload.ts +14 -0
- package/src/theme/colors.ts +8 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +103 -3
- package/src/tickers/listing-venues.ts +38 -3
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +2 -4
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +198 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +7 -1
- package/src/time-series/resolve.ts +116 -15
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +5 -3
- package/src/time-series/studies.ts +278 -74
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +3 -3
- package/src/time-series/types.ts +27 -1
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +73 -2
- package/src/types/headless.ts +61 -5
- package/src/types/instrument.ts +3 -0
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +10 -1
- package/src/ui/host.tsx +3 -0
- package/src/updater/command-names.ts +12 -7
- 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/financial-statements.ts +6 -1
- 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/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +12 -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
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { METRIC_CHOICES, benchmarkKey, metricWizardFields, parseMetricAlert } from "./metric-command";
|
|
2
|
+
import { firstDailyCheck, metricAvailabilityError, metricReadings } from "./metric-data";
|
|
3
|
+
import { alertLogRule } from "./alert-log";
|
|
4
|
+
import { closeBasis, isDailyMetric, isRelativeMetric, metricReadingText, metricTrigger, validMetricRule, type MetricReading } from "./metric-model";
|
|
5
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
1
6
|
import { startCatalystAlerts } from "./catalysts";
|
|
2
7
|
import { notTheCoinNote } from "../../../cli/commands/crypto-hints";
|
|
3
|
-
import { createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
|
|
8
|
+
import { alertChangeEntry, createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
|
|
4
9
|
import { checkIvLevelAlertsNow, startIvLevelAlerts } from "./iv-level";
|
|
5
10
|
import { logFailure } from "../../../utils/log-failure";
|
|
6
11
|
import { RESEARCH_FORM_KINDS, RESEARCH_LABELS, researchWizardFields, createResearchAlert } from "./research-builder";
|
|
@@ -19,12 +24,15 @@ import { isResearchAlertKind, MAX_LEAD_DAYS } from "./research-rules";
|
|
|
19
24
|
import { EVENT_ALERT_TEMPLATES, MAX_EVENT_ALERTS, createEventAlert, eventAlertStoreKey, readEventAlertStores, updateEventAlerts } from "./events";
|
|
20
25
|
import { formatMarketPrice } from "../../../market-data/market/format";
|
|
21
26
|
import { settleTypedListingKey } from "../../../tickers/listing-venues";
|
|
27
|
+
import { parseListingArg } from "../../../cli/listing-arg";
|
|
28
|
+
import { notATickerSymbol } from "../../../sources/provider-errors";
|
|
22
29
|
import type { Quote } from "../../../types/financials";
|
|
23
30
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
24
31
|
import {
|
|
25
32
|
createAlert,
|
|
26
33
|
evaluateAlert,
|
|
27
34
|
formatAlertDescription,
|
|
35
|
+
sameLevelRule,
|
|
28
36
|
} from "./alert-engine";
|
|
29
37
|
import {
|
|
30
38
|
parseAlertCommandValues,
|
|
@@ -62,11 +70,21 @@ let alertStream: AlertQuoteStream | null = null;
|
|
|
62
70
|
*/
|
|
63
71
|
const STREAM_PERSIST_MS = 60_000;
|
|
64
72
|
|
|
73
|
+
/** BTC/USD, BRK/B and EUR/USD are one listing written with a slash, as `alerts add` reads them; XLK/SPY is not. */
|
|
74
|
+
function slashSpellsListing(symbol: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
return parseListingArg(symbol).key !== symbol.toUpperCase();
|
|
77
|
+
} catch {
|
|
78
|
+
// Intentional ignore: text the listing reader refuses is no pair to divide, so the quote's own error stands.
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
export const alertsPlugin: GloomPlugin = {
|
|
66
84
|
id: "alerts",
|
|
67
85
|
name: "Alerts",
|
|
68
86
|
version: "1.0.0",
|
|
69
|
-
description: "Price,
|
|
87
|
+
description: "Price, valuation, daily technical and event alerts",
|
|
70
88
|
toggleable: true,
|
|
71
89
|
cliCommands: [alertsCliCommand],
|
|
72
90
|
|
|
@@ -114,7 +132,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
114
132
|
let persist = false;
|
|
115
133
|
let touched = false;
|
|
116
134
|
for (const alert of alerts) {
|
|
117
|
-
if (alert.status !== "active") continue;
|
|
135
|
+
if (alert.metric || alert.status !== "active") continue;
|
|
118
136
|
const quote = readStreamedAlertQuote(alert, undefined, now);
|
|
119
137
|
if (!quote) continue;
|
|
120
138
|
// A re-armed or new alert has no stored baseline: judge nothing on this
|
|
@@ -136,7 +154,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
136
154
|
}
|
|
137
155
|
if (!persist && !(touched && now - lastStreamPersistAt >= STREAM_PERSIST_MS)) return;
|
|
138
156
|
lastStreamPersistAt = now;
|
|
139
|
-
void saveAlerts(ctx, alerts).catch(logFailure("alerts.stream", "Save streamed price alerts failed"));
|
|
157
|
+
void saveAlerts(ctx, alerts, "price").catch(logFailure("alerts.stream", "Save streamed price alerts failed"));
|
|
140
158
|
if (persist) alertStream?.sync();
|
|
141
159
|
};
|
|
142
160
|
alertStream?.dispose();
|
|
@@ -144,8 +162,8 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
144
162
|
ctx.registerCommand({
|
|
145
163
|
id: "set-alert",
|
|
146
164
|
label: "Add Alert",
|
|
147
|
-
description: "Create a price
|
|
148
|
-
keywords: ["add", "set", "alert", "price", "trigger", "notify", "alarm", "watch"],
|
|
165
|
+
description: "Create a price, valuation or daily technical alert",
|
|
166
|
+
keywords: ["add", "set", "alert", "pe", "ps", "rsi", "moving average", "valuation", "price", "trigger", "notify", "alarm", "watch"],
|
|
149
167
|
category: "data",
|
|
150
168
|
shortcut: "SA",
|
|
151
169
|
shortcutArg: {
|
|
@@ -155,6 +173,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
155
173
|
},
|
|
156
174
|
wizardLayout: "form",
|
|
157
175
|
wizard: [
|
|
176
|
+
{ key: "kind", label: "Kind", type: "select", defaultValue: "price", options: METRIC_CHOICES },
|
|
158
177
|
{
|
|
159
178
|
key: "symbol",
|
|
160
179
|
label: "Symbol",
|
|
@@ -162,6 +181,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
162
181
|
type: "text",
|
|
163
182
|
},
|
|
164
183
|
{
|
|
184
|
+
dependsOn: { key: "kind", value: "price" },
|
|
165
185
|
key: "condition",
|
|
166
186
|
label: "Condition",
|
|
167
187
|
type: "select",
|
|
@@ -172,34 +192,84 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
172
192
|
],
|
|
173
193
|
},
|
|
174
194
|
{
|
|
195
|
+
dependsOn: { key: "kind", value: "price" },
|
|
175
196
|
key: "price",
|
|
176
197
|
label: "Target Price",
|
|
177
198
|
placeholder: "200.00",
|
|
178
199
|
type: "number",
|
|
179
200
|
},
|
|
201
|
+
...metricWizardFields,
|
|
180
202
|
],
|
|
181
203
|
async execute(values) {
|
|
182
|
-
|
|
204
|
+
let input = parseAlertCommandValues(values);
|
|
183
205
|
if (!input) throw new Error("Use a symbol, condition, and target price.");
|
|
184
206
|
// 7203:TSE, typed or from a link, is the venue its symbol trades on, as `alerts add` reads it.
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
const quoteFor = async (typed: string) => {
|
|
208
|
+
const key = (await settleTypedListingKey(typed, { store: ctx.tickerRepository, dataProvider: ctx.marketData })).toUpperCase();
|
|
209
|
+
return { key, quote: await resolveAlertQuote(ctx.marketData, key) };
|
|
210
|
+
};
|
|
211
|
+
let resolved: Awaited<ReturnType<typeof quoteFor>>;
|
|
212
|
+
try {
|
|
213
|
+
resolved = await quoteFor(input.symbol);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// XLK/SPY is a listing first; only one the service says is no ticker is the ratio of the two, as `alerts add` reads it.
|
|
216
|
+
// A timeout stays a timeout, and BTC/USD, BRK/B or EUR/USD, one listing written with a slash, keeps its own answer.
|
|
217
|
+
const pair = !input.metric && /^[^/\s]+\/[^/\s]+$/.test(input.symbol) && notATickerSymbol(error instanceof Error ? error.message : String(error))
|
|
218
|
+
&& !slashSpellsListing(input.symbol) ? parseMetricAlert(`${input.symbol} ratio ${input.condition} ${input.price}`) : null;
|
|
219
|
+
if (!pair) throw error;
|
|
220
|
+
input = pair;
|
|
221
|
+
resolved = await quoteFor(pair.symbol);
|
|
222
|
+
}
|
|
223
|
+
const { key: symbol, quote } = resolved;
|
|
224
|
+
let metric = input.metric;
|
|
225
|
+
if (metric && isRelativeMetric(metric)) {
|
|
226
|
+
const reference = (await quoteFor(benchmarkKey(metric))).quote;
|
|
227
|
+
const benchmarkExchange = canonicalExchange(reference.listingExchangeName || reference.exchangeName || "");
|
|
228
|
+
const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName || "");
|
|
229
|
+
const benchmark = reference.symbol || metric.benchmark;
|
|
230
|
+
if (benchmark === (quote.symbol || symbol) && benchmarkExchange === exchange) throw new Error(`Compare ${benchmark} with another listing.`);
|
|
231
|
+
metric = { ...metric, benchmark, ...(benchmarkExchange ? { benchmarkExchange } : {}) };
|
|
232
|
+
// Never save a rule its own list would refuse to read back.
|
|
233
|
+
if (!validMetricRule({ metric, condition: input.condition, targetPrice: input.price })) throw new Error(`${benchmark} cannot be a benchmark: relative strength alerts compare two US listings.`);
|
|
234
|
+
}
|
|
188
235
|
// A target ten times away is a slip or another instrument's price, such as bitcoin's on the BTC fund.
|
|
189
236
|
const far = farTargetMessage(symbol, input.price, quote.price, symbol.includes(":") ? null : notTheCoinNote(symbol, quote));
|
|
190
|
-
if (far) throw new Error(far);
|
|
237
|
+
if (!input.metric && far) throw new Error(far);
|
|
191
238
|
|
|
192
|
-
const alert = {
|
|
239
|
+
const alert: AlertRule = {
|
|
193
240
|
...createAlert(quote.symbol || symbol, input.condition, input.price),
|
|
194
|
-
...quoteAlertFields(quote)
|
|
241
|
+
...(!metric ? quoteAlertFields(quote) : {
|
|
242
|
+
metric, exchange: quote.listingExchangeName || quote.exchangeName,
|
|
243
|
+
...quote.currency ? { lastQuoteCurrency: quote.currency } : {},
|
|
244
|
+
}),
|
|
195
245
|
};
|
|
246
|
+
// A close not yet final is read once it is: the rule is kept and says when it is first judged.
|
|
247
|
+
let firstCheck: string | null = null;
|
|
248
|
+
if (metric) {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
if (isDailyMetric(metric)) {
|
|
251
|
+
const check = firstDailyCheck(alert.exchange, now, metric.kind === "rrg");
|
|
252
|
+
if (check) firstCheck = `first checked on the ${check.day} ${closeBasis(metric) === "weekly close" ? "weekly close" : "close"}, from ${new Date(check.at).toISOString().slice(11, 16)} UTC`;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const reading = await metricReadings(ctx.marketData, now)(alert);
|
|
256
|
+
Object.assign(alert, { lastCheckedPrice: reading.value, lastCheckedTarget: reading.target, lastQuoteUpdatedAt: reading.at,
|
|
257
|
+
lastCheckedAt: now, ...reading.day ? { lastDailyClose: reading.day } : {} });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!isDailyMetric(metric) || metricAvailabilityError(alert)) throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
196
262
|
const existing = loadAlerts(ctx);
|
|
263
|
+
// The same rule on the same listing is one alert: adding it again names it and adds nothing.
|
|
264
|
+
const duplicate = existing.find((entry) => sameLevelRule(entry, alert));
|
|
265
|
+
if (duplicate) throw new Error(`${formatAlertDescription(alert)} already exists (alert ${duplicate.id}). Nothing was added.`);
|
|
197
266
|
existing.push(alert);
|
|
198
|
-
await saveAlerts(ctx, existing);
|
|
267
|
+
await saveAlerts(ctx, existing, input.metric ? "metric" : "price");
|
|
268
|
+
recordFired(alertChangeEntry("created", alertLogRule(alert)));
|
|
199
269
|
alertStream?.sync();
|
|
200
270
|
|
|
201
271
|
ctx.notify({
|
|
202
|
-
body: `Alert set: ${formatAlertDescription(alert)} (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`,
|
|
272
|
+
body: `Alert set: ${formatAlertDescription(alert)}${input.metric ? firstCheck ? `, ${firstCheck}` : "" : ` (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`}`,
|
|
203
273
|
type: "success",
|
|
204
274
|
});
|
|
205
275
|
},
|
|
@@ -270,6 +340,7 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
270
340
|
// An IV level goes in its own list, which released versions never read.
|
|
271
341
|
const key = eventAlertStoreKey(rule);
|
|
272
342
|
await ctx.configState.set(key, updateEventAlerts(ctx.configState.get<string>(key), (rules) => [...rules, rule]));
|
|
343
|
+
recordFired(alertChangeEntry("created", alertLogRule(rule)));
|
|
273
344
|
ctx.notify({ body: "Event alert saved.", type: "success" });
|
|
274
345
|
if (scheduled) checkScheduledEventAlertsNow();
|
|
275
346
|
if (rule.kind === "iv_level") checkIvLevelAlertsNow();
|
|
@@ -278,16 +349,16 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
278
349
|
|
|
279
350
|
// The poll carries whatever the stream does not: symbols the feed has not
|
|
280
351
|
// delivered lately, and every symbol while the app is hidden.
|
|
281
|
-
const
|
|
352
|
+
const pollLevels = async (metrics: boolean) => {
|
|
282
353
|
alertStream?.sync();
|
|
283
|
-
const pending = loadAlerts(ctx).filter((alert) => alert.status === "active" && !readStreamedAlertQuote(alert));
|
|
354
|
+
const pending = loadAlerts(ctx).filter((alert) => Boolean(alert.metric) === metrics && alert.status === "active" && !readStreamedAlertQuote(alert));
|
|
284
355
|
if (pending.length === 0) return;
|
|
285
356
|
|
|
286
357
|
ctx.log.info("poll", { active: pending.length });
|
|
287
358
|
|
|
288
359
|
// One batched pass over the distinct symbols: the alerts pane reads the same
|
|
289
360
|
// persisted store, so this is the only place that talks to the provider.
|
|
290
|
-
const quoteKeys = [...new Set(pending.map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
|
|
361
|
+
const quoteKeys = [...new Set(pending.filter(alert => !alert.metric).map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
|
|
291
362
|
const results = await Promise.all(quoteKeys.map(async (key): Promise<[string, Quote | string]> => {
|
|
292
363
|
const [symbol, exchange = ""] = key.split("\0");
|
|
293
364
|
try {
|
|
@@ -297,15 +368,50 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
297
368
|
return [key, createQuoteErrorMessage(symbol ?? "", err)];
|
|
298
369
|
}
|
|
299
370
|
}));
|
|
371
|
+
const readMetric = metricReadings(ctx.marketData);
|
|
372
|
+
const readings = new Map<string, { armedAt: number; reading: MetricReading | string }>();
|
|
373
|
+
for (const alert of pending.filter(a => a.metric)) {
|
|
374
|
+
try { readings.set(alert.id, { armedAt: alert.armedAt ?? alert.createdAt, reading: await readMetric(alert) }); }
|
|
375
|
+
catch (error) { readings.set(alert.id, { armedAt: alert.armedAt ?? alert.createdAt, reading: error instanceof Error ? error.message : String(error) }); }
|
|
376
|
+
}
|
|
300
377
|
if (generation !== pollGeneration) return;
|
|
301
378
|
const quotes = new Map<string, Quote | string>(results);
|
|
302
379
|
|
|
303
380
|
// Read the store again: the stream or the pane may have changed an alert
|
|
304
381
|
// while the quotes were in flight, and an alert must trigger only once.
|
|
305
|
-
const alerts = loadAlerts(ctx);
|
|
382
|
+
const alerts = loadAlerts(ctx).filter(alert => Boolean(alert.metric) === metrics);
|
|
306
383
|
let changed = false;
|
|
307
384
|
for (const alert of alerts) {
|
|
308
385
|
if (alert.status !== "active") continue;
|
|
386
|
+
if (alert.metric) {
|
|
387
|
+
const result = readings.get(alert.id);
|
|
388
|
+
if (!result || result.armedAt !== (alert.armedAt ?? alert.createdAt)) continue;
|
|
389
|
+
const reading = result.reading;
|
|
390
|
+
if (typeof reading !== "string" && reading.day && reading.day === alert.lastDailyClose && !alert.lastCheckError) continue;
|
|
391
|
+
if (typeof reading === "string" && reading === alert.lastCheckError) continue;
|
|
392
|
+
if (typeof reading === "string") Object.assign(alert, quoteErrorAlertFields(reading));
|
|
393
|
+
else {
|
|
394
|
+
if (metricTrigger(alert, reading)) {
|
|
395
|
+
const firedAt = Date.now();
|
|
396
|
+
alert.status = "triggered"; alert.triggeredAt = firedAt;
|
|
397
|
+
const description = formatAlertDescription(alert);
|
|
398
|
+
const readingText = metricReadingText(alert.metric, reading);
|
|
399
|
+
recordFired({ kind: alert.metric.kind, ruleId: alert.id, rule: description, firedAt, value: reading.value,
|
|
400
|
+
unit: alert.metric.kind === "pe" || alert.metric.kind === "ps" ? "x" : undefined,
|
|
401
|
+
// An RRG quadrant is two readings; the figure is strength, the detail says both.
|
|
402
|
+
...(alert.metric.kind === "rrg" ? { detail: readingText } : {}),
|
|
403
|
+
quoteAt: firedAlertTime(reading.at), source: closeBasis(alert.metric),
|
|
404
|
+
symbol: alert.symbol, exchange: alert.exchange });
|
|
405
|
+
ctx.notify({ body: alert.metric.kind === "rrg" ? `${description} triggered: ${readingText}` : `${description} triggered at ${readingText}`, type: "success",
|
|
406
|
+
refId: `price:${alert.id}`, desktop: "always", persistent: true, sound: "Glass",
|
|
407
|
+
action: { label: "Open", onClick: () => ctx.showPane("alerts") } });
|
|
408
|
+
}
|
|
409
|
+
Object.assign(alert, { lastCheckedPrice: reading.value, lastCheckedTarget: reading.target,
|
|
410
|
+
lastDailyClose: reading.day, lastQuoteUpdatedAt: reading.at, lastCheckedAt: Date.now(), lastCheckError: undefined });
|
|
411
|
+
}
|
|
412
|
+
changed = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
309
415
|
if (readStreamedAlertQuote(alert)) continue;
|
|
310
416
|
const quote = quotes.get(`${alert.symbol}\0${alert.exchange ?? ""}`);
|
|
311
417
|
if (quote === undefined) continue;
|
|
@@ -327,10 +433,17 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
327
433
|
}
|
|
328
434
|
|
|
329
435
|
if (changed) {
|
|
330
|
-
await saveAlerts(ctx, alerts);
|
|
436
|
+
await saveAlerts(ctx, alerts, metrics ? "metric" : "price");
|
|
331
437
|
}
|
|
332
438
|
};
|
|
333
439
|
|
|
440
|
+
// Metric work may span a poll, but cannot delay the next price check or overlap itself.
|
|
441
|
+
let metricPoll: Promise<void> | null = null;
|
|
442
|
+
const poll = () => {
|
|
443
|
+
metricPoll ??= pollLevels(true).catch(logFailure("alerts.metrics", "Check metric alerts failed")).finally(() => { metricPoll = null; });
|
|
444
|
+
return pollLevels(false);
|
|
445
|
+
};
|
|
446
|
+
|
|
334
447
|
// Re-armed each cycle so a change to the pane's Check interval setting takes
|
|
335
448
|
// effect on the next tick without a restart.
|
|
336
449
|
const scheduleNextPoll = () => {
|
|
@@ -377,9 +490,9 @@ export const alertsPlugin: GloomPlugin = {
|
|
|
377
490
|
id: "alerts-pane",
|
|
378
491
|
paneId: "alerts",
|
|
379
492
|
label: "Alerts",
|
|
380
|
-
description: "Price,
|
|
493
|
+
description: "Price, valuation, daily technical and event alerts",
|
|
381
494
|
keywords: [
|
|
382
|
-
"alerts", "alert", "price", "trigger", "alarm", "watch", "notify", "notification",
|
|
495
|
+
"alerts", "alert", "price", "valuation", "pe", "ps", "rsi", "moving average", "trigger", "alarm", "watch", "notify", "notification",
|
|
383
496
|
"earnings", "cpi", "fomc", "filing", "filings", "sec", "8-k", "13f", "news", "keyword",
|
|
384
497
|
"analyst", "upgrade", "downgrade", "52-week", "unusual volume", "volume", "short interest",
|
|
385
498
|
"insider", "congress", "catalyst", "implied volatility", "iv spike", "options flow", "perps", "funding",
|
|
@@ -39,11 +39,11 @@ export function streamedAlertQuote(quote: Quote | null | undefined, now = Date.n
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function readStreamedAlertQuote(
|
|
42
|
-
alert: Pick<AlertRule, "symbol" | "exchange">,
|
|
42
|
+
alert: Pick<AlertRule, "symbol" | "exchange" | "metric">,
|
|
43
43
|
coordinator: MarketDataCoordinator | null = getSharedMarketDataCoordinator(),
|
|
44
44
|
now = Date.now(),
|
|
45
45
|
): Quote | null {
|
|
46
|
-
if (!coordinator) return null;
|
|
46
|
+
if (alert.metric || !coordinator) return null;
|
|
47
47
|
return streamedAlertQuote(resolveEntryData(coordinator.getQuoteEntry(alertInstrument(alert))), now);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -93,7 +93,7 @@ export function createAlertQuoteStream({
|
|
|
93
93
|
const next = getSharedMarketDataCoordinator();
|
|
94
94
|
const instruments = new Map<string, InstrumentRef>();
|
|
95
95
|
for (const alert of readAlerts()) {
|
|
96
|
-
if (alert.status !== "active") continue;
|
|
96
|
+
if (alert.metric || alert.status !== "active") continue;
|
|
97
97
|
const instrument = alertInstrument(alert);
|
|
98
98
|
instruments.set(buildQuoteKey(instrument), instrument);
|
|
99
99
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { AlertMetric, RotationQuadrant } from "./metric-model";
|
|
2
|
+
import { ROTATION_QUADRANTS, validMetricRule } from "./metric-model";
|
|
3
|
+
import { readAlertPrice } from "./price";
|
|
4
|
+
import type { AlertCondition } from "./types";
|
|
5
|
+
import type { WizardStep } from "../../../types/plugin";
|
|
6
|
+
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
7
|
+
|
|
8
|
+
export interface MetricAlertInput { symbol: string; condition: AlertCondition; price: number; metric: AlertMetric }
|
|
9
|
+
const direction = (s: string) => /^(above|over|>)$/i.test(s) ? "above" as const : /^(below|under|<)$/i.test(s) ? "below" as const : null;
|
|
10
|
+
const USAGE = "Use AAPL pe below 25, AAPL ps above 8, NVDA rsi(14) under 30, SPY ma(50) crosses above ma(200), or TSLA closes below ma(200).";
|
|
11
|
+
function checked(input: MetricAlertInput): MetricAlertInput {
|
|
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).");
|
|
13
|
+
return input;
|
|
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
|
+
}
|
|
61
|
+
/** Parses both compact CLI notation and the words traders type, without swallowing malformed metrics as calendar events. */
|
|
62
|
+
export function parseMetricAlert(input: string): MetricAlertInput | null {
|
|
63
|
+
const words = input.trim().replace(/\s+/g, " ");
|
|
64
|
+
const parts = words.split(" ");
|
|
65
|
+
const relative = parseRelativeAlert(parts);
|
|
66
|
+
if (relative) return relative;
|
|
67
|
+
if (!/^(p\/?e|p\/?s|rsi(?:\(|$)|ma(?:\(|$)|\d+-day|closes?$)/i.test(parts[1] ?? "")) return null;
|
|
68
|
+
const ratio = /^(\S+) (p\/?e|p\/?s) (above|below|over|under|>|<) (\S+)$/i.exec(words);
|
|
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]!) });
|
|
70
|
+
const rsi = /^(\S+) rsi(?:\((\d+)\)| (\d+))? (above|below|over|under|>|<) (\S+)$/i.exec(words);
|
|
71
|
+
if (rsi) return checked({ symbol: rsi[1]!.toUpperCase(), metric: { kind: "rsi", period: Number(rsi[2] ?? rsi[3] ?? 14) }, condition: direction(rsi[4]!)!, price: readAlertPrice(rsi[5]!) });
|
|
72
|
+
const ma = words.replace(/(?:its|the) /gi, "").replace(/(\d+)-day(?: (?:ma|moving average))?/gi, "ma($1)").replace(/ma (\d+)/gi, "ma($1)");
|
|
73
|
+
const cross = /^(\S+) ma\((\d+)\) crosses (above|below) ma\((\d+)\)$/i.exec(ma);
|
|
74
|
+
if (cross) return checked({ symbol: cross[1]!.toUpperCase(), metric: { kind: "ma_cross", period: Number(cross[2]), slowPeriod: Number(cross[4]), direction: direction(cross[3]!)! }, condition: "crosses", price: 0 });
|
|
75
|
+
const close = /^(\S+) closes? (above|below|over|under) ma\((\d+)\)$/i.exec(ma);
|
|
76
|
+
if (close) return checked({ symbol: close[1]!.toUpperCase(), metric: { kind: "close_ma", period: Number(close[3]) }, condition: direction(close[2]!)!, price: 0 });
|
|
77
|
+
throw new Error(USAGE);
|
|
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
|
+
}
|
|
86
|
+
export function metricShortcut(input: MetricAlertInput): Record<string, string> {
|
|
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) };
|
|
90
|
+
return { symbol: input.symbol, kind: m.kind, [`${m.kind}:condition`]: m.kind === "ma_cross" ? m.direction : input.condition,
|
|
91
|
+
[`${m.kind}:target`]: String(input.price), ...("period" in m ? { [`${m.kind}:period`]: String(m.period) } : {}),
|
|
92
|
+
...(m.kind === "ma_cross" ? { "ma_cross:slowPeriod": String(m.slowPeriod) } : {}) };
|
|
93
|
+
}
|
|
94
|
+
export function metricFormInput(values: Record<string, string>): MetricAlertInput | null {
|
|
95
|
+
const kind = values.kind;
|
|
96
|
+
if (!kind || kind === "price") return null;
|
|
97
|
+
const symbol = values.symbol?.trim().toUpperCase();
|
|
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}`);
|
|
104
|
+
const condition = values[`${kind}:condition`] ?? "below";
|
|
105
|
+
const window = values[`${kind}:period`] ?? (kind === "rsi" ? "14" : kind === "ma_cross" ? "50" : "200");
|
|
106
|
+
const text = kind === "ma_cross" ? `${symbol} ma(${window}) crosses ${condition} ma(${values["ma_cross:slowPeriod"] ?? "200"})`
|
|
107
|
+
: kind === "close_ma" ? `${symbol} closes ${condition} ma(${window})`
|
|
108
|
+
: `${symbol} ${kind === "rsi" ? `rsi(${window})` : kind} ${condition} ${values[`${kind}:target`] ?? ""}`;
|
|
109
|
+
return parseMetricAlert(text);
|
|
110
|
+
}
|
|
111
|
+
export const METRIC_CHOICES = [
|
|
112
|
+
{ value: "price", label: "Price" }, { value: "pe", label: "P/E (TTM)" }, { value: "ps", label: "P/S (TTM)" },
|
|
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" } },
|
|
124
|
+
];
|
|
125
|
+
export const metricWizardFields: WizardStep[] = [...METRIC_CHOICES.filter(x => x.value !== "price" && x.value !== "rs_ratio" && x.value !== "rrg").flatMap(({ value: kind }) => {
|
|
126
|
+
const dependsOn = { key: "kind", value: kind };
|
|
127
|
+
return [
|
|
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 }] : []),
|
|
129
|
+
{ key: `${kind}:condition`, label: "Condition", type: "select" as const, defaultValue: "below", dependsOn,
|
|
130
|
+
options: [{ label: kind === "ma_cross" ? "Crosses above" : "Above", value: "above" }, { label: kind === "ma_cross" ? "Crosses below" : "Below", value: "below" }] },
|
|
131
|
+
...(kind === "ma_cross" ? [{ key: "ma_cross:slowPeriod", label: "Slow MA (days)", type: "number" as const, defaultValue: "200", dependsOn }]
|
|
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 }]),
|
|
133
|
+
];
|
|
134
|
+
}), ...RELATIVE_FIELDS];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AssetDataProvider } from "../../../types/data-provider";
|
|
2
|
+
import type { TickerFinancials } from "../../../types/financials";
|
|
3
|
+
import { liveMarketCapitalization, liveTrailingPE } from "../../../market-data/quotes/live-valuation";
|
|
4
|
+
import { comparablePriceEarnings } from "../../../utils/price-earnings";
|
|
5
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
6
|
+
import { getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
|
|
7
|
+
import { barSessionDay } from "../../../market-data/market/period-return";
|
|
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";
|
|
11
|
+
import type { AlertRule } from "./types";
|
|
12
|
+
|
|
13
|
+
// Keep valuation fixtures in step with the delivery evaluator and the research panes.
|
|
14
|
+
/** Current trailing figures on the same regular-session and currency basis as the research panes. */
|
|
15
|
+
export function valuationAlertValue(kind: "pe" | "ps", data: TickerFinancials): number | null {
|
|
16
|
+
const { quote, fundamentals: f } = data;
|
|
17
|
+
if (!quote || quote.stale || !Number.isFinite(quote.price) || quote.price <= 0 || !f || f.stale) return null;
|
|
18
|
+
if (kind === "pe") return comparablePriceEarnings(liveTrailingPE(quote, f));
|
|
19
|
+
const cap = liveMarketCapitalization(quote, f);
|
|
20
|
+
if (!cap || !f.revenue || !Number.isFinite(f.revenue) || f.revenue <= 0
|
|
21
|
+
|| !f.financialCurrency || cap.currency !== f.financialCurrency) return null;
|
|
22
|
+
const value = cap.value / f.revenue;
|
|
23
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
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 {
|
|
29
|
+
if (!rule.metric || !isDailyMetric(rule.metric)) return null;
|
|
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;
|
|
34
|
+
}
|
|
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
|
+
|
|
63
|
+
const dailyCaches = new WeakMap<AssetDataProvider, DailyMetricCache>();
|
|
64
|
+
|
|
65
|
+
/** A pass shares each listing's reads; there is no second provider or quote subscription. */
|
|
66
|
+
export function metricReadings(provider: AssetDataProvider, now = Date.now()) {
|
|
67
|
+
const daily = dailyCaches.get(provider) ?? new DailyMetricCache();
|
|
68
|
+
dailyCaches.set(provider, daily);
|
|
69
|
+
const financials = new Map<string, Promise<TickerFinancials>>();
|
|
70
|
+
const history = new Map<string, Promise<Awaited<ReturnType<AssetDataProvider["getPriceHistory"]>> >>();
|
|
71
|
+
return async (rule: AlertRule): Promise<MetricReading> => {
|
|
72
|
+
const m = rule.metric!;
|
|
73
|
+
const key = `${rule.symbol}|${rule.exchange ?? ""}`;
|
|
74
|
+
if (!isDailyMetric(m)) {
|
|
75
|
+
const data = financials.get(key) ?? provider.getTickerFinancials(rule.symbol, rule.exchange);
|
|
76
|
+
financials.set(key, data);
|
|
77
|
+
const loaded = await data;
|
|
78
|
+
const value = valuationAlertValue(m.kind as "pe" | "ps", loaded);
|
|
79
|
+
if (value == null) throw new Error(m.kind === "pe" ? "Trailing P/E is unavailable or not meaningful." : "Trailing P/S is unavailable on a matching currency basis.");
|
|
80
|
+
return { value, target: rule.targetPrice, at: loaded.quote?.lastUpdated ?? now };
|
|
81
|
+
}
|
|
82
|
+
const exchange = canonicalExchange(rule.exchange ?? "");
|
|
83
|
+
const session = (day: string) => getPublishedUsEquitySession(exchange, day);
|
|
84
|
+
const unavailable = metricAvailabilityError(rule, now);
|
|
85
|
+
if (unavailable) throw new Error(unavailable);
|
|
86
|
+
const latest = latestSettledClose(now, session);
|
|
87
|
+
const ruleKey = JSON.stringify([rule.id, rule.armedAt ?? rule.createdAt, key, rule.condition, rule.targetPrice, m]);
|
|
88
|
+
return daily.read(ruleKey, latest.day, now, async () => {
|
|
89
|
+
const load = async (symbol: string, venue: string) => {
|
|
90
|
+
// An explicit end at the settled session avoids today's forming-bar quote read.
|
|
91
|
+
const end = new Date(`${latest.day}T23:59:59Z`);
|
|
92
|
+
const start = new Date(Date.parse(latest.day) - 366 * 86_400_000);
|
|
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" });
|
|
95
|
+
throw new Error("Completed daily history is unavailable from this connection.");
|
|
96
|
+
};
|
|
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);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
}
|