gloomberb 0.16.4 → 0.16.5

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 (554) hide show
  1. package/README.md +14 -91
  2. package/package.json +4 -1
  3. package/src/{plugins/builtin/cloud → account}/auth-form.tsx +9 -9
  4. package/src/{plugins/builtin/cloud → account}/auth-model.ts +4 -4
  5. package/src/account/billing.ts +57 -0
  6. package/src/{plugins/builtin/cloud → account}/device-signin-dialog.tsx +18 -12
  7. package/src/{plugins/builtin/cloud → account}/device-signin.ts +2 -2
  8. package/src/{plugins/builtin/cloud → account}/email-auth-attempt.ts +2 -2
  9. package/src/account/session.ts +16 -0
  10. package/src/{plugins/builtin/cloud → account}/upgrade-dialog.tsx +16 -15
  11. package/src/{plugins/builtin/cloud → account}/upgrade-personalization.ts +7 -7
  12. package/src/{plugins/builtin/cloud → account}/upgrade-tickers.ts +6 -6
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/data.ts +24 -2
  15. package/src/api-client/earnings.ts +6 -0
  16. package/src/api-client/futures-curve.ts +56 -0
  17. package/src/api-client/index.ts +4 -1
  18. package/src/api-client/market-discovery.ts +10 -1
  19. package/src/api-client/paths.ts +7 -0
  20. package/src/api-client/research-activity.ts +15 -10
  21. package/src/api-client/types.ts +17 -1
  22. package/src/api-client/yield-curves.ts +8 -0
  23. package/src/app/global-shortcuts.ts +15 -1
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +7 -3
  25. package/src/architecture/core-builtin-allowlist.json +4 -13
  26. package/src/brokers/signed-in/sign-in-dialog.tsx +11 -5
  27. package/src/capabilities/chart-series.ts +11 -1
  28. package/src/cli/commands/crypto-hints.ts +7 -0
  29. package/src/cli/commands/fred-series.ts +61 -0
  30. package/src/cli/commands/market.ts +407 -104
  31. package/src/cli/commands/news-hints.ts +62 -0
  32. package/src/cli/commands/news-search.ts +53 -0
  33. package/src/cli/commands/options-leaps.ts +5 -2
  34. package/src/cli/commands/overview.ts +93 -20
  35. package/src/cli/commands/remote.ts +2 -1
  36. package/src/cli/commands/search.ts +82 -9
  37. package/src/cli/commands/system.ts +55 -8
  38. package/src/cli/commands/ticker.ts +88 -11
  39. package/src/cli/context.ts +45 -14
  40. package/src/cli/desktop-pane-shot.ts +59 -7
  41. package/src/cli/earnings-calendar.ts +71 -0
  42. package/src/cli/freshness.ts +18 -7
  43. package/src/cli/glossary.ts +429 -0
  44. package/src/cli/history-rows.ts +48 -7
  45. package/src/cli/index-roots.ts +7 -0
  46. package/src/cli/index.ts +21 -7
  47. package/src/cli/known-gaps.ts +27 -0
  48. package/src/cli/listing-arg.ts +417 -56
  49. package/src/cli/not-a-ticker.ts +12 -5
  50. package/src/cli/pane-functions/access-gate.ts +5 -0
  51. package/src/cli/pane-functions/capabilities.ts +3 -2
  52. package/src/cli/pane-functions/catalog.ts +35 -5
  53. package/src/cli/pane-functions/data.ts +12 -8
  54. package/src/cli/pane-functions/discovery.ts +1 -0
  55. package/src/cli/pane-functions/dom.ts +20 -6
  56. package/src/cli/pane-functions/freshness.ts +363 -43
  57. package/src/cli/pane-functions/function-help.ts +12 -5
  58. package/src/cli/pane-functions/headless.ts +45 -17
  59. package/src/cli/pane-functions/index.ts +63 -10
  60. package/src/cli/pane-functions/market-session.ts +22 -8
  61. package/src/cli/pane-functions/options.ts +63 -22
  62. package/src/cli/pane-functions/report-notices.ts +25 -7
  63. package/src/cli/pane-functions/report.ts +15 -8
  64. package/src/cli/pane-functions/resolver.ts +49 -12
  65. package/src/cli/pane-functions/screenshot.ts +110 -23
  66. package/src/cli/report-tables.ts +3 -0
  67. package/src/cli/result.ts +4 -1
  68. package/src/cli/staged-tickers.ts +52 -0
  69. package/src/cli/symbol-args.ts +9 -0
  70. package/src/cli/symbol-list.ts +172 -0
  71. package/src/cli/ticker-resolution.ts +223 -30
  72. package/src/cli/types.ts +5 -0
  73. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  74. package/src/components/chart/composite/composite-chart.tsx +40 -6
  75. package/src/components/chart/composite/legend.tsx +49 -2
  76. package/src/components/chart/composite/panel-surface.tsx +44 -17
  77. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  78. package/src/components/command-bar/panel/index.tsx +11 -3
  79. package/src/components/command-bar/panel/runtime.ts +3 -0
  80. package/src/components/command-bar/panel/types.ts +2 -0
  81. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  82. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  83. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  84. package/src/components/command-bar/workflow/tickers.ts +23 -4
  85. package/src/components/layout/detached-pane-shell.tsx +9 -0
  86. package/src/components/layout/floating-pane.tsx +3 -0
  87. package/src/components/layout/header.tsx +20 -5
  88. package/src/components/layout/pane/content.tsx +7 -13
  89. package/src/components/layout/pane/footer/index.tsx +6 -2
  90. package/src/components/layout/pane/footer/model.ts +35 -1
  91. package/src/components/layout/pane/header.tsx +24 -1
  92. package/src/components/layout/pane/index.tsx +12 -2
  93. package/src/components/layout/pane/sidebar.tsx +78 -10
  94. package/src/components/layout/shell/active-drag.ts +21 -5
  95. package/src/components/layout/shell/drag/index.ts +39 -9
  96. package/src/components/layout/shell/drag/live.ts +61 -51
  97. package/src/components/layout/shell/drag/runtime.ts +54 -12
  98. package/src/components/layout/shell/index.tsx +25 -7
  99. package/src/components/layout/shell/menu.ts +5 -0
  100. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  101. package/src/components/layout/shell/pane/layers.tsx +192 -45
  102. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  103. package/src/components/markdown-text.tsx +62 -20
  104. package/src/components/metric-treemap/heat-surface.tsx +10 -1
  105. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  106. package/src/components/onboarding/account-step/index.tsx +1 -1
  107. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  108. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  109. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  110. package/src/components/onboarding/onboarding-wizard.tsx +2 -2
  111. package/src/components/onboarding/use-onboarding-upgrade.ts +3 -3
  112. package/src/components/onboarding/wizard-account.ts +2 -2
  113. package/src/components/pane-template-wizard.tsx +9 -1
  114. package/src/components/price-selector-dialog.tsx +11 -2
  115. package/src/components/sign-in-gate.tsx +3 -3
  116. package/src/components/ticker/list-table-view.tsx +24 -1
  117. package/src/components/ui/action-row.tsx +2 -1
  118. package/src/components/ui/button.tsx +7 -1
  119. package/src/components/ui/choice-dialog.tsx +6 -4
  120. package/src/components/ui/confirm-dialog.tsx +28 -18
  121. package/src/components/ui/data-table/index.tsx +17 -6
  122. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  123. package/src/components/ui/data-table/types.ts +12 -0
  124. package/src/components/ui/dialog-actions.tsx +64 -0
  125. package/src/components/ui/display.tsx +17 -3
  126. package/src/components/ui/frame.tsx +35 -2
  127. package/src/components/ui/icon.tsx +2 -0
  128. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  129. package/src/data/cached-query.ts +2 -0
  130. package/src/data/config/store/normalize.ts +7 -2
  131. package/src/data/ticker-repository.ts +5 -0
  132. package/src/data/ticker-store.ts +15 -0
  133. package/src/docs/content.ts +215 -0
  134. package/src/docs/load.ts +139 -0
  135. package/src/docs/requests.ts +39 -0
  136. package/src/docs/snapshot.chunk.ts +8 -0
  137. package/src/docs/snapshot.json +7416 -0
  138. package/src/docs/topics.ts +216 -0
  139. package/src/i18n/es.ts +61 -4
  140. package/src/i18n/ja.ts +61 -4
  141. package/src/i18n/ko.ts +61 -4
  142. package/src/i18n/zh-cn.ts +61 -4
  143. package/src/i18n/zh-tw.ts +61 -4
  144. package/src/layout/desks.ts +1 -1
  145. package/src/layout/pane-manager/dock-tree.ts +18 -0
  146. package/src/layout/pane-manager/index.ts +1 -0
  147. package/src/market-data/coordinator/chart.ts +10 -4
  148. package/src/market-data/coordinator/index.ts +6 -3
  149. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  150. package/src/market-data/futures-contract.ts +151 -0
  151. package/src/market-data/hooks.tsx +10 -2
  152. package/src/market-data/market/format.ts +39 -21
  153. package/src/market-data/market/freshness.ts +81 -18
  154. package/src/market-data/market/status.ts +19 -23
  155. package/src/market-data/published-apac-sessions.ts +23 -1
  156. package/src/market-data/quotes/contributions.ts +3 -0
  157. package/src/market-data/quotes/metadata.ts +7 -24
  158. package/src/market-data/quotes/resolution.ts +2 -0
  159. package/src/market-data/quotes/screener-live-quotes.ts +5 -4
  160. package/src/market-data/result-types.ts +2 -0
  161. package/src/plugins/builtin/account-management/model.ts +1 -56
  162. package/src/plugins/builtin/account-management/pane.tsx +6 -4
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  164. package/src/plugins/builtin/alerts/cli.ts +313 -47
  165. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  166. package/src/plugins/builtin/alerts/events.ts +23 -2
  167. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  168. package/src/plugins/builtin/alerts/history.ts +2 -0
  169. package/src/plugins/builtin/alerts/index.tsx +41 -9
  170. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  171. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  172. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  173. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  174. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  175. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  176. package/src/plugins/builtin/alerts/targets.ts +92 -0
  177. package/src/plugins/builtin/analytics/index.tsx +4 -2
  178. package/src/plugins/builtin/analytics/risk-headless.ts +30 -12
  179. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  180. package/src/plugins/builtin/analytics/risk-model.ts +35 -5
  181. package/src/plugins/builtin/analytics/risk-pane.tsx +15 -8
  182. package/src/plugins/builtin/attention/index.tsx +2 -0
  183. package/src/plugins/builtin/attention/pane.tsx +3 -1
  184. package/src/plugins/builtin/attention/series.ts +26 -0
  185. package/src/plugins/builtin/awards/index.tsx +2 -0
  186. package/src/plugins/builtin/awards/pane.tsx +3 -1
  187. package/src/plugins/builtin/awards/series.ts +29 -0
  188. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  189. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  190. package/src/plugins/builtin/cds/client.ts +70 -13
  191. package/src/plugins/builtin/cds/headless.ts +116 -19
  192. package/src/plugins/builtin/cds/index.tsx +3 -1
  193. package/src/plugins/builtin/cds/model.ts +193 -31
  194. package/src/plugins/builtin/cds/pane.tsx +56 -14
  195. package/src/plugins/builtin/cds/series.ts +19 -0
  196. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  197. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  198. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  199. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  200. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  201. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  202. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  203. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  204. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  205. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  206. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  207. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  208. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  209. package/src/plugins/builtin/chat/controller/index.ts +2 -0
  210. package/src/plugins/builtin/cloud/askg/controller.ts +111 -19
  211. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  212. package/src/plugins/builtin/cloud/askg/manifest.ts +32 -7
  213. package/src/plugins/builtin/cloud/askg/model.ts +27 -8
  214. package/src/plugins/builtin/cloud/askg/protocol.ts +10 -1
  215. package/src/plugins/builtin/cloud/askg/tool-display.ts +3 -0
  216. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  217. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  218. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  219. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  220. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  221. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +3 -0
  222. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +77 -9
  223. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +6 -1
  224. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +9 -0
  225. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +35 -11
  226. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  227. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  228. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  229. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  230. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  231. package/src/plugins/builtin/company-attention/model.ts +26 -7
  232. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  233. package/src/plugins/builtin/company-attention/series.ts +52 -0
  234. package/src/plugins/builtin/company-kpis/pane.tsx +4 -2
  235. package/src/plugins/builtin/composite-plugins.ts +19 -13
  236. package/src/plugins/builtin/cot/index.tsx +3 -1
  237. package/src/plugins/builtin/cot/model.ts +40 -11
  238. package/src/plugins/builtin/cot/series.ts +27 -0
  239. package/src/plugins/builtin/credit-boards/index.tsx +2 -0
  240. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  241. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  242. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  243. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  244. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  245. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  246. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  247. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  248. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  249. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  250. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  251. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  252. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  253. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  254. package/src/plugins/builtin/dividend-yield/index.tsx +2 -0
  255. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  256. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  257. package/src/plugins/builtin/docs/index.tsx +68 -0
  258. package/src/plugins/builtin/docs/pane.tsx +426 -0
  259. package/src/plugins/builtin/docs/targets.ts +72 -0
  260. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  261. package/src/plugins/builtin/doe/index.tsx +2 -0
  262. package/src/plugins/builtin/doe/series.ts +62 -0
  263. package/src/plugins/builtin/earnings/client.ts +3 -2
  264. package/src/plugins/builtin/earnings/format.ts +1 -5
  265. package/src/plugins/builtin/earnings/headless.ts +58 -6
  266. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  267. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  268. package/src/plugins/builtin/earnings/history.tsx +7 -3
  269. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  270. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  271. package/src/plugins/builtin/econ/index.tsx +2 -0
  272. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  273. package/src/plugins/builtin/econ-statistics/headless.ts +19 -4
  274. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  276. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  277. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  278. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  279. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  280. package/src/plugins/builtin/futures/index.tsx +43 -3
  281. package/src/plugins/builtin/futures/model.ts +1 -1
  282. package/src/plugins/builtin/futures/returns.ts +1 -1
  283. package/src/plugins/builtin/futures/series.ts +13 -0
  284. package/src/plugins/builtin/futures/table.tsx +1 -1
  285. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  286. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  287. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  288. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  289. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  290. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  291. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  292. package/src/plugins/builtin/gpu/board.tsx +13 -10
  293. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  294. package/src/plugins/builtin/gpu/client.ts +19 -1
  295. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  296. package/src/plugins/builtin/gpu/headless.ts +56 -25
  297. package/src/plugins/builtin/gpu/history.tsx +3 -3
  298. package/src/plugins/builtin/gpu/index.tsx +2 -0
  299. package/src/plugins/builtin/gpu/model.ts +101 -16
  300. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  301. package/src/plugins/builtin/gpu/series.ts +38 -0
  302. package/src/plugins/builtin/help/function-card.tsx +25 -6
  303. package/src/plugins/builtin/help/index.tsx +17 -8
  304. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  305. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  306. package/src/plugins/builtin/holders/beneficial-view.tsx +55 -11
  307. package/src/plugins/builtin/holders/client.ts +22 -3
  308. package/src/plugins/builtin/holders/format.ts +18 -3
  309. package/src/plugins/builtin/holders/headless.ts +36 -133
  310. package/src/plugins/builtin/holders/pane.tsx +11 -4
  311. package/src/plugins/builtin/holders/report-header.ts +40 -5
  312. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  313. package/src/plugins/builtin/insider/client.ts +35 -20
  314. package/src/plugins/builtin/insider/headless.ts +41 -11
  315. package/src/plugins/builtin/insider/index.tsx +26 -9
  316. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  317. package/src/plugins/builtin/insider/issuer.ts +49 -0
  318. package/src/plugins/builtin/insider/model.ts +3 -0
  319. package/src/plugins/builtin/iv-history/client.ts +14 -4
  320. package/src/plugins/builtin/iv-history/headless.ts +7 -4
  321. package/src/plugins/builtin/iv-history/index.tsx +2 -0
  322. package/src/plugins/builtin/iv-history/model.ts +111 -7
  323. package/src/plugins/builtin/iv-history/screen-pane.tsx +5 -9
  324. package/src/plugins/builtin/iv-history/series.ts +29 -0
  325. package/src/plugins/builtin/jobs/headless.ts +28 -3
  326. package/src/plugins/builtin/jobs/index.tsx +3 -1
  327. package/src/plugins/builtin/jobs/series.ts +19 -0
  328. package/src/plugins/builtin/kelly-sizer/pane.tsx +2 -1
  329. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  330. package/src/plugins/builtin/macro-day/client.ts +44 -3
  331. package/src/plugins/builtin/macro-day/headless.ts +13 -10
  332. package/src/plugins/builtin/macro-day/model.ts +56 -10
  333. package/src/plugins/builtin/macro-day/pane.tsx +30 -20
  334. package/src/plugins/builtin/market-heatmap/model.ts +19 -11
  335. package/src/plugins/builtin/market-heatmap/portfolio.ts +2 -2
  336. package/src/plugins/builtin/market-movers/client.ts +7 -5
  337. package/src/plugins/builtin/market-movers/index.tsx +3 -2
  338. package/src/plugins/builtin/market-movers/model.ts +30 -0
  339. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  340. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  341. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  342. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  343. package/src/plugins/builtin/members/headless.ts +7 -2
  344. package/src/plugins/builtin/members/model.ts +31 -3
  345. package/src/plugins/builtin/members/pane.tsx +3 -6
  346. package/src/plugins/builtin/mna/headless.ts +15 -3
  347. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  348. package/src/plugins/builtin/money-markets/series.ts +41 -0
  349. package/src/plugins/builtin/notes/cli.ts +12 -7
  350. package/src/plugins/builtin/options/view.tsx +1 -1
  351. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  352. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  353. package/src/plugins/builtin/options-positioning/headless.ts +1 -0
  354. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  355. package/src/plugins/builtin/options-scenario/client.ts +2 -1
  356. package/src/plugins/builtin/options-scenario/headless.ts +1 -0
  357. package/src/plugins/builtin/options-scenario/hedge.ts +2 -1
  358. package/src/plugins/builtin/perps/client.ts +40 -6
  359. package/src/plugins/builtin/perps/equity-row.tsx +33 -11
  360. package/src/plugins/builtin/perps/freshness.ts +66 -0
  361. package/src/plugins/builtin/perps/headless.ts +45 -30
  362. package/src/plugins/builtin/perps/index.tsx +18 -1
  363. package/src/plugins/builtin/perps/model.ts +20 -5
  364. package/src/plugins/builtin/perps/pane.tsx +76 -20
  365. package/src/plugins/builtin/perps/series.ts +76 -0
  366. package/src/plugins/builtin/plugin-marketplace/pane.tsx +67 -61
  367. package/src/plugins/builtin/plugin-module.ts +3 -0
  368. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  369. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  370. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  371. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +331 -65
  372. package/src/plugins/builtin/portfolio-list/cli/render.ts +200 -31
  373. package/src/plugins/builtin/portfolio-list/cli/shared.ts +24 -0
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  375. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  376. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  377. package/src/plugins/builtin/portfolio-list/headless.ts +34 -4
  378. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  379. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  381. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  382. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  383. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  384. package/src/plugins/builtin/power/client.ts +16 -0
  385. package/src/plugins/builtin/power/evidence.ts +6 -2
  386. package/src/plugins/builtin/power/headless.ts +71 -7
  387. package/src/plugins/builtin/power/index.tsx +4 -1
  388. package/src/plugins/builtin/power/model.ts +72 -1
  389. package/src/plugins/builtin/power/pane.tsx +32 -9
  390. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  391. package/src/plugins/builtin/realized-vol/headless.ts +5 -0
  392. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -2
  393. package/src/plugins/builtin/research/event-model.ts +68 -24
  394. package/src/plugins/builtin/research/events-headless.ts +2 -1
  395. package/src/plugins/builtin/research/headless.ts +2 -1
  396. package/src/plugins/builtin/seasonality/client.ts +15 -7
  397. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  398. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  399. package/src/plugins/builtin/seasonality/model.ts +119 -0
  400. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  401. package/src/plugins/builtin/series-inventory.ts +91 -0
  402. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  403. package/src/plugins/builtin/shared/command-tab.ts +30 -12
  404. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  405. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  406. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  407. package/src/plugins/builtin/shared/open-description.ts +29 -0
  408. package/src/plugins/builtin/shared/report-freshness.ts +9 -0
  409. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  410. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  411. package/src/plugins/builtin/short-interest/index.tsx +2 -0
  412. package/src/plugins/builtin/short-interest/series.ts +36 -0
  413. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  414. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  415. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  416. package/src/plugins/builtin/short-volume/index.tsx +2 -0
  417. package/src/plugins/builtin/short-volume/series.ts +23 -0
  418. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  419. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  420. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  421. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  422. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  423. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  424. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  425. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  426. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  427. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  428. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  429. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  430. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  431. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  432. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  433. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  434. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  435. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  436. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  437. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  438. package/src/plugins/builtin/treasury-auctions/headless.ts +16 -2
  439. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  440. package/src/plugins/builtin/treasury-auctions/model.ts +8 -0
  441. package/src/plugins/builtin/vol-surface/headless.ts +1 -0
  442. package/src/plugins/builtin/volatility/index.tsx +2 -0
  443. package/src/plugins/builtin/volatility/series.ts +84 -0
  444. package/src/plugins/builtin/world-venue-map/geo-series.ts +26 -1
  445. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  446. package/src/plugins/builtin/world-venue-map/index.ts +2 -1
  447. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  448. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  449. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  450. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  451. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  452. package/src/plugins/builtin/yield-curve/index.tsx +3 -1
  453. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  454. package/src/plugins/catalog-browser.ts +2 -1
  455. package/src/plugins/registry/index.ts +89 -4
  456. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  457. package/src/portfolio/position-value.ts +40 -0
  458. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  459. package/src/public/time-series.ts +17 -0
  460. package/src/react/auto-refresh.ts +13 -3
  461. package/src/remote/app-host.tsx +20 -4
  462. package/src/remote/confirm-change.tsx +85 -0
  463. package/src/remote/controller-utils.ts +8 -1
  464. package/src/remote/controller.ts +123 -6
  465. package/src/remote/pane-screenshot.ts +47 -0
  466. package/src/remote/schema.ts +62 -0
  467. package/src/remote/types.ts +30 -0
  468. package/src/remote/watchlist.ts +306 -0
  469. package/src/renderers/browser/main.tsx +3 -2
  470. package/src/renderers/cloudflare/worker.ts +1 -1
  471. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  472. package/src/renderers/dom/data-table/index.tsx +33 -6
  473. package/src/renderers/dom/data-table/row.tsx +27 -1
  474. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  475. package/src/renderers/dom/desktop/controls.tsx +30 -23
  476. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  477. package/src/renderers/dom/desktop/icons.tsx +7 -0
  478. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  479. package/src/renderers/dom/host/box.tsx +3 -0
  480. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  481. package/src/renderers/dom/host/live-frame.ts +6 -2
  482. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  483. package/src/renderers/dom/host/size-observer.ts +26 -0
  484. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  485. package/src/renderers/dom/host/text.tsx +1 -1
  486. package/src/renderers/dom/styles.css +61 -0
  487. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +17 -2
  488. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +19 -11
  489. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  490. package/src/renderers/electrobun/view/main.tsx +2 -0
  491. package/src/series/capability.ts +75 -0
  492. package/src/series/csv.ts +160 -0
  493. package/src/series/files.ts +107 -0
  494. package/src/series/registry.ts +198 -0
  495. package/src/series/resolve.ts +107 -0
  496. package/src/series/standalone.ts +48 -0
  497. package/src/series/types.ts +145 -0
  498. package/src/series/validate.ts +171 -0
  499. package/src/sources/gloomberb-cloud/index.ts +7 -7
  500. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  501. package/src/sources/history-result.ts +1 -0
  502. package/src/sources/listing-symbols.ts +4 -1
  503. package/src/sources/provider-router/batches.ts +4 -1
  504. package/src/sources/provider-router/cache.ts +4 -2
  505. package/src/sources/provider-router/cached-routes.ts +1 -1
  506. package/src/sources/provider-router/financial-routes.ts +1 -0
  507. package/src/sources/provider-router/financials.ts +13 -0
  508. package/src/sources/provider-router/history.ts +43 -25
  509. package/src/sources/provider-router/search.ts +1 -0
  510. package/src/sync/core-contributors.ts +1 -0
  511. package/src/tickers/company-name.ts +75 -0
  512. package/src/tickers/instrument-kind.ts +25 -0
  513. package/src/tickers/open-target.ts +3 -2
  514. package/src/tickers/search/index.ts +45 -7
  515. package/src/tickers/search/ranking.ts +33 -17
  516. package/src/tickers/search/types.ts +7 -1
  517. package/src/tickers/us-listing.ts +34 -0
  518. package/src/time-series/hooks.ts +9 -0
  519. package/src/time-series/resolve.ts +95 -16
  520. package/src/time-series/types.ts +4 -2
  521. package/src/time-series/use-chart-resolution.ts +32 -0
  522. package/src/types/config.ts +6 -0
  523. package/src/types/data-provider.ts +5 -0
  524. package/src/types/financials.ts +52 -1
  525. package/src/types/headless.ts +61 -0
  526. package/src/types/plugin.ts +19 -0
  527. package/src/types/price-history.ts +7 -0
  528. package/src/types/ticker.ts +14 -0
  529. package/src/ui/host.tsx +18 -0
  530. package/src/ui/live-resize.ts +81 -0
  531. package/src/ui/live-stretch.ts +121 -0
  532. package/src/ui/measured-cells.ts +58 -0
  533. package/src/ui/pane-fill.ts +11 -0
  534. package/src/utils/basis-points.ts +17 -0
  535. package/src/utils/cli-output.ts +7 -1
  536. package/src/utils/concurrency.ts +25 -0
  537. package/src/utils/crypto-pair.ts +20 -0
  538. package/src/utils/currency-pair.ts +19 -0
  539. package/src/utils/decimal-comma.ts +19 -0
  540. package/src/utils/dom-screenshot-css.ts +265 -0
  541. package/src/utils/dom-screenshot-rasters.ts +276 -0
  542. package/src/utils/dom-screenshot.ts +437 -52
  543. package/src/utils/exchange-rate-snapshot.ts +6 -2
  544. package/src/utils/exchanges.ts +107 -1
  545. package/src/utils/format.ts +16 -3
  546. package/src/utils/fundamentals.ts +66 -0
  547. package/src/utils/futures-generic.ts +6 -12
  548. package/src/utils/fx-market-hours.ts +15 -5
  549. package/src/utils/price-history.ts +99 -42
  550. package/src/utils/sec-eps-basis.ts +24 -11
  551. package/src/utils/sec.ts +61 -7
  552. package/src/utils/utc-time.ts +51 -4
  553. package/src/plugins/builtin/fx-matrix/freshness.ts +0 -65
  554. /package/src/{plugins/builtin/cloud → tickers}/starter-symbols.ts +0 -0
@@ -10,7 +10,7 @@ import type {
10
10
  Quote,
11
11
  TickerFinancials,
12
12
  } from "../../types/financials";
13
- import { currencyMinorDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
13
+ import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
14
14
  import {
15
15
  EXTENDED_SESSION_LABELS,
16
16
  getExtendedSessionDisplay,
@@ -21,7 +21,7 @@ import {
21
21
  } from "../../market-data/market/status";
22
22
  import { formatCompact, formatDistributionAmount, formatPercent, formatPercentRaw } from "../../utils/format";
23
23
  import { withCliServices, withMarketData } from "../context";
24
- import { isoDate, parsePositiveInt, rejectExtraArgs, requireOneArg, takeFlag, takeOption } from "./command-utils";
24
+ import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
25
25
  import type { BuiltinCliCommandDef } from "../command-options";
26
26
  import { CLI_COMMAND_GROUPS, TABLE_SECTION_OPTION } from "../help";
27
27
  import {
@@ -30,7 +30,7 @@ import {
30
30
  formatFractionPercentCell,
31
31
  formatPriceRange,
32
32
  } from "../helpers";
33
- import { cliStyles, renderStats } from "../../utils/cli-output";
33
+ import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
34
34
  import { formatPerShareNumber } from "../../utils/reported-money";
35
35
  import {
36
36
  analystTargetCurrency,
@@ -43,7 +43,9 @@ import {
43
43
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
44
44
  import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
45
45
  import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
46
- import { fundamentalsReportTables, renderFundamentalsReport } from "./ticker";
46
+ import { cliFuturesContract, futuresContractMetrics, fundamentalsReportTables, renderFundamentalsReport } from "./ticker";
47
+ import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
48
+ import { futuresSettleText, isFuturesInstrument } from "../../market-data/futures-contract";
47
49
  import {
48
50
  chainHasExpiry,
49
51
  chainWithModelFigures,
@@ -69,6 +71,7 @@ import {
69
71
  import {
70
72
  historyFacts,
71
73
  historyFlagNote,
74
+ historyGapNote,
72
75
  historyAllRangeYears,
73
76
  historyIntervalsByRange,
74
77
  historyNotes,
@@ -77,10 +80,13 @@ import {
77
80
  historyUnit,
78
81
  } from "../history-rows";
79
82
  import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
83
+ import { bareFuturesCode } from "../../market-data/futures-board";
84
+ import { newsEmptyMessage } from "./news-hints";
85
+ import { newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, searchNewsStories } from "./news-search";
80
86
  import { formatUtcTime } from "../../utils/utc-time";
81
87
  import { isFiniteNumber } from "../../utils/guards";
82
88
  import { exportedFundamentals } from "../../utils/price-earnings";
83
- import { fundamentalsFreshness, quotesFreshness, rowsFreshness } from "../freshness";
89
+ import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness } from "../freshness";
84
90
  import {
85
91
  barHistoryFreshness,
86
92
  barResolutionFromDates,
@@ -92,13 +98,19 @@ import {
92
98
  import {
93
99
  bareListingNote,
94
100
  EXCHANGE_OPTION,
101
+ futuresTryLine,
95
102
  isNoProviderError,
96
103
  listingHeading,
97
104
  listingIdentity,
98
105
  listingTitle,
99
106
  listingVenues,
107
+ listingChoiceNotes,
108
+ listingReadNotes,
109
+ loadBareVenues,
110
+ type ListingVenue,
100
111
  loadForListing,
101
112
  loadListingQuote,
113
+ noLongerTradingNote,
102
114
  notTradedMessage,
103
115
  quoteBareListing,
104
116
  requireCliListing,
@@ -106,18 +118,22 @@ import {
106
118
  type ListingDataRequest,
107
119
  type ListingIdentity,
108
120
  } from "../listing-arg";
109
- import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "../index-roots";
121
+ import { indexRootAlsoLine, indexRootFor, indexRootTryLine, isYieldIndexSymbol } from "../index-roots";
122
+ import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
123
+ import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
110
124
  import { providerMissReason } from "../../sources/provider-errors";
111
125
  import { isNotATickerMessage } from "../not-a-ticker";
126
+ import { splitSymbolArgs } from "../symbol-args";
112
127
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
113
128
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
114
129
  import { isUsListingExchange } from "../../utils/exchanges";
115
- import { nonUsSecListingVenue } from "../../utils/sec";
130
+ import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
116
131
  import type { MarketContext } from "../types";
117
132
  import { windowRows } from "../row-window";
118
133
  import { crossRate, describeFxRate, isUsableRate, parseFxRequest } from "../fx-pair";
119
134
  import {
120
135
  holderListFacts,
136
+ holderListPeriod,
121
137
  holderShareBasisMarker,
122
138
  holderShareBasisNote,
123
139
  holderValueBasis,
@@ -125,15 +141,27 @@ import {
125
141
  nonUsHolderCaveat,
126
142
  sharedReportDate,
127
143
  } from "../../plugins/builtin/holders/report-header";
128
- import { formatHolderOwnershipPercent, holderValueCurrency } from "../../plugins/builtin/holders/format";
144
+ import { formatPercentHeld, holderStakeMarketCap, holderValueCurrency } from "../../plugins/builtin/holders/format";
145
+ import {
146
+ holderReportColumns,
147
+ holderReportDates,
148
+ holderReportNotices,
149
+ holderReportRows,
150
+ holderReportUnits,
151
+ unvaluedHoldersNote,
152
+ } from "../../plugins/builtin/holders/report-rows";
129
153
  import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
154
+ import { classSharesToday } from "../../plugins/builtin/holders/client";
130
155
  import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
156
+ import { loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
157
+ import { insiderEmptyNotice } from "../../plugins/builtin/insider/issuer";
131
158
  import {
132
159
  BENEFICIAL_REPORT_COLUMNS,
133
160
  beneficialCoverageNotices,
134
161
  beneficialListComplete,
135
162
  beneficialListFacts,
136
163
  beneficialListUnreadable,
164
+ beneficialReportLegend,
137
165
  beneficialRouteForm,
138
166
  buildBeneficialReportRows,
139
167
  HOLDER_FORMS,
@@ -144,7 +172,13 @@ import {
144
172
  const VALID_RANGES = new Set<TimeRange>(TIME_RANGES);
145
173
  const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
146
174
 
147
- type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & { error: string | null };
175
+ type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
176
+ error: string | null;
177
+ /** The listing the row is, named from its saved ticker when the quote names nothing. */
178
+ listing?: { name: string | null };
179
+ /** A futures row's contract, terms and latest confirmed settlement, as `ticker` reads them. */
180
+ futuresContract?: FuturesContractTermsPayload;
181
+ };
148
182
  type FinancialsCliData = TickerFinancials & {
149
183
  symbol: string;
150
184
  exchange: string;
@@ -161,11 +195,9 @@ function parseNewsFeed(value: string | undefined): NewsQuery["feed"] | undefined
161
195
  return value && VALID_NEWS_FEEDS.has(value as NewsFeed) ? value as NewsQuery["feed"] : undefined;
162
196
  }
163
197
 
198
+ /** The symbols of a command's arguments, one each however they were separated: spaces, commas or pasted lines. */
164
199
  function normalizeSymbols(args: string[]): string[] {
165
- return args
166
- .flatMap((arg) => arg.split(","))
167
- .map((symbol) => symbol.trim().toUpperCase())
168
- .filter(Boolean);
200
+ return splitSymbolArgs(args).map((symbol) => symbol.toUpperCase());
169
201
  }
170
202
 
171
203
  /** What JSON metadata says about the listing a command resolved. */
@@ -186,10 +218,15 @@ const QUOTE_LEAD_COLUMNS = [
186
218
  row.error && !value
187
219
  // A typo is not an outage: say what is wrong with the symbol itself.
188
220
  ? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
189
- : String(value ?? "")
221
+ : yieldLevelCell(value, row)
190
222
  ),
191
223
  },
192
- { key: "changePercent", header: "Chg%", align: "right" as const, format: formatChangePercentCell },
224
+ {
225
+ key: "changePercent",
226
+ header: "Chg%",
227
+ align: "right" as const,
228
+ format: (value: unknown, row: QuoteRow) => row.yieldIndex ? yieldChangeCell(row) : formatChangePercentCell(value),
229
+ },
193
230
  // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
194
231
  extendedColumn("PRE"),
195
232
  extendedColumn("POST"),
@@ -198,6 +235,17 @@ const QUOTE_LEAD_COLUMNS = [
198
235
 
199
236
  type QuoteRow = ReturnType<typeof quoteRows>[number];
200
237
 
238
+ /** A Cboe yield index level is a yield: `5.24%`, not a price. */
239
+ function yieldLevelCell(value: unknown, row: QuoteRow): string {
240
+ const text = String(value ?? "");
241
+ return row.yieldIndex && text ? `${text}%` : text;
242
+ }
243
+
244
+ /** A yield index moves in basis points: `+1.3bp`, from the real previous close, not a percent of a percent. */
245
+ function yieldChangeCell(row: QuoteRow): string {
246
+ return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
247
+ }
248
+
201
249
  /** The pre-market or after-hours print and its move from the regular close, in its own column. */
202
250
  function extendedColumn(session: ExtendedSession) {
203
251
  return {
@@ -240,7 +288,7 @@ function quoteColumns() {
240
288
  function compareColumns() {
241
289
  return [
242
290
  ...QUOTE_LEAD_COLUMNS,
243
- { key: "previousClose", header: "Prev Close", align: "right" as const },
291
+ { key: "previousClose", header: "Prev Close", align: "right" as const, format: yieldLevelCell },
244
292
  // A narrow terminal drops these before it cuts the names short.
245
293
  { key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
246
294
  { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
@@ -267,13 +315,14 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
267
315
  // pre-market or after-hours print is its own column, measured from that session's close.
268
316
  const display = getRegularSessionDisplay(quote);
269
317
  const extended = getExtendedSessionDisplay(quote);
318
+ const yieldIndex = isYieldIndexSymbol(result.target.symbol);
270
319
  // An index level is in points, not in the currency its members trade in.
271
320
  const indexPoints = quote?.instrumentType?.trim().toUpperCase() === "INDEX";
272
321
  // Pad to two decimals so a column lines up, but never past the currency's minor unit (JPY has none).
273
322
  // Points have no minor unit, so a yen-listed index still pads to two.
274
323
  const options = {
275
324
  ...quoteFormatOptions(quote),
276
- minimumFractionDigits: indexPoints ? 2 : Math.min(2, currencyMinorDigits(quote?.currency)),
325
+ minimumFractionDigits: indexPoints ? 2 : currencyPriceMinDigits(quote?.currency),
277
326
  };
278
327
  const price = (value: number | undefined) => (
279
328
  quote && value != null
@@ -282,13 +331,16 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
282
331
  );
283
332
  return {
284
333
  symbol: result.target.symbol,
285
- name: quote?.name ?? "",
334
+ name: quote?.name || result.listing?.name || "",
286
335
  price: price(display?.price),
287
336
  rawPrice: display?.price ?? null,
288
337
  priceBasis: quote?.priceBasis ?? null,
289
338
  instrumentType: quote?.instrumentType ?? null,
290
339
  change: display?.change ?? null,
291
340
  changePercent: roundedPercent(display?.changePercent),
341
+ // A Cboe yield index is read as a yield in percent and moves in basis points.
342
+ yieldIndex,
343
+ changeBasisPoints: yieldIndex && display?.change != null ? toBasisPoints(display.change, 1) : null,
292
344
  extendedSession: extended?.session ?? null,
293
345
  extendedPrice: price(extended?.price),
294
346
  rawExtendedPrice: extended?.price ?? null,
@@ -349,22 +401,6 @@ function filingRows(filings: SecFilingItem[]) {
349
401
  }));
350
402
  }
351
403
 
352
- function holderRows(data: HolderData, ownerTypes?: Set<string>) {
353
- const holders = ownerTypes
354
- ? data.holders.filter((holder) => ownerTypes.has(holder.ownerType))
355
- : data.holders;
356
- return holders.map((holder) => ({
357
- type: holder.ownerType,
358
- name: holder.name,
359
- reportDate: holder.reportDate ?? "",
360
- shares: holder.shares ?? null,
361
- value: holder.value ?? null,
362
- percentHeld: holder.percentHeld ?? null,
363
- changeShares: holder.changeShares ?? null,
364
- shareBasis: holder.shareBasis ?? null,
365
- }));
366
- }
367
-
368
404
  function analystSummary(data: AnalystResearchData): string {
369
405
  const target = data.priceTarget;
370
406
  const currency = analystTargetCurrency(data);
@@ -459,6 +495,8 @@ function earningsRows(events: EarningsEvent[]) {
459
495
  symbol: event.symbol,
460
496
  name: event.name,
461
497
  date: isoDate(event.earningsDate).slice(0, 10),
498
+ // Whether the company announced the date or it is projected from past reports.
499
+ status: earningsDateLabel(event),
462
500
  timing: event.timing,
463
501
  epsEstimate: event.epsEstimate,
464
502
  epsActual: event.epsActual,
@@ -467,6 +505,9 @@ function earningsRows(events: EarningsEvent[]) {
467
505
  }));
468
506
  }
469
507
 
508
+ /** Symbols at most whose venues a command looks up to name the company each bare one went to. */
509
+ const MAX_CHOICE_LOOKUPS = 10;
510
+
470
511
  async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
471
512
  const args = [...rawArgs];
472
513
  const exchange = takeOption(args, "--exchange") ?? "";
@@ -479,38 +520,87 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
479
520
  symbol, exchange, market, ctx, { ownExchangeWins: symbols.length > 1 },
480
521
  )));
481
522
  const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
523
+ // A bare symbol's venues load beside its quote, to name the company it was read as when several list it.
524
+ // Each costs a search, so a long list (a portfolio check, not a lookup) names none.
525
+ const venues = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
526
+ : listings.map((listing) => [listing, loadBareVenues(listing, market)] as const));
482
527
  const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh });
483
528
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
484
529
  const request = { command: commandName, noun: "quote" };
485
- const answers = await Promise.all(results.map((result, index) => answerQuote(result, listingOf(result, index), market, request, ctx.cliOptions.refresh)));
530
+ const answers = await Promise.all(results.map((result, index) => {
531
+ const listing = listingOf(result, index);
532
+ return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null);
533
+ }));
486
534
  const failed = answers[0]?.failure;
487
535
  if (listings.length === 1 && failed) ctx.fail(failed.message, failed.details);
536
+ // A futures quote adds its dated settlement and contract terms, as `ticker` prints them; `compare` stays a table.
537
+ const contracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
538
+ ? cliFuturesContract(result.quote.symbol || result.target.symbol, result.quote.instrumentType)
539
+ : null));
488
540
  // Each row names its listing by key (SAN:EPA) and company, so the table needs no line above it.
489
- const data = answers.map(({ result, listing, failure }) => ({
541
+ const data = answers.map(({ result, listing, failure }, index) => ({
490
542
  target: result.target,
491
543
  listing: listingMetadata(listingIdentity(listing, result.quote)),
492
544
  // The figures the table shows, beside the quote as the source sent it.
493
545
  ...getQuoteSessionFields(result.quote),
494
546
  quote: result.quote,
547
+ ...contracts[index] ? { futuresContract: contracts[index] } : {},
495
548
  error: failure?.oneLine ?? errorMessage(result.error),
549
+ ...yieldIndexFields(result),
496
550
  }));
551
+ const now = Date.now();
497
552
  const notes = [
498
553
  // Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
499
554
  ...answers.flatMap((answer) => answer.notes),
555
+ // A row that stopped trading long ago reads as a quote that is merely stale.
556
+ ...data.flatMap((row) => row.quote
557
+ ? noLongerTradingNote(`${row.target.symbol}${row.listing.name ? ` (${row.listing.name})` : ""}`, row.quote, now) ?? []
558
+ : []),
500
559
  // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
501
560
  ...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
502
561
  ];
562
+ const columns = commandName === "compare" ? compareColumns() : quoteColumns();
563
+ // Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
564
+ const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
503
565
  ctx.printResult({
504
566
  data,
505
567
  warnings: notes.length > 0 ? notes : undefined,
506
568
  freshness: quotesFreshness(data.map((row) => row.quote)),
507
569
  }, {
508
570
  rows: quoteRows,
509
- columns: commandName === "compare" ? compareColumns() : quoteColumns(),
571
+ details: quoteFuturesContractText,
572
+ columns,
573
+ ...(yieldsOnly ? { textColumns: columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column) } : {}),
510
574
  });
511
575
  });
512
576
  }
513
577
 
578
+ /**
579
+ * Under the table, each futures row's Settle line and Contract block, worded
580
+ * as `ticker` words them. Several symbols name theirs in the block's title.
581
+ */
582
+ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
583
+ return data.flatMap((row) => {
584
+ const terms = row.futuresContract;
585
+ if (!terms || !row.quote) return [];
586
+ const settle: [string, string] = ["Settle", futuresSettleText(terms, row.quote)];
587
+ const contract = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
588
+ if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...contract])}`];
589
+ return [[renderStats([settle]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
590
+ }).join("\n\n");
591
+ }
592
+
593
+ /**
594
+ * What a Cboe yield index adds to its JSON row: the level is a yield in percent
595
+ * and its change since the previous close is in basis points (the quote's own
596
+ * `change` is the same move in percentage points).
597
+ */
598
+ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeBasisPoints?: number } {
599
+ if (!isYieldIndexSymbol(result.target.symbol)) return {};
600
+ const change = getRegularSessionDisplay(result.quote)?.change;
601
+ return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
602
+ }
603
+
514
604
  interface QuoteAnswer {
515
605
  result: QuoteBatchResult;
516
606
  /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
@@ -524,8 +614,10 @@ interface QuoteAnswer {
524
614
  * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
525
615
  * is not listed on says so. A bare symbol the data service read on its home
526
616
  * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
527
- * quoted on its listings (`quoteBareListing`), else names them; a bare index
528
- * root (VIX) also names its index (^VIX). Venues are only looked up for a
617
+ * quoted on its listings (`quoteBareListing`), else names them, or asks for an
618
+ * exchange when several companies list it (2222); a bare index root (VIX) also
619
+ * names its index (^VIX), and a futures board code a security keeps (GC) its
620
+ * future (GC=F). Venues are only looked up for a
529
621
  * symbol with no quote, so a request that succeeds costs no search.
530
622
  */
531
623
  async function answerQuote(
@@ -534,12 +626,18 @@ async function answerQuote(
534
626
  market: MarketContext,
535
627
  request: ListingDataRequest,
536
628
  refresh: boolean,
629
+ venues: Promise<ListingVenue[]> | null,
537
630
  ): Promise<QuoteAnswer> {
538
631
  const root = listing.exchange ? null : indexRootFor(listing.symbol);
539
632
  // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
540
633
  const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
541
634
  ? [indexRootAlsoLine(root, request.command)] : [];
542
- if (result.quote) return { result, listing, notes: [bareListingNote(listing, result.quote) ?? [], also(result.quote)].flat() };
635
+ if (result.quote) {
636
+ // "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
637
+ const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
638
+ const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
639
+ return { result, listing, notes: [...choice, venue ?? [], also(result.quote)].flat() };
640
+ }
543
641
  const notTraded = await notTradedMessage(listing, market);
544
642
  if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
545
643
  if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
@@ -551,14 +649,18 @@ async function answerQuote(
551
649
  notes: [other.note, ...also(other.quote)],
552
650
  };
553
651
  }
554
- const indexLine = root ? indexRootTryLine(root, request.command) : null;
555
- if (!other && !indexLine) return { result, listing, notes: [] };
652
+ // Other listings already name a futures board code's future (GC=F); with none, it is named here.
653
+ const tryLines = [
654
+ root ? indexRootTryLine(root, request.command) : null,
655
+ other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
656
+ ].filter((line): line is string => !!line);
657
+ if (!other && tryLines.length === 0) return { result, listing, notes: [] };
556
658
  const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
557
- const details = [other?.details, indexLine].filter(Boolean).join("\n");
659
+ const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
558
660
  const oneLine = [
559
661
  message,
560
- other ? `Other listings: ${other.others.join(", ")}.` : "",
561
- indexLine ?? "",
662
+ other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
663
+ ...tryLines,
562
664
  ].filter(Boolean).join(" ");
563
665
  return { result, listing, failure: { message, details, oneLine }, notes: [] };
564
666
  }
@@ -581,6 +683,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
581
683
  const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
582
684
  await withMarketData(ctx, async (market) => {
583
685
  const listing = await requireCliListing(raw, requestedExchange, market, ctx);
686
+ const venues = loadBareVenues(listing, market);
584
687
  const { symbol, exchange } = listing.request;
585
688
  const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
586
689
  const load = () => market.dataProvider.getPriceHistoryWithMetadata
@@ -596,7 +699,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
596
699
  const identity = listingIdentity(listing, quote);
597
700
  // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
598
701
  const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
599
- const unit = historyUnit(listing.symbol, listed ?? quote);
702
+ const unit = historyUnit(listing.key, listed ?? quote);
600
703
  const data = historyRows(points, resolution, unit.currency);
601
704
  const interval = data[0]?.interval ?? null;
602
705
  const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
@@ -608,8 +711,16 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
608
711
  ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
609
712
  observedKey: "date",
610
713
  oldest: null,
714
+ // Bars stop arriving on the listing's own trading days, not New York's.
715
+ cadenceExchange: quote?.listingExchangeName || exchange || undefined,
716
+ });
717
+ const gaps = historyGapNote(data, {
718
+ listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
611
719
  });
612
- const notes = [...historyNotes(range, interval, data[0]?.date), historyFlagNote(data) ?? []].flat();
720
+ const notes = [
721
+ ...await listingChoiceNotes(listing, market, quote, venues),
722
+ ...historyNotes(range, interval, data[0]?.date), gaps ?? [], historyFlagNote(data) ?? [],
723
+ ].flat();
613
724
  const priceColumns: CliResultColumn[] = [
614
725
  intraday
615
726
  ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
@@ -687,10 +798,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
687
798
  };
688
799
  await withMarketData(ctx, async (market) => {
689
800
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
801
+ const venues = loadBareVenues(listing, market);
690
802
  const financials = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
691
803
  listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
692
804
  ), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
693
805
  const identity = listingIdentity(listing, financials.quote);
806
+ const choice = await listingChoiceNotes(listing, market, financials.quote, venues);
694
807
  const data: FinancialsCliData = {
695
808
  symbol: listing.key,
696
809
  exchange: identity.exchange,
@@ -703,6 +816,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
703
816
  // JSON reads a multiple over a loss as null with its reason, never as a number.
704
817
  data: { ...data, fundamentals: exportedFundamentals(data.fundamentals) },
705
818
  freshness,
819
+ ...(choice.length > 0 ? { warnings: choice } : {}),
706
820
  }, {
707
821
  text: () => renderFundamentalsReport(data, view),
708
822
  ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
@@ -731,6 +845,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
731
845
  profile: financials.profile,
732
846
  },
733
847
  freshness,
848
+ ...(choice.length > 0 ? { warnings: choice } : {}),
734
849
  }, {
735
850
  heading: listingHeading(identity),
736
851
  rows: () => rows,
@@ -740,16 +855,55 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
740
855
  });
741
856
  }
742
857
 
858
+ const NEWS_COLUMNS = [
859
+ // UTC with the zone named, as every CLI time prints, rather than the host zone unlabeled.
860
+ {
861
+ key: "publishedAt",
862
+ header: "Published",
863
+ format: (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "",
864
+ },
865
+ { key: "source", header: "Source", maxWidth: 20 },
866
+ { key: "title", header: "Title" },
867
+ { key: "tickers", header: "Tickers", maxWidth: 16 },
868
+ { key: "url", header: "URL", optional: true },
869
+ ];
870
+
871
+ /** Stories matching text, printed as a symbol's news is, under a line saying what they match. */
872
+ async function printNewsSearch(
873
+ text: string,
874
+ articles: NewsArticle[],
875
+ { reason, empty }: { reason?: string; empty?: string },
876
+ ctx: Parameters<CliCommandDef["execute"]>[1],
877
+ ) {
878
+ ctx.printResult({
879
+ data: articles,
880
+ metadata: { ticker: null, search: text, feed: null },
881
+ freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
882
+ }, {
883
+ heading: newsSearchHeading(text, reason),
884
+ rows: newsRows,
885
+ columns: NEWS_COLUMNS,
886
+ empty: empty ?? `No stories match "${text}".`,
887
+ });
888
+ }
889
+
743
890
  async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
744
891
  const args = [...rawArgs];
745
892
  const feed = parseNewsFeed(takeOption(args, "--feed"));
746
893
  const exchangeOption = takeOption(args, "--exchange");
894
+ // Stories come newest first, so the newest n are the first n either way.
895
+ const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
896
+ const text = newsSearchText(args);
897
+ if (text != null) {
898
+ if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloomberb news <query>");
899
+ const problem = newsSearchTextProblem(text);
900
+ if (problem) ctx.fail(problem);
901
+ await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
902
+ return;
903
+ }
747
904
  if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloomberb news <symbol> --exchange <code>");
748
- rejectExtraArgs(args, 1, { usage: "news [symbol] [--feed <feed>]", takes: "one symbol at most", advice: "Run it once per symbol." }, ctx);
749
905
  await withMarketData(ctx, async (market) => {
750
906
  const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
751
- // Stories come newest first, so the newest n are the first n either way.
752
- const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
753
907
  const loadNews = () => market.dataProvider.getNews({
754
908
  feed: feed ?? (listing ? "ticker" : "latest"),
755
909
  // Still set for news plugins that read the deprecated scope.
@@ -763,25 +917,33 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
763
917
  listing ? loadListingQuote(market.dataProvider, listing) : null,
764
918
  ]);
765
919
  const identity = listing ? listingIdentity(listing, quote) : null;
920
+ const resolved = quote !== null || identity?.name != null;
921
+ // A bare word no listing answers to is text, and so is a futures board code (TTF), which resolves
922
+ // to its future: news is filed under company tickers, so the stories that mention the word are the news.
923
+ const word = listing && !listing.exchange && !feed && articles.length === 0 ? newsFallbackText(args[0]!) : null;
924
+ const futuresCode = !!word && !!bareFuturesCode(word) && isFuturesInstrument(listing!.symbol, quote?.instrumentType);
925
+ const reason = !resolved ? "no ticker by that name" : futuresCode ? "a futures code, not a ticker" : null;
926
+ const emptyMessage = (searched: boolean) => listing && identity
927
+ ? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
928
+ : undefined;
929
+ if (word && reason) {
930
+ // The lookup that found no ticker already answered; a search that fails leaves that answer.
931
+ const found = await searchNewsStories(word, limit).catch(() => []);
932
+ await printNewsSearch(word, found, { reason, empty: emptyMessage(true) }, ctx);
933
+ return;
934
+ }
935
+ const empty = emptyMessage(false);
936
+ const read = listing ? listingReadNotes(listing) : [];
766
937
  ctx.printResult({
767
938
  data: articles,
939
+ ...(read.length > 0 ? { warnings: read } : {}),
768
940
  metadata: { ticker: listing?.key ?? null, ...(identity ? listingMetadata(identity) : {}), feed: feed ?? null },
769
941
  freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
770
942
  }, {
771
943
  ...(identity ? { heading: listingHeading(identity) } : {}),
772
944
  rows: newsRows,
773
- columns: [
774
- // UTC with the zone named, as every CLI time prints, rather than the host zone unlabeled.
775
- {
776
- key: "publishedAt",
777
- header: "Published",
778
- format: (value) => typeof value === "string" ? formatUtcTime(value) : "",
779
- },
780
- { key: "source", header: "Source", maxWidth: 20 },
781
- { key: "title", header: "Title" },
782
- { key: "tickers", header: "Tickers", maxWidth: 16 },
783
- { key: "url", header: "URL", optional: true },
784
- ],
945
+ columns: NEWS_COLUMNS,
946
+ ...(empty ? { empty } : {}),
785
947
  });
786
948
  });
787
949
  }
@@ -809,6 +971,7 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
809
971
  const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
810
972
  await withMarketData(ctx, async (market) => {
811
973
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
974
+ const venues = loadBareVenues(listing, market);
812
975
  const { symbol, exchange } = listing.request;
813
976
  // A listing outside the US sends its company to the lookup, which needs its quote first.
814
977
  const quotePromise = loadListingQuote(market.dataProvider, listing);
@@ -825,15 +988,20 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
825
988
  quotePromise,
826
989
  ]);
827
990
  const resolved = identity ?? listingIdentity(listing, quote);
991
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
828
992
  ctx.printResult({
829
993
  data: filings,
830
994
  metadata: { ...listingMetadata(resolved), ...(form ? { form } : {}) },
831
995
  freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
996
+ ...(choice.length > 0 ? { warnings: choice } : {}),
832
997
  }, {
833
998
  heading: listingHeading(resolved),
834
999
  rows: filingRows,
835
1000
  columns: FILING_COLUMNS,
836
- empty: form ? `No ${form} filings found for ${listingTitle(resolved)}.` : `No SEC filings found for ${listingTitle(resolved)}.`,
1001
+ // A listing abroad is looked up by its company: none found means the company files nothing with the SEC.
1002
+ empty: form ? `No ${form} filings found for ${listingTitle(resolved)}.`
1003
+ : identity ? noSecReportsNotice(resolved)
1004
+ : `No SEC filings found for ${listingTitle(resolved)}.`,
837
1005
  });
838
1006
  } catch (error) {
839
1007
  if (!(error instanceof SecRegistrantMismatchError)) throw error;
@@ -851,17 +1019,50 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
851
1019
  });
852
1020
  }
853
1021
 
1022
+ type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
1023
+
1024
+ /**
1025
+ * Whether the company has Form 4 filings of its own, read as `fn INS` reads
1026
+ * them; null when they could not be read. A listing abroad the SEC lists
1027
+ * nothing for files no SEC reports at all.
1028
+ */
1029
+ async function loadForm4Insiders(market: MarketContext, listing: CliListing): Promise<Form4Insiders | null> {
1030
+ try {
1031
+ const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, listing.request.symbol, {
1032
+ limit: 1,
1033
+ exchange: listing.request.exchange,
1034
+ });
1035
+ if (parsed.length > 0) return "available";
1036
+ if (noFilings && nonUsSecListingVenue(listing.request.symbol, listing.request.exchange)) return "no-sec-reports";
1037
+ return foreignPrivateIssuer ? "foreign-private-issuer" : "none";
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+
854
1043
  /**
855
1044
  * Holder lists name institutions and funds; individual insiders rarely appear.
856
- * The insider share of the company is reported either way, so say it.
1045
+ * The insider share of the company is reported either way, so say it. The
1046
+ * Form 4 trades are pointed to only when the company has some; otherwise the
1047
+ * line says why there are none, in the words `fn INS` uses.
857
1048
  */
858
- function insiderSummary(data: HolderData, ownerTypes: Set<string>): string {
1049
+ function insiderSummary(
1050
+ data: HolderData,
1051
+ ownerTypes: Set<string>,
1052
+ listing: Pick<CliListing, "key">,
1053
+ identity: ListingIdentity,
1054
+ form4: Form4Insiders | null,
1055
+ ): string {
859
1056
  const held = data.summary?.insidersPercentHeld;
860
1057
  const share = held == null || !Number.isFinite(held)
861
1058
  ? ""
862
1059
  : `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
863
1060
  const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
864
- const transactions = listed ? "" : `Form 4 transactions: gloomberb fn INS ${data.symbol}`;
1061
+ // The listing as the command read it (CAR:ASX), not the source's spelling of it (CAR.AX), which fn does not take.
1062
+ const transactions = form4 === "available"
1063
+ ? listed ? "" : `Form 4 transactions: gloomberb fn INS ${listing.key}`
1064
+ : form4 === "no-sec-reports" ? noSecReportsNotice(identity)
1065
+ : form4 ? insiderEmptyNotice(form4 === "foreign-private-issuer") : "";
865
1066
  return [share, transactions].filter(Boolean).join("\n");
866
1067
  }
867
1068
 
@@ -878,14 +1079,18 @@ async function printBeneficialOwners(
878
1079
  const quotePromise = loadListingQuote(market.dataProvider, listing);
879
1080
  const abroad = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
880
1081
  const request = { form: beneficialRouteForm(form), history, listing: { exchange, name: abroad?.name ?? undefined } };
881
- const [payload, holders, quote] = await Promise.all([
1082
+ const [payload, holders, quote, financials] = await Promise.all([
882
1083
  loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, request)),
883
1084
  market.dataProvider.getHolders?.(symbol, exchange).catch(() => null) ?? null,
884
1085
  quotePromise,
1086
+ // Today's share count, which the filed percents are set against.
1087
+ market.dataProvider.getTickerFinancials(symbol, exchange).catch(() => null),
885
1088
  ]);
886
1089
  const identity = listingIdentity(listing, quote);
887
- const rows = buildBeneficialReportRows(payload, { history, holders });
1090
+ const sharesOutstanding = classSharesToday(financials);
1091
+ const rows = buildBeneficialReportRows(payload, { history, holders, sharesOutstanding });
888
1092
  const notices = beneficialCoverageNotices(payload.coverage);
1093
+ const legend = beneficialReportLegend(rows, sharesOutstanding);
889
1094
  ctx.printResult({
890
1095
  data: rows,
891
1096
  ...(notices.length ? { warnings: notices } : {}),
@@ -898,12 +1103,16 @@ async function printBeneficialOwners(
898
1103
  asOf: payload.asOf,
899
1104
  coverage: payload.coverage,
900
1105
  complete: beneficialListComplete(payload),
1106
+ sharesOutstanding,
901
1107
  },
902
1108
  }, {
903
1109
  heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
904
- textColumns: BENEFICIAL_REPORT_COLUMNS,
1110
+ // Text draws each column as the HDS report does; CSV and JSON keep the raw values.
1111
+ textColumns: BENEFICIAL_REPORT_COLUMNS.map((column) => ({ ...column, format: column.textFormat ?? column.format })),
1112
+ ...(legend ? { footnote: legend } : {}),
905
1113
  columns: [
906
- ...BENEFICIAL_REPORT_COLUMNS,
1114
+ ...BENEFICIAL_REPORT_COLUMNS.map(({ key, header, align }) => ({ key, header, ...(align ? { align } : {}) })),
1115
+ { key: "otherPersons", header: "Other persons" },
907
1116
  { key: "status", header: "Status" },
908
1117
  { key: "filerCik", header: "Filer CIK" },
909
1118
  { key: "accessionNumber", header: "Accession" },
@@ -911,6 +1120,8 @@ async function printBeneficialOwners(
911
1120
  ],
912
1121
  empty: beneficialListUnreadable(payload)
913
1122
  ? `13D/13G filings for ${listingTitle(identity)} are listed but could not be read.`
1123
+ // The SEC knows no company for a listing abroad that files there under no name of its own.
1124
+ : abroad && !payload.cik ? noSecReportsNotice(identity)
914
1125
  : `No 13D/13G filings in the last 4 years for ${listingTitle(identity)}.`,
915
1126
  });
916
1127
  }
@@ -932,7 +1143,9 @@ async function runHolders(
932
1143
  await withMarketData(ctx, async (market) => {
933
1144
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
934
1145
  if (form !== "13f") return printBeneficialOwners(listing, market, form, history, ctx);
935
- const [data, quote] = await Promise.all([
1146
+ const insiderCommand = commandName === "insider" && ownerTypes != null;
1147
+ const venues = loadBareVenues(listing, market);
1148
+ const [data, quote, form4] = await Promise.all([
936
1149
  loadForListing(
937
1150
  listing, market, ctx,
938
1151
  () => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
@@ -940,43 +1153,70 @@ async function runHolders(
940
1153
  { command: commandName, noun: "holder data" },
941
1154
  ),
942
1155
  loadListingQuote(market.dataProvider, listing),
1156
+ insiderCommand ? loadForm4Insiders(market, listing) : null,
943
1157
  ]);
944
1158
  const identity = listingIdentity(listing, quote);
945
- const rows = holderRows(data, ownerTypes);
946
- const shown = windowRows(rows, ctx.cliOptions).rows.length;
1159
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
1160
+ // A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
1161
+ const valueCurrency = holderValueCurrency(data, "USD")!;
1162
+ // The rows, their units and their order are HDS's, so `holders --csv` and `fn HDS --csv` agree.
1163
+ const rows = holderReportRows(data, {
1164
+ marketCap: holderStakeMarketCap(quote?.marketCap, quote?.currency, valueCurrency),
1165
+ ownerTypes,
1166
+ });
1167
+ const shownRows = windowRows(rows, ctx.cliOptions).rows;
1168
+ const shown = shownRows.length;
947
1169
  const institutional = !ownerTypes?.has("insider");
948
1170
  const total = institutional ? data.summary?.institutionsCount ?? null : null;
949
1171
  const reportDate = sharedReportDate(rows);
950
- // A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
951
- const valueCurrency = holderValueCurrency(data);
952
1172
  const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
953
1173
  const positionsBasis = rows.length > 0 ? nonUsHolderCaveat(identity.exchange || data.exchange, data.currency) : null;
954
1174
  const shareBasisNote = holderShareBasisNote(data, rows);
955
1175
  const markedRows = rows.some((row) => holderShareBasisMarker(row));
956
1176
  // The heading names the listing; its unit, date and how much of the list follows on the same line.
1177
+ // One "as of" only when every row shown is of that date; each row carries its own.
957
1178
  const facts = holderListFacts({
958
- currency: valueCurrency, listingCurrency: data.currency, asOf: data.asOf, shown, reported: rows.length, total,
1179
+ currency: valueCurrency,
1180
+ listingCurrency: data.currency,
1181
+ period: holderListPeriod(shownRows, data.asOf),
1182
+ shown,
1183
+ reported: rows.length,
1184
+ total,
1185
+ ranked: !positionsBasis,
959
1186
  });
1187
+ const unvaluedNote = unvaluedHoldersNote(shownRows);
1188
+ const columns = holderReportColumns(shownRows, valueCurrency);
1189
+ // Dated as HDS dates it: by the list's as-of.
1190
+ const freshness = rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
1191
+ // Named as fn HDS takes it, so the closing notes point to the same listing.
1192
+ const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
1193
+ const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
960
1194
  ctx.printResult({
961
- data,
1195
+ data: rows,
1196
+ ...(freshness ? { freshness } : {}),
962
1197
  metadata: {
963
1198
  ...listingMetadata(identity),
964
- summary: data.summary,
1199
+ summary: data.summary ?? null,
965
1200
  currency: data.currency ?? null,
966
- valueCurrency: valueCurrency ?? null,
1201
+ valueCurrency,
1202
+ units: holderReportUnits(valueCurrency),
967
1203
  asOf: data.asOf ?? null,
1204
+ // Each holder's latest period; several when the list mixes quarters.
1205
+ reportDates: holderReportDates(shownRows),
968
1206
  shown,
969
1207
  reported: rows.length,
970
1208
  total,
971
1209
  truncated: shown < (total ?? rows.length),
972
1210
  valueBasis,
973
1211
  positionsBasis,
1212
+ ...(insiderCommand ? { form4Insiders: form4 } : {}),
974
1213
  },
1214
+ ...(choice.length > 0 ? { warnings: choice } : {}),
975
1215
  }, {
976
1216
  heading: `${listingHeading(identity)}${cliStyles.muted(facts.map((fact) => ` · ${fact}`).join(""))}`,
977
- rows: () => rows,
978
- columns: [
979
- { key: "type", header: "Type" },
1217
+ columns,
1218
+ textColumns: [
1219
+ { key: "ownerType", header: "Type" },
980
1220
  { key: "name", header: "Holder" },
981
1221
  { key: "reportDate", header: "Date" },
982
1222
  { key: "shares", header: "Shares", align: "right", format: formatCountCell },
@@ -992,12 +1232,17 @@ async function runHolders(
992
1232
  key: "percentHeld",
993
1233
  header: "% Held",
994
1234
  align: "right",
995
- format: (value) => isFiniteNumber(value) ? formatHolderOwnershipPercent(value) : "",
1235
+ format: (value) => isFiniteNumber(value) ? formatPercentHeld(value) : "",
996
1236
  },
997
1237
  ],
998
- summary: (holderData: HolderData) => [
999
- [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? ""].filter(Boolean).join(" "),
1000
- commandName === "insider" && ownerTypes ? insiderSummary(holderData, ownerTypes) : "",
1238
+ // CSV and NDJSON are the HDS report's table and closing lines.
1239
+ tables: {
1240
+ tables: [exportRowsTable("Holders", columns, shownRows)],
1241
+ footer: reportFooterLines({ freshness, notes: notices }),
1242
+ },
1243
+ summary: () => [
1244
+ [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
1245
+ insiderCommand ? insiderSummary(data, ownerTypes, listing, identity, form4) : "",
1001
1246
  ].filter(Boolean).join("\n"),
1002
1247
  empty: `No holders reported for ${listingTitle(identity)}.`,
1003
1248
  });
@@ -1010,6 +1255,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1010
1255
  const raw = requireOneArg(args, "analyst <symbol>", "symbol", ctx);
1011
1256
  await withMarketData(ctx, async (market) => {
1012
1257
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1258
+ const venues = loadBareVenues(listing, market);
1013
1259
  const [data, quote] = await Promise.all([
1014
1260
  loadForListing(
1015
1261
  listing, market, ctx, () => market.dataProvider.getAnalystResearch(listing.request.symbol, listing.request.exchange),
@@ -1018,6 +1264,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1018
1264
  loadListingQuote(market.dataProvider, listing),
1019
1265
  ]);
1020
1266
  const identity = listingIdentity(listing, quote);
1267
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
1021
1268
  ctx.printResult({
1022
1269
  data,
1023
1270
  metadata: {
@@ -1029,6 +1276,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1029
1276
  freshness: rowsFreshness(analystRows(data), {
1030
1277
  ...REPORTED_DATA, basis: "analyst ratings", observedKey: "date", oldest: null,
1031
1278
  }, { stale: data.stale === true }),
1279
+ ...(choice.length > 0 ? { warnings: choice } : {}),
1032
1280
  }, {
1033
1281
  heading: listingHeading(identity),
1034
1282
  rows: analystRows,
@@ -1050,6 +1298,7 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
1050
1298
  const raw = requireOneArg(args, "events <symbol>", "symbol", ctx);
1051
1299
  await withMarketData(ctx, async (market) => {
1052
1300
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1301
+ const venues = loadBareVenues(listing, market);
1053
1302
  const [data, quote] = await Promise.all([
1054
1303
  loadForListing(
1055
1304
  listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
@@ -1058,12 +1307,14 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
1058
1307
  loadListingQuote(market.dataProvider, listing),
1059
1308
  ]);
1060
1309
  const identity = listingIdentity(listing, quote);
1310
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
1061
1311
  ctx.printResult({
1062
1312
  data,
1063
1313
  metadata: listingMetadata(identity),
1064
1314
  freshness: rowsFreshness(corporateActionRows(data), {
1065
1315
  ...REPORTED_DATA, basis: "corporate actions", observedKey: "date", oldest: null,
1066
1316
  }),
1317
+ ...(choice.length > 0 ? { warnings: choice } : {}),
1067
1318
  }, {
1068
1319
  heading: listingHeading(identity),
1069
1320
  rows: corporateActionRows,
@@ -1111,6 +1362,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1111
1362
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1112
1363
  const { symbol, exchange } = listing.request;
1113
1364
  const refresh = ctx.cliOptions.refresh;
1365
+ const venues = loadBareVenues(listing, market);
1114
1366
  const quotePromise = loadListingQuote(market.dataProvider, listing);
1115
1367
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1116
1368
  const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
@@ -1140,7 +1392,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1140
1392
  const miss = missingExpiryMessage(listing.key, expirationDate, listed);
1141
1393
  ctx.fail(miss.message, miss.details);
1142
1394
  }
1143
- const identity = listingIdentity(listing, await quotePromise);
1395
+ const quote = await quotePromise;
1396
+ const identity = listingIdentity(listing, quote);
1144
1397
  const chain = chainWithModelFigures(loaded, inputs);
1145
1398
  // A failed refresh falls back to the stored chain, which can be days old.
1146
1399
  const refreshWarning = result?.refreshError == null ? null
@@ -1150,7 +1403,10 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1150
1403
  const modelWarning = inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
1151
1404
  ? `IV and delta need a current ${listing.key} quote; those columns are blank`
1152
1405
  : null;
1153
- const warnings = [refreshWarning ?? sessionWarning, modelWarning].filter((warning): warning is string => warning != null);
1406
+ const warnings = [
1407
+ ...await listingChoiceNotes(listing, market, quote, venues),
1408
+ ...[refreshWarning ?? sessionWarning, modelWarning].filter((warning): warning is string => warning != null),
1409
+ ];
1154
1410
  // Dated by the chain's last trade; a chain that failed to refresh is the stored one, stale.
1155
1411
  const freshness = rowsFreshness([], { asOf: chain.asOf ?? null }, {
1156
1412
  ...(chain.dataSource ? { dataSource: chain.dataSource } : {}),
@@ -1226,12 +1482,16 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1226
1482
  const asOf = observed.length > 0 ? new Date(Math.min(...observed)).toISOString() : null;
1227
1483
  const stale = legs.some((leg) => leg != null && (leg.staleAt <= Date.now() || leg.refreshError != null));
1228
1484
  const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate };
1229
- ctx.printResult({ data: [row] }, {
1230
- text: () => {
1231
- const time = asOf ? `As of ${formatUtcTime(asOf)}` : "";
1232
- const when = stale ? cliStyles.warning(time ? `${time}, stale` : "Stale") : time && cliStyles.muted(time);
1233
- return [describeFxRate(request, rate), when].filter(Boolean).join("\n");
1234
- },
1485
+ // The closing line dates the rate, and says when it is stale, as `fn FXC` does.
1486
+ const freshness = fxPairFreshness(new Map(codes.flatMap((code, index) => {
1487
+ const leg = legs[index];
1488
+ return leg ? [[code, {
1489
+ rate: leg.value,
1490
+ timing: { asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes },
1491
+ }] as const] : [];
1492
+ })));
1493
+ ctx.printResult({ data: [row], freshness }, {
1494
+ text: () => describeFxRate(request, rate),
1235
1495
  columns: [
1236
1496
  { key: "currency", header: "Currency" },
1237
1497
  { key: "baseCurrency", header: "Base" },
@@ -1254,24 +1514,55 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
1254
1514
  const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
1255
1515
  symbol, exchangeOption, services, ctx, { ownExchangeWins: symbols.length > 1 },
1256
1516
  )));
1257
- const events = await loadForListing(
1258
- listings, services, ctx,
1259
- () => services.dataProvider.getEarningsCalendar(listings.map((listing) => listing.key)),
1260
- (found) => found.length === 0,
1261
- );
1517
+ const keys = listings.map((listing) => listing.key);
1518
+ // Each bare symbol's venues cost a search, so a long list (a portfolio check, not a lookup) names none.
1519
+ const venues = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
1520
+ : listings.map((listing) => [listing, loadBareVenues(listing, services)] as const));
1521
+ let events: EarningsEvent[];
1522
+ try {
1523
+ events = await loadForListing(
1524
+ listings, services, ctx,
1525
+ () => services.dataProvider.getEarningsCalendar(keys),
1526
+ (found) => found.length === 0,
1527
+ );
1528
+ } catch (error) {
1529
+ const failure = earningsCalendarFailure(error, keys);
1530
+ if (failure) ctx.fail(failure);
1531
+ throw error;
1532
+ }
1533
+ const eventOf = (listing: CliListing) => events.find((event) => event.symbol.toUpperCase() === listing.key.toUpperCase());
1534
+ const [abroadNotes, choiceNotes] = await Promise.all([
1535
+ // A listing abroad with no date of its own says so, with its US line's date when that line is the same company.
1536
+ Promise.all(listings.filter((listing) => isListingAbroad(listing) && !eventOf(listing))
1537
+ .map((listing) => noListingCalendarNote(listing, services))),
1538
+ Promise.all(listings.map(async (listing) => {
1539
+ const listed = venues.get(listing);
1540
+ return listed ? earningsChoiceNote(listing, eventOf(listing), await listed) : null;
1541
+ })),
1542
+ ]);
1543
+ const noCalendar = abroadNotes.join("\n");
1544
+ // Text prints the listings without a calendar under the table; other formats carry them as warnings.
1545
+ const warnings = [
1546
+ ...listings.flatMap(listingReadNotes),
1547
+ ...choiceNotes.filter((note): note is string => !!note),
1548
+ ...ctx.cliOptions.format === "text" ? [] : abroadNotes,
1549
+ ];
1262
1550
  ctx.printResult({
1263
1551
  data: events,
1264
1552
  freshness: rowsFreshness(earningsRows(events), { status: "not-a-feed", basis: "calendar", observedKey: "date", oldest: null }),
1553
+ ...(warnings.length > 0 ? { warnings } : {}),
1265
1554
  }, {
1266
1555
  rows: earningsRows,
1267
1556
  columns: [
1268
1557
  { key: "date", header: "Date" },
1558
+ { key: "status", header: "Status" },
1269
1559
  { key: "symbol", header: "Symbol" },
1270
1560
  { key: "name", header: "Name" },
1271
1561
  { key: "timing", header: "Timing" },
1272
1562
  { key: "epsEstimate", header: "EPS Est", align: "right", format: formatEpsCell },
1273
1563
  { key: "epsActual", header: "EPS", align: "right", format: formatEpsCell },
1274
1564
  ],
1565
+ ...(noCalendar ? { empty: noCalendar, footnote: noCalendar } : {}),
1275
1566
  });
1276
1567
  });
1277
1568
  }
@@ -1312,7 +1603,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1312
1603
  sections: [{
1313
1604
  title: "Bars",
1314
1605
  lines: [
1315
- `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.`,
1606
+ `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.`,
1316
1607
  "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.",
1317
1608
  ],
1318
1609
  }],
@@ -1472,7 +1763,13 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1472
1763
  { flags: "--history", description: "With --form 13d, 13g or all, every report newest first instead of the latest per filer" },
1473
1764
  EXCHANGE_OPTION,
1474
1765
  ],
1475
- examples: ["holders AAPL", "holders SAN:EPA", "holders CAR --form all", "holders CAR --form 13g --history --json"],
1766
+ sections: [{
1767
+ title: "Output",
1768
+ lines: [
1769
+ "Holders run largest value first, as in fn HDS. --csv and --json carry the HDS report's columns: values in the currency the header names, shares, and the quarter's change and the stake in percent (0.59 is 0.59%).",
1770
+ ],
1771
+ }],
1772
+ examples: ["holders AAPL", "holders SAN:EPA", "holders SAN --csv", "holders CAR --form all", "holders CAR --form 13g --history --json"],
1476
1773
  },
1477
1774
  execute: (args, ctx) => runHolders(args, ctx, "holders"),
1478
1775
  },
@@ -1519,15 +1816,21 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1519
1816
  },
1520
1817
  {
1521
1818
  name: "news",
1522
- description: "Fetch market headlines, or news for one symbol",
1819
+ description: "Fetch market headlines, news for one symbol, or stories matching text",
1523
1820
  help: {
1524
1821
  group: CLI_COMMAND_GROUPS.companyData,
1525
- usage: ["news [symbol] [--feed <feed>]"],
1822
+ usage: ["news [symbol] [--feed <feed>]", "news <query>"],
1526
1823
  options: [
1527
1824
  { flags: "--feed <feed>", description: "latest, top, or breaking for market news (default latest)" },
1528
1825
  EXCHANGE_OPTION,
1529
1826
  ],
1530
- examples: ["news", "news TSLA", "news SAN:EPA", "news --feed top --limit 10"],
1827
+ sections: [{
1828
+ title: "Text",
1829
+ lines: [
1830
+ "Several words, in quotes or not, search the stories that mention them. So does a single word of three letters or more that no listing answers to, and a futures board code such as TTF, a gas benchmark rather than a ticker. A real ticker always gives its own news. The best matches print newest first, 20 unless --limit says otherwise; a search takes neither --feed nor --exchange.",
1831
+ ],
1832
+ }],
1833
+ examples: ["news", "news TSLA", "news SAN:EPA", "news TTF", "news \"european gas\"", "news --feed top --limit 10"],
1531
1834
  },
1532
1835
  execute: runNews,
1533
1836
  },