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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HeadlessPaneColumn, HeadlessPaneRow } from "../../../types/headless";
|
|
2
|
+
import { fxCloseOnOrBefore, loadDailyFxCloses, type FxCloses, type FxHistoryClient } from "../../../market-data/fx-closes";
|
|
3
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
|
+
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
5
|
+
|
|
6
|
+
/** A Form 4 states its prices and values in US dollars. */
|
|
7
|
+
export const FORM4_CURRENCY = "USD";
|
|
8
|
+
|
|
9
|
+
/** The rows with their currency, and for a base currency other than USD each value in it, with the columns and note that say so. */
|
|
10
|
+
export interface InsiderBaseValues {
|
|
11
|
+
rows: HeadlessPaneRow[];
|
|
12
|
+
/** The money columns labelled with their currency, and the converted value and its rate; null for a USD base, whose columns stay as they are. */
|
|
13
|
+
columns: HeadlessPaneColumn[] | null;
|
|
14
|
+
note: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dash = (value: unknown, format: (value: number) => string) => (typeof value === "number" && Number.isFinite(value) ? format(value) : "-");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every row carries the currency its price and value are in (USD, as a Form 4
|
|
21
|
+
* states them). With another base currency (CAD), each value is also given
|
|
22
|
+
* in it at the daily close of USD/CAD on its transaction day, or the last
|
|
23
|
+
* close before a weekend or holiday (`fxCloseOnOrBefore` over the history
|
|
24
|
+
* PORT and PEB read), with the rate and that close's date beside it. When the
|
|
25
|
+
* closes cannot be read the values stay in USD and the note says why.
|
|
26
|
+
*/
|
|
27
|
+
export async function withInsiderBaseValues(
|
|
28
|
+
rows: readonly HeadlessPaneRow[],
|
|
29
|
+
baseCurrency: string,
|
|
30
|
+
client: FxHistoryClient,
|
|
31
|
+
now = new Date(),
|
|
32
|
+
): Promise<InsiderBaseValues> {
|
|
33
|
+
const labelled: HeadlessPaneRow[] = rows.map((row) => ({ ...row, currency: FORM4_CURRENCY }));
|
|
34
|
+
const base = baseCurrency.trim().toUpperCase();
|
|
35
|
+
if (!base || base === FORM4_CURRENCY) return { rows: labelled, columns: null, note: null };
|
|
36
|
+
const pair = `${FORM4_CURRENCY}/${base}`;
|
|
37
|
+
const days = labelled.flatMap((row) => typeof row.transactionDate === "string" && typeof row.totalValue === "number"
|
|
38
|
+
? [row.transactionDate.slice(0, 10)] : []).sort();
|
|
39
|
+
let closes: FxCloses | null = null;
|
|
40
|
+
if (days.length > 0) {
|
|
41
|
+
try {
|
|
42
|
+
closes = await loadDailyFxCloses(base, days[0]!, client, now);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logFailure("insider.base-values", `Daily ${pair} closes failed`)(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const converted = labelled.map((row) => {
|
|
48
|
+
const day = typeof row.transactionDate === "string" ? row.transactionDate.slice(0, 10) : null;
|
|
49
|
+
const reading = closes && day && typeof row.totalValue === "number" ? fxCloseOnOrBefore(closes, day) : null;
|
|
50
|
+
// The close is USD per unit of the base currency; the rate is the base currency per dollar.
|
|
51
|
+
const fxRate = reading ? 1 / reading.rate : null;
|
|
52
|
+
return {
|
|
53
|
+
...row,
|
|
54
|
+
baseCurrency: base,
|
|
55
|
+
valueBase: fxRate != null ? (row.totalValue as number) * fxRate : null,
|
|
56
|
+
fxRate,
|
|
57
|
+
fxDate: reading?.date ?? null,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
const columns: HeadlessPaneColumn[] = [
|
|
61
|
+
{ key: "pricePerShare", header: `Price (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, (price) => formatNumber(price, 2)) },
|
|
62
|
+
{ key: "totalValue", header: `Value (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, formatCompact) },
|
|
63
|
+
...closes ? [
|
|
64
|
+
{ key: "valueBase", header: `Value (${base})`, align: "right" as const, format: (value: unknown) => dash(value, formatCompact) },
|
|
65
|
+
{ key: "fxRate", header: pair, align: "right" as const, stackPriority: 1, format: (value: unknown) => dash(value, (rate) => formatNumber(rate, 4)) },
|
|
66
|
+
] : [],
|
|
67
|
+
];
|
|
68
|
+
const note = days.length === 0 ? null
|
|
69
|
+
: closes ? `Value (${base}) is each value at the ${pair} daily close of its transaction day, or of the trading day before it; ${pair} is that rate.`
|
|
70
|
+
: `Daily ${pair} closes are unavailable, so values stay in ${FORM4_CURRENCY}.`;
|
|
71
|
+
return { rows: closes ? converted : labelled, columns, note };
|
|
72
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
9
9
|
import { loadParsedInsiderFilings, type InsiderFilingsLoad } from "./client";
|
|
10
10
|
import { insiderEmptyNotice } from "./issuer";
|
|
11
|
+
import { FORM4_CURRENCY, withInsiderBaseValues } from "./base-values";
|
|
11
12
|
import {
|
|
12
13
|
buildInsiderRows,
|
|
13
14
|
buildInsiderSummary,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
insiderUsualRatio,
|
|
59
60
|
} from "./signals-model";
|
|
60
61
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
62
|
+
import type { FxHistoryClient } from "../../../market-data/fx-closes";
|
|
61
63
|
|
|
62
64
|
const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
63
65
|
{ key: "form", header: "Form" },
|
|
@@ -106,6 +108,20 @@ const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
106
108
|
{ key: "note", header: "Note", underRow: true, format: (value) => typeof value === "string" ? value : "" },
|
|
107
109
|
];
|
|
108
110
|
|
|
111
|
+
/** With a base currency other than USD, the dollar columns say they are in USD ("Bought (USD)"), as $ alone is several currencies' sign. */
|
|
112
|
+
function labelDollarColumns(columns: HeadlessPaneColumn[], keys: readonly string[], baseCurrency: string): HeadlessPaneColumn[] {
|
|
113
|
+
if (baseCurrency.trim().toUpperCase() === FORM4_CURRENCY) return columns;
|
|
114
|
+
return columns.map((column) => keys.includes(column.key) ? { ...column, header: `${column.header} (${FORM4_CURRENCY})` } : column);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The trades' columns with the money ones labelled by currency and the converted value and rate after them (`withInsiderBaseValues`). */
|
|
118
|
+
function withMoneyColumns(money: readonly HeadlessPaneColumn[]): HeadlessPaneColumn[] {
|
|
119
|
+
const byKey = new Map(money.map((column) => [column.key, column]));
|
|
120
|
+
return INSIDER_COLUMNS.flatMap((column) => column.key === "totalValue"
|
|
121
|
+
? money.filter((entry) => entry.key !== "pricePerShare")
|
|
122
|
+
: [byKey.get(column.key) ?? column]);
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
const figureOrDash = (value: unknown) => (typeof value === "number" ? value : null);
|
|
110
126
|
|
|
111
127
|
/**
|
|
@@ -183,6 +199,7 @@ function insidersRows(activity: InsiderCompanyActivity, side: InsiderSignalSide)
|
|
|
183
199
|
stakeBothWays: line.buyer.twoWay === true,
|
|
184
200
|
bought: line.bought,
|
|
185
201
|
sold: line.sold,
|
|
202
|
+
currency: FORM4_CURRENCY,
|
|
186
203
|
shares: line.buyer.shares ?? null,
|
|
187
204
|
last: line.buyer.lastBuy,
|
|
188
205
|
}));
|
|
@@ -211,6 +228,7 @@ function signalRows(payload: InsiderSignalsPayload): HeadlessPaneRow[] {
|
|
|
211
228
|
insiders: row.insiders,
|
|
212
229
|
buys: row.buys,
|
|
213
230
|
value: row.value,
|
|
231
|
+
currency: FORM4_CURRENCY,
|
|
214
232
|
shares: row.shares,
|
|
215
233
|
firstBuy: row.firstBuy,
|
|
216
234
|
lastBuy: row.lastBuy,
|
|
@@ -251,6 +269,8 @@ function amendmentNote(entry: ParsedInsiderFiling): string {
|
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
export interface InsiderHeadlessDependencies {
|
|
272
|
+
/** Daily FX closes for values in a base currency other than USD; the report's API client unless a test stands in. */
|
|
273
|
+
fxHistory?: FxHistoryClient;
|
|
254
274
|
loadSignals?(query: InsiderSignalsQuery, limit: number, ctx: HeadlessPaneContext): Promise<InsiderSignalsPayload>;
|
|
255
275
|
loadCompany?(query: InsiderCompanyQuery, ctx: HeadlessPaneContext): Promise<InsiderCompanyActivity>;
|
|
256
276
|
loadParsed(
|
|
@@ -441,9 +461,9 @@ export function createInsiderHeadless(
|
|
|
441
461
|
const payload = await (dependencies.loadSignals ?? defaultDependencies.loadSignals!)(query, Number(args.options.limit), ctx);
|
|
442
462
|
const side = payload.side ?? "buy";
|
|
443
463
|
return {
|
|
444
|
-
columns: signalColumns(side),
|
|
464
|
+
columns: labelDollarColumns(signalColumns(side), ["value"], ctx.config.baseCurrency),
|
|
445
465
|
// The exports carry each row's insiders; the text leaves them to fn INS <ticker> --view insiders.
|
|
446
|
-
textColumns: signalColumns(side).filter((column) => column.key !== "buyers"),
|
|
466
|
+
textColumns: labelDollarColumns(signalColumns(side).filter((column) => column.key !== "buyers"), ["value"], ctx.config.baseCurrency),
|
|
447
467
|
rows: signalRows(payload),
|
|
448
468
|
...(payload.rows.length === 0 ? { empty: insiderSignalsEmptyTitle(payload.window, side, insiderSignalsFiltered(query)) } : {}),
|
|
449
469
|
metadata: {
|
|
@@ -472,7 +492,7 @@ export function createInsiderHeadless(
|
|
|
472
492
|
.filter((notice): notice is string => !!notice))];
|
|
473
493
|
const rows = insidersRows(activity, side);
|
|
474
494
|
return {
|
|
475
|
-
columns: INSIDERS_COLUMNS,
|
|
495
|
+
columns: labelDollarColumns(INSIDERS_COLUMNS, ["bought", "sold"], ctx.config.baseCurrency),
|
|
476
496
|
rows,
|
|
477
497
|
...(rows.length === 0 ? { empty: insiderActivityEmptyTitle(window) } : {}),
|
|
478
498
|
metadata: {
|
|
@@ -503,12 +523,17 @@ export function createInsiderHeadless(
|
|
|
503
523
|
.filter((row) => row.status !== "parsed" && row.status !== "disclosure")
|
|
504
524
|
.map((row) => row.accessionNumber))];
|
|
505
525
|
// A filing's lines sit together, so its note goes once, under the last of them.
|
|
506
|
-
const
|
|
526
|
+
const filed: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
|
|
507
527
|
const entry = filtered[index]!;
|
|
508
528
|
const last = filtered[index + 1]?.filing.accessionNumber !== entry.filing.accessionNumber;
|
|
509
529
|
return isAmendedInsiderFiling(entry) && last ? { ...row, note: amendmentNote(entry) || null } : row;
|
|
510
530
|
});
|
|
531
|
+
// Every value says its currency; a base currency other than USD gets each value converted beside it.
|
|
532
|
+
const based = await withInsiderBaseValues(filed, ctx.config.baseCurrency, dependencies.fxHistory ?? ctx.apiClient);
|
|
533
|
+
const rows = based.rows;
|
|
511
534
|
return {
|
|
535
|
+
...(based.columns ? { columns: withMoneyColumns(based.columns) } : {}),
|
|
536
|
+
...(based.note ? { notes: [based.note] } : {}),
|
|
512
537
|
rows,
|
|
513
538
|
// Nothing at all: the company has no Form 4 of its own, which is the whole answer.
|
|
514
539
|
...(empty ? { empty, unavailableSymbols: [] } : {}),
|
|
@@ -183,11 +183,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
183
183
|
const symbol = args.symbols[0];
|
|
184
184
|
if (symbol) {
|
|
185
185
|
const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
|
|
186
|
-
if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
|
|
186
|
+
if (state.kind === "denied") throw await explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol, context: ctx });
|
|
187
187
|
return companyBundle(symbol, state, limit);
|
|
188
188
|
}
|
|
189
189
|
const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
|
|
190
|
-
.catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
190
|
+
.catch(async (error: unknown) => { throw await explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
191
191
|
const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
|
|
192
192
|
ticker: row.ticker,
|
|
193
193
|
company: row.company,
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import type { HiringPayload } from "../../../api-client/hiring";
|
|
3
|
+
import { fetchAttention } from "../company-attention/client";
|
|
4
|
+
import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
|
|
5
|
+
|
|
6
|
+
/** HIRE's preview, for a company whose careers system it reads: Coffee Holding (JVA) is mapped but uncovered. */
|
|
7
|
+
const HIRE_ROUTE: FreeRoute = {
|
|
8
|
+
text: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
|
|
9
|
+
answers: async (symbol, context) => {
|
|
10
|
+
const data = await fetchAttention("hiring", symbol, { limit: 1 }, context.apiClient, context.signal) as HiringPayload;
|
|
11
|
+
return data.evidence.length + data.series.length + data.functions.length > 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* JOBS is Pro whole. HIRE reads the same careers systems and needs no
|
|
6
17
|
* account: a company's latest roles, without the full history.
|
|
7
18
|
*/
|
|
8
|
-
export function jobsLockNote(lock: PaneLock): string {
|
|
19
|
+
export function jobsLockNote(lock: PaneLock): Promise<string> {
|
|
9
20
|
return proLockNote(lock, {
|
|
10
21
|
token: "JOBS",
|
|
11
22
|
argument: "optional",
|
|
12
23
|
does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
|
|
13
24
|
shows: "it",
|
|
14
|
-
free:
|
|
25
|
+
free: [HIRE_ROUTE],
|
|
15
26
|
});
|
|
16
27
|
}
|
|
@@ -180,14 +180,33 @@ const DAY_MS = 86_400_000;
|
|
|
180
180
|
/** Unresolved halts move out of the recent views at 30 elapsed days. */
|
|
181
181
|
const LONG_TERM_MS = 30 * DAY_MS;
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/** How long a halt has kept trading stopped: to the trade resumption, or to now while it lasts. Quotes resuming do not end it. */
|
|
184
|
+
function haltDurationMs(record: HaltRecord, now: number): number {
|
|
184
185
|
const end = record.tradeResumeAt == null ? now : Math.min(now, record.tradeResumeAt);
|
|
185
|
-
|
|
186
|
+
return Math.max(0, end - record.haltedAt);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** How long since the halt began, whether or not trading has resumed. */
|
|
190
|
+
function haltAgeMs(record: HaltRecord, now: number): number {
|
|
191
|
+
return Math.max(0, now - record.haltedAt);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatElapsed(elapsed: number): string {
|
|
186
195
|
if (elapsed >= DAY_MS) return `${Math.floor(elapsed / DAY_MS)}d`;
|
|
187
196
|
if (elapsed >= 3_600_000) return `${Math.floor(elapsed / 3_600_000)}h`;
|
|
188
197
|
return `${Math.floor(elapsed / 60_000)}m`;
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/** HALTED FOR: RAIL halted 16:40 and resumed 17:15 was halted for 35m, read at any later time. */
|
|
201
|
+
export function formatHaltDuration(record: HaltRecord, now: number): string {
|
|
202
|
+
return formatElapsed(haltDurationMs(record, now));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** AGE: RAIL's 16:40 halt is 1h old at 17:45, though it lasted 35m. */
|
|
206
|
+
export function formatHaltAge(record: HaltRecord, now: number): string {
|
|
207
|
+
return formatElapsed(haltAgeMs(record, now));
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
export const HALT_FILTERS: ReadonlyArray<{ label: string; value: HaltFilter }> = [
|
|
192
211
|
{ label: "All", value: "all" },
|
|
193
212
|
{ label: "Active", value: "active" },
|
|
@@ -221,6 +240,7 @@ export type HaltColumnId =
|
|
|
221
240
|
| "halted"
|
|
222
241
|
| "quote"
|
|
223
242
|
| "trade"
|
|
243
|
+
| "duration"
|
|
224
244
|
| "status";
|
|
225
245
|
|
|
226
246
|
export type HaltColumn = DataTableColumn & { id: HaltColumnId };
|
|
@@ -235,6 +255,7 @@ export const HALT_SORT_COLUMN_IDS: readonly HaltColumnId[] = [
|
|
|
235
255
|
"halted",
|
|
236
256
|
"quote",
|
|
237
257
|
"trade",
|
|
258
|
+
"duration",
|
|
238
259
|
"status",
|
|
239
260
|
];
|
|
240
261
|
|
|
@@ -262,16 +283,18 @@ const HALT_COLUMNS: readonly HaltColumn[] = [
|
|
|
262
283
|
{ id: "halted", label: "HALT ET", width: 9, align: "left" },
|
|
263
284
|
{ id: "quote", label: "QUOTE ET", width: 11, align: "left" },
|
|
264
285
|
{ id: "trade", label: "TRADE ET", width: 11, align: "left" },
|
|
286
|
+
{ id: "duration", label: "HALTED FOR", width: 12, align: "right" },
|
|
265
287
|
{ id: "status", label: "STATUS", width: 8, align: "left" },
|
|
266
288
|
];
|
|
267
289
|
|
|
268
290
|
/**
|
|
269
291
|
* What a narrow pane gives up first, so STATUS never scrolls out of view. MKT
|
|
270
292
|
* is only the venue, STATUS already reads QUOTE once quoting resumes, and
|
|
271
|
-
* REASON spells out CODE. Trade resumption
|
|
272
|
-
* the
|
|
293
|
+
* REASON spells out CODE. Trade resumption, halt time and then the age, which
|
|
294
|
+
* the date and halt time also give, go only in the narrowest panes; symbol,
|
|
295
|
+
* reason, halt date, how long it was halted for and status always stay.
|
|
273
296
|
*/
|
|
274
|
-
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted"];
|
|
297
|
+
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted", "age"];
|
|
275
298
|
|
|
276
299
|
export function buildHaltColumns(width: number): HaltColumn[] {
|
|
277
300
|
const columns = fitChartTableColumns(HALT_COLUMNS, width, NARROW_DROP_ORDER);
|
|
@@ -304,7 +327,9 @@ function haltSortValue(columnId: HaltColumnId, record: HaltRecord, now: number):
|
|
|
304
327
|
case "halted":
|
|
305
328
|
return record.haltedAt;
|
|
306
329
|
case "age":
|
|
307
|
-
return
|
|
330
|
+
return haltAgeMs(record, now);
|
|
331
|
+
case "duration":
|
|
332
|
+
return haltDurationMs(record, now);
|
|
308
333
|
case "quote":
|
|
309
334
|
return record.quoteResumeAt;
|
|
310
335
|
case "trade":
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
formatEtResumption,
|
|
30
30
|
formatEtTime,
|
|
31
31
|
formatHaltAge,
|
|
32
|
+
formatHaltDuration,
|
|
32
33
|
haltStatusColor,
|
|
33
34
|
haltStatusLabel,
|
|
34
35
|
nextHaltFilter,
|
|
@@ -179,6 +180,8 @@ export function MarketHaltsPane({ focused, width, height }: PaneProps) {
|
|
|
179
180
|
text: formatEtResumption(row.tradeResumeAt, row.haltedAt),
|
|
180
181
|
color: selectedColor ?? colors.textDim,
|
|
181
182
|
};
|
|
183
|
+
case "duration":
|
|
184
|
+
return { text: formatHaltDuration(row, now), color: selectedColor ?? colors.textMuted };
|
|
182
185
|
case "status": {
|
|
183
186
|
const rowStatus = resolveHaltStatus(row, now);
|
|
184
187
|
return {
|
|
@@ -110,10 +110,10 @@ export async function loadMarketMoverTab(
|
|
|
110
110
|
return { ...result, quotes: quotes.slice(0, LIST_ROWS), rankedBy, tab };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/** The server refused the list for the account's plan. */
|
|
113
|
+
/** The server refused the list for the account's plan (gaps, without Pro). */
|
|
114
114
|
export class SessionMoversAccessError extends Error {
|
|
115
115
|
constructor() {
|
|
116
|
-
super("
|
|
116
|
+
super("Gap lists need Pro");
|
|
117
117
|
this.name = "SessionMoversAccessError";
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -8,7 +8,6 @@ import type { PluginModule } from "../plugin-module";
|
|
|
8
8
|
import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
|
|
9
9
|
import { publicTickerKey } from "../../../utils/exchanges";
|
|
10
10
|
import { usePaneSettingValue } from "../../../state/app/context";
|
|
11
|
-
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
12
11
|
import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
13
12
|
import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
|
|
14
13
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
@@ -87,7 +86,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
87
86
|
const [savedTab, setSavedTab] = usePluginPaneState<TabId>("activeTab", tabs[0]!.id);
|
|
88
87
|
const [pickedIn, setPickedIn] = usePluginPaneState<string | null>("activeTabSession", null);
|
|
89
88
|
const [requestedTab, setRequestedTab] = usePluginPaneState<TabId | null>("requestedTab", null);
|
|
90
|
-
const access = usePlanAccess();
|
|
91
89
|
const session = useUsSession();
|
|
92
90
|
const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]);
|
|
93
91
|
const activeTab = resolveActiveTab({
|
|
@@ -96,7 +94,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
96
94
|
pickedIn,
|
|
97
95
|
session,
|
|
98
96
|
requested: requestedTab,
|
|
99
|
-
sessionListsOpen: access.signedIn && access.emailVerified && access.hasProAccess,
|
|
100
97
|
});
|
|
101
98
|
|
|
102
99
|
// The index summary is a quote board like any other, so it runs on the shared
|
|
@@ -24,6 +24,7 @@ import { summaryFooterSegments } from "./footer";
|
|
|
24
24
|
import type { MarketSummaryQuote } from "./screener";
|
|
25
25
|
import {
|
|
26
26
|
earlierSessionLabel,
|
|
27
|
+
listAsOfLabel,
|
|
27
28
|
overlaySessionMovers,
|
|
28
29
|
resolveSide,
|
|
29
30
|
sessionListIsLive,
|
|
@@ -46,7 +47,13 @@ interface LoadedList {
|
|
|
46
47
|
payload: CloudSessionMoversPayload;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Pre-market, after-hours and gap lists. Pre-market and after hours open on
|
|
52
|
+
* every plan: without Pro the server answers with the list as it was built a
|
|
53
|
+
* delay before (`access: "delayed"`), as old as the account's quotes, and the
|
|
54
|
+
* footer gives that time. Gaps are Pro only, like the ranked lists they sit
|
|
55
|
+
* beside.
|
|
56
|
+
*/
|
|
50
57
|
export function SessionMoversBody(props: {
|
|
51
58
|
view: CloudSessionMoversCategory;
|
|
52
59
|
session: UsSession;
|
|
@@ -57,29 +64,31 @@ export function SessionMoversBody(props: {
|
|
|
57
64
|
liveStreaming: boolean;
|
|
58
65
|
}) {
|
|
59
66
|
const access = usePlanAccess();
|
|
67
|
+
const proOnly = props.view === "gaps";
|
|
60
68
|
const [denied, setDenied] = useState(false);
|
|
61
69
|
const deny = useCallback(() => setDenied(true), []);
|
|
62
70
|
// A refusal is about the plan the account had then; a new plan asks again.
|
|
63
71
|
useEffect(() => setDenied(false), [access.signedIn, access.emailVerified, access.hasProAccess]);
|
|
64
|
-
const entitled = access.emailVerified && access.hasProAccess && !denied;
|
|
72
|
+
const entitled = !proOnly || (access.emailVerified && access.hasProAccess && !denied);
|
|
65
73
|
// Behind a wall the footer still carries the index summary; the list registers its own.
|
|
66
74
|
usePaneFooter("market-movers-wall", () => (
|
|
67
75
|
entitled ? null : { info: summaryFooterSegments(props.summaryQuotes) }
|
|
68
76
|
), [entitled, props.summaryQuotes]);
|
|
69
77
|
|
|
70
|
-
if (!access.signedIn) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see
|
|
71
|
-
if (!access.emailVerified) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see
|
|
78
|
+
if (proOnly && !access.signedIn) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see gap movers" />;
|
|
79
|
+
if (proOnly && !access.emailVerified) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see gap movers" needsVerification />;
|
|
72
80
|
if (!entitled) {
|
|
73
81
|
return (
|
|
74
82
|
<ProWall
|
|
75
83
|
placement="most-wall"
|
|
76
84
|
width={props.width} height={props.height}
|
|
77
|
-
title="
|
|
78
|
-
message="
|
|
85
|
+
title="Gap movers are part of Gloom Cloud Pro."
|
|
86
|
+
message="Every listed US stock that opened away from its prior close, with relative volume, VWAP, float and catalysts."
|
|
79
87
|
/>
|
|
80
88
|
);
|
|
81
89
|
}
|
|
82
|
-
|
|
90
|
+
// A new plan reads the list again: live with Pro, a delay before without.
|
|
91
|
+
return <SessionMoversTable key={access.hasProAccess ? "pro" : "free"} {...props} onDenied={proOnly ? deny : undefined} />;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
function SessionMoversTable({ view, session, focused, width, summaryQuotes, liveStreaming, onDenied }: {
|
|
@@ -89,7 +98,8 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
89
98
|
width: number;
|
|
90
99
|
summaryQuotes: MarketSummaryQuote[];
|
|
91
100
|
liveStreaming: boolean;
|
|
92
|
-
|
|
101
|
+
/** Gaps: the server refused the list for the account's plan. */
|
|
102
|
+
onDenied?: () => void;
|
|
93
103
|
}) {
|
|
94
104
|
const { pinTicker } = usePluginTickerActions();
|
|
95
105
|
const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
|
|
@@ -106,7 +116,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
106
116
|
|
|
107
117
|
const payload = loaded?.key === listKey ? loaded.payload : null;
|
|
108
118
|
const listRows = payload?.items ?? NO_ROWS;
|
|
109
|
-
|
|
119
|
+
// A free account's list is the one built a delay before: no live prices on its rows.
|
|
120
|
+
const delayed = payload?.access === "delayed";
|
|
121
|
+
const sessionTrading = !!payload && sessionListIsLive(view, payload.session, session);
|
|
122
|
+
const live = sessionTrading && !delayed;
|
|
110
123
|
// While the list's session trades it is rebuilt every minute, and a failed or
|
|
111
124
|
// stale answer is asked for again as often; a finished list does not change,
|
|
112
125
|
// so it follows the refresh interval the user set.
|
|
@@ -126,7 +139,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
126
139
|
setLastLoadedAt(Date.now());
|
|
127
140
|
} catch (error) {
|
|
128
141
|
if (fetchGenRef.current !== gen) return;
|
|
129
|
-
if (error instanceof SessionMoversAccessError) {
|
|
142
|
+
if (error instanceof SessionMoversAccessError && onDenied) {
|
|
130
143
|
onDenied();
|
|
131
144
|
return;
|
|
132
145
|
}
|
|
@@ -172,11 +185,14 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
172
185
|
}, [rows, selectedId]);
|
|
173
186
|
|
|
174
187
|
const sessionLabel = payload ? earlierSessionLabel(payload.session, session) : null;
|
|
188
|
+
// While its session trades, a free list says when it was built; a finished one is final and its date says enough.
|
|
189
|
+
const asOfLabel = payload && delayed && sessionTrading ? listAsOfLabel(payload.asOf, payload.session, session) : null;
|
|
175
190
|
usePaneFooter("market-movers", () => ({
|
|
176
191
|
info: [
|
|
177
192
|
...(loadError ? [{ id: "load-error", parts: [{ text: loadError, tone: "warning" as const }] }] : []),
|
|
178
193
|
...summaryFooterSegments(summaryQuotes),
|
|
179
194
|
...(sessionLabel ? [{ id: "session", parts: [{ text: sessionLabel, tone: "muted" as const }] }] : []),
|
|
195
|
+
...(asOfLabel ? [{ id: "as-of", asOf: true, parts: [{ text: asOfLabel, tone: "muted" as const }] }] : []),
|
|
180
196
|
...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
|
|
181
197
|
...(feedStatus ? [{
|
|
182
198
|
id: "feed",
|
|
@@ -184,7 +200,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
184
200
|
}] : []),
|
|
185
201
|
...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
|
|
186
202
|
],
|
|
187
|
-
}), [feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
|
|
203
|
+
}), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
|
|
188
204
|
|
|
189
205
|
const openRow = useCallback((row: SessionMoverRow) => {
|
|
190
206
|
pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
|
|
@@ -11,7 +11,7 @@ import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors"
|
|
|
11
11
|
import type { QueryEntry } from "../../../market-data/result-types";
|
|
12
12
|
import type { Quote } from "../../../types/financials";
|
|
13
13
|
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
14
|
-
import { zonedDateKey, zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
|
|
14
|
+
import { zonedDateKey, zonedDateTimeParts, zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
|
|
15
15
|
import type { SessionTabId } from "./model";
|
|
16
16
|
|
|
17
17
|
const NEW_YORK = "America/New_York";
|
|
@@ -67,9 +67,8 @@ export function usSessionAt(now: number): UsSession {
|
|
|
67
67
|
* stays while the session it was picked in lasts; otherwise the pane follows
|
|
68
68
|
* the session that is trading: Pre-market before 09:30, the day's lists during
|
|
69
69
|
* the regular session, After hours after the close. Overnight and on closed
|
|
70
|
-
* days the last pick stands.
|
|
71
|
-
*
|
|
72
|
-
* onto a wall.
|
|
70
|
+
* days the last pick stands. Every plan opens the two lists it follows, a
|
|
71
|
+
* free one as of a delay before (session-body.tsx).
|
|
73
72
|
*/
|
|
74
73
|
export function resolveActiveTab<T extends string>(args: {
|
|
75
74
|
tabs: readonly T[];
|
|
@@ -77,14 +76,13 @@ export function resolveActiveTab<T extends string>(args: {
|
|
|
77
76
|
pickedIn: string | null | undefined;
|
|
78
77
|
session: UsSession;
|
|
79
78
|
requested?: string | null;
|
|
80
|
-
sessionListsOpen: boolean;
|
|
81
79
|
}): T {
|
|
82
80
|
const { tabs, session } = args;
|
|
83
81
|
const requested = tabs.find((tab) => tab === args.requested);
|
|
84
82
|
if (requested) return requested;
|
|
85
83
|
const saved = tabs.find((tab) => tab === args.saved) ?? tabs[0]!;
|
|
86
84
|
if (session.phase === "closed" || args.pickedIn === session.key) return saved;
|
|
87
|
-
if (session.phase === "regular"
|
|
85
|
+
if (session.phase === "regular") {
|
|
88
86
|
return REGULAR_TABS.has(saved) ? saved : tabs.find((tab) => REGULAR_TABS.has(tab)) ?? saved;
|
|
89
87
|
}
|
|
90
88
|
const sessionTab = session.phase === "pre" ? "premarket" : "afterhours";
|
|
@@ -223,11 +221,28 @@ export function sortSessionRows(rows: SessionMoverRow[], preference: SessionMove
|
|
|
223
221
|
return [...rows].sort((left, right) => compareSortValues(sortValue(columnId, left), sortValue(columnId, right), preference.direction));
|
|
224
222
|
}
|
|
225
223
|
|
|
224
|
+
function shortDay(date: string): string | null {
|
|
225
|
+
const day = new Date(`${date}T12:00:00Z`);
|
|
226
|
+
return Number.isFinite(day.getTime())
|
|
227
|
+
? day.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
|
|
228
|
+
: null;
|
|
229
|
+
}
|
|
230
|
+
|
|
226
231
|
/** The session date when it is not today's, e.g. last evening's after-hours list. */
|
|
227
232
|
export function earlierSessionLabel(listSession: string | null, session: UsSession): string | null {
|
|
228
233
|
if (!listSession || listSession === session.date) return null;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
return shortDay(listSession);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* When a free account's list was built, New York time: "as of 07:42 ET", and
|
|
239
|
+
* "as of Oct 19 19:58 ET" when that is not on the list's own session day.
|
|
240
|
+
*/
|
|
241
|
+
export function listAsOfLabel(asOf: string, listSession: string | null, session: UsSession): string | null {
|
|
242
|
+
const at = Date.parse(asOf);
|
|
243
|
+
if (!Number.isFinite(at)) return null;
|
|
244
|
+
const { hour, minute } = zonedDateTimeParts(at, NEW_YORK);
|
|
245
|
+
const clock = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} ET`;
|
|
246
|
+
const day = zonedDateKey(at, NEW_YORK);
|
|
247
|
+
return day === (listSession ?? session.date) ? `as of ${clock}` : `as of ${shortDay(day) ?? day} ${clock}`;
|
|
233
248
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
|
+
DELAYED_SEGMENT,
|
|
3
4
|
PaneStatusBody, type DataTableCell,
|
|
4
5
|
type DataTableColumn,
|
|
5
6
|
type PaneFooterSegment
|
|
@@ -140,7 +141,7 @@ export function MarketValuationPane({ focused, width, height }: PaneProps) {
|
|
|
140
141
|
if (!selectedView) return [];
|
|
141
142
|
const info: PaneFooterSegment[] = [
|
|
142
143
|
{ id: "as-of", parts: [{ text: `as of ${selectedView.asOf}`, tone: "muted" }] },
|
|
143
|
-
...(width >= 80 ? [
|
|
144
|
+
...(width >= 80 ? [DELAYED_SEGMENT] : []),
|
|
144
145
|
];
|
|
145
146
|
if (selectedView.observationStale) {
|
|
146
147
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { FundChange, FundMember } from "../../../api-client/members";
|
|
3
|
-
import { DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
3
|
+
import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
4
4
|
isSectionedItemRow, renderSectionedRowHeader, useExternalLinkFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch,
|
|
5
5
|
type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -49,8 +49,13 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
|
|
|
49
49
|
return [
|
|
50
50
|
{ id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
|
|
51
51
|
// A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
...(data.quotesUnavailable
|
|
53
|
+
? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
|
|
54
|
+
: [
|
|
55
|
+
delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
|
|
56
|
+
...(date ? [{ id: "quote-date", title: `${date} · 15m delayed · holdings ${data.asOf}`, quoteStatus: true,
|
|
57
|
+
parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
|
|
58
|
+
]),
|
|
54
59
|
...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
|
|
55
60
|
];
|
|
56
61
|
}, [data, width]);
|