gloomberb 0.16.6 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -40,7 +40,7 @@
40
40
  "src/layout-marketplace/gallery.tsx": ["cloud"],
41
41
  "src/layout-marketplace/linked-sync.ts": ["cloud"],
42
42
  "src/layout/desks.ts": ["chart-composer"],
43
- "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","treasury-auctions","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
43
+ "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
44
44
  "src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
45
45
  "src/plugins/catalog.ts": ["debug"],
46
46
  "src/remote/controller.ts": ["custom-view"],
@@ -0,0 +1,28 @@
1
+ {
2
+ "src/components/layout/pane/header.tsx": 2,
3
+ "src/components/layout/pane/sizing.ts": 3,
4
+ "src/components/layout/shell/drag/slide.ts": 4,
5
+ "src/components/layout/shell/pane/layers.tsx": 6,
6
+ "src/components/ui/select-button.tsx": 2,
7
+ "src/renderers/dom/data-table/dom.ts": 26,
8
+ "src/renderers/dom/data-table/index.tsx": 21,
9
+ "src/renderers/dom/data-table/row.tsx": 11,
10
+ "src/renderers/dom/desktop/controls.tsx": 6,
11
+ "src/renderers/dom/desktop/list-view.tsx": 2,
12
+ "src/renderers/dom/desktop/popover.tsx": 6,
13
+ "src/renderers/dom/desktop/query-bar.tsx": 1,
14
+ "src/renderers/dom/desktop/select-field.tsx": 2,
15
+ "src/renderers/dom/dom-ui-host.tsx": 6,
16
+ "src/renderers/dom/host/input.tsx": 5,
17
+ "src/renderers/dom/host/live-frame.ts": 8,
18
+ "src/renderers/dom/host/mouse.ts": 10,
19
+ "src/renderers/dom/host/scroll-box.tsx": 22,
20
+ "src/renderers/dom/host/size-observer.ts": 6,
21
+ "src/renderers/dom/host/style.ts": 27,
22
+ "src/renderers/dom/host/surface-fit.ts": 4,
23
+ "src/renderers/dom/host/tabs.tsx": 2,
24
+ "src/renderers/dom/host/text.tsx": 2,
25
+ "src/renderers/dom/input-host.tsx": 4,
26
+ "src/renderers/dom/native-renderer.ts": 4,
27
+ "src/renderers/dom/styles.css": 17
28
+ }
@@ -1,8 +1,9 @@
1
+ import type { ResolvedPane } from "../layout/pane-manager";
1
2
  import type { PluginRegistry } from "../plugins/registry";
2
- import { paneShowsPersonalData } from "../shares/personal-pane";
3
- import type { AppConfig, PaneInstanceConfig } from "../types/config";
3
+ import { panePersonalData } from "../shares/personal-pane";
4
+ import { resolveTickerForPane, type AppState } from "../state/app/context";
5
+ import type { LayoutConfig } from "../types/config";
4
6
  import type { PaneSharePrivateFields } from "../types/plugin";
5
- import type { TickerRecord } from "../types/ticker";
6
7
  import type { AskGloomPaneAttachment } from "./pane-request";
7
8
 
8
9
  /** A handful of the pane's own settings is enough to say how it is set up. */
@@ -50,14 +51,13 @@ function functionCode(registry: Pick<PluginRegistry, "paneTemplates">, paneType:
50
51
  }
51
52
 
52
53
  export interface PaneAttachmentSource {
53
- registry: Pick<PluginRegistry, "panes" | "paneTemplates" | "brokers" | "getPanePluginId" | "getBrokerPluginId">;
54
- instance: PaneInstanceConfig;
54
+ registry: Pick<PluginRegistry, "paneTemplates" | "brokers" | "getPanePluginId" | "getBrokerPluginId">;
55
+ pane: Pick<ResolvedPane, "instance" | "def">;
56
+ state: AppState;
57
+ /** The layout the pane is in, as the share path reads it for `personalData`. */
58
+ layout: LayoutConfig;
55
59
  /** What the header calls it. */
56
60
  title: string;
57
- symbol: string | null;
58
- paneState?: Readonly<Record<string, unknown>>;
59
- config: Pick<AppConfig, "portfolios" | "brokerInstances">;
60
- tickers?: ReadonlyMap<string, TickerRecord>;
61
61
  /** The tab each of its tab strips has open, outermost first. */
62
62
  activeTabs?: readonly string[];
63
63
  visibleText: string | null;
@@ -70,16 +70,17 @@ export interface PaneAttachmentSource {
70
70
  */
71
71
  export function buildAskGloomPaneAttachment({
72
72
  registry,
73
- instance,
73
+ pane,
74
+ state,
75
+ layout,
74
76
  title,
75
- symbol,
76
- paneState,
77
- config,
78
- tickers,
79
77
  activeTabs = [],
80
78
  visibleText,
81
79
  }: PaneAttachmentSource): AskGloomPaneAttachment {
82
- const privateFields = registry.panes.get(instance.paneId)?.portableShare?.private;
80
+ const { instance } = pane;
81
+ const symbol = resolveTickerForPane(state, instance.instanceId);
82
+ const paneState = state.paneState[instance.instanceId];
83
+ const privateFields = pane.def.portableShare?.private;
83
84
  const args: Record<string, string> = {};
84
85
  const tabs = activeTabs.map((tab) => tab.trim()).filter(Boolean);
85
86
  if (tabs.length > 0) args.tab = tabs.join(" > ").slice(0, MAX_ARG_VALUE_LENGTH);
@@ -100,14 +101,17 @@ export function buildAskGloomPaneAttachment({
100
101
  symbol,
101
102
  args,
102
103
  visibleText,
103
- personal: showsPersonalData({ registry, instance, paneState, symbol, config, tickers }),
104
+ personal: showsPersonalData({ registry, state, layout, pane }),
104
105
  };
105
106
  }
106
107
 
107
- /** A pane that cannot be read for it counts as personal: its contents then wait for a yes. */
108
- function showsPersonalData(source: Parameters<typeof paneShowsPersonalData>[0]): boolean {
108
+ /**
109
+ * Whatever the pane shows as personal data, as sharing its picture reads it.
110
+ * A pane that cannot be read for it counts as personal: its contents then wait for a yes.
111
+ */
112
+ function showsPersonalData(source: Parameters<typeof panePersonalData>[0]): boolean {
109
113
  try {
110
- return paneShowsPersonalData(source);
114
+ return panePersonalData(source) !== null;
111
115
  } catch {
112
116
  return true;
113
117
  }
@@ -23,8 +23,8 @@ export interface AskGloomPaneAttachment {
23
23
  /** Its text as drawn when asked, bounded; null when none could be read. */
24
24
  visibleText: string | null;
25
25
  /**
26
- * It shows holdings, balances or account data, so Ask Gloom asks before
27
- * anything it shows leaves the device.
26
+ * The pane shows personal data (`PaneDef.personalData`), so Ask Gloom
27
+ * asks before anything it shows leaves the device.
28
28
  */
29
29
  personal: boolean;
30
30
  }
@@ -19,18 +19,44 @@ export function isSpreadSeries(seriesId: string): boolean {
19
19
  }
20
20
 
21
21
  /**
22
- * The field a series' observations export under, named by their unit: a
23
- * spread in basis points (`value_bp`), a percent (`value_pct`), any other
24
- * unit slugged from FRED's own (`Index 1982-1984=100` is
25
- * `value_index_1982_1984_100`), and `value_unit_unknown` when FRED's
26
- * metadata did not come back. Never a bare `value`.
22
+ * The unit a series' observations export in, for the `currency_or_unit`
23
+ * column beside its bare `value`: `bp` for a spread (exported in basis
24
+ * points), `pct` for a percent, otherwise FRED's own units, with "annual
25
+ * rate" as the text header says it. Null when FRED's metadata did not come
26
+ * back.
27
27
  */
28
- export function fredValueKey(seriesId: string, units: string | null | undefined): string {
29
- if (isSpreadSeries(seriesId)) return "value_bp";
30
- const unit = units?.trim() ?? "";
31
- if (/^percent$/i.test(unit)) return "value_pct";
32
- const slug = unit.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
33
- return `value_${slug || "unit_unknown"}`;
28
+ export function fredUnit(seriesId: string, info: { units?: string | null; seasonalAdjustment?: string | null } | null | undefined): string | null {
29
+ if (isSpreadSeries(seriesId)) return "bp";
30
+ const units = info?.units?.trim();
31
+ if (!units) return null;
32
+ if (/^percent$/i.test(units)) return "pct";
33
+ const annualRate = /annual rate/i.test(info?.seasonalAdjustment ?? "") && !/annual rate/i.test(units);
34
+ return `${units}${annualRate ? ", annual rate" : ""}`;
35
+ }
36
+
37
+ /** How often a series observes, in FRED's word lower-cased: `Weekly, Ending Friday` is `weekly`. Null when FRED does not say. */
38
+ export function fredInterval(frequency: string | null | undefined): string | null {
39
+ const word = frequency?.split(",")[0]?.trim().toLowerCase();
40
+ return word || null;
41
+ }
42
+
43
+ /**
44
+ * The observations from the series' first one on: Gloom Cloud answers a
45
+ * `--start` before a series begins with a blank row for every day in between
46
+ * (DEXSFUS from 1975 has five years of them before 1980-01-02). FRED's
47
+ * `observationStart` says where it begins; without it the blank rows before
48
+ * the first value go. Blank rows after it (a holiday's ".") stay.
49
+ */
50
+ export function fredObservationsFromStart<T extends { date: string; value: number | null }>(
51
+ observations: readonly T[],
52
+ seriesStart: string | null | undefined,
53
+ ): T[] {
54
+ const first = seriesStart && /^\d{4}-\d{2}-\d{2}$/.test(seriesStart)
55
+ ? seriesStart
56
+ : observations.reduce<string | null>((earliest, observation) => (
57
+ observation.value != null && Number.isFinite(observation.value) && (!earliest || observation.date < earliest) ? observation.date : earliest
58
+ ), null);
59
+ return first ? observations.filter((observation) => observation.date >= first) : [...observations];
34
60
  }
35
61
 
36
62
  /**
@@ -144,3 +170,43 @@ export function fredEmptyMessage(startDate: string, latest: { date: string; valu
144
170
  ? `Latest observation: ${latest.date}${latest.value == null ? "" : ` (${latest.value})`}. Nothing on or after ${startDate}.`
145
171
  : "No results.";
146
172
  }
173
+
174
+ /** What an export of nothing on or after `--start` says to try instead: a start that reaches the latest observation. */
175
+ export function fredEmptyAdvice(seriesId: string, latest: { date: string } | null): string {
176
+ return latest
177
+ ? `Latest observation: ${latest.date}. Try: gloomberb fred ${seriesId} --start ${latest.date.slice(0, 4)}`
178
+ : `No observations came back for ${seriesId}. Try an earlier --start, or gloomberb fred --list for the series served.`;
179
+ }
180
+
181
+ /** A series as `fred --list` shows it. */
182
+ export interface FredListRow {
183
+ id: string;
184
+ title: string;
185
+ group: string;
186
+ }
187
+
188
+ /** How well a word of the filter matches a series, best first: its id or group name, a word of its group or title, the start of its id, then any text. */
189
+ function matchTier(term: string, row: FredListRow, words: readonly string[]): number {
190
+ if (row.id.toLowerCase() === term || row.group.toLowerCase() === term) return 0;
191
+ if (words.some((word) => word.startsWith(term))) return 1;
192
+ if (row.id.toLowerCase().startsWith(term)) return 2;
193
+ return 3;
194
+ }
195
+
196
+ /**
197
+ * The series that carry every word of the filter in their id, title or group,
198
+ * the ones it names first: a group or a word of the title ahead of the id that
199
+ * only has it inside (`fx` is the FX group, not FDEFX, whose id spells it).
200
+ * Catalog order breaks ties; an empty filter keeps the catalog as it is.
201
+ */
202
+ export function filterFredSeries(rows: readonly FredListRow[], filter: string): FredListRow[] {
203
+ const terms = filter.toLowerCase().split(/\s+/).filter(Boolean);
204
+ const ranked = rows.flatMap((row, index) => {
205
+ const text = `${row.id} ${row.title} ${row.group}`.toLowerCase();
206
+ if (!terms.every((term) => text.includes(term))) return [];
207
+ const words = `${row.title} ${row.group}`.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
208
+ const tiers = terms.map((term) => matchTier(term, row, words));
209
+ return [{ row, index, worst: Math.max(0, ...tiers), total: tiers.reduce((sum, tier) => sum + tier, 0) }];
210
+ });
211
+ return ranked.sort((a, b) => a.worst - b.worst || a.total - b.total || a.index - b.index).map((entry) => entry.row);
212
+ }
@@ -16,7 +16,7 @@ import {
16
16
  getExtendedSessionDisplay,
17
17
  getQuoteSessionFields,
18
18
  getRegularSessionDisplay,
19
- marketStateLabel,
19
+ quoteSessionLabel,
20
20
  type ExtendedSession,
21
21
  } from "../../market-data/market/status";
22
22
  import { formatCompact, formatDistributionAmount, formatPercent, formatPercentRaw } from "../../utils/format";
@@ -62,6 +62,7 @@ import {
62
62
  } from "./options-chain";
63
63
  import { DEFAULT_LEAPS_CRITERIA, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions } from "./options-leaps";
64
64
  import type { CliResultColumn } from "../result";
65
+ import type { PriceHistoryResult } from "../../types/price-history";
65
66
  import {
66
67
  exportRowsTable,
67
68
  reportFooterLines,
@@ -70,9 +71,12 @@ import {
70
71
  } from "../report-tables";
71
72
  import {
72
73
  historyBarStatusNote,
74
+ historyCoverageWarning,
73
75
  historyFacts,
74
76
  historyFlagNote,
75
77
  historyGapNote,
78
+ historyJumpNote,
79
+ historyUnbrokenFrom,
76
80
  historyAllRangeYears,
77
81
  historyIntervalsByRange,
78
82
  historyNotes,
@@ -89,7 +93,9 @@ import { newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProb
89
93
  import { formatUtcTime } from "../../utils/utc-time";
90
94
  import { isFiniteNumber } from "../../utils/guards";
91
95
  import { exportedFundamentals } from "../../utils/price-earnings";
92
- import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness } from "../freshness";
96
+ import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
97
+ import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
98
+ import { selectMarketCapitalization } from "../../utils/market-capitalization";
93
99
  import {
94
100
  barHistoryFreshness,
95
101
  barResolutionFromDates,
@@ -128,13 +134,15 @@ import {
128
134
  import { indexRootAlsoLine, indexRootFor, indexRootTryLine, isYieldIndexSymbol } from "../index-roots";
129
135
  import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
130
136
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
131
- import { providerMissReason } from "../../sources/provider-errors";
132
- import { isNotATickerMessage } from "../not-a-ticker";
137
+ import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
138
+ import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
139
+ import { describeSuggestion, suggestListing } from "../listing-suggestion";
133
140
  import { splitSymbolArgs } from "../symbol-args";
134
141
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
135
142
  import { subMinuteDataMessage } from "../known-gaps";
136
143
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
137
144
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
145
+ import { looksLikeCurrencyPair } from "../../utils/currency-pair";
138
146
  import { isUsListingExchange } from "../../utils/exchanges";
139
147
  import { describeTickerChange, startTickerChangeLookup, tickerChangeLine, type TickerChange } from "../../tickers/ticker-changes";
140
148
  import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
@@ -245,6 +253,7 @@ const QUOTE_LEAD_COLUMNS = [
245
253
  // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
246
254
  extendedColumn("PRE"),
247
255
  extendedColumn("POST"),
256
+ extendedColumn("OVERNIGHT"),
248
257
  { key: "session", header: "Session", shrink: false },
249
258
  ];
250
259
 
@@ -261,17 +270,21 @@ function yieldChangeCell(row: QuoteRow): string {
261
270
  return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
262
271
  }
263
272
 
264
- /** The pre-market or after-hours print and its move from the regular close, in its own column. */
273
+ /**
274
+ * The pre-market, after-hours or overnight print and its move from the
275
+ * regular close, in its own column; "mid" when it is a bid and ask midpoint.
276
+ */
265
277
  function extendedColumn(session: ExtendedSession) {
278
+ const price = (row: QuoteRow) => row.extendedMark ? `${row.extendedPrice} mid` : row.extendedPrice;
266
279
  return {
267
- key: session === "PRE" ? "preMarket" : "afterHours",
280
+ key: session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight",
268
281
  header: EXTENDED_SESSION_LABELS[session],
269
282
  align: "right" as const,
270
283
  value: (row: QuoteRow) => row.extendedSession === session && row.extendedPrice
271
- ? [row.extendedPrice, row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
284
+ ? [price(row), row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
272
285
  : "",
273
286
  format: (value: unknown, row: QuoteRow) => (
274
- value ? [row.extendedPrice, formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
287
+ value ? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
275
288
  ),
276
289
  };
277
290
  }
@@ -361,7 +374,9 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
361
374
  rawExtendedPrice: extended?.price ?? null,
362
375
  extendedChange: extended?.change ?? null,
363
376
  extendedChangePercent: roundedPercent(extended?.changePercent),
364
- session: quote?.marketState ? marketStateLabel(quote.marketState) : "",
377
+ // The extended price is the session's bid and ask midpoint, nothing having traded in it.
378
+ extendedMark: extended?.mark === true,
379
+ session: quoteSessionLabel(quote) ?? "",
365
380
  // The close the shown move is measured from: the one before the session Last is.
366
381
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
367
382
  dayRange: quote?.low != null && quote.high != null
@@ -696,7 +711,7 @@ async function answerQuote(
696
711
  root ? indexRootTryLine(root, request.command) : null,
697
712
  other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
698
713
  ].filter((line): line is string => !!line);
699
- if (!other && tryLines.length === 0) return { result, listing, notes: [] };
714
+ if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
700
715
  const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
701
716
  const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
702
717
  const oneLine = [
@@ -707,6 +722,21 @@ async function answerQuote(
707
722
  return { result, listing, failure: { message, details, oneLine }, notes: [] };
708
723
  }
709
724
 
725
+ /**
726
+ * A symbol written like a currency pair that is not a ticker (USD_NGN, as some
727
+ * FX feeds write a pair) names the listing `gloomberb search` finds nearest to
728
+ * it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
729
+ * Naira)?". When search finds nothing near, the row keeps its own error and its
730
+ * search hint.
731
+ */
732
+ async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
733
+ const message = errorMessage(result.error);
734
+ const symbol = message ? notATickerSymbol(message) : null;
735
+ if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
736
+ const suggestion = await suggestListing(symbol, market);
737
+ return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
738
+ }
739
+
710
740
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
711
741
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
712
742
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
@@ -744,16 +774,17 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
744
774
  const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
745
775
  const load = () => market.dataProvider.getPriceHistoryWithMetadata
746
776
  ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
747
- : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points) => ({ points, resolution: null }));
777
+ : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
748
778
  const quoted = loadListingQuote(market.dataProvider, listing);
749
779
  const fate = listingFateLoader(listing, market, quoted);
750
- const [{ points, resolution }, quote] = await Promise.all([
780
+ const [loaded, quote] = await Promise.all([
751
781
  loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
752
782
  command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history", fate,
753
783
  })
754
784
  .catch((error) => failHistory(error, listing.key, ctx)),
755
785
  quoted,
756
786
  ]);
787
+ const { points, resolution } = loaded;
757
788
  const identity = listingIdentity(listing, quote);
758
789
  // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
759
790
  const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
@@ -783,7 +814,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
783
814
  // FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
784
815
  ...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
785
816
  ...await listingChoiceNotes(listing, market, quote, venues),
786
- ...historyNotes(range, interval, data[0]?.date), gaps ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(data) ?? [],
817
+ ...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
818
+ gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(data) ?? [],
787
819
  ].flat();
788
820
  const priceColumns: CliResultColumn[] = [
789
821
  intraday
@@ -821,6 +853,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
821
853
  interval,
822
854
  firstDate: data[0]?.date ?? null,
823
855
  lastDate: data.at(-1)?.date ?? null,
856
+ // The first bar after the last gap or jump; the first bar when there is none.
857
+ unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
824
858
  bars: data.length,
825
859
  asOf: freshness?.asOf ?? null,
826
860
  ...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
@@ -831,16 +865,14 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
831
865
  heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
832
866
  dateKey: "date",
833
867
  textColumns: [...priceColumns, flagTextColumn],
834
- // Every exported row says its currency, what a price is in and its bar size, so it survives head and concatenation.
835
868
  columns: [
836
869
  ...priceColumns,
837
- { key: "currency", header: "Currency" },
838
- { key: "unit", header: "Unit" },
839
- { key: "interval", header: "Interval" },
840
870
  flagColumn,
841
871
  ...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
842
872
  ...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
843
873
  ],
874
+ // Every exported row says what it is a bar of, the currency a price is in and its bar size, so it survives head and concatenation.
875
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
844
876
  });
845
877
  });
846
878
  }
@@ -889,15 +921,28 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
889
921
  ...financials,
890
922
  };
891
923
  if (view !== "statements") {
892
- const freshness = fundamentalsFreshness(financials);
924
+ // A valuation is measured at the quote's regular session price, so the quote dates it.
925
+ const freshness = view === "valuation" ? valuationFreshness(financials) : fundamentalsFreshness(financials);
926
+ // Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
927
+ const priced = atRegularSessionPrice(financials);
928
+ const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
893
929
  ctx.printResult({
894
930
  // JSON reads a multiple over a loss as null with its reason, never as a number.
895
- data: { ...data, fundamentals: exportedFundamentals(data.fundamentals) },
931
+ data: {
932
+ ...data,
933
+ ...view === "valuation" ? {
934
+ ...getQuoteSessionFields(financials.quote),
935
+ marketCap: capitalization?.value ?? null,
936
+ marketCapCurrency: capitalization?.currency ?? null,
937
+ } : {},
938
+ fundamentals: exportedFundamentals(priced.fundamentals),
939
+ },
896
940
  freshness,
897
941
  ...(choice.length > 0 ? { warnings: choice } : {}),
898
942
  }, {
899
943
  text: () => renderFundamentalsReport(data, view),
900
944
  ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
945
+ exportIdentity: { symbolOrSeries: listing.key },
901
946
  });
902
947
  return;
903
948
  }
@@ -929,6 +974,8 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
929
974
  rows: () => rows,
930
975
  columns: STATEMENT_COLUMNS,
931
976
  ...(tables && ctx.cliOptions.format === "csv" ? { tables } : {}),
977
+ // Annual statements in the company's reporting currency.
978
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: rows.find((row) => row.currency)?.currency ?? null, interval: "annual" },
932
979
  });
933
980
  });
934
981
  }
@@ -1318,6 +1365,7 @@ async function runHolders(
1318
1365
  tables: [exportRowsTable("Holders", columns, shownRows)],
1319
1366
  footer: reportFooterLines({ freshness, notes: notices }),
1320
1367
  },
1368
+ exportIdentity: { symbolOrSeries: listing.key },
1321
1369
  summary: () => [
1322
1370
  [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
1323
1371
  insiderCommand ? insiderSummary(data, ownerTypes, listing, identity, form4) : "",
@@ -1577,6 +1625,8 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1577
1625
  })));
1578
1626
  ctx.printResult({ data: [row], freshness }, {
1579
1627
  text: () => describeFxRate(request, rate),
1628
+ // The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
1629
+ exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
1580
1630
  columns: [
1581
1631
  { key: "currency", header: "Currency" },
1582
1632
  { key: "baseCurrency", header: "Base" },
@@ -1689,7 +1739,14 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1689
1739
  title: "Bars",
1690
1740
  lines: [
1691
1741
  `Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for, and for a coin, which trades every day, names each day it has no bar for.`,
1692
- "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. Every exported row (--csv, --ndjson, --json) carries currency, interval and flag.",
1742
+ "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. Every --csv and --ndjson row carries its flag and ends with symbol_or_series, currency_or_unit, interval, source and as_of; --json rows carry currency, unit, interval and flag.",
1743
+ "A break in the series flags the bar after it, which keeps its prices: gap after more than three months with no bar (weeks for weekly, daily and intraday bars), jump when the close is over 100 times the one before or under a hundredth of it. A warning names each break, and the heading says the date the series runs unbroken from.",
1744
+ ],
1745
+ }, {
1746
+ title: "Long history",
1747
+ lines: [
1748
+ "ZAR has daily FRED data from 1980: gloomberb fred DEXSFUS --start 1980 --sort asc --csv",
1749
+ "NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (gloomberb fn G USDNGN=X --rangePreset 5Y --chartResolution 1d --csv); before that there are monthly bars only (gloomberb history USDNGN=X --range ALL).",
1693
1750
  ],
1694
1751
  }],
1695
1752
  examples: ["history AAPL", "history BHP:ASX --range 5Y", "history ZAR=X --range ALL", "history ZAR=X --tail 5", "history AAPL --range 5Y --csv > aapl.csv"],
@@ -40,13 +40,17 @@ import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
40
40
  import {
41
41
  basisPointsCell,
42
42
  DEFAULT_FRED_START,
43
+ filterFredSeries,
44
+ fredEmptyAdvice,
43
45
  fredEmptyMessage,
44
46
  fredExportValue,
45
47
  fredHistoryNotes,
48
+ fredInterval,
49
+ fredObservationsFromStart,
46
50
  fredProvenance,
47
51
  fredPublicationTime,
52
+ fredUnit,
48
53
  fredValueHeader,
49
- fredValueKey,
50
54
  isSpreadSeries,
51
55
  laggingSeriesNote,
52
56
  } from "./fred-series";
@@ -346,13 +350,10 @@ async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execut
346
350
  if (error instanceof ApiRequestError && error.status === 404) ctx.fail("The list of supported FRED series is not available yet.");
347
351
  throw error;
348
352
  }
349
- const terms = filter.toLowerCase().split(/\s+/).filter(Boolean);
350
- const rows = catalog.series
351
- .map((series) => ({ id: series.id, title: series.title?.trim() ?? "", group: series.group?.trim() ?? "" }))
352
- .filter((series) => {
353
- const text = `${series.id} ${series.title} ${series.group}`.toLowerCase();
354
- return terms.every((term) => text.includes(term));
355
- });
353
+ const rows = filterFredSeries(
354
+ catalog.series.map((series) => ({ id: series.id, title: series.title?.trim() ?? "", group: series.group?.trim() ?? "" })),
355
+ filter,
356
+ );
356
357
  ctx.printResult({ data: rows, metadata: { filter: filter || null, total: catalog.series.length } }, {
357
358
  columns: [
358
359
  { key: "id", header: "ID", shrink: false },
@@ -381,15 +382,17 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
381
382
  if (isUnsupportedSeries(error)) ctx.fail(`Unsupported FRED series ${seriesId}. List supported series with: gloomberb fred --list`);
382
383
  throw error;
383
384
  });
384
- // Each observation exports under its unit (`value_bp`, `value_pct`), a spread in basis points.
385
- const valueKey = fredValueKey(seriesId, data.info?.units);
385
+ // Every observation exports as `value`, a spread in basis points; its unit goes in the `currency_or_unit` column.
386
+ const unit = fredUnit(seriesId, data.info);
387
+ const interval = fredInterval(data.info?.frequency);
386
388
  // A value FRED does not list yet carries its publisher and when it published, in every export.
387
389
  const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string }) => ({
388
390
  date: observation.date,
389
- [valueKey]: fredExportValue(seriesId, observation.value),
391
+ value: fredExportValue(seriesId, observation.value),
390
392
  ...(observation.publisher?.trim() ? { publisher: observation.publisher.trim(), ...(observation.publishedAt ? { publishedAt: observation.publishedAt } : {}) } : {}),
391
393
  });
392
- const rows = data.observations.map(exportRow);
394
+ // A --start before the series begins comes back padded with blank days: the rows start at its first observation.
395
+ const rows = fredObservationsFromStart(data.observations, data.info?.observationStart).map(exportRow);
393
396
  // The default start hides older history and a start before the series begins is moved up silently: say where it really starts.
394
397
  const notes = [
395
398
  ...fredHistoryNotes({ startDate, startGiven: start != null, seriesStart: data.info?.observationStart }),
@@ -398,6 +401,10 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
398
401
  // Nothing on or after the start: name where the series ends, so an empty answer is not a dead end.
399
402
  const latest = rows.length > 0 ? null : await apiClient.getCloudFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
400
403
  .then((payload) => payload.observations[0] ?? null).catch(() => null);
404
+ // CSV and NDJSON are rows alone: an empty one would read as a series with no data, so it fails and says where the series ends.
405
+ if (rows.length === 0 && (ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson")) {
406
+ ctx.fail(`No ${seriesId} observations on or after ${startDate}.`, fredEmptyAdvice(seriesId, latest));
407
+ }
401
408
  const periodicity = periodicityOf(data.info?.frequency);
402
409
  const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
403
410
  // A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
@@ -408,7 +415,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
408
415
  ctx.printResult({
409
416
  data: rows,
410
417
  metadata: {
411
- info: data.info, seriesId, startDate, sortOrder,
418
+ info: data.info, seriesId, startDate, sortOrder, unit, interval,
412
419
  ...(notes.length ? { notes } : {}),
413
420
  ...(latest ? { latestObservation: exportRow(latest) } : {}),
414
421
  },
@@ -422,12 +429,13 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
422
429
  }, {
423
430
  dateKey: "date",
424
431
  empty: fredEmptyMessage(startDate, latest),
425
- ...(notes.length ? { footnote: notes.join("\n") } : {}),
432
+ ...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
433
+ exportIdentity: { symbolOrSeries: seriesId, currencyOrUnit: unit, interval },
426
434
  textColumns: [
427
435
  { key: "date", header: "Date" },
428
436
  isSpreadSeries(seriesId)
429
- ? { key: valueKey, header: "Value (bp)", align: "right", format: basisPointsCell }
430
- : { key: valueKey, header: fredValueHeader(data.info), align: "right" },
437
+ ? { key: "value", header: "Value (bp)", align: "right", format: basisPointsCell }
438
+ : { key: "value", header: fredValueHeader(data.info), align: "right" },
431
439
  ],
432
440
  });
433
441
  }
@@ -445,12 +453,12 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
445
453
  return {
446
454
  seriesId,
447
455
  date: latest?.date ?? "",
448
- // Yields in percent, named so in every export, as `fred` names a percent series.
449
- value_pct: latest?.value ?? null,
456
+ // Yields in percent, which the exports' `currency_or_unit` column says (`pct`), as `fred` does.
457
+ value: latest?.value ?? null,
450
458
  title: data.info?.title ?? "",
451
459
  // The move since the previous observation FRED has, not calendar yesterday: a Monday reads against Friday.
452
460
  previousDate: previous?.date ?? null,
453
- previousValue_pct: previous?.value ?? null,
461
+ previousValue: previous?.value ?? null,
454
462
  changeBasisPoints: latest?.value != null && previous?.value != null ? toBasisPoints(latest.value - previous.value, 1) : null,
455
463
  latest,
456
464
  publishedAt: latest?.publisher ? null : fredPublicationTime(data.info, latest?.date),
@@ -470,9 +478,10 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
470
478
  ...(publishedAt ? { publishedAt } : {}),
471
479
  }),
472
480
  }, {
481
+ exportIdentity: (row) => ({ symbolOrSeries: String(row.seriesId), currencyOrUnit: "pct", interval: "daily" }),
473
482
  textColumns: [
474
483
  { key: "tenor", header: "Tenor", value: (row) => YIELD_TENORS[String(row.seriesId)] ?? row.seriesId },
475
- { key: "value_pct", header: "Yield %", align: "right", format: (value) => typeof value === "number" ? value.toFixed(2) : "-" },
484
+ { key: "value", header: "Yield %", align: "right", format: (value) => typeof value === "number" ? value.toFixed(2) : "-" },
476
485
  {
477
486
  key: "changeBasisPoints",
478
487
  header: "1D bp",