@gloom-sh/terminal 0.18.5 → 0.19.1
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 +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- 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/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -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/core-builtin-allowlist.json +2 -2
- 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 +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- 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/saved-copy-persistence.ts +88 -0
- 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/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- 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 +8 -21
- 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 +18 -3
- package/src/components/ui/icon.tsx +4 -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/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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 +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- 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/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -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 +17 -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/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- 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 +34 -12
- 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/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- 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/follow-binding.ts +14 -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 +5 -6
- 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/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- 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/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -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 +10 -10
- 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/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- 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/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -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 +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- 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/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- 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 +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -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 +22 -4
- 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/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -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 +6 -7
- 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/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- 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 +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- 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/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- 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 +48 -5
- 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 +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- 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/headless-market-data.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 +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- 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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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 +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- 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-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- 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 +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- 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/icons.tsx +7 -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 +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- 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/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -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 +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- 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/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- 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-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- 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/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -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 +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- 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/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -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";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { METRIC_ALERTS_KEY,
|
|
1
|
+
import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, closeBasis, metricName, metricReadingText } from "./metric-model";
|
|
2
2
|
import { metricAvailabilityError } from "./metric-data";
|
|
3
|
-
import {
|
|
3
|
+
import { relativeAlertText } from "./metric-command";
|
|
4
|
+
import { levelAlertStoreKey, readLevelAlertStores } from "./storage";
|
|
4
5
|
import { Box, type ScrollBoxRenderable } from "../../../ui";
|
|
5
6
|
import { Button, DetailScrollBody, KeyValueRow, Notice, PaneStatusBody, PaneFooterScope, usePaneTabs } from "../../../components";
|
|
6
|
-
import { usePluginPaneState } from "../../runtime";
|
|
7
|
+
import { usePluginPaneState, usePluginState } from "../../runtime";
|
|
7
8
|
import { EventAlertsPane } from "./events-pane";
|
|
8
9
|
import { AlertHistoryPane } from "./history-pane";
|
|
9
10
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from "react";
|
|
@@ -30,16 +31,21 @@ import {
|
|
|
30
31
|
readAlertsStoreError,
|
|
31
32
|
serializeAlerts,
|
|
32
33
|
} from "./alert-engine";
|
|
33
|
-
import {
|
|
34
|
+
import { parseAlertEdit } from "./command";
|
|
34
35
|
import { ALERT_TABS, ALERTS_KEY } from "./constants";
|
|
35
36
|
import {
|
|
37
|
+
alertListing,
|
|
36
38
|
conditionLabel,
|
|
37
39
|
formatAlertDistance,
|
|
38
40
|
formatAlertTargetPrice,
|
|
39
41
|
formatCurrentPrice,
|
|
40
42
|
formatQuoteChecked,
|
|
43
|
+
isoUtc,
|
|
44
|
+
listingWithCurrency,
|
|
41
45
|
relativeTime,
|
|
42
46
|
} from "./format";
|
|
47
|
+
import { FIRED_LOG_KEY } from "./fired-log";
|
|
48
|
+
import { withAlertChange } from "./alert-log";
|
|
43
49
|
import type { AlertRule } from "./types";
|
|
44
50
|
import { useQuoteEntries } from "../../../market-data/hooks";
|
|
45
51
|
import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
|
|
@@ -57,7 +63,8 @@ type AlertColumnId =
|
|
|
57
63
|
| "away"
|
|
58
64
|
| "condition"
|
|
59
65
|
| "quote"
|
|
60
|
-
| "triggered"
|
|
66
|
+
| "triggered"
|
|
67
|
+
| "created";
|
|
61
68
|
|
|
62
69
|
type AlertColumn = DataTableColumn & { id: AlertColumnId };
|
|
63
70
|
|
|
@@ -69,8 +76,11 @@ const ALERT_COLUMNS: AlertColumn[] = [
|
|
|
69
76
|
{ id: "target", label: "Target", width: 11, align: "right" },
|
|
70
77
|
{ id: "away", label: "Away", width: 8, align: "right" },
|
|
71
78
|
{ id: "condition", label: "Trigger", width: 12, align: "left" },
|
|
72
|
-
|
|
79
|
+
// Wide enough to say "not checked yet" for an alert no check has read.
|
|
80
|
+
{ id: "quote", label: "Quote", width: 15, align: "left" },
|
|
73
81
|
{ id: "triggered", label: "Alerted", width: 8, align: "left" },
|
|
82
|
+
// The day the alert was made (UTC), as Events lists its rules; the detail gives the instant.
|
|
83
|
+
{ id: "created", label: "Since", width: 10, align: "left" },
|
|
74
84
|
];
|
|
75
85
|
|
|
76
86
|
const DETAIL_ROW = { labelWidth: 10, emphasis: false } as const;
|
|
@@ -89,20 +99,28 @@ function AlertDetail({ alert, scrollRef }: { alert: AlertRule; scrollRef: RefObj
|
|
|
89
99
|
const reason = alert.lastCheckError
|
|
90
100
|
? alert.lastCheckError.includes(alert.symbol) ? alert.lastCheckError : `${alert.symbol}: ${alert.lastCheckError}`
|
|
91
101
|
: null;
|
|
102
|
+
const basis = alert.metric ? closeBasis(alert.metric) : null;
|
|
92
103
|
return (
|
|
93
104
|
<DetailScrollBody ref={scrollRef} resetScrollKey={alert.id}>
|
|
94
105
|
<Box flexDirection="column">
|
|
95
106
|
{reason ? <Box paddingBottom={1}><Notice tone="warning">{reason}</Notice></Box> : null}
|
|
96
107
|
<KeyValueRow {...DETAIL_ROW} label="State" value={alert.status === "triggered" ? "Triggered" : "Active"} tone={alert.status === "triggered" ? "positive" : "muted"} />
|
|
108
|
+
<KeyValueRow {...DETAIL_ROW} label="Listing" value={listingWithCurrency(alertListing(alert), alert.lastQuoteCurrency)} />
|
|
97
109
|
{/* With no quote the reason above says why; the price and its distance wait for one. */}
|
|
98
|
-
{
|
|
110
|
+
{basis ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? `Awaiting ${basis}`} /> : null}
|
|
99
111
|
{reason ? null : <KeyValueRow {...DETAIL_ROW} label="Current" value={formatCurrentPrice(alert, 24)} />}
|
|
112
|
+
{/* The two readings that place the quadrant, as RRG states them. */}
|
|
113
|
+
{!reason && alert.metric?.kind === "rrg" && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null ? <>
|
|
114
|
+
<KeyValueRow {...DETAIL_ROW} label="Strength" value={alert.lastCheckedPrice.toFixed(2)} />
|
|
115
|
+
<KeyValueRow {...DETAIL_ROW} label="Momentum" value={alert.lastCheckedTarget.toFixed(2)} />
|
|
116
|
+
</> : null}
|
|
100
117
|
{reason ? null : <KeyValueRow {...DETAIL_ROW} label="Away" value={formatAlertDistance(alert)} />}
|
|
101
118
|
{reason
|
|
102
119
|
? alert.lastCheckedAt ? <KeyValueRow {...DETAIL_ROW} label="Checked" value={relativeTime(alert.lastCheckedAt)} tone="muted" /> : null
|
|
103
120
|
: <KeyValueRow {...DETAIL_ROW} label="Quote" value={formatQuoteChecked(alert)} tone="muted" />}
|
|
104
121
|
{alert.triggeredAt ? <KeyValueRow {...DETAIL_ROW} label="Alerted" value={relativeTime(alert.triggeredAt)} /> : null}
|
|
105
122
|
<KeyValueRow {...DETAIL_ROW} label="Armed" value={relativeTime(alert.armedAt ?? alert.createdAt)} tone="muted" />
|
|
123
|
+
<KeyValueRow {...DETAIL_ROW} label="Created" value={isoUtc(alert.createdAt)} tone="muted" />
|
|
106
124
|
</Box>
|
|
107
125
|
</DetailScrollBody>
|
|
108
126
|
);
|
|
@@ -144,7 +162,10 @@ export function AlertsPane(props: PaneProps) {
|
|
|
144
162
|
function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
145
163
|
const [alertsJson, setAlertsJson] = usePluginConfigState<unknown>(ALERTS_KEY, "[]");
|
|
146
164
|
const [metricsJson, setMetricsJson] = usePluginConfigState<unknown>(METRIC_ALERTS_KEY, "[]");
|
|
165
|
+
const [relativeJson, setRelativeJson] = usePluginConfigState<unknown>(RELATIVE_ALERTS_KEY, "[]");
|
|
147
166
|
const { openPluginCommandWorkflow, notify } = usePluginAppActions();
|
|
167
|
+
// Deleting or re-arming a rule goes in this device's alert log, beside the alerts that fired.
|
|
168
|
+
const [, setAlertLog] = usePluginState<unknown>(FIRED_LOG_KEY, []);
|
|
148
169
|
const dialog = useDialog();
|
|
149
170
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
150
171
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
@@ -152,9 +173,11 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
152
173
|
const detailAlertId = useRef<string | null>(null);
|
|
153
174
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
154
175
|
// Keep the rule and daily-close basis visible in a narrow pane; the detail keeps all figures.
|
|
155
|
-
const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered"
|
|
176
|
+
const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered" && column.id !== "created")
|
|
177
|
+
: width < 118 ? ALERT_COLUMNS.filter(column => column.id !== "created") : ALERT_COLUMNS;
|
|
156
178
|
const showHorizontalScrollbar = columns.reduce((sum, column) => sum + column.width + 1, 2) > width;
|
|
157
|
-
const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : metricsJson
|
|
179
|
+
const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : key === METRIC_ALERTS_KEY ? metricsJson : relativeJson),
|
|
180
|
+
[alertsJson, metricsJson, relativeJson]);
|
|
158
181
|
const storeError = stores.error;
|
|
159
182
|
|
|
160
183
|
const { alerts, rows } = useMemo(() => {
|
|
@@ -205,16 +228,17 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
205
228
|
}), [liveEntries, rows]);
|
|
206
229
|
|
|
207
230
|
const savePaneAlerts = useCallback((id: string, next: (current: AlertRule[]) => AlertRule[]): string | null => {
|
|
208
|
-
const
|
|
231
|
+
const found = alerts.find(alert => alert.id === id);
|
|
232
|
+
const key = found ? levelAlertStoreKey(found) : ALERTS_KEY;
|
|
209
233
|
if (stores.errors[key]) return stores.errors[key];
|
|
210
234
|
let error: string | null = null;
|
|
211
|
-
const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : setAlertsJson;
|
|
235
|
+
const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : key === RELATIVE_ALERTS_KEY ? setRelativeJson : setAlertsJson;
|
|
212
236
|
setJson((currentJson: unknown) => {
|
|
213
237
|
error = readAlertsStoreError(currentJson);
|
|
214
238
|
return error ? currentJson : serializeAlerts(next(deserializeAlerts(currentJson)));
|
|
215
239
|
});
|
|
216
240
|
return error;
|
|
217
|
-
}, [alerts, setAlertsJson, setMetricsJson, stores]);
|
|
241
|
+
}, [alerts, setAlertsJson, setMetricsJson, setRelativeJson, stores]);
|
|
218
242
|
|
|
219
243
|
const deleteAlert = useCallback(async (id: string) => {
|
|
220
244
|
const alert = alerts.find((a) => a.id === id);
|
|
@@ -230,15 +254,24 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
230
254
|
notify({ body: error, type: "error" });
|
|
231
255
|
return;
|
|
232
256
|
}
|
|
257
|
+
setAlertLog((saved: unknown) => withAlertChange(saved, "deleted", alert));
|
|
233
258
|
setSelectedIdx((prev) => Math.max(0, Math.min(prev, rows.length - 2)));
|
|
234
|
-
}, [alerts, dialog, notify, rows.length, savePaneAlerts]);
|
|
259
|
+
}, [alerts, dialog, notify, rows.length, savePaneAlerts, setAlertLog]);
|
|
235
260
|
|
|
236
261
|
const rearmAlert = useCallback((id: string) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
// Only an alert that went off is re-armed, as the store reads at the write: one still waiting is left, and not logged.
|
|
263
|
+
const rearmed: AlertRule[] = [];
|
|
264
|
+
const error = savePaneAlerts(id, (current) => current.map((a) => {
|
|
265
|
+
if (a.id !== id || a.status !== "triggered") return a;
|
|
266
|
+
rearmed.push(a);
|
|
267
|
+
return rebuildAlert(a);
|
|
268
|
+
}));
|
|
269
|
+
if (error) {
|
|
270
|
+
notify({ body: error, type: "error" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (const alert of rearmed) setAlertLog((saved: unknown) => withAlertChange(saved, "rearmed", alert));
|
|
274
|
+
}, [notify, savePaneAlerts, setAlertLog]);
|
|
242
275
|
|
|
243
276
|
const startAddAlert = useCallback(() => {
|
|
244
277
|
openPluginCommandWorkflow("set-alert");
|
|
@@ -276,11 +309,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
276
309
|
description: "SYMBOL above|below|crosses PRICE",
|
|
277
310
|
placeholder: "AAPL above 200",
|
|
278
311
|
}}
|
|
279
|
-
currentValue={selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
|
|
312
|
+
currentValue={relativeAlertText(selected) ?? (selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`)}
|
|
280
313
|
onApply={async (value) => {
|
|
281
|
-
const parsed =
|
|
282
|
-
if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
|
|
283
|
-
if (!!parsed.metric !== !!selected.metric) throw new Error("Add a new alert to change between price and a valuation or technical measure.");
|
|
314
|
+
const parsed = parseAlertEdit(selected, value);
|
|
284
315
|
const error = savePaneAlerts(selected.id, (current) => current.map((alert) => (
|
|
285
316
|
alert.id === selected.id
|
|
286
317
|
? { ...editAlert(alert, parsed.symbol, parsed.condition, parsed.price), ...(parsed.metric ? { metric: parsed.metric } : {}) }
|
|
@@ -382,6 +413,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
382
413
|
case "current":
|
|
383
414
|
return {
|
|
384
415
|
text: formatCurrentPrice(alert, column.width),
|
|
416
|
+
// The quadrant word exports with the readings that place it.
|
|
417
|
+
...(alert.metric?.kind === "rrg" && !alert.lastCheckError && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null
|
|
418
|
+
? { value: metricReadingText(alert.metric, { value: alert.lastCheckedPrice, target: alert.lastCheckedTarget }) } : {}),
|
|
385
419
|
color: alert.lastCheckError ? colors.negative : colors.text,
|
|
386
420
|
};
|
|
387
421
|
case "target":
|
|
@@ -392,10 +426,12 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
392
426
|
color: colors.textDim,
|
|
393
427
|
};
|
|
394
428
|
case "condition":
|
|
395
|
-
return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : conditionLabel(alert.condition) };
|
|
429
|
+
return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : alert.metric?.kind === "rrg" ? "Enters" : conditionLabel(alert.condition) };
|
|
396
430
|
case "quote":
|
|
397
431
|
return {
|
|
398
432
|
text: formatQuoteChecked(alert),
|
|
433
|
+
// A settled close exports as its session date.
|
|
434
|
+
...(alert.metric && closeBasis(alert.metric) && alert.lastDailyClose && !alert.lastCheckError ? { value: alert.lastDailyClose } : {}),
|
|
399
435
|
color: alert.lastCheckError ? colors.negative : colors.textDim,
|
|
400
436
|
};
|
|
401
437
|
case "triggered":
|
|
@@ -403,6 +439,8 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
403
439
|
text: alert.triggeredAt ? relativeTime(alert.triggeredAt) : "-",
|
|
404
440
|
color: colors.textDim,
|
|
405
441
|
};
|
|
442
|
+
case "created":
|
|
443
|
+
return { text: isoUtc(alert.createdAt).slice(0, 10), color: colors.textDim };
|
|
406
444
|
}
|
|
407
445
|
}, []);
|
|
408
446
|
|
|
@@ -2,6 +2,7 @@ import type { DataProvider } from "../../../types/data-provider";
|
|
|
2
2
|
import type { Quote } from "../../../types/financials";
|
|
3
3
|
import { getActiveQuoteDisplay } from "../../../market-data/market/status";
|
|
4
4
|
import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
|
|
5
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
5
6
|
import type { AlertRule } from "./types";
|
|
6
7
|
|
|
7
8
|
export function normalizeAlertSymbol(value: string | null | undefined): string {
|
|
@@ -46,13 +47,18 @@ export function quoteAlertFields(
|
|
|
46
47
|
| "lastQuoteUpdatedAt"
|
|
47
48
|
| "lastQuoteSource"
|
|
48
49
|
| "lastQuoteProviderId"
|
|
50
|
+
| "lastQuoteCurrency"
|
|
51
|
+
| "lastQuoteExchange"
|
|
49
52
|
> & { lastCheckError?: undefined } {
|
|
53
|
+
const exchange = quote.listingExchangeName || quote.exchangeName;
|
|
50
54
|
return {
|
|
51
55
|
lastCheckedPrice: quote.price,
|
|
52
56
|
lastCheckedAt: checkedAt,
|
|
53
57
|
lastQuoteUpdatedAt: quote.lastUpdated,
|
|
54
58
|
lastQuoteSource: quote.dataSource,
|
|
55
59
|
lastQuoteProviderId: quote.providerId,
|
|
60
|
+
...(quote.currency ? { lastQuoteCurrency: quote.currency } : {}),
|
|
61
|
+
...(exchange ? { lastQuoteExchange: canonicalExchange(exchange) } : {}),
|
|
56
62
|
lastCheckError: undefined,
|
|
57
63
|
};
|
|
58
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { METRIC_ALERTS_KEY } from "./metric-model";
|
|
1
|
+
import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, metricAlertsKey } from "./metric-model";
|
|
2
2
|
import type { AppConfig } from "../../../types/config";
|
|
3
3
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
4
4
|
import {
|
|
@@ -21,8 +21,9 @@ export async function saveAlerts(
|
|
|
21
21
|
scope: "all" | "price" | "metric" = "all",
|
|
22
22
|
): Promise<void> {
|
|
23
23
|
const stores = readLevelAlertStores((key) => ctx.configState.get<unknown>(key));
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// An unreadable list is never written over; one that has nothing to write does not hold up the others.
|
|
25
|
+
const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key, value]) => (scope === "all"
|
|
26
|
+
|| (scope === "price" ? key === ALERTS_KEY : key !== ALERTS_KEY)) && !(stores.errors[key] && value === "[]"));
|
|
26
27
|
for (const [key] of entries) if (stores.errors[key]) throw new Error(stores.errors[key]!);
|
|
27
28
|
for (const [key, value] of entries) {
|
|
28
29
|
if (ctx.configState.get<unknown>(key) !== value) await ctx.configState.set(key, value);
|
|
@@ -46,17 +47,20 @@ export function readAlertStores(config: AppConfig) {
|
|
|
46
47
|
export function readLevelAlertStores(get: (key: string) => unknown): { alerts: AlertRule[]; error: string | null; errors: Record<string, string | null> } {
|
|
47
48
|
const alerts: AlertRule[] = [];
|
|
48
49
|
const errors: Record<string, string | null> = {};
|
|
49
|
-
for (const key of
|
|
50
|
+
for (const key of LEVEL_ALERT_KEYS) {
|
|
50
51
|
const raw = get(key);
|
|
51
52
|
let error = readAlertsStoreError(raw);
|
|
52
53
|
const rows = error ? [] : deserializeAlerts(raw);
|
|
53
|
-
if (rows.some(a =>
|
|
54
|
-
errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
|
|
54
|
+
if (rows.some(a => levelAlertStoreKey(a) !== key)) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
|
|
55
|
+
errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
|
|
56
|
+
: key === RELATIVE_ALERTS_KEY ? error?.replaceAll("price alerts", "relative strength alerts") ?? null : error;
|
|
55
57
|
if (!error) alerts.push(...rows);
|
|
56
58
|
}
|
|
57
59
|
return { alerts, errors, error: Object.values(errors).filter(Boolean).join(" ") || null };
|
|
58
60
|
}
|
|
59
61
|
export function writeLevelAlertStores(alerts: AlertRule[]): Record<string, string> {
|
|
60
|
-
return
|
|
61
|
-
[METRIC_ALERTS_KEY]: serializeAlerts(alerts.filter(a => a.metric)) };
|
|
62
|
+
return Object.fromEntries(LEVEL_ALERT_KEYS.map(key => [key, serializeAlerts(alerts.filter(a => levelAlertStoreKey(a) === key))]));
|
|
62
63
|
}
|
|
64
|
+
/** Price, valuation/technical and relative strength rules each keep their own synced list. */
|
|
65
|
+
const LEVEL_ALERT_KEYS = [ALERTS_KEY, METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY] as const;
|
|
66
|
+
export const levelAlertStoreKey = (alert: Pick<AlertRule, "metric">): string => alert.metric ? metricAlertsKey(alert.metric) : ALERTS_KEY;
|
|
@@ -23,5 +23,9 @@ export interface AlertRule {
|
|
|
23
23
|
lastQuoteUpdatedAt?: number;
|
|
24
24
|
lastQuoteSource?: QuoteDataSource;
|
|
25
25
|
lastQuoteProviderId?: string;
|
|
26
|
+
/** The currency of the quote the alert was last checked on (USD): its listing's, which lists show beside it. */
|
|
27
|
+
lastQuoteCurrency?: string;
|
|
28
|
+
/** The venue that quote was from (ARCA), which names the listing of a rule set by symbol alone. */
|
|
29
|
+
lastQuoteExchange?: string;
|
|
26
30
|
message?: string;
|
|
27
31
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { usePaneSettingValue } from "../../../public/react";
|
|
2
2
|
import { PortfolioRiskPane } from "./risk-pane";
|
|
3
3
|
import { portfolioRiskHeadless } from "./risk-headless";
|
|
4
|
+
import { NO_HOLDINGS_HINT } from "./risk-model";
|
|
4
5
|
import { portfolioRiskCache } from "./risk-client";
|
|
5
6
|
import { Box, Text } from "../../../ui";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
@@ -398,7 +399,7 @@ function LegacyPortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
|
|
|
398
399
|
message={accountsError ?? undefined}
|
|
399
400
|
hint={accountsError
|
|
400
401
|
? "Reconnect the broker in the Brokers pane (BR), then refresh."
|
|
401
|
-
:
|
|
402
|
+
: NO_HOLDINGS_HINT}
|
|
402
403
|
/>
|
|
403
404
|
</Box>
|
|
404
405
|
) : historyOnly ? (
|
|
@@ -58,7 +58,12 @@ interface RiskMarketHistory {
|
|
|
58
58
|
* Set when the listing quotes in another currency and its closes were
|
|
59
59
|
* restated in USD at same-date daily FX closes; quote and closeMark are then in USD.
|
|
60
60
|
*/
|
|
61
|
-
converted?: {
|
|
61
|
+
converted?: {
|
|
62
|
+
currency: string;
|
|
63
|
+
fxAsOf: string;
|
|
64
|
+
/** US sessions converted at the FX close before them, the pair having none that day (bridgedFxClose). */
|
|
65
|
+
fxBridged?: string[];
|
|
66
|
+
} | null;
|
|
62
67
|
error: string | null;
|
|
63
68
|
}
|
|
64
69
|
export interface RiskMarketSnapshot {
|
|
@@ -162,6 +167,8 @@ export function validateRiskHistory(
|
|
|
162
167
|
closeMark: { price: number; date: string; currency: string } | null;
|
|
163
168
|
/** Latest completed close in the returns' currency, whatever marks the holding. */
|
|
164
169
|
latestClose: { price: number; date: string };
|
|
170
|
+
/** With a conversion, the US sessions converted at the FX close before them (bridgedFxClose). */
|
|
171
|
+
fxBridged?: string[];
|
|
165
172
|
} {
|
|
166
173
|
if (
|
|
167
174
|
response.status !== "success" ||
|
|
@@ -244,13 +251,14 @@ export function validateRiskHistory(
|
|
|
244
251
|
if (local.integrity)
|
|
245
252
|
throw new Error("Daily history has inconsistent OHLC observations");
|
|
246
253
|
// A converted series is on the NYSE calendar the basket and SPY share.
|
|
247
|
-
const
|
|
254
|
+
const usd = conversion
|
|
248
255
|
? convertClosesToUsd(
|
|
249
256
|
points.map((point) => ({ date: new Date(point.date).toISOString().slice(0, 10), close: point.close })),
|
|
250
257
|
conversion.fx,
|
|
251
258
|
conversion.divisor,
|
|
252
259
|
)
|
|
253
|
-
:
|
|
260
|
+
: null;
|
|
261
|
+
const series = usd ? usd.points : prices;
|
|
254
262
|
const returns = conversion ? resolveDatedReturns(series).returns : local.returns;
|
|
255
263
|
const cadence = qualifySharpeCadence(returns, [
|
|
256
264
|
{ symbol: instrument.symbol, exchange: conversion ? "NYSE" : exchange, history: series },
|
|
@@ -264,6 +272,7 @@ export function validateRiskHistory(
|
|
|
264
272
|
};
|
|
265
273
|
return {
|
|
266
274
|
returns,
|
|
275
|
+
...(usd?.bridged.length ? { fxBridged: usd.bridged } : {}),
|
|
267
276
|
asOf: latestClose.date,
|
|
268
277
|
// A missing or stale quote is not a current mark; the latest dated close is.
|
|
269
278
|
closeMark:
|
|
@@ -591,7 +600,7 @@ export async function fetchPortfolioRiskMarket(
|
|
|
591
600
|
historyParams,
|
|
592
601
|
signal ? { signal } : undefined,
|
|
593
602
|
);
|
|
594
|
-
const { latestClose, ...resolved } = validateRiskHistory(
|
|
603
|
+
const { latestClose, fxBridged, ...resolved } = validateRiskHistory(
|
|
595
604
|
response,
|
|
596
605
|
instrument,
|
|
597
606
|
quote,
|
|
@@ -610,7 +619,9 @@ export async function fetchPortfolioRiskMarket(
|
|
|
610
619
|
quote: current,
|
|
611
620
|
currency: "USD",
|
|
612
621
|
closeMark: current ? null : { ...latestClose, currency: "USD" },
|
|
613
|
-
converted: {
|
|
622
|
+
converted: {
|
|
623
|
+
currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf, ...(fxBridged?.length ? { fxBridged } : {}),
|
|
624
|
+
},
|
|
614
625
|
error: null,
|
|
615
626
|
};
|
|
616
627
|
continue;
|