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
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { fetchCommodityBalances } from "./client";
|
|
3
|
-
import { formatBalanceValue, formatForecast } from "./model";
|
|
3
|
+
import { cropEstimateMonth, formatBalanceValue, formatForecast, PSD_CROPS, psdCrop, unknownCropMessage } from "./model";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* USDA sets coffee and sugar twice a year, in its own reports rather than the
|
|
7
|
+
* monthly WASDE, and forecasts no season-average price for either.
|
|
8
|
+
*/
|
|
9
|
+
const SEMIANNUAL_CROPS = new Set(["Coffee", "Sugar"]);
|
|
4
10
|
|
|
5
11
|
const amount = (value: unknown) => typeof value === "number" ? formatBalanceValue(value) : "--";
|
|
6
12
|
const forecast = (value: unknown) => typeof value === "number" ? formatForecast(value) : "--";
|
|
7
13
|
|
|
8
14
|
export const commodityBalancesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
9
15
|
shape: "bundle",
|
|
10
|
-
argument: {
|
|
16
|
+
argument: {
|
|
17
|
+
kind: "free-text",
|
|
18
|
+
optional: true,
|
|
19
|
+
placeholder: "crop",
|
|
20
|
+
description: "Wheat, corn, soybeans, cotton, coffee or sugar; omit for every crop.",
|
|
21
|
+
examples: ["coffee", "sugar"],
|
|
22
|
+
},
|
|
11
23
|
options: [],
|
|
12
24
|
discovery: {
|
|
13
25
|
aliases: ["PSD"],
|
|
@@ -15,18 +27,40 @@ export const commodityBalancesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
15
27
|
limitations: [
|
|
16
28
|
"Latest market year only",
|
|
17
29
|
"Largest producers, and the world total when the file has one",
|
|
18
|
-
"Cotton is in 480 lb bales; grains and
|
|
30
|
+
"Cotton is in 480 lb bales and coffee in thousand 60 kg bags; grains, soybeans and sugar are in thousand metric tons",
|
|
31
|
+
"Coffee and sugar are USDA's semiannual estimates; price forecasts cover grains, soybeans and cotton",
|
|
19
32
|
],
|
|
20
33
|
},
|
|
21
|
-
describe: "Wheat, corn, soybean and
|
|
34
|
+
describe: "Wheat, corn, soybean, cotton, coffee and sugar balances",
|
|
22
35
|
freshness: { source: "USDA", status: "not-a-feed", basis: "monthly supply and demand estimates", periodicity: "monthly" },
|
|
23
|
-
async load(
|
|
24
|
-
const
|
|
36
|
+
async load(args, ctx) {
|
|
37
|
+
const typed = typeof args.argument === "string" && args.argument.trim() ? args.argument.trim() : null;
|
|
38
|
+
const crop = typed ? psdCrop(typed) : null;
|
|
39
|
+
if (typed && !crop) throw new Error(unknownCropMessage(typed));
|
|
40
|
+
const board = await fetchCommodityBalances(ctx.signal, ctx.apiClient);
|
|
41
|
+
const rows = crop ? board.rows.filter((row) => row.crop === crop) : board.rows;
|
|
42
|
+
// A crop the server does not serve yet is said, not printed as an empty table.
|
|
43
|
+
if (crop && rows.length === 0) throw new Error(`${crop} balances are not available yet.`);
|
|
44
|
+
const prices = crop ? board.prices.filter((row) => row.crop === crop) : board.prices;
|
|
45
|
+
// One crop is dated by its own estimates: coffee's are not the month's WASDE.
|
|
46
|
+
const month = (crop ? cropEstimateMonth(rows) : null) ?? board.month;
|
|
47
|
+
const marketYear = crop ? [...new Set(rows.map((row) => row.year))].join(" ") : board.marketYear;
|
|
48
|
+
const semiannual = crop != null && SEMIANNUAL_CROPS.has(crop);
|
|
49
|
+
// Another crop's gap ("Soybean balances are unavailable.") is not this one's; one naming no crop stays,
|
|
50
|
+
// but for a crop without price forecasts, not theirs.
|
|
51
|
+
const names = (name: string, gap: string) => new RegExp(`\\b${name.replace(/s$/, "")}`, "i").test(gap);
|
|
52
|
+
const gaps = crop
|
|
53
|
+
? board.gaps.filter((gap) => names(crop, gap)
|
|
54
|
+
|| (!PSD_CROPS.some((name) => names(name, gap)) && !(semiannual && /\bprice forecasts?\b/i.test(gap))))
|
|
55
|
+
: board.gaps;
|
|
56
|
+
const data = { ...board, rows, prices, month, marketYear, gaps };
|
|
25
57
|
return {
|
|
58
|
+
// Coffee and sugar are not the month's supply and demand estimates: `Jul 2026 (semiannual)`.
|
|
59
|
+
...(semiannual ? { freshness: { basis: "supply and distribution estimates", periodicity: "semiannual" as const } } : {}),
|
|
26
60
|
complete: data.rows.length > 0,
|
|
27
61
|
sections: [
|
|
28
62
|
{
|
|
29
|
-
title: data.marketYear ? `
|
|
63
|
+
title: `${crop ? `${crop} balances` : "Balances"}${data.marketYear ? ` ${data.marketYear}` : ""}`,
|
|
30
64
|
columns: [
|
|
31
65
|
{ key: "crop", header: "Crop" },
|
|
32
66
|
{ key: "country", header: "Country" },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { commandRequestParams } from "../shared/command-tab";
|
|
2
3
|
import { commodityBalancesHeadless } from "./headless";
|
|
4
|
+
import { PSD_CROPS, psdCrop, unknownCropMessage } from "./model";
|
|
3
5
|
import { CommodityBalancesPane } from "./pane";
|
|
4
6
|
|
|
5
7
|
export const commodityBalancesModule: PluginModule = {
|
|
@@ -22,11 +24,24 @@ export const commodityBalancesModule: PluginModule = {
|
|
|
22
24
|
id: "commodity-balances-pane",
|
|
23
25
|
paneId: "commodity-balances",
|
|
24
26
|
label: "Crop Balances",
|
|
25
|
-
description: "World balances for wheat, corn, soybeans and
|
|
26
|
-
keywords: ["wheat", "corn", "soybeans", "cotton", "crop", "balances", "production", "exports", "stocks", "harvest"],
|
|
27
|
-
shortcut: {
|
|
27
|
+
description: "World balances for wheat, corn, soybeans, cotton, coffee and sugar, with season-average price forecasts when a table is published.",
|
|
28
|
+
keywords: ["wheat", "corn", "soybeans", "cotton", "coffee", "sugar", "crop", "balances", "production", "exports", "stocks", "harvest"],
|
|
29
|
+
shortcut: {
|
|
30
|
+
prefix: "PSD",
|
|
31
|
+
argKind: "text",
|
|
32
|
+
argPlaceholder: "crop",
|
|
33
|
+
argOptional: true,
|
|
34
|
+
openWithoutArg: true,
|
|
35
|
+
argOptions: () => PSD_CROPS.map((crop) => ({ value: crop.toLowerCase(), label: crop })),
|
|
36
|
+
},
|
|
28
37
|
headless: commodityBalancesHeadless,
|
|
29
|
-
|
|
38
|
+
// PSD coffee opens the board at coffee's first row.
|
|
39
|
+
createInstance: (_context, options) => {
|
|
40
|
+
const typed = options?.arg?.trim();
|
|
41
|
+
const crop = typed ? psdCrop(typed) : null;
|
|
42
|
+
if (typed && !crop) throw new Error(unknownCropMessage(typed));
|
|
43
|
+
return { placement: "floating", ...(crop ? { params: commandRequestParams({ crop }) } : {}) };
|
|
44
|
+
},
|
|
30
45
|
},
|
|
31
46
|
],
|
|
32
47
|
};
|
|
@@ -12,6 +12,28 @@ export type PriceColumnId = "crop" | "forecast" | "unit" | "season";
|
|
|
12
12
|
export const BALANCE_SORT: SortPreference<BalanceColumnId> = { columnId: null, direction: "asc" };
|
|
13
13
|
export const PRICE_SORT: SortPreference<PriceColumnId> = { columnId: null, direction: "asc" };
|
|
14
14
|
|
|
15
|
+
/** The crops the board carries, in its order; the server names them so. */
|
|
16
|
+
export const PSD_CROPS = ["Wheat", "Corn", "Soybeans", "Cotton", "Coffee", "Sugar"] as const;
|
|
17
|
+
export type PsdCrop = typeof PSD_CROPS[number];
|
|
18
|
+
|
|
19
|
+
/** A typed crop as the board names it: "coffee", "Soybean" and "soy" read as Coffee and Soybeans. Null for anything else. */
|
|
20
|
+
export function psdCrop(input: string): PsdCrop | null {
|
|
21
|
+
const typed = input.trim().toLowerCase().replace(/s$/, "");
|
|
22
|
+
if (typed.length < 3) return null;
|
|
23
|
+
return PSD_CROPS.find((crop) => crop.toLowerCase().replace(/s$/, "").startsWith(typed)) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What the CLI answers a crop the board does not carry. */
|
|
27
|
+
export function unknownCropMessage(input: string): string {
|
|
28
|
+
const names = PSD_CROPS.map((crop) => crop.toLowerCase());
|
|
29
|
+
return `No crop "${input.trim()}". PSD covers ${names.slice(0, -1).join(", ")} and ${names.at(-1)}.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The month a crop's balances were estimated, from its rows; null when the board does not date them. */
|
|
33
|
+
export function cropEstimateMonth(rows: readonly BalanceRow[]): string | null {
|
|
34
|
+
return rows.reduce<string | null>((latest, row) => row.month && (!latest || row.month > latest) ? row.month : latest, null);
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
export function formatBalanceValue(value: number): string {
|
|
16
38
|
const rounded = Math.round(value * 100) / 100;
|
|
17
39
|
const shown = Math.abs(rounded - Math.round(rounded)) < 0.001 ? Math.round(rounded) : rounded;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
DataTableView,
|
|
4
4
|
PaneStatusBody,
|
|
@@ -12,6 +12,7 @@ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
|
12
12
|
import type { PaneFooterSegment } from "../../../components/layout/pane/footer";
|
|
13
13
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
14
14
|
import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
|
|
15
|
+
import { usePaneInstance } from "../../../state/app/context";
|
|
15
16
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
16
17
|
import type { PaneProps } from "../../../types/plugin";
|
|
17
18
|
import { Box, TextAttributes } from "../../../ui";
|
|
@@ -76,6 +77,13 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
76
77
|
[balanceSort, data?.rows],
|
|
77
78
|
);
|
|
78
79
|
const priceRows = useMemo(() => sortPriceRows(prices, priceSort), [priceSort, prices]);
|
|
80
|
+
// PSD coffee opens the board at coffee's first row, once per command: a later move is the reader's.
|
|
81
|
+
const pane = usePaneInstance();
|
|
82
|
+
const requestedCrop = pane?.params?.crop ?? null;
|
|
83
|
+
const cropRequest = requestedCrop ? `${requestedCrop}|${pane?.params?.tabAt ?? ""}` : "";
|
|
84
|
+
const [shownRequest, setShownRequest] = usePluginPaneState("cropRequest", "");
|
|
85
|
+
// The row the request scrolls to the top, until the reader sorts or moves on.
|
|
86
|
+
const [cropAnchor, setCropAnchor] = useState<{ id: string; version: number } | null>(null);
|
|
79
87
|
const { strip, rows: tabRows } = usePaneTabs(showPrices ? {
|
|
80
88
|
tabs: TABS,
|
|
81
89
|
activeValue: activeTab,
|
|
@@ -103,11 +111,24 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
103
111
|
});
|
|
104
112
|
usePaneNoticeFooter({ registrationId: `${PANE_ID}:notices`, focused, notices: data?.gaps ?? [] });
|
|
105
113
|
|
|
114
|
+
const cropPending = !!cropRequest && cropRequest !== shownRequest;
|
|
106
115
|
useEffect(() => {
|
|
107
|
-
if (
|
|
116
|
+
if (!cropPending || !data) return;
|
|
117
|
+
setShownRequest(cropRequest);
|
|
118
|
+
const first = balanceRows.find((row) => row.crop === requestedCrop);
|
|
119
|
+
if (!first) return;
|
|
120
|
+
setTab("balances");
|
|
121
|
+
setBalanceId(first.id);
|
|
122
|
+
setCropAnchor((current) => ({ id: first.id, version: (current?.version ?? 0) + 1 }));
|
|
123
|
+
}, [balanceRows, cropPending, cropRequest, data, requestedCrop, setBalanceId, setShownRequest, setTab]);
|
|
124
|
+
const anchorIndex = cropAnchor ? balanceRows.findIndex((row) => row.id === cropAnchor.id) : -1;
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
// The crop's row is chosen above; the first row would overwrite it in the same pass.
|
|
128
|
+
if (cropPending || balanceRows.length === 0) return;
|
|
108
129
|
if (balanceId && balanceRows.some((row) => row.id === balanceId)) return;
|
|
109
130
|
setBalanceId(balanceRows[0]!.id);
|
|
110
|
-
}, [balanceId, balanceRows, setBalanceId]);
|
|
131
|
+
}, [balanceId, balanceRows, cropPending, setBalanceId]);
|
|
111
132
|
|
|
112
133
|
useEffect(() => {
|
|
113
134
|
if (priceRows.length === 0) return;
|
|
@@ -116,6 +137,7 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
116
137
|
}, [priceId, priceRows, setPriceId]);
|
|
117
138
|
|
|
118
139
|
const onBalanceHeader = useCallback((columnId: string) => {
|
|
140
|
+
setCropAnchor(null);
|
|
119
141
|
setBalanceSort((current) => nextHeaderSort(current, columnId as BalanceColumnId, {
|
|
120
142
|
firstDirection: columnId === "value" ? "desc" : "asc",
|
|
121
143
|
resetTo: BALANCE_SORT,
|
|
@@ -212,14 +234,23 @@ export function CommodityBalancesPane({ width, height, focused }: PaneProps) {
|
|
|
212
234
|
sortColumnId={balanceSort.columnId}
|
|
213
235
|
sortDirection={balanceSort.direction}
|
|
214
236
|
onHeaderClick={onBalanceHeader}
|
|
215
|
-
onSortChange={(columnId, direction) =>
|
|
237
|
+
onSortChange={(columnId, direction) => {
|
|
238
|
+
setCropAnchor(null);
|
|
239
|
+
setBalanceSort({ columnId: columnId as BalanceColumnId, direction });
|
|
240
|
+
}}
|
|
216
241
|
selectedTextOverridesCellColor
|
|
217
242
|
selection={{
|
|
218
243
|
kind: "id",
|
|
219
244
|
selectedId: balanceId,
|
|
220
245
|
getId: (row) => row.id,
|
|
221
|
-
onChange: (id) =>
|
|
246
|
+
onChange: (id) => {
|
|
247
|
+
if (id !== cropAnchor?.id) setCropAnchor(null);
|
|
248
|
+
setBalanceId(id);
|
|
249
|
+
},
|
|
222
250
|
}}
|
|
251
|
+
scrollToIndex={anchorIndex >= 0 ? anchorIndex : undefined}
|
|
252
|
+
scrollToIndexAlign={anchorIndex >= 0 ? "start" : "nearest"}
|
|
253
|
+
scrollToIndexVersion={cropAnchor?.version ?? 0}
|
|
223
254
|
renderCell={renderBalanceCell}
|
|
224
255
|
emptyStateTitle="No crop balances."
|
|
225
256
|
resetScrollKey={activeTab}
|
|
@@ -84,9 +84,10 @@ function answersFor(answered: string, symbol: string, abroad: ListingAbroad | nu
|
|
|
84
84
|
const parsed = parsePublicTickerKey(answered);
|
|
85
85
|
return parsed.symbol === abroad.symbol && (!parsed.exchange || parsed.exchange === abroad.exchange);
|
|
86
86
|
}
|
|
87
|
-
export async function fetchCompanyData(mode: CompanyMode, symbol: string, options: KpiQueryOptions = {}, client: CompanyClient = apiClient, listing: IssuerListingParams = {}) {
|
|
87
|
+
export async function fetchCompanyData(mode: CompanyMode, symbol: string, options: KpiQueryOptions = {}, client: CompanyClient = apiClient, listing: IssuerListingParams = {}, signal?: AbortSignal) {
|
|
88
88
|
try {
|
|
89
|
-
const
|
|
89
|
+
const request = signal ? { signal } : undefined;
|
|
90
|
+
const data = validateCompanyData(await (mode === "kpis" ? client.getCloudCompanyKpis(symbol, options, listing, request) : client.getCloudCompanyGuidance(symbol, options, listing, request)));
|
|
90
91
|
if (("series" in data) !== (mode === "kpis") || !answersFor(data.symbol, symbol, listingAbroad(symbol, listing.exchange, listing.name))) throw new Error("Company disclosures do not match the requested company");
|
|
91
92
|
return data;
|
|
92
93
|
}
|
|
@@ -4,7 +4,8 @@ import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneDefinition
|
|
|
4
4
|
import { withoutQuietColumns } from "../shared/research-cells";
|
|
5
5
|
import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
6
6
|
import { companyQuery, fetchCompanyData, type CompanyDataset, type CompanyMode } from "./client";
|
|
7
|
-
import { allGuidance, allObservations, companyCellText, companyColumns, companyPreviewNote,
|
|
7
|
+
import { allGuidance, allObservations, companyCellText, companyColumns, companyPreviewNote, KPI_COVERAGE_NOTE, kpiFreeLimitNote, QUIET_COLUMNS,
|
|
8
|
+
type CompanyRow } from "./model";
|
|
8
9
|
import type { KpiObservation } from "../../../api-client/company-kpis";
|
|
9
10
|
|
|
10
11
|
/** What the server takes as a metric; anything else is not a metric id and is not sent. */
|
|
@@ -57,11 +58,8 @@ function metricFilter(requested: string, data: CompanyDataset, mode: CompanyMode
|
|
|
57
58
|
const keep = (row: CompanyRow) => row.metricId === requested;
|
|
58
59
|
if (rows.some(keep)) return { keep };
|
|
59
60
|
if (data.access === "preview" && data.lockedRows > 0) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const all = `Gloom Pro shows all ${data.totalRows.toLocaleString("en-US")}`;
|
|
63
|
-
return { keep, empty: shown === 0 ? `The free preview shows no ${noun(0)}; ${all}.`
|
|
64
|
-
: `${known.name} is not ${shown === 1 ? "the" : "among the"} ${shown.toLocaleString("en-US")} free ${noun(shown)}; ${all}.` };
|
|
61
|
+
return { keep, empty: kpiFreeLimitNote({ metric: known.name, shown: Math.max(0, data.totalRows - data.lockedRows), total: data.totalRows,
|
|
62
|
+
guidance: mode === "guidance" }) };
|
|
65
63
|
}
|
|
66
64
|
return { keep, empty: `${data.symbol} has no ${known.name} (${known.id}) ${mode === "kpis" ? "disclosures" : "guidance"}${offered ? `; it has ${offered}` : ""}.` };
|
|
67
65
|
}
|
|
@@ -83,7 +81,7 @@ export function companyHeadless(mode: CompanyMode): HeadlessPaneDefinition<"bund
|
|
|
83
81
|
freshness: { source: "Company filings and calls", status: "not-a-feed", basis: "reported data" },
|
|
84
82
|
argument: { kind: "ticker", description: "Listed company ticker, including exchange-qualified global symbols.", placeholder: "ticker" },
|
|
85
83
|
discovery: { aliases: [mode === "kpis" ? "KPIS" : "GUIDE"], dataRequirements: ["Gloom Cloud company disclosures"],
|
|
86
|
-
limitations: ["Pro dataset with a fixed latest preview on Free.",
|
|
84
|
+
limitations: ["Pro dataset with a fixed latest preview on Free.", KPI_COVERAGE_NOTE, "Compare matching definition, period, currency, basis and scope.", "Fiscal labels without explicit calendar dates are retained without inferred dates."] },
|
|
87
85
|
options: [
|
|
88
86
|
{ key: "tab", type: "enum", settingKey: "tab", defaultValue: "table", values: [{ value: "table" }, { value: "chart" }, ...(mode === "guidance" ? [{ value: "history" }] : []), { value: "evidence" }], description: "The pane section to render." },
|
|
89
87
|
{ key: "metric", type: "string", settingKey: "metric", description: "Canonical metric ID, such as arr or subscribers.", normalize: (value) => value.trim().toLowerCase() },
|
|
@@ -12,6 +12,59 @@ export const dimensionLabel = (dimensions: Record<string, string>) => Object.ent
|
|
|
12
12
|
const rank = (key: string) => ["segment", "product", "geography", "region", "cohort"].includes(key) ? 0 : key === "comparison" ? 2 : 1;
|
|
13
13
|
return rank(a) - rank(b) || a.localeCompare(b);
|
|
14
14
|
}).map(([key, value]) => `${key.replaceAll("_", " ")}: ${value.replaceAll("_", " ")}`).join(" · ");
|
|
15
|
+
/** Labels that name a part of the company, shown before the basis and the qualifiers. */
|
|
16
|
+
const PART_KEY = /segment|product|offering|business|brand|platform|market|geograph|region|division|cloud|franchise|program|channel|line|customer|entity|scope|type|category/;
|
|
17
|
+
/** Labels that say how a figure is measured, shown after the basis. */
|
|
18
|
+
const QUALIFIER_KEY = /exclud|includ|except|without|compar|dilution|window|funded|horizon/;
|
|
19
|
+
const FLAG_VALUE = /^(?:true|yes|y)$/i;
|
|
20
|
+
const FALSE_VALUE = /^(?:false|no|n)$/i;
|
|
21
|
+
const EXCLUDED_VALUE = /^(?:excluded|excludes|excluding|exclusive|ex)$/i;
|
|
22
|
+
const INCLUDED_VALUE = /^(?:included|includes|including|inclusive)$/i;
|
|
23
|
+
const plain = (text: string) => text.replaceAll("_", " ").replace(/\s+/g, " ").trim();
|
|
24
|
+
/** One label in plain words: "Data Center", "excl. fuel", "year over year", never its key. */
|
|
25
|
+
function labelWords(key: string, value: string): string | null {
|
|
26
|
+
const said = plain(value);
|
|
27
|
+
const subject = plain(key).replace(/\b(?:excluded|excludes|excluding|exclude|without|included|includes|including|include|ex)\b/gi, " ").replace(/\s+/g, " ").trim();
|
|
28
|
+
if (!said || FALSE_VALUE.test(said)) return null;
|
|
29
|
+
if (EXCLUDED_VALUE.test(said) || (FLAG_VALUE.test(said) && /exclud|without|(?:^|_)ex(?:_|$)/i.test(key))) return subject ? `excl. ${subject}` : null;
|
|
30
|
+
if (INCLUDED_VALUE.test(said) || (FLAG_VALUE.test(said) && /includ/i.test(key))) return subject ? `incl. ${subject}` : null;
|
|
31
|
+
if (FLAG_VALUE.test(said)) return subject || null;
|
|
32
|
+
if (/^(?:exclud|except|without)/i.test(key) && !/^(?:excl|exclud|except|without|ex\b)/i.test(said)) return `excl. ${said}`;
|
|
33
|
+
if (/^includ/i.test(key) && !/^incl/i.test(said)) return `incl. ${said}`;
|
|
34
|
+
return said;
|
|
35
|
+
}
|
|
36
|
+
const labelRank = (key: string) => QUALIFIER_KEY.test(key) ? 2 : PART_KEY.test(key) ? 0 : 1;
|
|
37
|
+
/** What a series' labels name, in plain words: its parts, then its qualifiers. Empty for the company's consolidated figure. */
|
|
38
|
+
export function scopeWords(dimensions: Record<string, string>, which: "parts" | "qualifiers" | "all" = "all"): string {
|
|
39
|
+
return Object.entries(dimensions)
|
|
40
|
+
.filter(([key]) => which === "all" || (which === "qualifiers") === (labelRank(key) === 2))
|
|
41
|
+
.sort(([a], [b]) => labelRank(a) - labelRank(b) || a.localeCompare(b))
|
|
42
|
+
.map(([key, value]) => labelWords(key, value)).filter((words): words is string => !!words)
|
|
43
|
+
.filter((words, index, list) => list.indexOf(words) === index).join(" · ");
|
|
44
|
+
}
|
|
45
|
+
/** The labels that tell a row's series apart: the server's `seriesLabels`, else (a server before them) its stored labels. */
|
|
46
|
+
export const seriesLabels = (row: Pick<CompanyRow, "dimensions" | "seriesLabels">) => row.seriesLabels ?? row.dimensions;
|
|
47
|
+
/**
|
|
48
|
+
* What tells a series apart from the metric's other series, in plain words
|
|
49
|
+
* (GLO-1046): its parts, a basis other than reported, then its qualifiers.
|
|
50
|
+
* "Data Center", "Adjusted · diluted", "International · Constant currency".
|
|
51
|
+
*/
|
|
52
|
+
export function seriesDetail(row: Pick<CompanyRow, "basis" | "dimensions" | "seriesLabels">): string {
|
|
53
|
+
const labels = seriesLabels(row);
|
|
54
|
+
return [scopeWords(labels, "parts"), row.basis === "reported" ? "" : basisLabel(row.basis), scopeWords(labels, "qualifiers")].filter(Boolean).join(" · ");
|
|
55
|
+
}
|
|
56
|
+
/** A series named in plain words: "Revenue · Data Center", for a chart's series list and a detail title. */
|
|
57
|
+
export const seriesName = (row: Pick<CompanyRow, "metric" | "basis" | "dimensions" | "seriesLabels">) => [row.metric.name, seriesDetail(row)].filter(Boolean).join(" · ");
|
|
58
|
+
const KIND_ORDER = ["quarter", "half", "ytd", "year", "instant", "other"];
|
|
59
|
+
const BASIS_ORDER = ["reported", "adjusted", "constant_currency", "organic"];
|
|
60
|
+
/** Series in reading order: by metric, the consolidated figure before its parts, reported before other bases, a quarter before its year. */
|
|
61
|
+
export function seriesOrder(a: CompanyRow, b: CompanyRow): number {
|
|
62
|
+
const rank = (list: string[], value: string) => { const index = list.indexOf(value); return index < 0 ? list.length : index; };
|
|
63
|
+
const parts = (row: CompanyRow) => scopeWords(seriesLabels(row), "parts");
|
|
64
|
+
return a.metric.name.localeCompare(b.metric.name) || Number(parts(a) !== "") - Number(parts(b) !== "") || parts(a).localeCompare(parts(b))
|
|
65
|
+
|| rank(BASIS_ORDER, a.basis) - rank(BASIS_ORDER, b.basis) || scopeWords(seriesLabels(a), "qualifiers").localeCompare(scopeWords(seriesLabels(b), "qualifiers"))
|
|
66
|
+
|| rank(KIND_ORDER, a.period.kind) - rank(KIND_ORDER, b.period.kind);
|
|
67
|
+
}
|
|
15
68
|
export const unitLabel = (row: Pick<CompanyRow, "unit" | "currency" | "dimensions">) => ({
|
|
16
69
|
currency: row.currency ?? "Currency", currency_per_share: `${row.currency ?? "Currency"}/share`, currency_per_unit: `${row.currency ?? "Currency"}/${row.dimensions.unit ?? "unit"}`,
|
|
17
70
|
percent: "%", basis_points: "bp", count: "Count", ratio: "x", volume: row.dimensions.unit ?? "Volume",
|
|
@@ -118,7 +171,11 @@ export function companyCellText(row: CompanyRow, column: string, observations: K
|
|
|
118
171
|
: column === "outcome" ? guide ? outcomeLabel(guide) : "--" : column === "published" ? row.evidence[0]!.publishedAt.slice(0, 10)
|
|
119
172
|
: column === "confidence" ? `${Math.round(row.confidence * 100)}%` : column === "revision" ? `${row.revision} ${row.current ? "current" : "superseded"}` : row.evidence[0]!.quote;
|
|
120
173
|
}
|
|
121
|
-
|
|
174
|
+
/**
|
|
175
|
+
* The table's columns. `named` is the desktop's: the metric cell names the
|
|
176
|
+
* series in plain words (seriesDetail), so no basis or scope column repeats it.
|
|
177
|
+
*/
|
|
178
|
+
export function companyColumns(guidance: boolean, evidence: boolean, history: boolean, trend = false, named = false): DataTableColumn[] {
|
|
122
179
|
const columns: DataTableColumn[] = [
|
|
123
180
|
{ id: "metric", label: "Metric", width: 27, flexGrow: 1, align: "left" },
|
|
124
181
|
{ id: "period", label: "Fiscal period", width: 14, align: "left" },
|
|
@@ -134,7 +191,33 @@ export function companyColumns(guidance: boolean, evidence: boolean, history: bo
|
|
|
134
191
|
];
|
|
135
192
|
const order = evidence ? ["metric", "period", "quote", "published", "confidence", "revision", "value", "basis", "scope", "issued", "change"]
|
|
136
193
|
: history ? ["metric", "period", "actual", "outcome", "value", "issued", "change", "basis", "scope"] : null;
|
|
137
|
-
|
|
194
|
+
const shown = named ? columns.filter((column) => column.id !== "basis" && column.id !== "scope") : columns;
|
|
195
|
+
return order ? shown.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)) : shown;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** What a missing KPI means, as KPIS states it among its limitations and EQS beside a KPI column with gaps. */
|
|
199
|
+
export const KPI_COVERAGE_NOTE = "Coverage follows available company disclosures; missing data is not zero.";
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Why Free shows no value of a metric, in the same words in `fn KPIS`, `fn GUIDE` and EQS: "Order backlog is not
|
|
203
|
+
* among the 3 free KPI values; Gloom Pro shows all 63." With no free value at all, `where` says which free view
|
|
204
|
+
* shows none; without a total (EQS counts no company's disclosures), Pro shows each company's metric.
|
|
205
|
+
*/
|
|
206
|
+
export function kpiFreeLimitNote({ metric, shown, total, guidance = false, where = "The free preview" }: {
|
|
207
|
+
/** The metric's name: "Order backlog", "Backlog". */
|
|
208
|
+
metric: string;
|
|
209
|
+
/** How many values Free shows. */
|
|
210
|
+
shown: number;
|
|
211
|
+
/** How many Gloom Pro shows, when the response says. */
|
|
212
|
+
total: number | null;
|
|
213
|
+
guidance?: boolean;
|
|
214
|
+
where?: string;
|
|
215
|
+
}): string {
|
|
216
|
+
const count = (value: number) => value.toLocaleString("en-US");
|
|
217
|
+
const noun = (value: number) => `${guidance ? "guidance figure" : "KPI value"}${value === 1 ? "" : "s"}`;
|
|
218
|
+
const pro = total == null ? `Gloom Pro shows each company's ${metric}` : `Gloom Pro shows all ${count(total)}`;
|
|
219
|
+
return shown === 0 ? `${where} shows no ${noun(0)}; ${pro}.`
|
|
220
|
+
: `${metric} is not ${shown === 1 ? "the" : "among the"} ${count(shown)} free ${noun(shown)}; ${pro}.`;
|
|
138
221
|
}
|
|
139
222
|
|
|
140
223
|
/** The lock line of a free preview, in the same words in the pane and `fn KPIS` / `fn GUIDE`. */
|
|
@@ -10,6 +10,7 @@ import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActi
|
|
|
10
10
|
import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
|
|
11
11
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
12
12
|
import { quoteBlockStyle } from "../../../components/ui/quote";
|
|
13
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
13
14
|
import type { PaneProps } from "../../../types/plugin";
|
|
14
15
|
import { Box, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
15
16
|
import { SignInWall } from "../cloud/auth-actions";
|
|
@@ -20,8 +21,8 @@ import { isCloudSessionRequired, useResearchCloudSession } from "../shared/resea
|
|
|
20
21
|
import { cachedCompanyData, loadCompanyData, KPI_UNAVAILABLE, validateCompanyData, type CompanyDataset, type CompanyMode } from "./client";
|
|
21
22
|
import { useCompanyEvidence } from "./screenshot-evidence";
|
|
22
23
|
import { allGuidance, allObservations, basisLabel, changeText, changeTone, companyCellText, companyColumns, dimensionLabel, directionLabel, directionTone, guideGroupKey, guidanceSeries, fiscalLabel,
|
|
23
|
-
isGuidance, exactObservation, observationValue, observationChange, observationChart, periodOrder, outcomeLabel, QUIET_COLUMNS, rangeText,
|
|
24
|
-
type CompanyRow } from "./model";
|
|
24
|
+
isGuidance, exactObservation, observationValue, observationChange, observationChart, periodOrder, outcomeLabel, QUIET_COLUMNS, rangeText, scopeWords, seriesDetail, seriesLabel, seriesLabels, seriesName,
|
|
25
|
+
seriesOrder, unitLabel, valueText, companyPreviewNote, type CompanyRow } from "./model";
|
|
25
26
|
import { PriceSparkline } from "../../../components/price-sparkline/view";
|
|
26
27
|
import type { PricePoint } from "../../../types/financials";
|
|
27
28
|
import { toneColor, withoutQuietColumns } from "../shared/research-cells";
|
|
@@ -41,7 +42,7 @@ function DisclosureEvidence({ row, sourceId, onSource, width }: { row: CompanyRo
|
|
|
41
42
|
<KeyValueRow label="Value" value={isGuidance(row) ? rangeText(row) : observationValue(row)} detail={row.currency || row.unit === "volume" || row.unit === "count" ? unitLabel(row) : undefined} labelWidth={18} />
|
|
42
43
|
<KeyValueRow label="Fiscal period" value={row.period.label} detail={row.period.end ? row.period.start ? `${row.period.start} to ${row.period.end}` : `${row.period.kind === "instant" ? "As of" : "Ended"} ${row.period.end}` : "Calendar dates unavailable"} labelWidth={18} />
|
|
43
44
|
<KeyValueRow label="Basis" value={basisLabel(row.basis)} labelWidth={18} />
|
|
44
|
-
<KeyValueRow label="Scope" value={dimensionLabel(row.dimensions) || "Consolidated"} labelWidth={18} />
|
|
45
|
+
<KeyValueRow label="Scope" value={(desktop ? scopeWords(seriesLabels(row)) : dimensionLabel(row.dimensions)) || "Consolidated"} labelWidth={18} />
|
|
45
46
|
<KeyValueRow label="Definition" value={row.metric.definition} labelWidth={18} />
|
|
46
47
|
<KeyValueRow label="Revision" value={`${row.revision}${row.current ? " · current" : " · superseded"}${!isGuidance(row) && (row.conflict || row.contested) ? " · conflicting disclosures" : ""}`} labelWidth={18} />
|
|
47
48
|
{row.revisionReason ? <KeyValueRow label="Correction" value={row.revisionReason} labelWidth={18} /> : null}
|
|
@@ -106,15 +107,17 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
106
107
|
const [selectedId, setSelected] = usePluginPaneState<string | null>(`company:selected:${tab}`, null);
|
|
107
108
|
const [openId, setOpen] = usePluginPaneState<string | null>("company:open", null);
|
|
108
109
|
const [sourceId, setSource] = usePluginPaneState<string | null>("company:source", null);
|
|
109
|
-
|
|
110
|
+
// The desktop lists KPIs by metric, each series named in plain words (GLO-1046); the terminal keeps its frames.
|
|
111
|
+
const [sort, setSort] = usePluginPaneState<{ column: string; direction: "asc" | "desc" }>("company:sort",
|
|
112
|
+
desktop && mode === "kpis" ? { column: "metric", direction: "asc" } : { column: "period", direction: "desc" });
|
|
110
113
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
111
114
|
const observations = useMemo(() => kpis ? allObservations(kpis) : [], [kpis]);
|
|
112
115
|
const guides = useMemo(() => guidance ? allGuidance(guidance) : [], [guidance]);
|
|
113
116
|
const allRows = useMemo<CompanyRow[]>(() => [...observations, ...guides], [observations, guides]);
|
|
114
117
|
const eligible = useMemo(() => allRows.filter((row) => metric === "all" || row.metricId === metric), [allRows, metric]);
|
|
115
118
|
const plotOptions = useMemo(() => mode === "kpis"
|
|
116
|
-
? [...new Map(eligible.filter((row) => !isGuidance(row)).map((row) => [`${row.seriesKey}:${row.period.kind}`, { value: `${row.seriesKey}:${row.period.kind}`, label: `${seriesLabel(row)} · ${row.period.kind}` }])).values()]
|
|
117
|
-
: [...new Map(eligible.filter(isGuidance).map((row) => [guideGroupKey(row), { value: guideGroupKey(row), label: `${seriesLabel(row)} · ${row.period.label}` }])).values()], [eligible, mode]);
|
|
119
|
+
? [...new Map(eligible.filter((row) => !isGuidance(row)).map((row) => [`${row.seriesKey}:${row.period.kind}`, { value: `${row.seriesKey}:${row.period.kind}`, label: `${desktop ? seriesName(row) : seriesLabel(row)} · ${row.period.kind}` }])).values()]
|
|
120
|
+
: [...new Map(eligible.filter(isGuidance).map((row) => [guideGroupKey(row), { value: guideGroupKey(row), label: `${desktop ? seriesName(row) : seriesLabel(row)} · ${row.period.label}` }])).values()], [eligible, mode, desktop]);
|
|
118
121
|
const activePlot = plotOptions.some((option) => option.value === plotKey) ? plotKey : plotOptions[0]?.value ?? "";
|
|
119
122
|
const rows = useMemo(() => {
|
|
120
123
|
const values = eligible.filter((row) => tab === "chart" ? (isGuidance(row) ? guideGroupKey(row) : `${row.seriesKey}:${row.period.kind}`) === activePlot
|
|
@@ -130,11 +133,13 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
130
133
|
: sort.column === "quote" ? row.evidence[0]!.quote : sort.column === "change" ? guide ? guide.direction : observationChange(row as KpiObservation, observations).value
|
|
131
134
|
: periodOrder(row);
|
|
132
135
|
};
|
|
136
|
+
const direction = sort.direction === "asc" ? 1 : -1;
|
|
137
|
+
if (desktop && sort.column === "metric") return [...values].sort((a, b) => (seriesOrder(a, b) || periodOrder(b).localeCompare(periodOrder(a)) || a.id.localeCompare(b.id)) * direction);
|
|
133
138
|
return [...values].sort((a, b) => { const x = key(a), y = key(b);
|
|
134
139
|
return x === null || y === null ? x === y ? a.id.localeCompare(b.id) : x === null ? 1 : -1
|
|
135
|
-
: (x < y ? -1 : x > y ? 1 : a.metric.name.localeCompare(b.metric.name) || dimensionLabel(a.dimensions).localeCompare(dimensionLabel(b.dimensions)) || a.id.localeCompare(b.id)) *
|
|
140
|
+
: (x < y ? -1 : x > y ? 1 : a.metric.name.localeCompare(b.metric.name) || dimensionLabel(a.dimensions).localeCompare(dimensionLabel(b.dimensions)) || a.id.localeCompare(b.id)) * direction;
|
|
136
141
|
});
|
|
137
|
-
}, [eligible, tab, activePlot, guidance, kpis, sort, observations]);
|
|
142
|
+
}, [eligible, tab, activePlot, guidance, kpis, sort, observations, desktop]);
|
|
138
143
|
const selected = rows.find((row) => row.id === selectedId) ?? rows[0];
|
|
139
144
|
const openRow = allRows.find((row) => row.id === openId);
|
|
140
145
|
const activeRow = openRow ?? selected;
|
|
@@ -153,8 +158,8 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
153
158
|
}
|
|
154
159
|
return map;
|
|
155
160
|
}, [kpis]);
|
|
156
|
-
const columns = useMemo(() => withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100),
|
|
157
|
-
rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS), [mode, tab, trends, rows, width]);
|
|
161
|
+
const columns = useMemo(() => withoutQuietColumns(companyColumns(mode === "guidance", tab === "evidence", tab === "history", mode === "kpis" && tab === "table" && trends.size > 0 && width >= 100, desktop),
|
|
162
|
+
rows, (row, id) => id === "basis" ? basisLabel(row.basis) : id === "scope" ? dimensionLabel(row.dimensions) || "Consolidated" : "", QUIET_COLUMNS), [mode, tab, trends, rows, width, desktop]);
|
|
158
163
|
const metricOptions = useMemo(() => [{ value: "all", label: "All metrics" }, ...[...new Map(allRows.map((row) => [row.metricId, { value: row.metricId, label: row.metric.name }])).values()]], [allRows]);
|
|
159
164
|
const kpiChart = useMemo(() => observationChart(rows.filter((row): row is KpiObservation => !isGuidance(row)), colors.warning), [rows, colors.warning]);
|
|
160
165
|
const series = useMemo(() => tab !== "chart" ? [] : mode === "kpis" ? kpiChart.series
|
|
@@ -190,7 +195,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
190
195
|
const query = <QueryBar width={width} filters={[{ id: "metric", label: "Metric", value: metric, defaultValue: "all", options: metricOptions,
|
|
191
196
|
onChange: (value: string) => { setMetric(value); setSelected(null); setOpen(null); } }, ...(tab === "chart" && plotOptions.length ? [{ id: "series", label: "Series", value: activePlot,
|
|
192
197
|
options: plotOptions, onChange: (value: string) => { setPlotKey(value); setSelected(null); } }] : [])]} />;
|
|
193
|
-
const renderCell = (item: Item, column: (typeof columns)[number]): DataTableCell => {
|
|
198
|
+
const renderCell = (item: Item, column: (typeof columns)[number], _index: number, state: { selected: boolean }): DataTableCell => {
|
|
194
199
|
const row = item.row;
|
|
195
200
|
if (!row) {
|
|
196
201
|
if (isNote(item)) return { text: "" };
|
|
@@ -210,6 +215,15 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
210
215
|
const change = observationChange(row as KpiObservation, observations);
|
|
211
216
|
return { text: changeText(change), value: change.value, color: toneColor(changeTone(row as KpiObservation, change.value), colors) };
|
|
212
217
|
}
|
|
218
|
+
if (desktop && column.id === "metric") {
|
|
219
|
+
// The series in plain words: the metric bright, what tells it apart dim. The detail starts from no width and takes
|
|
220
|
+
// what the name leaves, so it gives way first and the name is cut only when it alone does not fit.
|
|
221
|
+
const detail = seriesDetail(row);
|
|
222
|
+
return { text: detail ? `${row.metric.name} ${detail}` : row.metric.name, content: <Box flexDirection="row" height={1} overflow="hidden" flexShrink={1} minWidth={0}>
|
|
223
|
+
<ClippedText fg={state.selected ? colors.selectedText : colors.textBright} text={row.metric.name} width={column.width} />
|
|
224
|
+
{detail ? <ClippedText fg={state.selected ? colors.selectedText : colors.textDim} lead=" " text={detail} width={column.width} flex="1 1 0%" /> : null}
|
|
225
|
+
</Box> };
|
|
226
|
+
}
|
|
213
227
|
const text = companyCellText(row, column.id, observations);
|
|
214
228
|
return { text, ...(column.id === "value" && !guide ? { value: exactObservation(row as KpiObservation) ? (row as KpiObservation).value : observationValue(row as KpiObservation) } : column.id === "actual" ? { value: guide?.actual?.value } : {}),
|
|
215
229
|
color: column.id === "metric" ? colors.textBright : column.id === "value" || column.id === "actual" ? colors.textBright
|
|
@@ -225,7 +239,7 @@ function CompanyView({ symbol, exchange, name, mode, width, height, focused }: P
|
|
|
225
239
|
selection={{ kind: "id", selectedId: selected?.id ?? null, getId, onChange: setSelected }}
|
|
226
240
|
onActivate={(item) => item.row ? setOpen(item.row.id) : isNote(item) ? undefined : openUpgrade()} renderCell={renderCell}
|
|
227
241
|
isNavigable={(item) => !isNote(item)} renderSectionHeader={(item) => "note" in item && item.note ? { text: item.note } : null} freezeFirstColumn selectedTextOverridesCellColor showHorizontalScrollbar
|
|
228
|
-
detailOpen={!!openRow} onBack={() => setOpen(null)} detailTitle={openRow ? `${openRow.metric.name} · ${fiscalLabel(openRow)}` : undefined}
|
|
242
|
+
detailOpen={!!openRow} onBack={() => setOpen(null)} detailTitle={openRow ? `${desktop ? seriesName(openRow) : openRow.metric.name} · ${fiscalLabel(openRow)}` : undefined}
|
|
229
243
|
detailScrollRef={detailScrollRef} detailContent={openRow ? <DetailScrollBody ref={detailScrollRef} resetScrollKey={openRow.id}><DisclosureEvidence row={openRow} sourceId={sourceId} onSource={setSource} width={width} /></DetailScrollBody> : null}
|
|
230
244
|
sortColumnId={sort.column} sortDirection={sort.direction} onHeaderClick={(column) => setSort((previous) => ({ column, direction: previous.column === column && previous.direction === "desc" ? "asc" : "desc" }))}
|
|
231
245
|
resetScrollKey={`${mode}:${symbol}:${tab}:${metric}:${activePlot}`}
|
|
@@ -10,6 +10,7 @@ import { supplyChainModule } from "./supply-chain";
|
|
|
10
10
|
import { awardsModule } from "./awards";
|
|
11
11
|
import { cryptoBoardModule } from "./crypto-board";
|
|
12
12
|
import { portfolioAnalyticsModule } from "./analytics";
|
|
13
|
+
import { brokerTradingModule } from "./broker-trading";
|
|
13
14
|
import { brokerManagerModule } from "./broker-manager";
|
|
14
15
|
import { changelogModule } from "./changelog";
|
|
15
16
|
import { connectionsModule } from "./connections";
|
|
@@ -189,9 +190,9 @@ export const brokerPlugin = composeBuiltinPlugin({
|
|
|
189
190
|
id: "broker",
|
|
190
191
|
name: "Broker",
|
|
191
192
|
version: "1.0.0",
|
|
192
|
-
description: "Broker profiles, account sync, and
|
|
193
|
+
description: "Broker profiles, account sync, order review, and trading status.",
|
|
193
194
|
toggleable: true,
|
|
194
|
-
modules: [brokerManagerModule],
|
|
195
|
+
modules: [brokerManagerModule, brokerTradingModule],
|
|
195
196
|
});
|
|
196
197
|
|
|
197
198
|
export const globalMarketsPlugin = composeBuiltinPlugin({
|
|
@@ -29,7 +29,7 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
29
29
|
shape: "bundle",
|
|
30
30
|
// Tuesday positions published the Friday after; two weeks without a new report means one was missed.
|
|
31
31
|
freshness: { source: "CFTC", status: "not-a-feed", basis: "weekly report", maxAgeMinutes: 14 * 24 * 60 },
|
|
32
|
-
argument: { kind: "free-text", optional: true, placeholder: "code or
|
|
32
|
+
argument: { kind: "free-text", optional: true, placeholder: "code or name", description: "Futures root (KC), commodity name (coffee) or CFTC market code; omit for the extremes board." },
|
|
33
33
|
options: [{ key: "report", type: "enum", values: [{ value: "legacy" }, { value: "disaggregated" }], defaultValue: "legacy", description: "CFTC report family." },
|
|
34
34
|
{ key: "traderClass", type: "enum", values: ["noncommercial", "commercial", "producer", "swap", "managed-money", "other-reportable", "nonreportable"].map((value) => ({ value })), description: "Trader class for the extremes board; defaults by report family.",
|
|
35
35
|
pluginState: { pluginId: COMMODITIES_VIEW_STATE.cot, key: "class" } },
|
|
@@ -13,7 +13,7 @@ export const cotModule: PluginModule = {
|
|
|
13
13
|
paneTemplates: [{ id: "cot-cot", paneId: "cot", label: "CFTC Positioning",
|
|
14
14
|
description: "Weekly futures positioning, changes, historical percentiles and cross-market extremes.",
|
|
15
15
|
keywords: ["cot", "cftc", "positioning", "commitments", "managed money", "net spec", "crude oil", "natural gas"],
|
|
16
|
-
shortcut: { prefix: "COT", aliases: ["CFTC"], argKind: "text" as const, argPlaceholder: "code or
|
|
16
|
+
shortcut: { prefix: "COT", aliases: ["CFTC"], argKind: "text" as const, argPlaceholder: "code or name", argOptional: true,
|
|
17
17
|
argOptions: () => COT_ROOT_OPTIONS }, headless: cotHeadless,
|
|
18
18
|
createInstance: (context, options) => {
|
|
19
19
|
const input = options?.arg?.trim();
|