gloomberb 0.18.0 → 0.18.2
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/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +31 -7
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/onboarding/wizard-positions.ts +8 -0
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +57 -22
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +63 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/ticker.ts +9 -0
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +41 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -20,6 +20,7 @@ import type { PluginModule } from "../plugin-module";
|
|
|
20
20
|
import { colors, blendHex } from "../../../theme/colors";
|
|
21
21
|
import type { EconEvent } from "./types";
|
|
22
22
|
import { FRED_CATALOG_SERIES } from "./fred-catalog";
|
|
23
|
+
import { ECON_PRINT_NOTIFICATIONS_ENABLED_KEY, retainVisiblePrintRefresh, setupHighImpactPrintNotifications } from "./print-watch";
|
|
23
24
|
import { EconDetailView } from "./detail-view";
|
|
24
25
|
import {
|
|
25
26
|
COUNTRY_CYCLE,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
formatCountdown,
|
|
36
37
|
formatStaleness,
|
|
37
38
|
getCalendarCache,
|
|
39
|
+
subscribeCalendar,
|
|
38
40
|
impactIndicator,
|
|
39
41
|
loadCalendar,
|
|
40
42
|
matchesCountry,
|
|
@@ -75,10 +77,16 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
75
77
|
// loadCalendar serves a fresh cache without a request, so the pane can always
|
|
76
78
|
// ask and still follow the global cadence once the cache goes stale.
|
|
77
79
|
const calendar = useAsyncResource<CalendarResult>(loadEvents, { initialData: cachedEvents });
|
|
80
|
+
// A load made elsewhere, such as the print watch asking while a release is
|
|
81
|
+
// due, reaches the open pane through the cache; the newer of the two wins.
|
|
82
|
+
const [cacheVersion, setCacheVersion] = useState(0);
|
|
83
|
+
useEffect(() => subscribeCalendar(() => setCacheVersion((version) => version + 1)), []);
|
|
84
|
+
const cached = useMemo(() => getCalendarCache(), [cacheVersion]);
|
|
85
|
+
const shown = cached && (!calendar.data || cached.fetchedAt > calendar.data.fetchedAt) ? cached : calendar.data;
|
|
78
86
|
const { loading } = calendar;
|
|
79
|
-
const events =
|
|
80
|
-
const stale =
|
|
81
|
-
const fetchedAt =
|
|
87
|
+
const events = shown?.data ?? NO_EVENTS;
|
|
88
|
+
const stale = shown?.stale ?? false;
|
|
89
|
+
const fetchedAt = shown?.fetchedAt ?? null;
|
|
82
90
|
// A cache that could not be refreshed arrives as a result, not a failure.
|
|
83
91
|
const error = calendar.error ?? (loading ? null : calendar.data?.refreshError ?? null);
|
|
84
92
|
// The selected row and the open event are remembered by event id, so a
|
|
@@ -94,8 +102,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
94
102
|
|
|
95
103
|
useAutoRefresh(stale ? null : fetchedAt, calendar.load);
|
|
96
104
|
|
|
97
|
-
// Tick every 30s to update staleness + countdown, only while the pane can be seen.
|
|
98
105
|
const paneVisible = usePaneVisible();
|
|
106
|
+
// In view, a due high-impact print is asked for on the print watch's timing
|
|
107
|
+
// and lands here through the cache; a covered or hidden pane asks nothing.
|
|
108
|
+
useEffect(() => (paneVisible ? retainVisiblePrintRefresh() : undefined), [paneVisible]);
|
|
109
|
+
// Tick every 30s to update staleness + countdown, only while the pane can be seen.
|
|
99
110
|
useEffect(() => {
|
|
100
111
|
if (!paneVisible) return;
|
|
101
112
|
setNow(Date.now());
|
|
@@ -437,6 +448,8 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
437
448
|
);
|
|
438
449
|
}
|
|
439
450
|
|
|
451
|
+
let stopPrintWatch: (() => void) | null = null;
|
|
452
|
+
|
|
440
453
|
export const economicCalendarModule: PluginModule = {
|
|
441
454
|
series: FRED_CATALOG_SERIES,
|
|
442
455
|
panes: [{
|
|
@@ -449,6 +462,16 @@ export const economicCalendarModule: PluginModule = {
|
|
|
449
462
|
defaultMode: "floating",
|
|
450
463
|
defaultFloatingSize: { width: 100, height: 30 },
|
|
451
464
|
tableExport: true,
|
|
465
|
+
settings: {
|
|
466
|
+
title: "Economic Calendar Settings",
|
|
467
|
+
fields: [{
|
|
468
|
+
key: ECON_PRINT_NOTIFICATIONS_ENABLED_KEY,
|
|
469
|
+
label: "Notifications",
|
|
470
|
+
description: "Notify when a high-impact release prints, even while this pane is closed.",
|
|
471
|
+
type: "toggle",
|
|
472
|
+
storage: "plugin",
|
|
473
|
+
}],
|
|
474
|
+
},
|
|
452
475
|
}],
|
|
453
476
|
paneTemplates: [{
|
|
454
477
|
id: "econ-calendar-pane",
|
|
@@ -461,8 +484,12 @@ export const economicCalendarModule: PluginModule = {
|
|
|
461
484
|
}],
|
|
462
485
|
setup(ctx) {
|
|
463
486
|
attachEconCalendarPersistence(ctx.persistence);
|
|
487
|
+
stopPrintWatch?.();
|
|
488
|
+
stopPrintWatch = setupHighImpactPrintNotifications(ctx);
|
|
464
489
|
},
|
|
465
490
|
dispose() {
|
|
491
|
+
stopPrintWatch?.();
|
|
492
|
+
stopPrintWatch = null;
|
|
466
493
|
resetEconCalendarPersistence();
|
|
467
494
|
},
|
|
468
495
|
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { GloomPluginContext } from "../../../types/plugin";
|
|
2
|
+
import {
|
|
3
|
+
getCalendarCache,
|
|
4
|
+
landedHighImpactPrints,
|
|
5
|
+
loadCalendar,
|
|
6
|
+
pendingHighImpactPrints,
|
|
7
|
+
PRINT_PENDING_MS,
|
|
8
|
+
subscribeCalendar,
|
|
9
|
+
type EconCalendarCacheEntry,
|
|
10
|
+
} from "./calendar-model";
|
|
11
|
+
import type { EconEvent } from "./types";
|
|
12
|
+
|
|
13
|
+
/** The ECO setting that turns the print toast on. Off until the user turns it on, as breaking news is. */
|
|
14
|
+
export const ECON_PRINT_NOTIFICATIONS_ENABLED_KEY = "econPrintNotificationsEnabled";
|
|
15
|
+
|
|
16
|
+
const TOAST_DURATION_MS = 20_000;
|
|
17
|
+
/**
|
|
18
|
+
* The waits between asks while a print is due: short just after the release
|
|
19
|
+
* time, when most prints land, then longer until the window closes, which
|
|
20
|
+
* keeps a release to about twenty asks per app instead of eighty.
|
|
21
|
+
*/
|
|
22
|
+
const PRINT_BACKOFF_MS = [15_000, 30_000, 60_000] as const;
|
|
23
|
+
/** Each wait moves by up to a fifth either way, so apps that started together drift apart. */
|
|
24
|
+
const PRINT_JITTER = 0.2;
|
|
25
|
+
/** How often the watch looks at the calendar it already has while nothing is due. */
|
|
26
|
+
const PRINT_IDLE_MS = 60_000;
|
|
27
|
+
/** While idle, a calendar this old is asked for again, so a release added since is known. */
|
|
28
|
+
const CALENDAR_RECHECK_MS = 6 * 60 * 60 * 1000;
|
|
29
|
+
/** With no calendar at all, how long a failed ask waits before the next. */
|
|
30
|
+
const NO_CALENDAR_RETRY_MS = 5 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
type CalendarRead = Pick<EconCalendarCacheEntry, "data" | "fetchedAt">;
|
|
33
|
+
|
|
34
|
+
export interface PrintWatchDeps {
|
|
35
|
+
now: () => number;
|
|
36
|
+
/** Asks for the calendar; `force` skips a fresh cache. */
|
|
37
|
+
load: (force: boolean) => Promise<CalendarRead>;
|
|
38
|
+
/** The cached calendar, however old, without a request. */
|
|
39
|
+
read: () => CalendarRead | null;
|
|
40
|
+
/** Called whenever anyone, a pane included, loads the calendar. */
|
|
41
|
+
subscribe: (listener: () => void) => () => void;
|
|
42
|
+
schedule: (run: () => Promise<void> | void, delay: number) => () => void;
|
|
43
|
+
random: () => number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_DEPS: PrintWatchDeps = {
|
|
47
|
+
now: Date.now,
|
|
48
|
+
load: (force) => loadCalendar(force),
|
|
49
|
+
read: () => getCalendarCache({ allowExpired: true }),
|
|
50
|
+
subscribe: subscribeCalendar,
|
|
51
|
+
schedule: (run, delay) => {
|
|
52
|
+
const timer = setTimeout(run, delay);
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
},
|
|
55
|
+
random: Math.random,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function printBody(event: EconEvent): string {
|
|
59
|
+
return event.forecast ? `${event.event} ${event.actual} vs ${event.forecast}` : `${event.event} ${event.actual}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type PrintWatchContext = Pick<GloomPluginContext, "notify" | "showPane">;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Toasts a high-impact print once, when it lands within
|
|
66
|
+
* {@link PRINT_PENDING_MS} of its release time. While one is due the
|
|
67
|
+
* calendar is asked for on {@link PRINT_BACKOFF_MS}, and every read is
|
|
68
|
+
* compared: the watch's own, its idle look at the cache, and a pane's
|
|
69
|
+
* refresh. The first read only records what has already printed.
|
|
70
|
+
*/
|
|
71
|
+
export function startHighImpactPrintWatch(
|
|
72
|
+
ctx: PrintWatchContext,
|
|
73
|
+
overrides: Partial<PrintWatchDeps> = {},
|
|
74
|
+
): () => void {
|
|
75
|
+
const deps: PrintWatchDeps = { ...DEFAULT_DEPS, ...overrides };
|
|
76
|
+
let stopped = false;
|
|
77
|
+
let running = false;
|
|
78
|
+
let cancel: (() => void) | null = null;
|
|
79
|
+
/** The newest calendar the watch has seen. */
|
|
80
|
+
let latest: CalendarRead | null = null;
|
|
81
|
+
let lastAskAt = Number.NEGATIVE_INFINITY;
|
|
82
|
+
let primed = false;
|
|
83
|
+
/**
|
|
84
|
+
* Each high-impact row not yet past its window, by id: null while blank,
|
|
85
|
+
* then its number. A number stays once seen, so a print that blanks and
|
|
86
|
+
* comes back is not a new print.
|
|
87
|
+
*/
|
|
88
|
+
const actuals = new Map<string, string | null>();
|
|
89
|
+
const expiries = new Map<string, number>();
|
|
90
|
+
let attempt = 0;
|
|
91
|
+
let dueIds = new Set<string>();
|
|
92
|
+
|
|
93
|
+
const toast = (event: EconEvent) => {
|
|
94
|
+
ctx.notify({
|
|
95
|
+
title: event.country,
|
|
96
|
+
body: printBody(event),
|
|
97
|
+
type: "info",
|
|
98
|
+
desktop: "always",
|
|
99
|
+
sound: "Glass",
|
|
100
|
+
duration: TOAST_DURATION_MS,
|
|
101
|
+
refId: `econ:${event.id}`,
|
|
102
|
+
action: { label: "Open", onClick: () => ctx.showPane("econ-calendar") },
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** `now` is when the read was asked for, so a print the last ask in the window brings back still counts. */
|
|
107
|
+
const observe = (read: CalendarRead | null, now = deps.now()) => {
|
|
108
|
+
if (stopped || !read) return;
|
|
109
|
+
if (!latest || read.fetchedAt >= latest.fetchedAt) latest = read;
|
|
110
|
+
const landed = primed ? landedHighImpactPrints(actuals, read.data) : [];
|
|
111
|
+
for (const event of read.data) {
|
|
112
|
+
if (event.impact !== "high") continue;
|
|
113
|
+
const expiresAt = event.date.getTime() + PRINT_PENDING_MS;
|
|
114
|
+
if (expiresAt < now) continue;
|
|
115
|
+
expiries.set(event.id, expiresAt);
|
|
116
|
+
if (event.actual) actuals.set(event.id, event.actual);
|
|
117
|
+
else if (!actuals.has(event.id)) actuals.set(event.id, null);
|
|
118
|
+
}
|
|
119
|
+
for (const [id, expiresAt] of expiries) {
|
|
120
|
+
if (expiresAt >= now) continue;
|
|
121
|
+
expiries.delete(id);
|
|
122
|
+
actuals.delete(id);
|
|
123
|
+
}
|
|
124
|
+
primed = true;
|
|
125
|
+
// A print that turns up after its window is recorded, not announced late.
|
|
126
|
+
for (const event of landed) {
|
|
127
|
+
if (now - event.date.getTime() <= PRINT_PENDING_MS) toast(event);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Whether a print in the newest calendar is due and still within its window. */
|
|
132
|
+
const printDue = (now: number): boolean => {
|
|
133
|
+
const pending = latest ? pendingHighImpactPrints(latest.data, now) : [];
|
|
134
|
+
// A release that has just come due starts the waits over.
|
|
135
|
+
if (pending.length === 0 || pending.some((event) => !dueIds.has(event.id))) attempt = 0;
|
|
136
|
+
dueIds = new Set(pending.map((event) => event.id));
|
|
137
|
+
return pending.length > 0;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const backoff = (): number => {
|
|
141
|
+
const base = PRINT_BACKOFF_MS[Math.min(attempt, PRINT_BACKOFF_MS.length - 1)]!;
|
|
142
|
+
attempt += 1;
|
|
143
|
+
return Math.round(base * (1 + (deps.random() * 2 - 1) * PRINT_JITTER));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const askWhileIdle = (now: number): boolean => (latest
|
|
147
|
+
? now - Math.max(latest.fetchedAt, lastAskAt) >= CALENDAR_RECHECK_MS
|
|
148
|
+
: now - lastAskAt >= NO_CALENDAR_RETRY_MS);
|
|
149
|
+
|
|
150
|
+
const arm = (delay: number) => {
|
|
151
|
+
if (stopped || cancel) return;
|
|
152
|
+
cancel = deps.schedule(() => {
|
|
153
|
+
cancel = null;
|
|
154
|
+
return run();
|
|
155
|
+
}, delay);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const run = async () => {
|
|
159
|
+
if (stopped || running) return;
|
|
160
|
+
observe(deps.read());
|
|
161
|
+
const before = deps.now();
|
|
162
|
+
const due = printDue(before);
|
|
163
|
+
if (!due && !askWhileIdle(before)) {
|
|
164
|
+
arm(PRINT_IDLE_MS);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
running = true;
|
|
168
|
+
lastAskAt = before;
|
|
169
|
+
try {
|
|
170
|
+
// A due print is asked for past the cache; otherwise a fresh cache answers without a request.
|
|
171
|
+
observe(await deps.load(due), before);
|
|
172
|
+
} catch {
|
|
173
|
+
// The next ask follows the same schedule.
|
|
174
|
+
} finally {
|
|
175
|
+
running = false;
|
|
176
|
+
}
|
|
177
|
+
if (stopped) return;
|
|
178
|
+
// Past the window nothing is due, so the asks stop and the watch goes back to idle looks.
|
|
179
|
+
arm(printDue(deps.now()) ? backoff() : PRINT_IDLE_MS);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// A load the watch is waiting on is compared when it answers, at the time it was asked.
|
|
183
|
+
const unsubscribe = deps.subscribe(() => {
|
|
184
|
+
if (!running) observe(deps.read());
|
|
185
|
+
});
|
|
186
|
+
void run();
|
|
187
|
+
return () => {
|
|
188
|
+
stopped = true;
|
|
189
|
+
unsubscribe();
|
|
190
|
+
cancel?.();
|
|
191
|
+
cancel = null;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type PrintNotificationContext = PrintWatchContext & Pick<GloomPluginContext, "configState" | "on">;
|
|
196
|
+
|
|
197
|
+
function printNotificationsEnabled(ctx: Pick<GloomPluginContext, "configState">): boolean {
|
|
198
|
+
return ctx.configState.get<boolean>(ECON_PRINT_NOTIFICATIONS_ENABLED_KEY) === true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The one print watch, run while anything wants it: the Notifications
|
|
203
|
+
* setting, or an ECO pane in view, which fills a print in as it lands. Every
|
|
204
|
+
* reason shares its timing and its requests; only the setting toasts.
|
|
205
|
+
*/
|
|
206
|
+
export interface PrintRefresh {
|
|
207
|
+
/** The plugin's context, for toasts and their Open; null once it is disposed. */
|
|
208
|
+
attach(ctx: PrintWatchContext | null): void;
|
|
209
|
+
setNotifying(on: boolean): void;
|
|
210
|
+
/** An ECO pane can be seen; the returned release says it no longer can. */
|
|
211
|
+
retainVisiblePane(): () => void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createPrintRefresh(
|
|
215
|
+
start: (ctx: PrintWatchContext) => () => void = startHighImpactPrintWatch,
|
|
216
|
+
): PrintRefresh {
|
|
217
|
+
let host: PrintWatchContext | null = null;
|
|
218
|
+
let notifying = false;
|
|
219
|
+
let visiblePanes = 0;
|
|
220
|
+
let stopWatch: (() => void) | null = null;
|
|
221
|
+
// A print that lands while only a pane wants the watch is recorded, not toasted.
|
|
222
|
+
const watchContext: PrintWatchContext = {
|
|
223
|
+
notify: (notice) => {
|
|
224
|
+
if (notifying) host?.notify(notice);
|
|
225
|
+
},
|
|
226
|
+
showPane: (paneId) => host?.showPane(paneId),
|
|
227
|
+
};
|
|
228
|
+
const sync = () => {
|
|
229
|
+
const wanted = notifying || visiblePanes > 0;
|
|
230
|
+
if (wanted && !stopWatch) stopWatch = start(watchContext);
|
|
231
|
+
if (!wanted && stopWatch) {
|
|
232
|
+
stopWatch();
|
|
233
|
+
stopWatch = null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
attach(ctx) {
|
|
238
|
+
host = ctx;
|
|
239
|
+
},
|
|
240
|
+
setNotifying(on) {
|
|
241
|
+
notifying = on;
|
|
242
|
+
sync();
|
|
243
|
+
},
|
|
244
|
+
retainVisiblePane() {
|
|
245
|
+
visiblePanes += 1;
|
|
246
|
+
sync();
|
|
247
|
+
let released = false;
|
|
248
|
+
return () => {
|
|
249
|
+
if (released) return;
|
|
250
|
+
released = true;
|
|
251
|
+
visiblePanes -= 1;
|
|
252
|
+
sync();
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const printRefresh = createPrintRefresh();
|
|
259
|
+
|
|
260
|
+
/** For the ECO pane: while it can be seen, a due high-impact print is asked for on the watch's timing. */
|
|
261
|
+
export function retainVisiblePrintRefresh(): () => void {
|
|
262
|
+
return printRefresh.retainVisiblePane();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Toasts prints while the Notifications setting is on; off, only a pane in view keeps the watch asking. */
|
|
266
|
+
export function setupHighImpactPrintNotifications(
|
|
267
|
+
ctx: PrintNotificationContext,
|
|
268
|
+
refresh: PrintRefresh = printRefresh,
|
|
269
|
+
): () => void {
|
|
270
|
+
refresh.attach(ctx);
|
|
271
|
+
const sync = () => refresh.setNotifying(printNotificationsEnabled(ctx));
|
|
272
|
+
const disposeConfigListener = ctx.on("config:changed", sync);
|
|
273
|
+
sync();
|
|
274
|
+
return () => {
|
|
275
|
+
disposeConfigListener();
|
|
276
|
+
refresh.setNotifying(false);
|
|
277
|
+
refresh.attach(null);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import type { HeadlessPaneColumn, HeadlessPaneContext } from "../../../types/headless";
|
|
3
3
|
import {
|
|
4
|
+
KPI_FIELDS,
|
|
4
5
|
NUMERIC_FIELDS,
|
|
5
6
|
type NumericField,
|
|
6
7
|
type ScreenRow,
|
|
7
8
|
} from "../../../api-client/equity-screener";
|
|
9
|
+
import { KPI_COVERAGE_NOTE } from "../company-kpis/model";
|
|
8
10
|
import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
|
|
9
11
|
import { buildQuoteKey } from "../../../market-data/selectors";
|
|
10
12
|
import type { Quote } from "../../../types/financials";
|
|
@@ -17,8 +19,11 @@ import {
|
|
|
17
19
|
formatScreenValue,
|
|
18
20
|
metricDate,
|
|
19
21
|
parseScreenDefinition,
|
|
22
|
+
screenColumnLabel,
|
|
20
23
|
screenDefinitionSyntax,
|
|
21
|
-
|
|
24
|
+
screenLabel,
|
|
25
|
+
screenYtdBase,
|
|
26
|
+
withheldMetricNote,
|
|
22
27
|
} from "./model";
|
|
23
28
|
import type { ScreenDefinition } from "../../../api-client/equity-screener";
|
|
24
29
|
|
|
@@ -128,12 +133,21 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
128
133
|
// A sort on one of them is redone on those figures.
|
|
129
134
|
const rows = sortLiveScreenRows(overlayScreenQuotes(data.rows, await loadRowQuotes(data.rows, ctx)), data.definition.sort);
|
|
130
135
|
const extendedSessions = screenExtendedSessions(rows);
|
|
136
|
+
// A Pro metric the server held back from Free says so once, as KPIS says it; a KPI
|
|
137
|
+
// with gaps says what a gap is, as KPIS does.
|
|
138
|
+
const withheld = rows.some((row) => row.metrics[metric].access === "pro");
|
|
139
|
+
const errors = withheld ? [...data.warnings, withheldMetricNote(metric)] : data.warnings;
|
|
140
|
+
const missing = rows.filter((row) => row.metrics[metric].value === null).length;
|
|
141
|
+
const kpiGaps = !withheld && missing > 0 && (KPI_FIELDS as readonly string[]).includes(metric)
|
|
142
|
+
? [`${missing} of ${rows.length} ${rows.length === 1 ? "row has" : "rows have"} no ${screenLabel(metric)}. ${KPI_COVERAGE_NOTE}`]
|
|
143
|
+
: [];
|
|
131
144
|
// Payload status also counts ordinary per-field sparsity; only generation
|
|
132
145
|
// warnings are failures, and more pages are not an error.
|
|
133
146
|
return {
|
|
134
147
|
freshness: { asOf: data.snapshot?.sourceNewestAt ?? null },
|
|
135
|
-
complete: !data.nextCursor &&
|
|
136
|
-
errors
|
|
148
|
+
complete: !data.nextCursor && errors.length === 0,
|
|
149
|
+
errors,
|
|
150
|
+
...(kpiGaps.length ? { notes: kpiGaps } : {}),
|
|
137
151
|
sections: [
|
|
138
152
|
{
|
|
139
153
|
title: "Matching equities",
|
|
@@ -145,7 +159,8 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
145
159
|
...(metric === "price" ? [] : [{ key: "price", header: "Last", align: "right" as const, format: price }]),
|
|
146
160
|
...(metric === "changePercent" ? [] : [{ key: "changePercent", header: "Chg%", align: "right" as const, format: percent }]),
|
|
147
161
|
...extendedSessions.map(extendedColumn),
|
|
148
|
-
|
|
162
|
+
// YTD names the close it is measured from: `YTD% vs 31 Dec close`.
|
|
163
|
+
{ key: "value", header: screenColumnLabel(metric, screenYtdBase(data.rows, data.snapshot?.assembledAt), "report"), align: "right" },
|
|
149
164
|
{ key: "unit", header: "Unit" },
|
|
150
165
|
{ key: "percentile", header: "Covered pctl", align: "right" },
|
|
151
166
|
{ key: "asOf", header: "As of" },
|
|
@@ -172,7 +187,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
172
187
|
? null
|
|
173
188
|
: Math.round(observation.percentile.value),
|
|
174
189
|
asOf: stamp.collected ? `${stamp.text} (collected)` : stamp.text,
|
|
175
|
-
state: observation.state,
|
|
190
|
+
state: observation.access === "pro" ? "locked" : observation.state,
|
|
176
191
|
};
|
|
177
192
|
}),
|
|
178
193
|
},
|
|
@@ -17,9 +17,11 @@ import {
|
|
|
17
17
|
type ScreenPayload,
|
|
18
18
|
type ScreenRow,
|
|
19
19
|
} from "../../../api-client/equity-screener";
|
|
20
|
+
import { yearToDateBaseLabel } from "../../../market-data/market/year-to-date";
|
|
20
21
|
import { canonicalExchange } from "../../../utils/exchanges";
|
|
21
22
|
import { LATEST_DISTRIBUTION, TRAILING_DIVIDEND_BASIS } from "../../../utils/forward-dividend";
|
|
22
23
|
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
24
|
+
import { kpiFreeLimitNote } from "../company-kpis/model";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Column headers. Currency sits in the footer; percentage fields carry %.
|
|
@@ -160,8 +162,29 @@ export function resultFields(definition: ScreenDefinition, metric: NumericField)
|
|
|
160
162
|
return [...new Set(fields)].filter((field) => definition.currency || !MONETARY.has(field) || field === metric);
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The close the screen's YTD returns are measured from, `31 Dec`: NYSE's last
|
|
167
|
+
* session of the year before the newest session a row's YTD ends on, the
|
|
168
|
+
* snapshot's day without one.
|
|
169
|
+
*/
|
|
170
|
+
export function screenYtdBase(rows: readonly ScreenRow[], assembledAt?: string | null, now = Date.now()): string | null {
|
|
171
|
+
const day = rows.map((row) => row.metrics.returnYtdPercent?.asOf?.slice(0, 10) ?? "").filter(Boolean).sort().at(-1)
|
|
172
|
+
?? assembledAt?.slice(0, 10);
|
|
173
|
+
return day ? yearToDateBaseLabel(day, "NYSE", now) : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A column's header. YTD names the close it is measured from: `YTD% vs 31 Dec
|
|
178
|
+
* close` in a report, `YTD% vs 31 Dec` in the pane and `YTD% 31 Dec` where
|
|
179
|
+
* the pane is short of room.
|
|
180
|
+
*/
|
|
181
|
+
export function screenColumnLabel(field: NumericField, ytdBase: string | null, form: "report" | "pane" | "compact" = "pane"): string {
|
|
182
|
+
if (field !== "returnYtdPercent" || !ytdBase) return SHORT_LABELS[field];
|
|
183
|
+
return form === "compact" ? `YTD% ${ytdBase}` : `YTD% vs ${ytdBase}${form === "report" ? " close" : ""}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
163
186
|
/** Prices get room for a separated six-figure quote (757,398.00). */
|
|
164
|
-
export const columnWidth = (field: NumericField) => Math.max(field === "price" ? 10 : 8,
|
|
187
|
+
export const columnWidth = (field: NumericField, label = SHORT_LABELS[field]) => Math.max(field === "price" ? 10 : 8, label.length);
|
|
165
188
|
|
|
166
189
|
export const DEFAULT_SCREEN: ScreenDefinition = {
|
|
167
190
|
version: 1,
|
|
@@ -481,6 +504,20 @@ const unavailableMetric = (): ScreenMetric => ({
|
|
|
481
504
|
state: "unavailable", scope: "reported", reason: "Not provided by this server", sourceUrl: null,
|
|
482
505
|
percentile: { value: null, sampleCount: 0, scope: "covered-universe" },
|
|
483
506
|
});
|
|
507
|
+
/** Company KPIs, guidance, hiring and app attention: the server leaves them out of a screen for a caller without Gloom Pro. */
|
|
508
|
+
const PRO_FIELDS: ReadonlySet<string> = new Set([...KPI_FIELDS, ...HIRING_APP_FIELDS]);
|
|
509
|
+
const withheldMetric = (): ScreenMetric => ({ ...unavailableMetric(), reason: "Needs Gloom Pro", access: "pro" });
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* What a screen says once of a metric the server held back from Free, a
|
|
513
|
+
* KPI's in the words `fn KPIS` uses ("The free screener shows no KPI values;
|
|
514
|
+
* Gloom Pro shows each company's Backlog.").
|
|
515
|
+
*/
|
|
516
|
+
export function withheldMetricNote(field: NumericField): string {
|
|
517
|
+
return (KPI_FIELDS as readonly string[]).includes(field)
|
|
518
|
+
? kpiFreeLimitNote({ metric: screenLabel(field), shown: 0, total: null, guidance: field.startsWith("guide"), where: "The free screener" })
|
|
519
|
+
: `${screenLabel(field)} needs Gloom Pro.`;
|
|
520
|
+
}
|
|
484
521
|
export function validateScreenPayload(value: unknown): ScreenPayload {
|
|
485
522
|
const data = value as ScreenPayload;
|
|
486
523
|
if (
|
|
@@ -523,8 +560,9 @@ export function validateScreenPayload(value: unknown): ScreenPayload {
|
|
|
523
560
|
ids.add(screenRowId(row));
|
|
524
561
|
for (const field of NUMERIC_FIELDS) {
|
|
525
562
|
// A server from before social, research, volatility or dividend fields omits them; they read as unavailable.
|
|
563
|
+
// A Pro field it left out was held back from this caller.
|
|
526
564
|
if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...VOLATILITY_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS, ...DIVIDEND_FIELDS] as readonly string[]).includes(field))
|
|
527
|
-
row.metrics[field] = unavailableMetric();
|
|
565
|
+
row.metrics[field] = PRO_FIELDS.has(field) ? withheldMetric() : unavailableMetric();
|
|
528
566
|
const metric = row.metrics[field];
|
|
529
567
|
if (
|
|
530
568
|
!metric ||
|
|
@@ -67,10 +67,11 @@ import {
|
|
|
67
67
|
metricDate,
|
|
68
68
|
parseScreenDefinition,
|
|
69
69
|
resultFields,
|
|
70
|
-
SHORT_LABELS,
|
|
71
70
|
SIGN_COLORED,
|
|
71
|
+
screenColumnLabel,
|
|
72
72
|
screenLabel,
|
|
73
73
|
screenRowId,
|
|
74
|
+
screenYtdBase,
|
|
74
75
|
} from "./model";
|
|
75
76
|
import { useScreenResults } from "./results";
|
|
76
77
|
import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, sortLiveScreenRows, type LiveScreenRow } from "./live";
|
|
@@ -101,20 +102,19 @@ const EXTENDED_COLUMN_PREFIX = "ext:";
|
|
|
101
102
|
* CHG% for each session in `extendedSessions`, and is added only whole, as
|
|
102
103
|
* the metrics are.
|
|
103
104
|
*/
|
|
104
|
-
function resultColumns(
|
|
105
|
+
export function resultColumns(
|
|
105
106
|
width: number,
|
|
106
107
|
definition: ScreenDefinition,
|
|
107
108
|
metric: NumericField,
|
|
108
109
|
asOf: boolean,
|
|
109
110
|
extendedSessions: readonly ExtendedSession[] = [],
|
|
111
|
+
ytdBase: string | null = null,
|
|
110
112
|
): DataTableColumn[] {
|
|
111
113
|
const [focus, ...others] = resultFields(definition, metric);
|
|
112
|
-
const metricColumn = (field: NumericField): DataTableColumn =>
|
|
113
|
-
|
|
114
|
-
label:
|
|
115
|
-
|
|
116
|
-
align: "right",
|
|
117
|
-
});
|
|
114
|
+
const metricColumn = (field: NumericField, form: "pane" | "compact"): DataTableColumn => {
|
|
115
|
+
const label = screenColumnLabel(field, ytdBase, form);
|
|
116
|
+
return { id: `m:${field}`, label, width: columnWidth(field, label), align: "right" };
|
|
117
|
+
};
|
|
118
118
|
const extendedColumns = extendedSessions.map((session): DataTableColumn => ({
|
|
119
119
|
id: `${EXTENDED_COLUMN_PREFIX}${session}`,
|
|
120
120
|
label: extendedSessionStateLabel(session),
|
|
@@ -122,20 +122,21 @@ function resultColumns(
|
|
|
122
122
|
align: "right",
|
|
123
123
|
}));
|
|
124
124
|
// The extended move reads beside CHG%, or first after the focus metric when that is CHG%.
|
|
125
|
-
const extras = [
|
|
125
|
+
const extras = (form: "pane" | "compact") => [
|
|
126
126
|
...(focus === "changePercent" ? extendedColumns : []),
|
|
127
|
-
...others.flatMap((field) => field === "changePercent" ? [metricColumn(field), ...extendedColumns] : [metricColumn(field)]),
|
|
127
|
+
...others.flatMap((field) => field === "changePercent" ? [metricColumn(field, form), ...extendedColumns] : [metricColumn(field, form)]),
|
|
128
128
|
];
|
|
129
129
|
const build = (
|
|
130
130
|
extra: DataTableColumn[],
|
|
131
131
|
exchange: boolean,
|
|
132
132
|
sector: boolean,
|
|
133
|
+
form: "pane" | "compact",
|
|
133
134
|
): DataTableColumn[] => [
|
|
134
135
|
{ id: "symbol", label: "SYMBOL", width: 8, align: "left" },
|
|
135
136
|
{ id: "name", label: "NAME", width: 18, flexGrow: 1, align: "left" },
|
|
136
137
|
...(exchange ? [{ id: "exchange", label: "EXCH", width: 7, align: "left" as const }] : []),
|
|
137
138
|
...(sector ? [{ id: "sector", label: "SECTOR", width: 22, align: "left" as const }] : []),
|
|
138
|
-
metricColumn(focus
|
|
139
|
+
metricColumn(focus!, form),
|
|
139
140
|
{ id: "percentile", label: "PCTL", width: 4, align: "right" },
|
|
140
141
|
...(asOf ? [{ id: "date", label: "AS OF", width: 10, align: "left" as const }] : []),
|
|
141
142
|
...extra,
|
|
@@ -143,15 +144,15 @@ function resultColumns(
|
|
|
143
144
|
// Measured the way the table draws them (header floor, gaps, lead gaps), so a
|
|
144
145
|
// column is only added when it fits whole.
|
|
145
146
|
const fits = (columns: DataTableColumn[]) => getTableWidth(columns) <= width;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
return build(shown, exchange, sector);
|
|
147
|
+
// Columns are chosen with YTD's short header (`YTD% 31 Dec`), which reads
|
|
148
|
+
// `YTD% vs 31 Dec` when that still fits: naming the close never costs a column.
|
|
149
|
+
const compact = extras("compact");
|
|
150
|
+
let shown = 0;
|
|
151
|
+
while (shown < compact.length && fits(build(compact.slice(0, shown + 1), false, false, "compact"))) shown++;
|
|
152
|
+
const exchange = fits(build(compact.slice(0, shown), true, false, "compact"));
|
|
153
|
+
const sector = exchange && fits(build(compact.slice(0, shown), true, true, "compact"));
|
|
154
|
+
const named = build(extras("pane").slice(0, shown), exchange, sector, "pane");
|
|
155
|
+
return fits(named) ? named : build(compact.slice(0, shown), exchange, sector, "compact");
|
|
155
156
|
}
|
|
156
157
|
const loadFields = () => fetchScreenFields();
|
|
157
158
|
function ScreenDetail({
|
|
@@ -351,7 +352,9 @@ function EquityScreenView({
|
|
|
351
352
|
const staleFocus = snapshotRows.some((row) => row.metrics[metric]?.state === "stale");
|
|
352
353
|
// A pre-market, after-hours or overnight column only while a row has such a print.
|
|
353
354
|
const extendedSessions = screenExtendedSessions(rows);
|
|
354
|
-
|
|
355
|
+
// YTD names the close it is measured from, `31 Dec`.
|
|
356
|
+
const ytdBase = screenYtdBase(snapshotRows, data?.snapshot?.assembledAt);
|
|
357
|
+
const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase);
|
|
355
358
|
const apply = (next: ScreenDefinition) => {
|
|
356
359
|
try {
|
|
357
360
|
setDefinition(parseScreenDefinition(next));
|
|
@@ -45,10 +45,11 @@ export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
45
45
|
instrument?.exchange ?? "",
|
|
46
46
|
ctx.apiClient,
|
|
47
47
|
ctx.marketData,
|
|
48
|
-
).catch((error: unknown) => {
|
|
49
|
-
throw explainRefusal(error, estimateRevisionsLockNote, {
|
|
48
|
+
).catch(async (error: unknown) => {
|
|
49
|
+
throw await explainRefusal(error, estimateRevisionsLockNote, {
|
|
50
50
|
symbol: args.symbols[0] ?? null,
|
|
51
51
|
token: ctx.settings?.tab === "guidance" ? "GUID" : "EM",
|
|
52
|
+
context: ctx,
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
const periods = pinnedEstimatePeriods(
|