gloomberb 0.18.1 → 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.
Files changed (307) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -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 (balanceRows.length === 0) return;
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) => setBalanceSort({ columnId: columnId as BalanceColumnId, 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) => setBalanceId(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 data = validateCompanyData(await (mode === "kpis" ? client.getCloudCompanyKpis(symbol, options, listing) : client.getCloudCompanyGuidance(symbol, options, listing)));
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, QUIET_COLUMNS, type CompanyRow } from "./model";
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
- const shown = Math.max(0, data.totalRows - data.lockedRows);
61
- const noun = (count: number) => `${mode === "kpis" ? "KPI value" : "guidance figure"}${count === 1 ? "" : "s"}`;
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.", "Coverage follows available company disclosures; missing data is not zero.", "Compare matching definition, period, currency, basis and scope.", "Fiscal labels without explicit calendar dates are retained without inferred dates."] },
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
- export function companyColumns(guidance: boolean, evidence: boolean, history: boolean, trend = false): DataTableColumn[] {
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
- return order ? columns.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)) : columns;
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, seriesLabel, unitLabel, valueText, companyPreviewNote,
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
- const [sort, setSort] = usePluginPaneState<{ column: string; direction: "asc" | "desc" }>("company:sort", { column: "period", direction: "desc" });
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)) * (sort.direction === "asc" ? 1 : -1);
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 connection status.",
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 root", description: "CFTC market code or verified futures root; omit for the extremes board." },
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 root", argOptional: true,
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();
@@ -5,7 +5,7 @@ import type { PricePoint } from "../../../types/financials";
5
5
  import { formatPriceObservation } from "../../../market-data/market/format";
6
6
  import { cleanFloat32Price } from "../../../cli/history-rows";
7
7
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
8
- import { FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, type FuturesContract } from "../../../market-data/futures-board";
8
+ import { bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByName, type FuturesContract } from "../../../market-data/futures-board";
9
9
 
10
10
  export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
11
11
  legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
@@ -124,25 +124,48 @@ export function cotScope(value: unknown): CotScope { return value === "all" ? "a
124
124
  const rootAlias = (input: string) => ROOTS[input === "VIX" ? "VX" : input]?.code ?? null;
125
125
  const marketCode = (input: string) => /^[0-9A-Z]{5}[0-9A-Z+]$/.test(input) ? input : null;
126
126
  /**
127
- * A CFTC code or root typed as an argument or stored in a pane: every alias,
128
- * crypto included, and every FUT board root the CFTC reports. A typed root is
129
- * always the future, so ZM here is soybean meal, never Zoom.
127
+ * The FUT board row a typed argument names, by code, symbol or commodity name
128
+ * as FUT reads them (CC, CC=F, cocoa), or a verified root named the same way
129
+ * (bitcoin, VIX, SOFR, fed funds).
130
+ */
131
+ function typedMarket(value: string): { board: FuturesContract } | { root: string } | null {
132
+ const board = futuresBoardContract(value) ?? futuresContractByName(value);
133
+ if (board) return { board };
134
+ const root = bestNameMatch(Object.keys(ROOTS), (key) => [ROOTS[key]!.name], value);
135
+ return root ? { root } : null;
136
+ }
137
+ /**
138
+ * A CFTC code, root or commodity name typed as an argument or stored in a
139
+ * pane: every alias, crypto included, every FUT board root the CFTC reports,
140
+ * and the names FUT finds them by (cocoa, coffee, sugar, gold, crude). A typed
141
+ * root is always the future, so ZM here is soybean meal, never Zoom. A CFTC
142
+ * code is tried after the names, as one could be spelled with six letters
143
+ * (COFFEE, COPPER).
130
144
  */
131
145
  export function cotContractCode(value: unknown): string | null {
132
146
  if (typeof value !== "string") return null;
133
147
  const input = value.trim().toUpperCase().replace(/=F$/, "");
134
- const board = futuresBoardContract(value);
135
- return rootAlias(input) ?? (board ? boardMarketCode(board) : null) ?? marketCode(input);
148
+ const alias = rootAlias(input);
149
+ if (alias) return alias;
150
+ const market = typedMarket(value);
151
+ if (market && "root" in market) return ROOTS[market.root]!.code;
152
+ return (market ? boardMarketCode(market.board) : null) ?? (market ? null : marketCode(input));
136
153
  }
154
+ /** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
155
+ const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
156
+ ["ES", "S&P 500"], ["CL", "crude oil"], ["NG", "natural gas"], ["GC", "gold"], ["HG", "copper"],
157
+ ["ZC", "corn"], ["CC", "cocoa"], ["KC", "coffee"], ["SB", "sugar"], ["ZN", "10-year note"],
158
+ ];
137
159
  /**
138
- * What a market `cotContractCode` does not know is told: a FUT board root the
139
- * CFTC does not report says so; anything else gets roots and a code that work.
160
+ * What a market `cotContractCode` does not know is told: a FUT board row the
161
+ * CFTC does not report says so; anything else gets codes and names that work,
162
+ * softs and metals among them, and a CFTC code.
140
163
  */
141
164
  export function cotUnknownMarketMessage(value: unknown): string {
142
- const board = futuresBoardContract(value);
143
- if (board) return `${board.code} (${board.name}) is not reported in the CFTC COT.`;
144
- const roots = ["ES", "CL", "GC", "ZN"].filter((root) => ROOTS[root]);
145
- return `Unknown market "${String(value)}". Try ${roots.join(", ")} or a CFTC market code such as ${ROOTS.GC!.code}.`;
165
+ const market = typeof value === "string" ? typedMarket(value) : null;
166
+ if (market && "board" in market) return `${market.board.code} (${market.board.name}) is not reported in the CFTC COT.`;
167
+ const known = SUGGESTED_MARKETS.map(([code, name]) => `${code} ${name}`).join(", ");
168
+ return `Unknown market "${String(value)}". Try a code or name such as ${known}, or a CFTC market code such as ${ROOTS.GC!.code}.`;
146
169
  }
147
170
  /**
148
171
  * The code for the ticker under the cursor, when COT is opened without an
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  MarketBoardStack,
@@ -107,7 +108,7 @@ export function CdxPane({ focused, width, height }: PaneProps) {
107
108
  const asOf = formatAsOf(payload?.asOf ?? null);
108
109
  const info = useMemo<PaneFooterSegment[]>(() => [
109
110
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
110
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
111
+ ...(payload ? [DELAYED_SEGMENT] : []),
111
112
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
112
113
  ], [asOf, payload, resource.data?.stale]);
113
114
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -112,7 +113,7 @@ export function SovrPane({ focused, width, height }: PaneProps) {
112
113
  const asOf = formatAsOf(payload?.asOf ?? null);
113
114
  const info = useMemo<PaneFooterSegment[]>(() => [
114
115
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
115
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
116
+ ...(payload ? [DELAYED_SEGMENT] : []),
116
117
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
117
118
  ], [asOf, payload, resource.data?.stale]);
118
119
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -115,7 +116,7 @@ export function CreditConditionsPane({ paneId, focused, width, height }: PanePro
115
116
  notices: mixedDates ? ["The indexes carry different observation dates, so the spreads are not one session."] : [],
116
117
  });
117
118
  const footerInfo = useMemo<PaneFooterSegment[]>(() => [
118
- ...(rows.length > 0 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
119
+ ...(rows.length > 0 ? [DELAYED_SEGMENT] : []),
119
120
  ...(partial ? [{ id: "partial", parts: [{ text: `PARTIAL ${rows.length}/${series.length}`, tone: "warning" as const, bold: true }] }] : []),
120
121
  ...(stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
121
122
  ...loadingErrorFooterInfo(loading, error),
@@ -84,7 +84,34 @@ function withValidStablecoinFields(asset: CryptoMarketAsset): CryptoMarketAsset
84
84
  return { ...asset, ...(peg ? { peg: null } : {}), ...(range ? { range24h: null } : {}) };
85
85
  }
86
86
 
87
- export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarketsPayload {
87
+ const DAY_MS = 86_400_000;
88
+ /** How many UTC days old a coin's latest daily close may be for its previous close to stand, as Gloom Cloud judges a coin quote's. */
89
+ const PREVIOUS_CLOSE_MAX_AGE_DAYS = 2;
90
+
91
+ /**
92
+ * A coin of a board from before Gloom Cloud measured its moves since 00:00
93
+ * UTC (a server not yet updated, a copy cached from one), whose change runs
94
+ * over the screener's 24 hours: moved from its previous close instead, the
95
+ * close of its last UTC day while its daily closes reach the last two days.
96
+ * Otherwise that close may be weeks old (Toncoin on 18 Oct 2026), and the
97
+ * coin has no move.
98
+ */
99
+ function onUtcDayBasis(asset: CryptoMarketAsset, now: number): CryptoMarketAsset {
100
+ const history = asset.history;
101
+ const last = history?.closes.findLastIndex((close) => close != null) ?? -1;
102
+ const lastDay = history && last >= 0 ? Date.parse(`${history.start}T00:00:00Z`) / DAY_MS + last : null;
103
+ const holds = lastDay != null && asset.previousClose != null && asset.previousClose > 0
104
+ && Math.floor(now / DAY_MS) - lastDay <= PREVIOUS_CLOSE_MAX_AGE_DAYS;
105
+ const change = holds ? asset.price - asset.previousClose! : null;
106
+ return {
107
+ ...asset,
108
+ previousClose: holds ? asset.previousClose : null,
109
+ change,
110
+ changePercent: change != null ? (change / asset.previousClose!) * 100 : null,
111
+ };
112
+ }
113
+
114
+ export function validateCryptoMarkets(data: CryptoMarketsPayload, now = Date.now()): CryptoMarketsPayload {
88
115
  if (
89
116
  !data
90
117
  || data.version !== 1
@@ -103,7 +130,11 @@ export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarkets
103
130
  ) {
104
131
  throw new Error("The server returned an invalid crypto board");
105
132
  }
106
- const assets = data.assets.map(withValidStablecoinFields);
133
+ const legacy = data.dayChangeBasis !== "utc-day";
134
+ const assets = data.assets.map((asset) => {
135
+ const valid = withValidStablecoinFields(asset);
136
+ return legacy ? onUtcDayBasis(valid, now) : valid;
137
+ });
107
138
  return assets.every((asset, index) => asset === data.assets[index]) ? data : { ...data, assets };
108
139
  }
109
140