@gloom-sh/terminal 0.18.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +24 -6
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +27 -3
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/financials.ts +32 -1
- package/src/types/headless.ts +21 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
|
|
1
2
|
import type { PricePoint } from "../../../types/financials";
|
|
2
3
|
import { rollingRealizedVolatility } from "../shared/volatility";
|
|
3
|
-
import type { IvCoverageStatus, IvHistoryPayload, IvMethod, IvPoint, IvScreenRow, IvStats } from "./client";
|
|
4
|
+
import type { IvCloseBasis, IvCoverageStatus, IvHistoryPayload, IvMethod, IvPoint, IvScreenRow, IvStats, ScreenHv } from "./client";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A history's warnings in words for the reader. A symbol demand coverage could
|
|
@@ -18,6 +19,75 @@ export type IvLookback = "1Y" | "2Y" | "ALL";
|
|
|
18
19
|
const RICH_PERCENTILE = 80;
|
|
19
20
|
const CHEAP_PERCENTILE = 20;
|
|
20
21
|
|
|
22
|
+
/** A reading trailing the symbol's newest session by more sessions than this is stale. */
|
|
23
|
+
const STALE_AFTER_SESSIONS = 5;
|
|
24
|
+
const DAY_MS = 86_400_000;
|
|
25
|
+
|
|
26
|
+
/** Whether `date` ("2026-10-23") is a US equity session: the published NYSE calendar where it covers, else a weekday. */
|
|
27
|
+
function isSession(date: string): boolean {
|
|
28
|
+
const published = getPublishedUsEquityCalendarDay("NYSE", date);
|
|
29
|
+
if (published) return published === "session";
|
|
30
|
+
const weekday = new Date(`${date}T00:00:00Z`).getUTCDay();
|
|
31
|
+
return weekday !== 0 && weekday !== 6;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** US sessions after `date` up to and including `through`: how far behind that session a reading is. */
|
|
35
|
+
export function sessionsBehind(date: string, through: string): number {
|
|
36
|
+
const end = Date.parse(`${through}T00:00:00Z`);
|
|
37
|
+
let behind = 0;
|
|
38
|
+
for (let time = Date.parse(`${date}T00:00:00Z`) + DAY_MS; time <= end; time += DAY_MS) {
|
|
39
|
+
if (isSession(new Date(time).toISOString().slice(0, 10))) behind += 1;
|
|
40
|
+
}
|
|
41
|
+
return behind;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The newest of the dates given: the newest session the symbol's stored readings, backfill and prices reach. */
|
|
45
|
+
function newestSession(...dates: ReadonlyArray<string | null | undefined>): string | null {
|
|
46
|
+
return dates.filter((date): date is string => !!date).map((date) => date.slice(0, 10)).sort().at(-1) ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How many sessions a reading is behind the newest, and whether that makes it stale. */
|
|
50
|
+
function staleness(date: string | null | undefined, newest: string | null): { sessionsBehind: number | null; stale: boolean } {
|
|
51
|
+
const behind = date && newest ? sessionsBehind(date.slice(0, 10), newest) : null;
|
|
52
|
+
return { sessionsBehind: behind, stale: behind != null && behind > STALE_AFTER_SESSIONS };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** An HV further than this many sessions from the IV's date says nothing about it: IV/HV is left empty. */
|
|
56
|
+
const HV_MAX_SESSIONS_APART = 3;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* HV after the close of the last session on or before `date`, when that is within
|
|
60
|
+
* `HV_MAX_SESSIONS_APART` sessions of it; null before the series starts or when the
|
|
61
|
+
* nearest HV is older than that.
|
|
62
|
+
*/
|
|
63
|
+
export function hvOn(hv: ScreenHv, date: string): { date: string; value: number } | null {
|
|
64
|
+
for (let index = hv.series.length - 1; index >= 0; index -= 1) {
|
|
65
|
+
const [sessionDate, value] = hv.series[index]!;
|
|
66
|
+
if (sessionDate <= date) return sessionsBehind(sessionDate, date) <= HV_MAX_SESSIONS_APART ? { date: sessionDate, value } : null;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* How a reading's method is worded wherever it is dated: "quote", "close", or "close mid" for a
|
|
73
|
+
* close solved from quote midpoints (mid or mixed). Two readings that word alike share one label.
|
|
74
|
+
*/
|
|
75
|
+
export function readingSource(method: IvMethod | null | undefined, basis?: IvCloseBasis | null): string {
|
|
76
|
+
return method === "quote-mid" ? "quote" : basis === "mid" || basis === "mixed" ? "close mid" : "close";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const STALE_NOTE_LIMIT = 5;
|
|
80
|
+
/**
|
|
81
|
+
* The stale readings, dated, and what that leaves out, in one wording for HIVG and VCA: "Stale, left
|
|
82
|
+
* out of rank: IV 1Y ATM 2025-01-17 (over 5 sessions behind)." The list stops at five, then "N more".
|
|
83
|
+
*/
|
|
84
|
+
export function staleReadingsNote(leftOut: string, items: ReadonlyArray<{ label: string; date: string }>): string | null {
|
|
85
|
+
if (!items.length) return null;
|
|
86
|
+
const listed = items.slice(0, STALE_NOTE_LIMIT).map((item) => `${item.label} ${item.date}`);
|
|
87
|
+
const more = items.length - listed.length;
|
|
88
|
+
return `Stale, left out of ${leftOut}: ${listed.join(", ")}${more > 0 ? `, ${more} more` : ""} (over ${STALE_AFTER_SESSIONS} sessions behind).`;
|
|
89
|
+
}
|
|
90
|
+
|
|
21
91
|
export interface DatedValue { date: Date; value: number | null }
|
|
22
92
|
export type IvStatUnit = "vol" | "points" | "ratio";
|
|
23
93
|
/** One measure's latest value ranked against its own prior 52 weeks. */
|
|
@@ -28,11 +98,21 @@ export interface IvStatRow {
|
|
|
28
98
|
value: number | null;
|
|
29
99
|
date: string | null;
|
|
30
100
|
method: IvMethod | "prices" | null;
|
|
101
|
+
/** Prices a trade close was solved from, when the server says (see `IvCloseBasis`). */
|
|
102
|
+
basis: IvCloseBasis | null;
|
|
31
103
|
low: number | null;
|
|
32
104
|
high: number | null;
|
|
33
105
|
rank: number | null;
|
|
34
106
|
percentile: number | null;
|
|
35
107
|
samples: number;
|
|
108
|
+
/**
|
|
109
|
+
* The reading trails the symbol's newest session by more than `STALE_AFTER_SESSIONS`
|
|
110
|
+
* sessions: its rank and percentile are left out, since they would rank an old
|
|
111
|
+
* figure as though it were current.
|
|
112
|
+
*/
|
|
113
|
+
stale: boolean;
|
|
114
|
+
/** Sessions between the reading and the symbol's newest session. */
|
|
115
|
+
sessionsBehind: number | null;
|
|
36
116
|
}
|
|
37
117
|
/** Rank and percentile need this many prior sessions, matching the server. */
|
|
38
118
|
const MIN_RANK_SAMPLES = 120;
|
|
@@ -96,6 +176,8 @@ export interface IvHistoryModel {
|
|
|
96
176
|
/** "skew from 2026-09-23", or why there is no skew at all. */
|
|
97
177
|
skewNote: string | null;
|
|
98
178
|
stats: IvStatRow[];
|
|
179
|
+
/** Which measures are stale and what that leaves out, when any is. */
|
|
180
|
+
staleNote: string | null;
|
|
99
181
|
since: string | null;
|
|
100
182
|
/** Where the 1Y series starts and how deep it is, when that differs from the rest or leaves it unranked. */
|
|
101
183
|
oneYearNote: string | null;
|
|
@@ -127,15 +209,16 @@ function lookbackStart(asOf: string, lookback: IvLookback): string | null {
|
|
|
127
209
|
/** The latest value against the 52 weeks before it, with the same rules as the server's IV rank. */
|
|
128
210
|
function seriesStat(id: string, label: string, unit: IvStatUnit, points: readonly DatedValue[], method: IvStatRow["method"]): IvStatRow {
|
|
129
211
|
const latest = [...points].reverse().find((point) => point.value != null);
|
|
130
|
-
if (!latest) return { id, label, unit, value: null, date: null, method, low: null, high: null, rank: null, percentile: null, samples: 0
|
|
212
|
+
if (!latest) return { id, label, unit, value: null, date: null, method, basis: null, low: null, high: null, rank: null, percentile: null, samples: 0,
|
|
213
|
+
stale: false, sessionsBehind: null };
|
|
131
214
|
const start = lookbackStart(day(latest.date), "1Y")!;
|
|
132
215
|
const window = points.filter((point) => point.value != null && day(point.date) >= start && point.date < latest.date).map((point) => point.value!);
|
|
133
216
|
const value = latest.value!;
|
|
134
217
|
const low = window.length ? Math.min(value, ...window) : null, high = window.length ? Math.max(value, ...window) : null;
|
|
135
218
|
const enough = window.length >= MIN_RANK_SAMPLES;
|
|
136
|
-
return { id, label, unit, value, date: day(latest.date), method, low, high,
|
|
219
|
+
return { id, label, unit, value, date: day(latest.date), method, basis: null, low, high,
|
|
137
220
|
rank: enough && high! > low! ? (value - low!) / (high! - low!) * 100 : null,
|
|
138
|
-
percentile: midrankPercentile(window, value), samples: window.length };
|
|
221
|
+
percentile: midrankPercentile(window, value), samples: window.length, stale: false, sessionsBehind: null };
|
|
139
222
|
}
|
|
140
223
|
|
|
141
224
|
function byMethod(series: readonly IvPoint[], method: IvMethod, field: "iv30" | "iv90" | "iv365") {
|
|
@@ -192,25 +275,37 @@ export function projectIvHistory(
|
|
|
192
275
|
return { date: point.date, value: point.value == null || iv90 == null || !(iv90 > 0) ? null : point.value / iv90 };
|
|
193
276
|
});
|
|
194
277
|
const fromServer = (id: string, label: string, stats: IvStats | null): IvStatRow => ({ id, label, unit: "vol",
|
|
195
|
-
value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null,
|
|
196
|
-
|
|
278
|
+
value: stats?.value ?? null, date: stats?.date ?? null, method: stats?.method ?? null, basis: stats?.basis ?? null,
|
|
279
|
+
low: stats?.low ?? null, high: stats?.high ?? null,
|
|
280
|
+
rank: stats?.rank ?? null, percentile: stats?.percentile ?? null, samples: stats?.samples ?? 0, stale: false, sessionsBehind: null });
|
|
197
281
|
const stats: IvStatRow[] = [fromServer("iv30", "IV 30d ATM", payload.stats.iv30)];
|
|
198
282
|
// A newer quote capture is shown on its own row: its rank would mix methods.
|
|
199
283
|
if (latest?.method === "quote-mid" && latest.iv30 != null && (!payload.stats.iv30 || latest.date > payload.stats.iv30.date)) {
|
|
200
|
-
stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid",
|
|
201
|
-
low: null, high: null, rank: null, percentile: null, samples: 0 });
|
|
284
|
+
stats.push({ id: "iv30-quote", label: "IV 30d quote", unit: "vol", value: latest.iv30, date: latest.date, method: "quote-mid", basis: null,
|
|
285
|
+
low: null, high: null, rank: null, percentile: null, samples: 0, stale: false, sessionsBehind: null });
|
|
202
286
|
}
|
|
203
287
|
// IV1Y ranks trade closes only; until there is one, a stored quote gives its level, unranked.
|
|
204
288
|
const oneYear = payload.stats.iv365 ?? null;
|
|
205
289
|
stats.push(fromServer("iv90", "IV 90d ATM", payload.stats.iv90),
|
|
206
290
|
!oneYear && latest?.iv365 != null
|
|
207
|
-
? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method,
|
|
208
|
-
low: null, high: null, rank: null, percentile: null, samples: 0 }
|
|
291
|
+
? { id: "iv365", label: "IV 1Y ATM", unit: "vol", value: latest.iv365, date: latest.date, method: latest.method, basis: latest.basis ?? null,
|
|
292
|
+
low: null, high: null, rank: null, percentile: null, samples: 0, stale: false, sessionsBehind: null }
|
|
209
293
|
: fromServer("iv365", "IV 1Y ATM", oneYear),
|
|
210
294
|
seriesStat("hv", `HV ${window}`, "vol", rolling, "prices"),
|
|
211
295
|
seriesStat("spread", `IV 30d - HV ${window}`, "points", spreadAll, "trade-close"),
|
|
212
296
|
seriesStat("term", "IV 30d / 90d", "ratio", termAll, "trade-close"));
|
|
213
297
|
|
|
298
|
+
// The newest session the symbol reaches, by its prices, its backfill and every reading it has: a reading more
|
|
299
|
+
// than STALE_AFTER_SESSIONS behind it is shown as it is, marked stale, and ranks nothing.
|
|
300
|
+
const newest = newestSession(payload.coverage?.backfilledThrough, latest?.date, latestDate,
|
|
301
|
+
...payload.series.map((point) => point.sessionDate), ...prices.map((price) => day(price.date)));
|
|
302
|
+
const measured = stats.map((row): IvStatRow => {
|
|
303
|
+
const behind = staleness(row.date, newest);
|
|
304
|
+
return { ...row, ...behind, ...behind.stale ? { rank: null, percentile: null } : {} };
|
|
305
|
+
});
|
|
306
|
+
const staleRows = measured.filter((row) => row.stale);
|
|
307
|
+
const staleText = staleReadingsNote("rank", staleRows.map((row) => ({ label: row.label, date: row.date! })));
|
|
308
|
+
|
|
214
309
|
// Worded as VCA words each symbol (screenCoverage).
|
|
215
310
|
if (payload.status === "queued") warnings.push(`${payload.symbol} ${queuedText(payload.coverage?.addedAt, options.now ?? Date.now())}.`);
|
|
216
311
|
else if (payload.status === "backfilling") {
|
|
@@ -232,7 +327,7 @@ export function projectIvHistory(
|
|
|
232
327
|
iv30: visible(tradeIv30), iv90: visible(byMethod(payload.series, "trade-close", "iv90")), iv365: visible(tradeIv365),
|
|
233
328
|
quoteIv30: visible(byMethod(payload.series, "quote-mid", "iv30")),
|
|
234
329
|
hv, spread: visible(spreadAll), skew: visible(skew.points),
|
|
235
|
-
skewSince: skew.points[0] ? day(skew.points[0].date) : null, skewNote: skew.note, stats,
|
|
330
|
+
skewSince: skew.points[0] ? day(skew.points[0].date) : null, skewNote: skew.note, stats: measured, staleNote: staleText,
|
|
236
331
|
since, oneYearNote, asOf: latestDate, warnings,
|
|
237
332
|
};
|
|
238
333
|
}
|
|
@@ -251,6 +346,16 @@ export interface RichCheapRow {
|
|
|
251
346
|
iv30: number | null;
|
|
252
347
|
date: string | null;
|
|
253
348
|
method: IvMethod | null;
|
|
349
|
+
/** Prices a trade close was solved from, when the server says (see `IvCloseBasis`). */
|
|
350
|
+
basis: IvCloseBasis | null;
|
|
351
|
+
/**
|
|
352
|
+
* The reading trails the symbol's newest session by more than `STALE_AFTER_SESSIONS`
|
|
353
|
+
* sessions. It is shown, marked stale, and left out of IV/HV, rank, percentile and
|
|
354
|
+
* Rich/Cheap, and so are its skew and IV1Y rank when they are as old.
|
|
355
|
+
*/
|
|
356
|
+
stale: boolean;
|
|
357
|
+
/** Sessions between the reading and the symbol's newest session. */
|
|
358
|
+
sessionsBehind: number | null;
|
|
254
359
|
/** The underlying price the reading was taken at: the session's close, or the spot at the quote capture. */
|
|
255
360
|
spot: number | null;
|
|
256
361
|
rank: number | null;
|
|
@@ -265,10 +370,18 @@ export interface RichCheapRow {
|
|
|
265
370
|
* series has no reading that session, or there is no IV1Y beside it to describe.
|
|
266
371
|
*/
|
|
267
372
|
iv1yPercentile: number | null;
|
|
268
|
-
|
|
269
|
-
|
|
373
|
+
/** The 25-delta risk reversal at 30 days: call IV minus put IV, so positive means calls are richer, as OVDV signs it. */
|
|
374
|
+
riskReversal: number | null;
|
|
375
|
+
/** Session of the quote capture the risk reversal comes from. */
|
|
270
376
|
skewDate: string | null;
|
|
377
|
+
/**
|
|
378
|
+
* Realized volatility on the reading's own date, so the row reads as IV30 over HV;
|
|
379
|
+
* the newest HV for a stale reading or one with no HV on its date.
|
|
380
|
+
*/
|
|
271
381
|
hv: number | null;
|
|
382
|
+
/** Session of the HV shown. */
|
|
383
|
+
hvDate: string | null;
|
|
384
|
+
/** IV30 over the HV of its own date; empty for a stale reading. */
|
|
272
385
|
ivHv: number | null;
|
|
273
386
|
verdict: RichCheap;
|
|
274
387
|
}
|
|
@@ -280,24 +393,29 @@ export interface RichCheapRow {
|
|
|
280
393
|
* is measured on. A group the rows disagree on gets its own column.
|
|
281
394
|
*/
|
|
282
395
|
export interface RichCheapDates {
|
|
283
|
-
reading: { date: string; method: IvMethod } | null;
|
|
396
|
+
reading: { date: string; method: IvMethod; basis: IvCloseBasis | null } | null;
|
|
284
397
|
/** The trade close every ranked row is ranked on. */
|
|
285
398
|
rank: string | null;
|
|
286
399
|
/** Ranked rows are ranked on different closes. */
|
|
287
400
|
rankApart: boolean;
|
|
288
|
-
/** Some
|
|
401
|
+
/** Some risk reversal comes from a different session than its row's reading. */
|
|
289
402
|
skewApart: boolean;
|
|
403
|
+
/** Some HV is dated on a different session than the IV it sits beside. */
|
|
404
|
+
hvApart: boolean;
|
|
290
405
|
}
|
|
291
406
|
export function richCheapDates(rows: readonly RichCheapRow[]): RichCheapDates {
|
|
292
407
|
const read = rows.filter((row) => row.date && row.method);
|
|
293
408
|
const ranked = rows.filter((row) => row.rankDate);
|
|
294
409
|
const first = read[0], firstRanked = ranked[0];
|
|
295
410
|
return {
|
|
296
|
-
|
|
297
|
-
|
|
411
|
+
// A stale row's own date is said in its row, never once for all.
|
|
412
|
+
// The label the rows would be dated with, not the raw basis: a mid and a mixed close are both "close mid".
|
|
413
|
+
reading: first && read.every((row) => row.date === first.date && readingSource(row.method, row.basis) === readingSource(first.method, first.basis) && !row.stale)
|
|
414
|
+
? { date: first.date!, method: first.method!, basis: first.basis } : null,
|
|
298
415
|
rank: firstRanked && ranked.every((row) => row.rankDate === firstRanked.rankDate) ? firstRanked.rankDate : null,
|
|
299
416
|
rankApart: !!firstRanked && ranked.some((row) => row.rankDate !== firstRanked.rankDate),
|
|
300
|
-
skewApart: rows.some((row) => row.
|
|
417
|
+
skewApart: rows.some((row) => row.riskReversal != null && row.skewDate !== row.date),
|
|
418
|
+
hvApart: rows.some((row) => row.hv != null && row.hvDate != null && row.hvDate !== row.date),
|
|
301
419
|
};
|
|
302
420
|
}
|
|
303
421
|
|
|
@@ -307,22 +425,34 @@ export function verdictFor(percentile: number | null): RichCheap {
|
|
|
307
425
|
return percentile >= RICH_PERCENTILE ? "rich" : percentile <= CHEAP_PERCENTILE ? "cheap" : "fair";
|
|
308
426
|
}
|
|
309
427
|
|
|
310
|
-
export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<string,
|
|
428
|
+
export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<string, ScreenHv | null> = new Map()): RichCheapRow[] {
|
|
311
429
|
return rows.map((row) => {
|
|
312
430
|
const latest = row.latest;
|
|
313
431
|
const iv30 = latest?.iv30 ?? null;
|
|
314
|
-
const
|
|
315
|
-
|
|
432
|
+
const series = hv.get(row.symbol) ?? null;
|
|
433
|
+
// The newest session the symbol reaches: its prices, its backfill and every reading it has.
|
|
434
|
+
const newest = newestSession(row.coverage?.backfilledThrough, latest?.date, row.iv30?.date, row.iv90?.date, row.iv365?.date,
|
|
435
|
+
row.skew?.date, series?.date);
|
|
436
|
+
const { stale, sessionsBehind: behind } = staleness(latest?.date, newest);
|
|
437
|
+
// A rank on a close that old ranks an old figure; so does a skew.
|
|
438
|
+
const rankStale = stale || staleness(row.iv30?.date, newest).stale;
|
|
439
|
+
const skewStale = row.skew ? staleness(row.skew.date, newest).stale : false;
|
|
440
|
+
const percentile = rankStale ? null : row.iv30?.percentile ?? null;
|
|
441
|
+
// HV on the reading's own date: today's HV beside an old IV says nothing about it.
|
|
442
|
+
const atReading = series && !stale && latest?.date ? hvOn(series, latest.date) : null;
|
|
443
|
+
const shown = atReading ?? (series?.value != null && series.date ? { date: series.date, value: series.value } : null);
|
|
316
444
|
return {
|
|
317
445
|
symbol: row.symbol, status: row.coverageStatus ?? row.status, queuedAt: row.coverage?.addedAt ?? null,
|
|
318
446
|
backfilledThrough: row.coverage?.backfilledThrough ?? null, warning: row.warnings?.[0] ?? null,
|
|
319
|
-
iv30, date: latest?.date ?? null, method: latest?.method ?? null,
|
|
320
|
-
|
|
447
|
+
iv30, date: latest?.date ?? null, method: latest?.method ?? null, basis: latest?.basis ?? null,
|
|
448
|
+
stale, sessionsBehind: behind, spot: latest?.spot ?? null,
|
|
449
|
+
rank: rankStale ? null : row.iv30?.rank ?? null, percentile, rankDate: rankStale ? null : row.iv30?.date ?? null,
|
|
321
450
|
termSlope: iv30 != null && latest?.iv90 != null ? iv30 - latest.iv90 : null,
|
|
322
451
|
iv1y: latest?.iv365 ?? null,
|
|
323
|
-
iv1yPercentile: latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
|
|
324
|
-
skew: row.skew
|
|
325
|
-
|
|
452
|
+
iv1yPercentile: !rankStale && latest?.iv365 != null && row.iv365 && row.iv365.date === row.iv30?.date ? row.iv365.percentile : null,
|
|
453
|
+
riskReversal: skewStale || !row.skew ? null : row.skew.call25 - row.skew.put25, skewDate: skewStale ? null : row.skew?.date ?? null,
|
|
454
|
+
hv: shown?.value ?? null, hvDate: shown?.date ?? null,
|
|
455
|
+
ivHv: iv30 != null && atReading != null && atReading.value > 0 ? iv30 / atReading.value : null,
|
|
326
456
|
verdict: verdictFor(percentile),
|
|
327
457
|
};
|
|
328
458
|
});
|
|
@@ -375,6 +505,11 @@ export function screenCoverage(
|
|
|
375
505
|
missing: pending.filter((row) => row.iv30 == null && row.date == null).map((row) => row.symbol) };
|
|
376
506
|
}
|
|
377
507
|
|
|
508
|
+
/** The symbols whose reading is stale, dated, and what that leaves out of the screen. */
|
|
509
|
+
export function screenStaleNote(rows: readonly RichCheapRow[]): string | null {
|
|
510
|
+
return staleReadingsNote("IV/HV, rank and Rich/Cheap", rows.flatMap((row) => row.stale && row.date ? [{ label: row.symbol, date: row.date }] : []));
|
|
511
|
+
}
|
|
512
|
+
|
|
378
513
|
/**
|
|
379
514
|
* The VCA pane's notices: the report's wording without a time to come back
|
|
380
515
|
* at, since the pane checks again by itself, and a count of ready symbols with
|
|
@@ -383,7 +518,8 @@ export function screenCoverage(
|
|
|
383
518
|
export function screenPaneNotices(rows: readonly RichCheapRow[], now = Date.now()): string[] {
|
|
384
519
|
const empty = rows.filter((row) => row.status === "ready" && row.iv30 == null).length;
|
|
385
520
|
return [...screenCoverage(rows, { now, retry: false }).notes,
|
|
386
|
-
...(empty ? [`${empty} symbol${empty === 1 ? " has" : "s have"} no stored implied volatility yet.`] : [])
|
|
521
|
+
...(empty ? [`${empty} symbol${empty === 1 ? " has" : "s have"} no stored implied volatility yet.`] : []),
|
|
522
|
+
...[screenStaleNote(rows)].filter((note): note is string => !!note)];
|
|
387
523
|
}
|
|
388
524
|
|
|
389
525
|
export const VCA_PRESETS = {
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { listingIdentity } from "../shared/ticker-request";
|
|
3
3
|
import { DataTableView, EmptyState, PaneStatusBody, QueryBar, usePaneFooter, usePaneNoticeFooter, usePaneTicker, type DataTableColumn } from "../../../components";
|
|
4
4
|
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
5
|
+
import { fxVolatilityNote } from "../../../market-data/options-alternatives";
|
|
5
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
6
7
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
7
8
|
import { useShortcut } from "../../../react/input";
|
|
@@ -24,7 +25,7 @@ const HV_OPTIONS = HV_WINDOWS.map((window) => ({ value: String(window), label: `
|
|
|
24
25
|
const STAT_COLUMNS: DataTableColumn[] = [
|
|
25
26
|
{ id: "label", label: "Measure", width: 16, align: "left" },
|
|
26
27
|
{ id: "value", label: "Current", width: 9, align: "right" },
|
|
27
|
-
{ id: "date", label: "As of", width:
|
|
28
|
+
{ id: "date", label: "As of", width: 20, align: "left" },
|
|
28
29
|
{ id: "low", label: "52w low", width: 9, align: "right" },
|
|
29
30
|
{ id: "high", label: "52w high", width: 9, align: "right" },
|
|
30
31
|
{ id: "rank", label: "Rank", width: 6, align: "right" },
|
|
@@ -54,7 +55,9 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
54
55
|
]);
|
|
55
56
|
return { payload, prices };
|
|
56
57
|
}, [instrumentKey]);
|
|
57
|
-
|
|
58
|
+
// An FX pair has no implied volatility history: the pane says what stands in, and asks for nothing.
|
|
59
|
+
const fxNote = symbol ? fxVolatilityNote(symbol, ticker?.metadata.exchange) : null;
|
|
60
|
+
const resource = useAsyncResource(instrument && !fxNote ? loader : null);
|
|
58
61
|
useEffect(() => () => controller.current?.abort(), [loader]);
|
|
59
62
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
60
63
|
const model = useMemo(() => resource.data
|
|
@@ -66,7 +69,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
66
69
|
const timer = setTimeout(() => { void resource.reload(); }, 60_000);
|
|
67
70
|
return () => clearTimeout(timer);
|
|
68
71
|
}, [model?.status, resource.updatedAt]);
|
|
69
|
-
const notices = [identityError, resource.error, resource.data?.prices.error, ...(model?.warnings ?? [])]
|
|
72
|
+
const notices = [identityError, resource.error, resource.data?.prices.error, ...(model?.warnings ?? []), model?.staleNote]
|
|
70
73
|
.filter((value): value is string => !!value);
|
|
71
74
|
usePaneNoticeFooter({ registrationId: "iv-history-notices", notices: [...new Set(notices)], focused });
|
|
72
75
|
const cycleLookback = () => setLookback(lookback === "1Y" ? "2Y" : lookback === "2Y" ? "ALL" : "1Y");
|
|
@@ -104,8 +107,9 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
104
107
|
const tone = row.percentile != null && (row.percentile >= 80 || row.percentile <= 20) ? colors.warning : colors.text;
|
|
105
108
|
switch (id) {
|
|
106
109
|
case "label": return { text: row.label, color: colors.textBright };
|
|
107
|
-
case "value": return { text: formatStat(row.value, row.unit), color: row.id.startsWith("iv30") ? colors.warning : colors.textBright };
|
|
108
|
-
|
|
110
|
+
case "value": return { text: formatStat(row.value, row.unit), color: row.stale ? colors.textDim : row.id.startsWith("iv30") ? colors.warning : colors.textBright };
|
|
111
|
+
// A stale reading says so in place of how it was taken: it is no current figure either way.
|
|
112
|
+
case "date": return { text: row.date ? `${row.date} ${row.stale ? "stale" : statSource(row)}` : "--", color: row.stale ? colors.warning : colors.textDim };
|
|
109
113
|
case "low": return { text: formatStat(row.low, row.unit), color: colors.textDim };
|
|
110
114
|
case "high": return { text: formatStat(row.high, row.unit), color: colors.textDim };
|
|
111
115
|
case "rank": return { text: formatRank(row.rank), color: tone };
|
|
@@ -119,7 +123,7 @@ export function IvHistoryPane({ width, height, focused }: PaneProps) {
|
|
|
119
123
|
{ id: "lookback", label: "Lookback", value: lookback, options: LOOKBACKS, onChange: setLookback },
|
|
120
124
|
{ id: "realized", label: "Realized", value: String(hvWindow), options: HV_OPTIONS, onChange: setHvWindow },
|
|
121
125
|
]} />
|
|
122
|
-
{!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
|
|
126
|
+
{!symbol ? <EmptyState title="Select a ticker." /> : fxNote ? <PaneStatusBody empty emptyTitle={fxNote} /> : <PaneStatusBody subject="implied volatility history" loading={resource.loading && !model}
|
|
123
127
|
error={!model ? resource.error ?? identityError ?? null : null} empty={!!model && !model.iv30.length && !model.quoteIv30.length}
|
|
124
128
|
emptyTitle={model?.status === "queued" || model?.status === "backfilling" ? `Backfilling ${symbol} implied volatility history...` : undefined}>
|
|
125
129
|
{model ? <>
|
|
@@ -8,14 +8,14 @@ import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
|
8
8
|
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
9
9
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
10
10
|
import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
|
|
11
|
-
import { loadIvScreen, loadScreenVolatilities } from "./client";
|
|
12
|
-
import { formatPoints, formatRank, formatSpot, formatVol, readingLabel, sharedDates, shortDate,
|
|
13
|
-
import { projectRichCheap, type RichCheapRow, richCheapDates, screenPaneNotices, type VcaPreset } from "./model";
|
|
11
|
+
import { loadIvScreen, loadScreenVolatilities, type ScreenHv } from "./client";
|
|
12
|
+
import { formatPoints, formatRank, formatSpot, formatVol, readingLabel, sharedDates, shortDate, vcaDefinitions, verdictLabel } from "./format";
|
|
13
|
+
import { projectRichCheap, readingSource, type RichCheapRow, richCheapDates, screenPaneNotices, type VcaPreset } from "./model";
|
|
14
14
|
import { vcaUniverse } from "./universe";
|
|
15
15
|
import { MovedTickerState } from "../../../components/layout/pane/ticker-fate-gate";
|
|
16
16
|
import type { MovedTickerChange } from "../../../tickers/ticker-changes";
|
|
17
17
|
|
|
18
|
-
type SortId = keyof RichCheapRow
|
|
18
|
+
type SortId = Exclude<keyof RichCheapRow, "stale">;
|
|
19
19
|
const HV_WINDOW = 20;
|
|
20
20
|
/** Pane borders and the footer's own padding. */
|
|
21
21
|
const FOOTER_MARGIN = 6;
|
|
@@ -31,9 +31,10 @@ const COLUMNS: DataTableColumn[] = [
|
|
|
31
31
|
{ id: "iv1y", label: "IV1Y", width: 7, align: "right" },
|
|
32
32
|
{ id: "iv1yPercentile", label: "IVP1Y", width: 6, align: "right" },
|
|
33
33
|
{ id: "termSlope", label: "30-90", width: 7, align: "right" },
|
|
34
|
-
{ id: "
|
|
35
|
-
{ id: "skewDate", label: "
|
|
34
|
+
{ id: "riskReversal", label: "25D RR", width: 9, align: "right" },
|
|
35
|
+
{ id: "skewDate", label: "RR as of", width: 11, align: "right" },
|
|
36
36
|
{ id: "hv", label: `HV${HV_WINDOW}`, width: 7, align: "right" },
|
|
37
|
+
{ id: "hvDate", label: "HV as of", width: 9, align: "right" },
|
|
37
38
|
{ id: "ivHv", label: "IV/HV", width: 6, align: "right" },
|
|
38
39
|
];
|
|
39
40
|
|
|
@@ -49,7 +50,7 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
|
|
|
49
50
|
const universe = useMemo(() => vcaUniverse(scope, symbolsText, collectionId, [...tickers.values()]), [scope, symbolsText, collectionId, tickers]);
|
|
50
51
|
const key = universe.instruments.map((instrument) => instrument.symbol).join(",");
|
|
51
52
|
const controller = useRef<AbortController | null>(null);
|
|
52
|
-
const [hv, setHv] = useState<Map<string,
|
|
53
|
+
const [hv, setHv] = useState<Map<string, ScreenHv | null>>(new Map());
|
|
53
54
|
// Symbols whose company moved: their rows have no HV and the footer says where each company went.
|
|
54
55
|
const [moved, setMoved] = useState<Array<{ symbol: string; change: MovedTickerChange }>>([]);
|
|
55
56
|
const loader = useCallback(async () => {
|
|
@@ -83,17 +84,21 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
|
|
|
83
84
|
}, [resource.data, hv, sort]);
|
|
84
85
|
const dates = useMemo(() => richCheapDates(rows), [rows]);
|
|
85
86
|
// A date every row shares moves to the footer; a column no row fills, or whose dates agree, is left out.
|
|
87
|
+
const hasSkew = useMemo(() => rows.some((row) => row.riskReversal != null), [rows]);
|
|
88
|
+
// A close solved from quote midpoints names itself, which a date alone has no room for.
|
|
89
|
+
const dateWidth = useMemo(() => rows.some((row) => !row.stale && readingSource(row.method, row.basis) === "close mid") ? 15 : 11, [rows]);
|
|
86
90
|
const columns = useMemo(() => COLUMNS.filter((column) => {
|
|
87
91
|
switch (column.id) {
|
|
88
92
|
case "date": return !dates.reading;
|
|
89
93
|
case "rankDate": return dates.rankApart;
|
|
90
94
|
case "iv1y": return rows.some((row) => row.iv1y != null);
|
|
91
95
|
case "iv1yPercentile": return rows.some((row) => row.iv1yPercentile != null);
|
|
92
|
-
case "
|
|
96
|
+
case "riskReversal": return hasSkew;
|
|
93
97
|
case "skewDate": return dates.skewApart;
|
|
98
|
+
case "hvDate": return dates.hvApart;
|
|
94
99
|
default: return true;
|
|
95
100
|
}
|
|
96
|
-
}), [rows, dates]);
|
|
101
|
+
}).map((column) => column.id === "date" ? { ...column, width: dateWidth } : column), [rows, dates, hasSkew, dateWidth]);
|
|
97
102
|
const [selected, setSelected] = useState<string | null>(null);
|
|
98
103
|
const coverageNotices = useMemo(() => screenPaneNotices(rows), [rows]);
|
|
99
104
|
// One symbol to screen whose company moved has nothing to show but where it went, as a ticker's panes say.
|
|
@@ -117,10 +122,10 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
|
|
|
117
122
|
const cell = (row: RichCheapRow, id: string): { text: string; color?: string } => {
|
|
118
123
|
switch (id) {
|
|
119
124
|
case "symbol": return { text: row.symbol, color: colors.textBright };
|
|
120
|
-
case "iv30": return { text: formatVol(row.iv30), color: colors.warning };
|
|
125
|
+
case "iv30": return { text: formatVol(row.iv30), color: row.stale ? colors.textDim : colors.warning };
|
|
121
126
|
case "spot": return { text: formatSpot(row.spot), color: colors.textDim };
|
|
122
|
-
case "date": return { text: row.date ? readingLabel(row.date, row.method, true)
|
|
123
|
-
: row.status === "queued" || row.status === "backfilling" ? row.status : "--", color: colors.textDim };
|
|
127
|
+
case "date": return { text: row.date ? readingLabel(row.date, row.method, true, { basis: row.basis, stale: row.stale })
|
|
128
|
+
: row.status === "queued" || row.status === "backfilling" ? row.status : "--", color: row.stale ? colors.warning : colors.textDim };
|
|
124
129
|
case "rankDate": return { text: row.rankDate ? `${shortDate(row.rankDate)} close` : "--", color: colors.textDim };
|
|
125
130
|
case "rank": return { text: formatRank(row.rank) };
|
|
126
131
|
case "percentile": return { text: formatRank(row.percentile) };
|
|
@@ -128,16 +133,18 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
|
|
|
128
133
|
case "iv1y": return { text: formatVol(row.iv1y) };
|
|
129
134
|
case "iv1yPercentile": return { text: formatRank(row.iv1yPercentile) };
|
|
130
135
|
case "termSlope": return { text: formatPoints(row.termSlope), color: row.termSlope != null && row.termSlope > 0 ? colors.negative : colors.text };
|
|
131
|
-
case "
|
|
136
|
+
case "riskReversal": return { text: formatPoints(row.riskReversal) };
|
|
132
137
|
case "skewDate": return { text: row.skewDate ? readingLabel(row.skewDate, "quote-mid", true) : "--", color: colors.textDim };
|
|
133
138
|
case "hv": return { text: hv.has(row.symbol) ? formatVol(row.hv) : "...", color: colors.positive };
|
|
139
|
+
case "hvDate": return { text: row.hvDate ? shortDate(row.hvDate) : "--", color: colors.textDim };
|
|
134
140
|
case "ivHv": return { text: row.ivHv == null ? "--" : row.ivHv.toFixed(2) };
|
|
135
141
|
default: return { text: "" };
|
|
136
142
|
}
|
|
137
143
|
};
|
|
138
144
|
const custom = scope === "custom";
|
|
139
145
|
// The skew's sign and VIX beside IV30 are units of the figures above them, so they stay under the table.
|
|
140
|
-
const
|
|
146
|
+
const definitionLines = useMemo(() => vcaDefinitions({ skew: hasSkew }), [hasSkew]);
|
|
147
|
+
const definitions = useMemo(() => definitionLines.flatMap((line) => wrapTextLines(line, Math.max(1, width - 2))), [definitionLines, width]);
|
|
141
148
|
return <Box width={width} height={height} flexDirection="column" overflow="hidden">
|
|
142
149
|
<QueryBar width={width} filters={[
|
|
143
150
|
{ id: "universe", label: "Universe", value: VCA_SCOPE_OPTIONS.some((option) => option.value === scope) ? scope : "etfs",
|
|
@@ -159,7 +166,7 @@ export function IvScreenPane({ paneId, width, height, focused }: PaneProps) {
|
|
|
159
166
|
{nativePaneChrome ? (
|
|
160
167
|
// The desktop and the web wrap each definition at the pane's width, under a table that fills the rest.
|
|
161
168
|
<Box flexDirection="column" paddingX={1} flexShrink={0}>
|
|
162
|
-
{
|
|
169
|
+
{definitionLines.map((line) => <Prose key={line} text={line} width={Math.max(1, width - 2)} color={colors.textDim} figures={false} />)}
|
|
163
170
|
</Box>
|
|
164
171
|
) : (
|
|
165
172
|
<Box flexDirection="column" paddingX={1} height={definitions.length} flexShrink={0}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fxVolatilityNote } from "../../../market-data/options-alternatives";
|
|
1
2
|
import type { InstrumentRef } from "../../../market-data/request-types";
|
|
2
3
|
import type { TickerRecord } from "../../../types/ticker";
|
|
3
4
|
import { canonicalExchange } from "../../../utils/exchanges";
|
|
@@ -33,7 +34,10 @@ export function vcaUniverse(scope: string, symbolsText: string, collectionId: st
|
|
|
33
34
|
instruments = VCA_PRESETS[preset].map((symbol) => ({ symbol, exchange: "" }));
|
|
34
35
|
}
|
|
35
36
|
const valid = [...new Map(instruments.filter((instrument) => SYMBOL.test(instrument.symbol)).map((instrument) => [instrument.symbol, instrument])).values()];
|
|
36
|
-
|
|
37
|
+
// An FX pair or currency future typed in a list is left out with what stands in for it, not as a symbol that does not parse.
|
|
38
|
+
const fx = scope === "custom" ? instruments.flatMap((instrument) => fxVolatilityNote(instrument.symbol, instrument.exchange) ?? []) : [];
|
|
39
|
+
const error = !valid.length ? fx[0] ?? (scope === "collection" ? "Link a watchlist or portfolio with US-listed symbols in pane settings." : "Add symbols in pane settings.")
|
|
40
|
+
: fx.length ? fx[0]!
|
|
37
41
|
: valid.length > VCA_LIMIT ? `Only the first ${VCA_LIMIT} symbols are screened.`
|
|
38
42
|
: skipped ? `${skipped} non-US listing${skipped === 1 ? "" : "s"} left out: stored implied volatility covers US option underlyings.` : null;
|
|
39
43
|
return { label, instruments: valid.slice(0, VCA_LIMIT), error };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { createTtlCache } from "../../../data/ttl-cache";
|
|
2
3
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
4
|
import type {
|
|
4
5
|
CloudJobsMoversPayload,
|
|
@@ -25,43 +26,38 @@ export type JobsCompanyState =
|
|
|
25
26
|
| { kind: "denied"; status: 401 | 402 | 403; message: string };
|
|
26
27
|
|
|
27
28
|
const SUMMARY_TTL_MS = 5 * 60_000;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* `pending` is never kept, so the pane's poll reads it afresh each time, and
|
|
31
|
+
* `denied` is not either: the account can change under the pane.
|
|
32
|
+
*/
|
|
33
|
+
const summaries = createTtlCache<JobsCompanyState>({
|
|
34
|
+
ttlMs: SUMMARY_TTL_MS,
|
|
35
|
+
keep: (state) => state.kind === "ready" || state.kind === "uncovered",
|
|
36
|
+
});
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
ticker: string,
|
|
38
|
-
options: { name?: string | null; force?: boolean; client?: JobsCloudClient } = {},
|
|
39
|
-
): Promise<JobsCompanyState> {
|
|
40
|
-
const key = ticker.toUpperCase();
|
|
41
|
-
if (!options.force) {
|
|
42
|
-
const cached = cachedJobs(key);
|
|
43
|
-
// A pending answer is never served from cache: the point is to poll.
|
|
44
|
-
if (cached && cached.kind !== "pending") return cached;
|
|
45
|
-
}
|
|
46
|
-
const client = options.client ?? apiClient;
|
|
47
|
-
let state: JobsCompanyState;
|
|
38
|
+
async function loadJobs(key: string, name: string | null | undefined, client: JobsCloudClient): Promise<JobsCompanyState> {
|
|
48
39
|
try {
|
|
49
|
-
const response: CloudJobsResponse = await client.getCloudJobs(key,
|
|
50
|
-
|
|
40
|
+
const response: CloudJobsResponse = await client.getCloudJobs(key, name);
|
|
41
|
+
return response.status === "pending"
|
|
51
42
|
? { kind: "pending", ticker: key, message: response.message }
|
|
52
43
|
: { kind: "ready", summary: response };
|
|
53
44
|
} catch (error) {
|
|
54
45
|
if (error instanceof ApiRequestError && error.status === 404) {
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
46
|
+
return { kind: "uncovered", ticker: key, message: error.message };
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof ApiRequestError && (error.status === 401 || error.status === 402 || error.status === 403)) {
|
|
58
49
|
return { kind: "denied", status: error.status, message: error.message };
|
|
59
|
-
} else {
|
|
60
|
-
throw error;
|
|
61
50
|
}
|
|
51
|
+
throw error;
|
|
62
52
|
}
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function fetchJobs(
|
|
56
|
+
ticker: string,
|
|
57
|
+
options: { name?: string | null; force?: boolean; client?: JobsCloudClient } = {},
|
|
58
|
+
): Promise<JobsCompanyState> {
|
|
59
|
+
const key = ticker.toUpperCase();
|
|
60
|
+
return summaries.load(key, () => loadJobs(key, options.name, options.client ?? apiClient), { force: options.force });
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
export function fetchJobsPostings(
|
|
@@ -81,5 +77,5 @@ export function fetchJobsMovers(
|
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
export function resetJobsCache(): void {
|
|
84
|
-
|
|
80
|
+
summaries.clear();
|
|
85
81
|
}
|