gloomberb 0.16.3 → 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 (820) hide show
  1. package/README.md +14 -91
  2. package/package.json +7 -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 +42 -17
  11. package/src/account/upgrade-personalization.ts +57 -0
  12. package/src/account/upgrade-tickers.ts +124 -0
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/askg.ts +43 -0
  15. package/src/api-client/auth.ts +15 -0
  16. package/src/api-client/beneficial-owners.ts +4 -0
  17. package/src/api-client/chat.ts +19 -0
  18. package/src/api-client/company-kpis.ts +14 -2
  19. package/src/api-client/data.ts +58 -18
  20. package/src/api-client/earnings.ts +6 -0
  21. package/src/api-client/futures-curve.ts +56 -0
  22. package/src/api-client/index.ts +16 -1
  23. package/src/api-client/market-discovery.ts +10 -1
  24. package/src/api-client/normalizers.ts +27 -0
  25. package/src/api-client/paths.ts +95 -23
  26. package/src/api-client/request.ts +2 -1
  27. package/src/api-client/research-activity.ts +46 -10
  28. package/src/api-client/socket.ts +39 -8
  29. package/src/api-client/types.ts +74 -2
  30. package/src/api-client/upgrade-experiment.ts +90 -0
  31. package/src/api-client/yield-curves.ts +8 -0
  32. package/src/app/global-shortcuts.ts +26 -2
  33. package/src/app/keybindings/actions.ts +10 -0
  34. package/src/app/pane-runtime/index.tsx +8 -0
  35. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  36. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  37. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  38. package/src/app/pane-runtime/ticker-open-runtime.ts +25 -4
  39. package/src/app/runtime/desktop-deeplink.ts +5 -0
  40. package/src/app/star-prompt/model.ts +98 -0
  41. package/src/app/star-prompt/runtime.ts +41 -0
  42. package/src/app/star-prompt/status-line.tsx +190 -0
  43. package/src/app.tsx +13 -7
  44. package/src/architecture/core-builtin-allowlist.json +61 -0
  45. package/src/brokers/signed-in/adapter.ts +33 -2
  46. package/src/brokers/signed-in/client.ts +10 -0
  47. package/src/brokers/signed-in/sign-in-dialog.tsx +34 -14
  48. package/src/brokers/signed-in/sign-in.ts +62 -5
  49. package/src/capabilities/chart-series.ts +11 -1
  50. package/src/capabilities/registry.ts +8 -1
  51. package/src/capabilities/types.ts +2 -0
  52. package/src/cli/commands/crypto-hints.ts +19 -4
  53. package/src/cli/commands/fred-series.ts +61 -0
  54. package/src/cli/commands/market.ts +744 -181
  55. package/src/cli/commands/news-hints.ts +62 -0
  56. package/src/cli/commands/news-search.ts +53 -0
  57. package/src/cli/commands/options-chain.ts +172 -0
  58. package/src/cli/commands/options-leaps.ts +425 -0
  59. package/src/cli/commands/overview.ts +251 -40
  60. package/src/cli/commands/remote.ts +2 -1
  61. package/src/cli/commands/search.ts +109 -15
  62. package/src/cli/commands/system.ts +151 -24
  63. package/src/cli/commands/themes.ts +64 -0
  64. package/src/cli/commands/ticker.ts +149 -40
  65. package/src/cli/context.ts +45 -14
  66. package/src/cli/desktop-pane-shot.ts +150 -17
  67. package/src/cli/earnings-calendar.ts +71 -0
  68. package/src/cli/errors.ts +25 -2
  69. package/src/cli/freshness.ts +22 -8
  70. package/src/cli/fx-pair.ts +57 -0
  71. package/src/cli/glossary.ts +429 -0
  72. package/src/cli/help.ts +14 -1
  73. package/src/cli/history-rows.ts +48 -7
  74. package/src/cli/index-roots.ts +58 -0
  75. package/src/cli/index.ts +29 -12
  76. package/src/cli/known-gaps.ts +27 -0
  77. package/src/cli/listing-arg.ts +595 -26
  78. package/src/cli/not-a-ticker.ts +44 -0
  79. package/src/cli/options.ts +50 -6
  80. package/src/cli/pane-functions/access-gate.ts +5 -0
  81. package/src/cli/pane-functions/capabilities.ts +11 -4
  82. package/src/cli/pane-functions/catalog.ts +48 -7
  83. package/src/cli/pane-functions/data.ts +48 -13
  84. package/src/cli/pane-functions/discovery.ts +1 -0
  85. package/src/cli/pane-functions/dom.ts +41 -19
  86. package/src/cli/pane-functions/freshness.ts +531 -32
  87. package/src/cli/pane-functions/function-help.ts +77 -25
  88. package/src/cli/pane-functions/headless.ts +50 -21
  89. package/src/cli/pane-functions/index.ts +66 -10
  90. package/src/cli/pane-functions/market-session.ts +147 -0
  91. package/src/cli/pane-functions/options.ts +77 -25
  92. package/src/cli/pane-functions/report-notices.ts +81 -0
  93. package/src/cli/pane-functions/report.ts +19 -8
  94. package/src/cli/pane-functions/resolver.ts +53 -18
  95. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  96. package/src/cli/pane-functions/screenshot.ts +199 -30
  97. package/src/cli/registry.ts +2 -1
  98. package/src/cli/report-tables.ts +3 -0
  99. package/src/cli/result.ts +53 -13
  100. package/src/cli/row-window.ts +76 -0
  101. package/src/cli/scroll-truncation.ts +34 -0
  102. package/src/cli/staged-tickers.ts +52 -0
  103. package/src/cli/symbol-args.ts +9 -0
  104. package/src/cli/symbol-list.ts +172 -0
  105. package/src/cli/ticker-resolution.ts +223 -30
  106. package/src/cli/types.ts +5 -0
  107. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  108. package/src/components/chart/composite/composite-chart.tsx +83 -10
  109. package/src/components/chart/composite/legend.tsx +88 -8
  110. package/src/components/chart/composite/panel-surface.tsx +44 -17
  111. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  112. package/src/components/chart/composite/rasterizer.ts +64 -4
  113. package/src/components/chart/composite/series-cues.ts +71 -0
  114. package/src/components/chart/composite/text-renderer.ts +16 -8
  115. package/src/components/chart/composite/types.ts +3 -0
  116. package/src/components/command-bar/commands/direct/index.ts +9 -1
  117. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  118. package/src/components/command-bar/commands/registry.ts +7 -0
  119. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  120. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  121. package/src/components/command-bar/panel/index.tsx +11 -3
  122. package/src/components/command-bar/panel/runtime.ts +3 -0
  123. package/src/components/command-bar/panel/types.ts +2 -0
  124. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  125. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  126. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  127. package/src/components/command-bar/routes/root/results.ts +40 -10
  128. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  129. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  130. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  131. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  132. package/src/components/command-bar/surface/index.tsx +18 -4
  133. package/src/components/command-bar/theme-picker.tsx +48 -13
  134. package/src/components/command-bar/workflow/ops.ts +5 -3
  135. package/src/components/command-bar/workflow/tickers.ts +23 -4
  136. package/src/components/data-table/view.tsx +28 -9
  137. package/src/components/layout/detached-pane-shell.tsx +9 -0
  138. package/src/components/layout/floating-pane.tsx +9 -2
  139. package/src/components/layout/header.tsx +20 -5
  140. package/src/components/layout/market-summary.ts +23 -9
  141. package/src/components/layout/pane/content.tsx +40 -29
  142. package/src/components/layout/pane/cycle-order.ts +2 -11
  143. package/src/components/layout/pane/footer/index.tsx +6 -2
  144. package/src/components/layout/pane/footer/model.ts +35 -1
  145. package/src/components/layout/pane/header.tsx +26 -2
  146. package/src/components/layout/pane/index.tsx +12 -2
  147. package/src/components/layout/pane/sidebar.tsx +78 -10
  148. package/src/components/layout/pane/title.ts +1 -1
  149. package/src/components/layout/presentation.ts +22 -0
  150. package/src/components/layout/shell/active-drag.ts +21 -5
  151. package/src/components/layout/shell/drag/index.ts +43 -9
  152. package/src/components/layout/shell/drag/live.ts +87 -6
  153. package/src/components/layout/shell/drag/runtime.ts +62 -20
  154. package/src/components/layout/shell/index.tsx +71 -12
  155. package/src/components/layout/shell/layout-state.ts +5 -11
  156. package/src/components/layout/shell/menu.ts +33 -1
  157. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  158. package/src/components/layout/shell/pane/actions.ts +2 -1
  159. package/src/components/layout/shell/pane/layers.tsx +216 -28
  160. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  161. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  162. package/src/components/layout/shell/shortcuts.ts +5 -2
  163. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  164. package/src/components/layout/shell/visible-layout.ts +44 -1
  165. package/src/components/layout/status-bar.tsx +4 -1
  166. package/src/components/markdown-text.tsx +69 -23
  167. package/src/components/metric-treemap/heat-surface.tsx +37 -9
  168. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  169. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  170. package/src/components/onboarding/account-step/index.tsx +1 -1
  171. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  172. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  173. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  174. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  175. package/src/components/onboarding/onboarding-wizard.tsx +50 -41
  176. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  177. package/src/components/onboarding/pro-step-layout.ts +86 -0
  178. package/src/components/onboarding/use-onboarding-upgrade.ts +8 -4
  179. package/src/components/onboarding/wizard-account.ts +2 -2
  180. package/src/components/pane-template-wizard.tsx +9 -1
  181. package/src/components/price-selector-dialog.tsx +11 -2
  182. package/src/components/sign-in-gate.tsx +3 -3
  183. package/src/components/ticker/list-table-view.tsx +24 -1
  184. package/src/components/ui/action-row.tsx +2 -1
  185. package/src/components/ui/button.tsx +17 -1
  186. package/src/components/ui/choice-dialog.tsx +6 -4
  187. package/src/components/ui/confirm-dialog.tsx +28 -18
  188. package/src/components/ui/data-table/index.tsx +49 -0
  189. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  190. package/src/components/ui/data-table/types.ts +20 -1
  191. package/src/components/ui/data-table/visible-range.ts +2 -0
  192. package/src/components/ui/dialog-actions.tsx +64 -0
  193. package/src/components/ui/display.tsx +17 -3
  194. package/src/components/ui/frame.tsx +35 -2
  195. package/src/components/ui/icon.tsx +13 -2
  196. package/src/components/ui/index.ts +1 -1
  197. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  198. package/src/core/connection-health.ts +1 -1
  199. package/src/core/state/app/layout-reducer.ts +89 -3
  200. package/src/core/state/app/state.ts +8 -5
  201. package/src/core/state/app/types.ts +13 -2
  202. package/src/data/cached-query.ts +2 -0
  203. package/src/data/config/layout.ts +55 -21
  204. package/src/data/config/store/fold-panes.ts +149 -0
  205. package/src/data/config/store/migrations.ts +7 -0
  206. package/src/data/config/store/node.ts +27 -6
  207. package/src/data/config/store/normalize.ts +44 -5
  208. package/src/data/ticker-repository.ts +5 -0
  209. package/src/data/ticker-store.ts +15 -0
  210. package/src/docs/content.ts +215 -0
  211. package/src/docs/load.ts +139 -0
  212. package/src/docs/requests.ts +39 -0
  213. package/src/docs/snapshot.chunk.ts +8 -0
  214. package/src/docs/snapshot.json +7416 -0
  215. package/src/docs/topics.ts +216 -0
  216. package/src/i18n/es.ts +75 -5
  217. package/src/i18n/ja.ts +75 -5
  218. package/src/i18n/ko.ts +75 -5
  219. package/src/i18n/zh-cn.ts +75 -5
  220. package/src/i18n/zh-tw.ts +75 -5
  221. package/src/layout/desks.ts +2 -3
  222. package/src/layout/pane-layout-move.ts +264 -0
  223. package/src/layout/pane-manager/dock-tree.ts +18 -0
  224. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  225. package/src/layout/pane-manager/index.ts +2 -0
  226. package/src/market-data/coordinator/chart.ts +10 -4
  227. package/src/market-data/coordinator/entries.ts +7 -1
  228. package/src/market-data/coordinator/index.ts +6 -3
  229. package/src/market-data/coordinator/quotes.ts +2 -2
  230. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  231. package/src/market-data/futures-contract.ts +151 -0
  232. package/src/market-data/hooks.tsx +10 -2
  233. package/src/market-data/market/format.ts +39 -21
  234. package/src/market-data/market/freshness.ts +193 -30
  235. package/src/market-data/market/status.ts +59 -23
  236. package/src/market-data/options-alternatives.ts +69 -0
  237. package/src/market-data/published-apac-sessions.ts +23 -1
  238. package/src/market-data/quotes/contributions.ts +22 -2
  239. package/src/market-data/quotes/metadata.ts +8 -9
  240. package/src/market-data/quotes/resolution.ts +2 -0
  241. package/src/market-data/quotes/screener-live-quotes.ts +61 -9
  242. package/src/market-data/result-types.ts +2 -0
  243. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  244. package/src/plugins/builtin/account-management/model.ts +2 -56
  245. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  246. package/src/plugins/builtin/account-management/pane.tsx +37 -8
  247. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  248. package/src/plugins/builtin/alerts/cli.ts +313 -47
  249. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  250. package/src/plugins/builtin/alerts/events.ts +23 -2
  251. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  252. package/src/plugins/builtin/alerts/history.ts +2 -0
  253. package/src/plugins/builtin/alerts/index.tsx +41 -9
  254. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  255. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  256. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  257. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  258. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  259. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  260. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  261. package/src/plugins/builtin/alerts/targets.ts +92 -0
  262. package/src/plugins/builtin/analytics/index.tsx +4 -2
  263. package/src/plugins/builtin/analytics/risk-headless.ts +33 -13
  264. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  265. package/src/plugins/builtin/analytics/risk-model.ts +71 -12
  266. package/src/plugins/builtin/analytics/risk-pane.tsx +23 -12
  267. package/src/plugins/builtin/attention/index.tsx +2 -0
  268. package/src/plugins/builtin/attention/pane.tsx +3 -1
  269. package/src/plugins/builtin/attention/series.ts +26 -0
  270. package/src/plugins/builtin/awards/index.tsx +2 -0
  271. package/src/plugins/builtin/awards/pane.tsx +3 -1
  272. package/src/plugins/builtin/awards/series.ts +29 -0
  273. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  274. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  275. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  276. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  277. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  278. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  279. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  280. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  281. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  282. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  283. package/src/plugins/builtin/cds/client.ts +70 -13
  284. package/src/plugins/builtin/cds/headless.ts +116 -19
  285. package/src/plugins/builtin/cds/index.tsx +3 -1
  286. package/src/plugins/builtin/cds/model.ts +193 -31
  287. package/src/plugins/builtin/cds/pane.tsx +56 -14
  288. package/src/plugins/builtin/cds/series.ts +19 -0
  289. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  290. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  291. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  292. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  293. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  294. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  295. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  296. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  297. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  298. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  299. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  300. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  301. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  302. package/src/plugins/builtin/chat/channels.ts +34 -36
  303. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  304. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  305. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  306. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  307. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  308. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  309. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  310. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  311. package/src/plugins/builtin/chat/controller/index.ts +50 -0
  312. package/src/plugins/builtin/chat/conversations.ts +256 -0
  313. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  314. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  315. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  316. package/src/plugins/builtin/chat/index.tsx +40 -2
  317. package/src/plugins/builtin/chat/layout.ts +2 -0
  318. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  319. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  320. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  321. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  322. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  323. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  324. package/src/plugins/builtin/chat/pane.tsx +8 -0
  325. package/src/plugins/builtin/cloud/askg/controller.ts +223 -23
  326. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  327. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  328. package/src/plugins/builtin/cloud/askg/manifest.ts +33 -7
  329. package/src/plugins/builtin/cloud/askg/model.ts +156 -78
  330. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  331. package/src/plugins/builtin/cloud/askg/protocol.ts +50 -3
  332. package/src/plugins/builtin/cloud/askg/tool-display.ts +440 -0
  333. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  334. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  335. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  336. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  337. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  338. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  339. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  340. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +363 -0
  341. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  342. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  343. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  344. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  345. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  346. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  347. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +734 -0
  348. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  349. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  350. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  351. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +239 -0
  352. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +162 -0
  353. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +192 -0
  354. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  355. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  356. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  357. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  358. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  359. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  360. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  361. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  362. package/src/plugins/builtin/company-attention/model.ts +26 -7
  363. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  364. package/src/plugins/builtin/company-attention/series.ts +52 -0
  365. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  366. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  367. package/src/plugins/builtin/company-kpis/pane.tsx +19 -9
  368. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  369. package/src/plugins/builtin/composite-plugins.ts +116 -67
  370. package/src/plugins/builtin/cot/index.tsx +3 -1
  371. package/src/plugins/builtin/cot/model.ts +40 -11
  372. package/src/plugins/builtin/cot/series.ts +27 -0
  373. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  374. package/src/plugins/builtin/credit-boards/index.tsx +23 -21
  375. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  376. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  377. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  378. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  379. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  380. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  381. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  382. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  383. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  384. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  385. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  386. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  387. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  388. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  389. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  390. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  391. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  392. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  393. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  394. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  395. package/src/plugins/builtin/dividend-yield/index.tsx +3 -1
  396. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  397. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  398. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  399. package/src/plugins/builtin/docs/index.tsx +68 -0
  400. package/src/plugins/builtin/docs/pane.tsx +426 -0
  401. package/src/plugins/builtin/docs/targets.ts +72 -0
  402. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  403. package/src/plugins/builtin/doe/index.tsx +2 -0
  404. package/src/plugins/builtin/doe/series.ts +62 -0
  405. package/src/plugins/builtin/earnings/client.ts +3 -2
  406. package/src/plugins/builtin/earnings/format.ts +1 -5
  407. package/src/plugins/builtin/earnings/headless.ts +58 -6
  408. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  409. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  410. package/src/plugins/builtin/earnings/history.tsx +7 -3
  411. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  412. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  413. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  414. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  415. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  416. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  417. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  418. package/src/plugins/builtin/econ/index.tsx +70 -22
  419. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  420. package/src/plugins/builtin/econ-statistics/headless.ts +30 -5
  421. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  422. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  423. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  424. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  425. package/src/plugins/builtin/executives/data.ts +17 -6
  426. package/src/plugins/builtin/executives/pane.tsx +16 -7
  427. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  428. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  429. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  430. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  431. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  432. package/src/plugins/builtin/futures/index.tsx +43 -3
  433. package/src/plugins/builtin/futures/model.ts +1 -1
  434. package/src/plugins/builtin/futures/returns.ts +1 -1
  435. package/src/plugins/builtin/futures/series.ts +13 -0
  436. package/src/plugins/builtin/futures/table.tsx +1 -1
  437. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  438. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  439. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  440. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  441. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  442. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  443. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  444. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  445. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  446. package/src/plugins/builtin/gpu/board.tsx +13 -10
  447. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  448. package/src/plugins/builtin/gpu/client.ts +19 -1
  449. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  450. package/src/plugins/builtin/gpu/headless.ts +64 -27
  451. package/src/plugins/builtin/gpu/history.tsx +3 -3
  452. package/src/plugins/builtin/gpu/index.tsx +2 -0
  453. package/src/plugins/builtin/gpu/model.ts +114 -20
  454. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  455. package/src/plugins/builtin/gpu/series.ts +38 -0
  456. package/src/plugins/builtin/help/function-card.tsx +25 -6
  457. package/src/plugins/builtin/help/index.tsx +18 -8
  458. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  459. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  460. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  461. package/src/plugins/builtin/holders/beneficial-view.tsx +62 -14
  462. package/src/plugins/builtin/holders/client.ts +22 -3
  463. package/src/plugins/builtin/holders/format.ts +18 -3
  464. package/src/plugins/builtin/holders/headless.ts +42 -134
  465. package/src/plugins/builtin/holders/pane.tsx +14 -4
  466. package/src/plugins/builtin/holders/report-header.ts +40 -5
  467. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  468. package/src/plugins/builtin/insider/client.ts +35 -20
  469. package/src/plugins/builtin/insider/headless.ts +41 -11
  470. package/src/plugins/builtin/insider/index.tsx +27 -10
  471. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  472. package/src/plugins/builtin/insider/issuer.ts +49 -0
  473. package/src/plugins/builtin/insider/model.ts +3 -0
  474. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  475. package/src/plugins/builtin/iv-history/client.ts +26 -6
  476. package/src/plugins/builtin/iv-history/format.ts +33 -2
  477. package/src/plugins/builtin/iv-history/headless.ts +27 -12
  478. package/src/plugins/builtin/iv-history/index.tsx +5 -3
  479. package/src/plugins/builtin/iv-history/model.ts +172 -20
  480. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  481. package/src/plugins/builtin/iv-history/screen-pane.tsx +45 -25
  482. package/src/plugins/builtin/iv-history/series.ts +29 -0
  483. package/src/plugins/builtin/jobs/headless.ts +28 -3
  484. package/src/plugins/builtin/jobs/index.tsx +3 -1
  485. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  486. package/src/plugins/builtin/jobs/series.ts +19 -0
  487. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  488. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  489. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  490. package/src/plugins/builtin/kelly-sizer/pane.tsx +111 -40
  491. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  492. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  493. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  494. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  495. package/src/plugins/builtin/macro-day/client.ts +68 -3
  496. package/src/plugins/builtin/macro-day/headless.ts +18 -12
  497. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  498. package/src/plugins/builtin/macro-day/model.ts +66 -15
  499. package/src/plugins/builtin/macro-day/pane.tsx +34 -20
  500. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  501. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  502. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  503. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  504. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  505. package/src/plugins/builtin/market-heatmap/model.ts +102 -3
  506. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  507. package/src/plugins/builtin/market-movers/client.ts +7 -5
  508. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  509. package/src/plugins/builtin/market-movers/index.tsx +11 -3
  510. package/src/plugins/builtin/market-movers/model.ts +101 -17
  511. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  512. package/src/plugins/builtin/market-movers/table.ts +50 -23
  513. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  514. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  515. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  516. package/src/plugins/builtin/members/headless.ts +7 -2
  517. package/src/plugins/builtin/members/model.ts +31 -3
  518. package/src/plugins/builtin/members/pane.tsx +3 -6
  519. package/src/plugins/builtin/mna/headless.ts +15 -3
  520. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  521. package/src/plugins/builtin/money-markets/series.ts +41 -0
  522. package/src/plugins/builtin/news/headless.ts +9 -2
  523. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  524. package/src/plugins/builtin/notes/cli.ts +12 -7
  525. package/src/plugins/builtin/openfda/client.ts +1 -1
  526. package/src/plugins/builtin/options/carry.ts +95 -0
  527. package/src/plugins/builtin/options/index.tsx +58 -5
  528. package/src/plugins/builtin/options/strike-window.ts +183 -0
  529. package/src/plugins/builtin/options/table.ts +55 -3
  530. package/src/plugins/builtin/options/types.ts +8 -1
  531. package/src/plugins/builtin/options/view.tsx +162 -13
  532. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  533. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  534. package/src/plugins/builtin/options-positioning/headless.ts +5 -2
  535. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  536. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  537. package/src/plugins/builtin/options-scenario/client.ts +75 -18
  538. package/src/plugins/builtin/options-scenario/headless.ts +72 -24
  539. package/src/plugins/builtin/options-scenario/hedge.ts +285 -0
  540. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  541. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  542. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  543. package/src/plugins/builtin/pe-band/model.ts +2 -2
  544. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  545. package/src/plugins/builtin/perps/client.ts +40 -6
  546. package/src/plugins/builtin/perps/equity-row.tsx +33 -8
  547. package/src/plugins/builtin/perps/freshness.ts +66 -0
  548. package/src/plugins/builtin/perps/headless.ts +45 -30
  549. package/src/plugins/builtin/perps/index.tsx +18 -1
  550. package/src/plugins/builtin/perps/model.ts +32 -6
  551. package/src/plugins/builtin/perps/pane.tsx +76 -20
  552. package/src/plugins/builtin/perps/series.ts +76 -0
  553. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  554. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  555. package/src/plugins/builtin/plugin-marketplace/pane.tsx +357 -39
  556. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  557. package/src/plugins/builtin/plugin-module.ts +90 -4
  558. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  559. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  560. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  561. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +400 -99
  562. package/src/plugins/builtin/portfolio-list/cli/render.ts +281 -43
  563. package/src/plugins/builtin/portfolio-list/cli/shared.ts +26 -16
  564. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  565. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  566. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  567. package/src/plugins/builtin/portfolio-list/headless.ts +70 -9
  568. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  569. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  570. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  571. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  572. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  573. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  574. package/src/plugins/builtin/power/client.ts +16 -0
  575. package/src/plugins/builtin/power/evidence.ts +6 -2
  576. package/src/plugins/builtin/power/headless.ts +71 -7
  577. package/src/plugins/builtin/power/index.tsx +4 -1
  578. package/src/plugins/builtin/power/model.ts +72 -1
  579. package/src/plugins/builtin/power/pane.tsx +32 -9
  580. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  581. package/src/plugins/builtin/realized-vol/headless.ts +8 -0
  582. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  583. package/src/plugins/builtin/research/corporate-actions-pane.tsx +7 -4
  584. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  585. package/src/plugins/builtin/research/event-model.ts +68 -24
  586. package/src/plugins/builtin/research/events-headless.ts +2 -1
  587. package/src/plugins/builtin/research/headless.ts +2 -1
  588. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  589. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  590. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  591. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  592. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  593. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  594. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  595. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  596. package/src/plugins/builtin/seasonality/client.ts +15 -7
  597. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  598. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  599. package/src/plugins/builtin/seasonality/model.ts +119 -0
  600. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  601. package/src/plugins/builtin/sec/index.tsx +5 -5
  602. package/src/plugins/builtin/sectors/client.ts +10 -2
  603. package/src/plugins/builtin/sectors/headless.ts +35 -6
  604. package/src/plugins/builtin/sectors/index.tsx +18 -1
  605. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  606. package/src/plugins/builtin/series-inventory.ts +91 -0
  607. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  608. package/src/plugins/builtin/shared/command-tab.ts +69 -0
  609. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  610. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  611. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  612. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  613. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  614. package/src/plugins/builtin/shared/open-description.ts +29 -0
  615. package/src/plugins/builtin/shared/report-freshness.ts +59 -2
  616. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  617. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  618. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  619. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  620. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  621. package/src/plugins/builtin/short-interest/index.tsx +21 -4
  622. package/src/plugins/builtin/short-interest/series.ts +36 -0
  623. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  624. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  625. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  626. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  627. package/src/plugins/builtin/short-volume/index.tsx +3 -10
  628. package/src/plugins/builtin/short-volume/series.ts +23 -0
  629. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  630. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  631. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  632. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  633. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  634. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  635. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  636. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  637. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  638. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  639. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  640. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  641. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  642. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  643. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  644. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  645. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  646. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  647. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  648. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  649. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  650. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  651. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  652. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  653. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  654. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  655. package/src/plugins/builtin/treasury-auctions/headless.ts +56 -6
  656. package/src/plugins/builtin/treasury-auctions/index.tsx +12 -9
  657. package/src/plugins/builtin/treasury-auctions/model.ts +205 -30
  658. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  659. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  660. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  661. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  662. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  663. package/src/plugins/builtin/vol-surface/headless.ts +125 -20
  664. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  665. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  666. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  667. package/src/plugins/builtin/volatility/index.tsx +2 -0
  668. package/src/plugins/builtin/volatility/series.ts +84 -0
  669. package/src/plugins/builtin/world-indices/client.ts +22 -4
  670. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  671. package/src/plugins/builtin/world-indices/model.ts +4 -2
  672. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  673. package/src/plugins/builtin/world-venue-map/geo-series.ts +27 -2
  674. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  675. package/src/plugins/builtin/world-venue-map/index.ts +3 -2
  676. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  677. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  678. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  679. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  680. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  681. package/src/plugins/builtin/yield-curve/index.tsx +4 -2
  682. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  683. package/src/plugins/builtin-editorial.ts +126 -0
  684. package/src/plugins/catalog-backend.ts +16 -9
  685. package/src/plugins/catalog-browser.ts +129 -64
  686. package/src/plugins/catalog-manifest.ts +73 -40
  687. package/src/plugins/catalog-ui.ts +26 -6
  688. package/src/plugins/extracted-plugins.ts +7 -1
  689. package/src/plugins/ownership.ts +150 -7
  690. package/src/plugins/plugin-functions.ts +54 -0
  691. package/src/plugins/plugin-off.ts +36 -0
  692. package/src/plugins/registry/host-actions.ts +16 -0
  693. package/src/plugins/registry/index.ts +173 -28
  694. package/src/plugins/registry/plugin-state.ts +25 -1
  695. package/src/plugins/runtime/context.tsx +29 -0
  696. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  697. package/src/portfolio/position-value.ts +40 -0
  698. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  699. package/src/public/time-series.ts +17 -0
  700. package/src/react/auto-refresh.ts +13 -3
  701. package/src/react/input.ts +48 -1
  702. package/src/remote/app-host.tsx +25 -4
  703. package/src/remote/confirm-change.tsx +85 -0
  704. package/src/remote/controller-utils.ts +8 -1
  705. package/src/remote/controller.ts +126 -59
  706. package/src/remote/matching.ts +69 -0
  707. package/src/remote/pane-screenshot.ts +47 -0
  708. package/src/remote/schema.ts +63 -1
  709. package/src/remote/semantic-tree.tsx +14 -0
  710. package/src/remote/types.ts +32 -0
  711. package/src/remote/watchlist.ts +306 -0
  712. package/src/renderers/browser/main.tsx +3 -2
  713. package/src/renderers/cloudflare/worker.ts +1 -1
  714. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  715. package/src/renderers/dom/data-table/index.tsx +33 -6
  716. package/src/renderers/dom/data-table/row.tsx +27 -1
  717. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  718. package/src/renderers/dom/desktop/controls.tsx +30 -23
  719. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  720. package/src/renderers/dom/desktop/icons.tsx +21 -0
  721. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  722. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  723. package/src/renderers/dom/host/box.tsx +4 -1
  724. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  725. package/src/renderers/dom/host/drag-shield.ts +17 -6
  726. package/src/renderers/dom/host/live-frame.ts +14 -2
  727. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  728. package/src/renderers/dom/host/size-observer.ts +26 -0
  729. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  730. package/src/renderers/dom/host/text.tsx +1 -1
  731. package/src/renderers/dom/input-host.tsx +6 -1
  732. package/src/renderers/dom/styles.css +74 -0
  733. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  734. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  735. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  736. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  737. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +18 -3
  738. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +109 -5
  739. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  740. package/src/renderers/electrobun/view/main.tsx +6 -1
  741. package/src/renderers/opentui/input-host.tsx +6 -1
  742. package/src/renderers/opentui/start.tsx +9 -0
  743. package/src/series/capability.ts +75 -0
  744. package/src/series/csv.ts +160 -0
  745. package/src/series/files.ts +107 -0
  746. package/src/series/registry.ts +198 -0
  747. package/src/series/resolve.ts +107 -0
  748. package/src/series/standalone.ts +48 -0
  749. package/src/series/types.ts +145 -0
  750. package/src/series/validate.ts +171 -0
  751. package/src/shares/portable-layout.ts +16 -6
  752. package/src/sources/gloomberb-cloud/index.ts +26 -17
  753. package/src/sources/gloomberb-cloud/normalizers.ts +5 -1
  754. package/src/sources/history-result.ts +1 -0
  755. package/src/sources/listing-symbols.ts +14 -5
  756. package/src/sources/provider-errors.ts +88 -3
  757. package/src/sources/provider-router/batches.ts +4 -1
  758. package/src/sources/provider-router/cache.ts +4 -2
  759. package/src/sources/provider-router/cached-routes.ts +9 -5
  760. package/src/sources/provider-router/financial-routes.ts +12 -5
  761. package/src/sources/provider-router/financials.ts +55 -15
  762. package/src/sources/provider-router/history.ts +68 -34
  763. package/src/sources/provider-router/index.ts +1 -1
  764. package/src/sources/provider-router/primary.ts +23 -5
  765. package/src/sources/provider-router/search.ts +1 -0
  766. package/src/sources/sec-edgar.ts +68 -8
  767. package/src/sync/core-contributors.ts +5 -3
  768. package/src/test-support/cli-context.ts +2 -1
  769. package/src/test-support/data-provider.ts +34 -0
  770. package/src/test-support/shell-pane-keys.tsx +1 -0
  771. package/src/theme/series-colors.ts +46 -0
  772. package/src/theme/themes.ts +149 -0
  773. package/src/tickers/company-name.ts +75 -0
  774. package/src/tickers/instrument-kind.ts +25 -0
  775. package/src/tickers/open-target.ts +3 -2
  776. package/src/tickers/search/index.ts +67 -9
  777. package/src/tickers/search/ranking.ts +78 -18
  778. package/src/tickers/search/types.ts +10 -2
  779. package/src/tickers/starter-symbols.ts +8 -0
  780. package/src/tickers/us-listing.ts +34 -0
  781. package/src/time-series/hooks.ts +9 -0
  782. package/src/time-series/resolve.ts +96 -30
  783. package/src/time-series/types.ts +4 -2
  784. package/src/time-series/use-chart-resolution.ts +32 -0
  785. package/src/types/config.ts +67 -1
  786. package/src/types/data-provider.ts +5 -0
  787. package/src/types/financials.ts +52 -1
  788. package/src/types/headless.ts +72 -0
  789. package/src/types/instrument.ts +8 -0
  790. package/src/types/plugin.ts +68 -3
  791. package/src/types/price-history.ts +7 -0
  792. package/src/types/ticker.ts +14 -0
  793. package/src/ui/host.tsx +26 -4
  794. package/src/ui/index.tsx +2 -0
  795. package/src/ui/live-resize.ts +81 -0
  796. package/src/ui/live-stretch.ts +121 -0
  797. package/src/ui/measured-cells.ts +58 -0
  798. package/src/ui/pane-fill.ts +11 -0
  799. package/src/utils/basis-points.ts +17 -0
  800. package/src/utils/cli-output.ts +21 -2
  801. package/src/utils/concurrency.ts +25 -0
  802. package/src/utils/crypto-pair.ts +20 -0
  803. package/src/utils/currency-pair.ts +19 -0
  804. package/src/utils/decimal-comma.ts +19 -0
  805. package/src/utils/dom-screenshot-css.ts +265 -0
  806. package/src/utils/dom-screenshot-rasters.ts +276 -0
  807. package/src/utils/dom-screenshot.ts +437 -52
  808. package/src/utils/exchange-rate-snapshot.ts +6 -2
  809. package/src/utils/exchanges.ts +120 -1
  810. package/src/utils/format.ts +17 -3
  811. package/src/utils/fundamentals.ts +66 -0
  812. package/src/utils/futures-generic.ts +6 -12
  813. package/src/utils/fx-market-hours.ts +44 -0
  814. package/src/utils/option-expiry.ts +126 -0
  815. package/src/utils/price-history.ts +130 -40
  816. package/src/utils/rpc-timeout-error.ts +6 -2
  817. package/src/utils/sec-eps-basis.ts +178 -36
  818. package/src/utils/sec.ts +98 -40
  819. package/src/utils/throttled-fetch.ts +51 -22
  820. package/src/utils/utc-time.ts +51 -4
@@ -7,13 +7,21 @@ import type {
7
7
  CorporateActionsData,
8
8
  HolderData,
9
9
  OptionsChain,
10
+ Quote,
10
11
  TickerFinancials,
11
12
  } from "../../types/financials";
12
- import { currencyMinorDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
13
- import { getActiveQuoteDisplay, marketStateLabel } from "../../market-data/market/status";
14
- import { formatCompact, formatDistributionAmount, formatPercent } from "../../utils/format";
13
+ import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
14
+ import {
15
+ EXTENDED_SESSION_LABELS,
16
+ getExtendedSessionDisplay,
17
+ getQuoteSessionFields,
18
+ getRegularSessionDisplay,
19
+ marketStateLabel,
20
+ type ExtendedSession,
21
+ } from "../../market-data/market/status";
22
+ import { formatCompact, formatDistributionAmount, formatPercent, formatPercentRaw } from "../../utils/format";
15
23
  import { withCliServices, withMarketData } from "../context";
16
- import { isoDate, parsePositiveInt, rejectExtraArgs, requireOneArg, takeFlag, takeOption } from "./command-utils";
24
+ import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
17
25
  import type { BuiltinCliCommandDef } from "../command-options";
18
26
  import { CLI_COMMAND_GROUPS, TABLE_SECTION_OPTION } from "../help";
19
27
  import {
@@ -22,7 +30,7 @@ import {
22
30
  formatFractionPercentCell,
23
31
  formatPriceRange,
24
32
  } from "../helpers";
25
- import { cliStyles, renderStats } from "../../utils/cli-output";
33
+ import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
26
34
  import { formatPerShareNumber } from "../../utils/reported-money";
27
35
  import {
28
36
  analystTargetCurrency,
@@ -32,9 +40,27 @@ import {
32
40
  recommendationTotal,
33
41
  targetUpside,
34
42
  } from "../../plugins/builtin/research/analyst-model";
35
- import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
36
43
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
37
- import { fundamentalsReportTables, renderFundamentalsReport } from "./ticker";
44
+ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
45
+ import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
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";
49
+ import {
50
+ chainHasExpiry,
51
+ chainWithModelFigures,
52
+ formatExpiryList,
53
+ loadOptionModelInputs,
54
+ formatOptionDeltaCell,
55
+ formatOptionIvCell,
56
+ formatOptionQuoteCell,
57
+ missingExpiryMessage,
58
+ OPTION_MODEL_RATE,
59
+ OPTIONS_USAGE,
60
+ optionRows,
61
+ parseOptionExpiration,
62
+ } from "./options-chain";
63
+ import { DEFAULT_LEAPS_CRITERIA, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions } from "./options-leaps";
38
64
  import type { CliResultColumn } from "../result";
39
65
  import {
40
66
  exportRowsTable,
@@ -45,6 +71,7 @@ import {
45
71
  import {
46
72
  historyFacts,
47
73
  historyFlagNote,
74
+ historyGapNote,
48
75
  historyAllRangeYears,
49
76
  historyIntervalsByRange,
50
77
  historyNotes,
@@ -53,36 +80,60 @@ import {
53
80
  historyUnit,
54
81
  } from "../history-rows";
55
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";
56
86
  import { formatUtcTime } from "../../utils/utc-time";
57
87
  import { isFiniteNumber } from "../../utils/guards";
58
88
  import { exportedFundamentals } from "../../utils/price-earnings";
59
- import { fundamentalsFreshness, quotesFreshness, rowsFreshness } from "../freshness";
89
+ import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness } from "../freshness";
60
90
  import {
61
91
  barHistoryFreshness,
62
92
  barResolutionFromDates,
93
+ cloudNewsFreshness,
94
+ cloudRealtimeAccess,
63
95
  REPORTED_DATA,
64
96
  SEC_FILINGS,
65
97
  } from "../../plugins/builtin/shared/report-freshness";
66
98
  import {
99
+ bareListingNote,
67
100
  EXCHANGE_OPTION,
101
+ futuresTryLine,
102
+ isNoProviderError,
68
103
  listingHeading,
69
104
  listingIdentity,
70
105
  listingTitle,
71
106
  listingVenues,
107
+ listingChoiceNotes,
108
+ listingReadNotes,
109
+ loadBareVenues,
110
+ type ListingVenue,
72
111
  loadForListing,
73
112
  loadListingQuote,
113
+ noLongerTradingNote,
74
114
  notTradedMessage,
115
+ quoteBareListing,
75
116
  requireCliListing,
76
117
  type CliListing,
118
+ type ListingDataRequest,
77
119
  type ListingIdentity,
78
120
  } from "../listing-arg";
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";
124
+ import { providerMissReason } from "../../sources/provider-errors";
125
+ import { isNotATickerMessage } from "../not-a-ticker";
126
+ import { splitSymbolArgs } from "../symbol-args";
79
127
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
80
128
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
81
129
  import { isUsListingExchange } from "../../utils/exchanges";
82
- import { nonUsSecListingVenue } from "../../utils/sec";
130
+ import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
83
131
  import type { MarketContext } from "../types";
132
+ import { windowRows } from "../row-window";
133
+ import { crossRate, describeFxRate, isUsableRate, parseFxRequest } from "../fx-pair";
84
134
  import {
85
135
  holderListFacts,
136
+ holderListPeriod,
86
137
  holderShareBasisMarker,
87
138
  holderShareBasisNote,
88
139
  holderValueBasis,
@@ -90,15 +141,27 @@ import {
90
141
  nonUsHolderCaveat,
91
142
  sharedReportDate,
92
143
  } from "../../plugins/builtin/holders/report-header";
93
- 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";
94
153
  import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
154
+ import { classSharesToday } from "../../plugins/builtin/holders/client";
95
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";
96
158
  import {
97
159
  BENEFICIAL_REPORT_COLUMNS,
98
160
  beneficialCoverageNotices,
99
161
  beneficialListComplete,
100
162
  beneficialListFacts,
101
163
  beneficialListUnreadable,
164
+ beneficialReportLegend,
102
165
  beneficialRouteForm,
103
166
  buildBeneficialReportRows,
104
167
  HOLDER_FORMS,
@@ -109,7 +172,13 @@ import {
109
172
  const VALID_RANGES = new Set<TimeRange>(TIME_RANGES);
110
173
  const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
111
174
 
112
- 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
+ };
113
182
  type FinancialsCliData = TickerFinancials & {
114
183
  symbol: string;
115
184
  exchange: string;
@@ -126,11 +195,9 @@ function parseNewsFeed(value: string | undefined): NewsQuery["feed"] | undefined
126
195
  return value && VALID_NEWS_FEEDS.has(value as NewsFeed) ? value as NewsQuery["feed"] : undefined;
127
196
  }
128
197
 
198
+ /** The symbols of a command's arguments, one each however they were separated: spaces, commas or pasted lines. */
129
199
  function normalizeSymbols(args: string[]): string[] {
130
- return args
131
- .flatMap((arg) => arg.split(","))
132
- .map((symbol) => symbol.trim().toUpperCase())
133
- .filter(Boolean);
200
+ return splitSymbolArgs(args).map((symbol) => symbol.toUpperCase());
134
201
  }
135
202
 
136
203
  /** What JSON metadata says about the listing a command resolved. */
@@ -138,38 +205,94 @@ function listingMetadata(identity: ListingIdentity) {
138
205
  return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
139
206
  }
140
207
 
208
+ // Fitted to a narrow width, the columns marked optional go first (higher dropPriority first), then Name is cut short.
209
+ // What a user types or reads the price by stays whole.
141
210
  const QUOTE_LEAD_COLUMNS = [
142
- { key: "symbol", header: "Symbol" },
211
+ { key: "symbol", header: "Symbol", shrink: false },
143
212
  { key: "name", header: "Name" },
144
213
  {
145
214
  key: "price",
146
215
  header: "Last",
147
216
  align: "right" as const,
148
- format: (value: unknown, row: ReturnType<typeof quoteRows>[number]) => (
149
- row.error && !value ? cliStyles.danger("unavailable") : String(value ?? "")
217
+ format: (value: unknown, row: QuoteRow) => (
218
+ row.error && !value
219
+ // A typo is not an outage: say what is wrong with the symbol itself.
220
+ ? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
221
+ : yieldLevelCell(value, row)
150
222
  ),
151
223
  },
152
- { key: "changePercent", header: "Chg%", align: "right" as const, format: formatChangePercentCell },
153
- { key: "session", header: "Session" },
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
+ },
230
+ // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
231
+ extendedColumn("PRE"),
232
+ extendedColumn("POST"),
233
+ { key: "session", header: "Session", shrink: false },
154
234
  ];
155
235
 
236
+ type QuoteRow = ReturnType<typeof quoteRows>[number];
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
+
249
+ /** The pre-market or after-hours print and its move from the regular close, in its own column. */
250
+ function extendedColumn(session: ExtendedSession) {
251
+ return {
252
+ key: session === "PRE" ? "preMarket" : "afterHours",
253
+ header: EXTENDED_SESSION_LABELS[session],
254
+ align: "right" as const,
255
+ value: (row: QuoteRow) => row.extendedSession === session && row.extendedPrice
256
+ ? [row.extendedPrice, row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
257
+ : "",
258
+ format: (value: unknown, row: QuoteRow) => (
259
+ value ? [row.extendedPrice, formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
260
+ ),
261
+ };
262
+ }
263
+
264
+ /** A percent to two decimals, as the table prints it. */
265
+ function roundedPercent(value: number | undefined): number | null {
266
+ return value == null ? null : Number(value.toFixed(2));
267
+ }
268
+
156
269
  function quoteColumns() {
157
270
  return [
158
271
  ...QUOTE_LEAD_COLUMNS,
159
- { key: "currency", header: "Cur" },
160
- // Whether the price is real-time or delayed: a feed state, not where it came from.
161
- { key: "source", header: "Feed", format: (value: unknown) => value === "live" || value === "delayed" ? value : "" },
162
- { key: "updatedAt", header: "Updated" },
272
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1 },
273
+ // Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
274
+ // row says so here, so the closing line's "N of M stale" always points at rows the table marks.
275
+ {
276
+ key: "feed",
277
+ header: "Feed",
278
+ shrink: false,
279
+ optional: true,
280
+ dropPriority: 2,
281
+ format: (value: unknown) => value === "stale" ? cliStyles.warning(value) : String(value ?? ""),
282
+ },
283
+ // In a narrow terminal the closing line's as-of stands in for each row's.
284
+ { key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
163
285
  ];
164
286
  }
165
287
 
166
288
  function compareColumns() {
167
289
  return [
168
290
  ...QUOTE_LEAD_COLUMNS,
169
- { key: "previousClose", header: "Prev Close", align: "right" as const },
170
- { key: "dayRange", header: "Day Range", align: "right" as const },
171
- { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell },
172
- { key: "currency", header: "Cur" },
291
+ { key: "previousClose", header: "Prev Close", align: "right" as const, format: yieldLevelCell },
292
+ // A narrow terminal drops these before it cuts the names short.
293
+ { key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
294
+ { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
295
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3 },
173
296
  ];
174
297
  }
175
298
 
@@ -178,18 +301,28 @@ function errorMessage(error: unknown): string | null {
178
301
  return error instanceof Error ? error.message : String(error);
179
302
  }
180
303
 
181
- function quoteRows(results: QuoteCliRecord[]) {
304
+ /** What the Feed cell says: stale wins over the feed's own live or delayed, as the closing line counts it. */
305
+ function quoteFeedState(quote: QuoteCliRecord["quote"], now: number): "stale" | "live" | "delayed" | "" {
306
+ if (!quote) return "";
307
+ if (isQuoteStaleForCurrentSession(quote, now)) return "stale";
308
+ return quote.dataSource === "live" || quote.dataSource === "delayed" ? quote.dataSource : "";
309
+ }
310
+
311
+ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
182
312
  return results.map((result) => {
183
313
  const quote = result.quote;
184
- // Same price and move as the quote monitor: the live session's print against the daily reference.
185
- const display = getActiveQuoteDisplay(quote);
314
+ // Last and Chg% are the regular session, as `ticker` and the quote monitor read it; a
315
+ // pre-market or after-hours print is its own column, measured from that session's close.
316
+ const display = getRegularSessionDisplay(quote);
317
+ const extended = getExtendedSessionDisplay(quote);
318
+ const yieldIndex = isYieldIndexSymbol(result.target.symbol);
186
319
  // An index level is in points, not in the currency its members trade in.
187
320
  const indexPoints = quote?.instrumentType?.trim().toUpperCase() === "INDEX";
188
321
  // Pad to two decimals so a column lines up, but never past the currency's minor unit (JPY has none).
189
322
  // Points have no minor unit, so a yen-listed index still pads to two.
190
323
  const options = {
191
324
  ...quoteFormatOptions(quote),
192
- minimumFractionDigits: indexPoints ? 2 : Math.min(2, currencyMinorDigits(quote?.currency)),
325
+ minimumFractionDigits: indexPoints ? 2 : currencyPriceMinDigits(quote?.currency),
193
326
  };
194
327
  const price = (value: number | undefined) => (
195
328
  quote && value != null
@@ -198,15 +331,23 @@ function quoteRows(results: QuoteCliRecord[]) {
198
331
  );
199
332
  return {
200
333
  symbol: result.target.symbol,
201
- name: quote?.name ?? "",
334
+ name: quote?.name || result.listing?.name || "",
202
335
  price: price(display?.price),
203
336
  rawPrice: display?.price ?? null,
204
337
  priceBasis: quote?.priceBasis ?? null,
205
338
  instrumentType: quote?.instrumentType ?? null,
206
339
  change: display?.change ?? null,
207
- changePercent: display?.changePercent == null ? null : Number(display.changePercent.toFixed(2)),
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,
344
+ extendedSession: extended?.session ?? null,
345
+ extendedPrice: price(extended?.price),
346
+ rawExtendedPrice: extended?.price ?? null,
347
+ extendedChange: extended?.change ?? null,
348
+ extendedChangePercent: roundedPercent(extended?.changePercent),
208
349
  session: quote?.marketState ? marketStateLabel(quote.marketState) : "",
209
- // The close the shown move is measured from; a pre-market move starts at the last close.
350
+ // The close the shown move is measured from: the one before the session Last is.
210
351
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
211
352
  dayRange: quote?.low != null && quote.high != null
212
353
  ? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
@@ -215,6 +356,7 @@ function quoteRows(results: QuoteCliRecord[]) {
215
356
  currency: quote?.currency ?? "",
216
357
  providerId: quote?.providerId ?? "",
217
358
  source: quote?.dataSource ?? quote?.providerId ?? "",
359
+ feed: quoteFeedState(quote, now),
218
360
  updatedAt: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : "",
219
361
  error: result.error ?? "",
220
362
  };
@@ -259,22 +401,6 @@ function filingRows(filings: SecFilingItem[]) {
259
401
  }));
260
402
  }
261
403
 
262
- function holderRows(data: HolderData, ownerTypes?: Set<string>) {
263
- const holders = ownerTypes
264
- ? data.holders.filter((holder) => ownerTypes.has(holder.ownerType))
265
- : data.holders;
266
- return holders.map((holder) => ({
267
- type: holder.ownerType,
268
- name: holder.name,
269
- reportDate: holder.reportDate ?? "",
270
- shares: holder.shares ?? null,
271
- value: holder.value ?? null,
272
- percentHeld: holder.percentHeld ?? null,
273
- changeShares: holder.changeShares ?? null,
274
- shareBasis: holder.shareBasis ?? null,
275
- }));
276
- }
277
-
278
404
  function analystSummary(data: AnalystResearchData): string {
279
405
  const target = data.priceTarget;
280
406
  const currency = analystTargetCurrency(data);
@@ -345,22 +471,6 @@ function corporateActionRows(data: CorporateActionsData) {
345
471
  ].sort((left, right) => right.date.localeCompare(left.date));
346
472
  }
347
473
 
348
- function optionRows(chain: OptionsChain) {
349
- return [...chain.calls.map((contract) => ({ side: "call", ...contract })), ...chain.puts.map((contract) => ({ side: "put", ...contract }))]
350
- .map((contract) => ({
351
- side: contract.side,
352
- contract: contract.contractSymbol,
353
- strike: contract.strike,
354
- last: contract.lastPrice,
355
- bid: contract.bid,
356
- ask: contract.ask,
357
- volume: contract.volume,
358
- openInterest: contract.openInterest,
359
- iv: contract.impliedVolatility,
360
- expiration: new Date(contract.expiration * 1000).toISOString().slice(0, 10),
361
- }));
362
- }
363
-
364
474
  /**
365
475
  * After a US open, a chain whose latest trade predates it still carries the
366
476
  * prior session's quotes and volume. The delayed feed lags the open by about
@@ -375,11 +485,6 @@ function priorSessionChainWarning(chain: OptionsChain, exchange: string, now: nu
375
485
  return `No option trades this session yet (last trade ${chain.asOf})`;
376
486
  }
377
487
 
378
- function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid" | "ask"): string {
379
- const quote = optionQuoteSide({ bid: Number(row.bid), ask: Number(row.ask) }, side);
380
- return quote == null ? "—" : String(quote);
381
- }
382
-
383
488
  /** Per-share earnings to the cent, as reported; consensus averages carry more digits. */
384
489
  function formatEpsCell(value: unknown): string {
385
490
  return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
@@ -390,6 +495,8 @@ function earningsRows(events: EarningsEvent[]) {
390
495
  symbol: event.symbol,
391
496
  name: event.name,
392
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),
393
500
  timing: event.timing,
394
501
  epsEstimate: event.epsEstimate,
395
502
  epsActual: event.epsActual,
@@ -398,6 +505,9 @@ function earningsRows(events: EarningsEvent[]) {
398
505
  }));
399
506
  }
400
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
+
401
511
  async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
402
512
  const args = [...rawArgs];
403
513
  const exchange = takeOption(args, "--exchange") ?? "";
@@ -410,33 +520,151 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
410
520
  symbol, exchange, market, ctx, { ownExchangeWins: symbols.length > 1 },
411
521
  )));
412
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));
413
527
  const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh });
414
528
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
415
- // A listing with no quote on an exchange its symbol is not listed on says so; venues are only looked up for those.
416
- const notTraded = await Promise.all(results.map((result, index) => (
417
- result.quote ? null : notTradedMessage(listingOf(result, index), market)
418
- )));
419
- if (listings.length === 1 && notTraded[0]) ctx.fail(notTraded[0]);
529
+ const request = { command: commandName, noun: "quote" };
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
+ }));
534
+ const failed = answers[0]?.failure;
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));
420
540
  // Each row names its listing by key (SAN:EPA) and company, so the table needs no line above it.
421
- const data = results.map((result, index) => ({
541
+ const data = answers.map(({ result, listing, failure }, index) => ({
422
542
  target: result.target,
423
- listing: listingMetadata(listingIdentity(listingOf(result, index), result.quote)),
543
+ listing: listingMetadata(listingIdentity(listing, result.quote)),
544
+ // The figures the table shows, beside the quote as the source sent it.
545
+ ...getQuoteSessionFields(result.quote),
424
546
  quote: result.quote,
425
- error: notTraded[index] ?? errorMessage(result.error),
547
+ ...contracts[index] ? { futuresContract: contracts[index] } : {},
548
+ error: failure?.oneLine ?? errorMessage(result.error),
549
+ ...yieldIndexFields(result),
426
550
  }));
427
- // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
428
- const notes = ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [];
551
+ const now = Date.now();
552
+ const notes = [
553
+ // Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
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
+ : []),
559
+ // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
560
+ ...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
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));
429
565
  ctx.printResult({
430
566
  data,
431
567
  warnings: notes.length > 0 ? notes : undefined,
432
568
  freshness: quotesFreshness(data.map((row) => row.quote)),
433
569
  }, {
434
570
  rows: quoteRows,
435
- columns: commandName === "compare" ? compareColumns() : quoteColumns(),
571
+ details: quoteFuturesContractText,
572
+ columns,
573
+ ...(yieldsOnly ? { textColumns: columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column) } : {}),
436
574
  });
437
575
  });
438
576
  }
439
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
+
604
+ interface QuoteAnswer {
605
+ result: QuoteBatchResult;
606
+ /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
607
+ listing: CliListing;
608
+ /** Why there is no quote, when there is more to say than the source's own error. */
609
+ failure?: { message: string; details?: string; oneLine: string };
610
+ notes: string[];
611
+ }
612
+
613
+ /**
614
+ * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
615
+ * is not listed on says so. A bare symbol the data service read on its home
616
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
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
621
+ * symbol with no quote, so a request that succeeds costs no search.
622
+ */
623
+ async function answerQuote(
624
+ result: QuoteBatchResult,
625
+ listing: CliListing,
626
+ market: MarketContext,
627
+ request: ListingDataRequest,
628
+ refresh: boolean,
629
+ venues: Promise<ListingVenue[]> | null,
630
+ ): Promise<QuoteAnswer> {
631
+ const root = listing.exchange ? null : indexRootFor(listing.symbol);
632
+ // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
633
+ const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
634
+ ? [indexRootAlsoLine(root, request.command)] : [];
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
+ }
641
+ const notTraded = await notTradedMessage(listing, market);
642
+ if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
643
+ if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
644
+ const other = await quoteBareListing(listing, market, request, { refresh });
645
+ if (other?.kind === "quoted") {
646
+ return {
647
+ result: { target: other.listing.request, quote: other.quote },
648
+ listing: other.listing,
649
+ notes: [other.note, ...also(other.quote)],
650
+ };
651
+ }
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: [] };
658
+ const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
659
+ const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
660
+ const oneLine = [
661
+ message,
662
+ other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
663
+ ...tryLines,
664
+ ].filter(Boolean).join(" ");
665
+ return { result, listing, failure: { message, details, oneLine }, notes: [] };
666
+ }
667
+
440
668
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
441
669
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
442
670
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
@@ -455,20 +683,23 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
455
683
  const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
456
684
  await withMarketData(ctx, async (market) => {
457
685
  const listing = await requireCliListing(raw, requestedExchange, market, ctx);
686
+ const venues = loadBareVenues(listing, market);
458
687
  const { symbol, exchange } = listing.request;
459
688
  const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
460
689
  const load = () => market.dataProvider.getPriceHistoryWithMetadata
461
690
  ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
462
691
  : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points) => ({ points, resolution: null }));
463
692
  const [{ points, resolution }, quote] = await Promise.all([
464
- loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0)
693
+ loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
694
+ command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history",
695
+ })
465
696
  .catch((error) => failHistory(error, listing.key, ctx)),
466
697
  loadListingQuote(market.dataProvider, listing),
467
698
  ]);
468
699
  const identity = listingIdentity(listing, quote);
469
700
  // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
470
701
  const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
471
- const unit = historyUnit(listing.symbol, listed ?? quote);
702
+ const unit = historyUnit(listing.key, listed ?? quote);
472
703
  const data = historyRows(points, resolution, unit.currency);
473
704
  const interval = data[0]?.interval ?? null;
474
705
  const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
@@ -480,8 +711,16 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
480
711
  ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
481
712
  observedKey: "date",
482
713
  oldest: null,
714
+ // Bars stop arriving on the listing's own trading days, not New York's.
715
+ cadenceExchange: quote?.listingExchangeName || exchange || undefined,
483
716
  });
484
- const notes = [...historyNotes(range, interval, data[0]?.date), historyFlagNote(data) ?? []].flat();
717
+ const gaps = historyGapNote(data, {
718
+ listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
719
+ });
720
+ const notes = [
721
+ ...await listingChoiceNotes(listing, market, quote, venues),
722
+ ...historyNotes(range, interval, data[0]?.date), gaps ?? [], historyFlagNote(data) ?? [],
723
+ ].flat();
485
724
  const priceColumns: CliResultColumn[] = [
486
725
  intraday
487
726
  ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
@@ -515,6 +754,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
515
754
  freshness,
516
755
  }, {
517
756
  heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
757
+ dateKey: "date",
518
758
  textColumns: [...priceColumns, flagColumn],
519
759
  // Every exported row says its currency and bar size, so it survives head and concatenation.
520
760
  columns: [
@@ -558,10 +798,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
558
798
  };
559
799
  await withMarketData(ctx, async (market) => {
560
800
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
801
+ const venues = loadBareVenues(listing, market);
561
802
  const financials = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
562
803
  listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
563
- ));
804
+ ), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
564
805
  const identity = listingIdentity(listing, financials.quote);
806
+ const choice = await listingChoiceNotes(listing, market, financials.quote, venues);
565
807
  const data: FinancialsCliData = {
566
808
  symbol: listing.key,
567
809
  exchange: identity.exchange,
@@ -574,6 +816,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
574
816
  // JSON reads a multiple over a loss as null with its reason, never as a number.
575
817
  data: { ...data, fundamentals: exportedFundamentals(data.fundamentals) },
576
818
  freshness,
819
+ ...(choice.length > 0 ? { warnings: choice } : {}),
577
820
  }, {
578
821
  text: () => renderFundamentalsReport(data, view),
579
822
  ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
@@ -602,6 +845,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
602
845
  profile: financials.profile,
603
846
  },
604
847
  freshness,
848
+ ...(choice.length > 0 ? { warnings: choice } : {}),
605
849
  }, {
606
850
  heading: listingHeading(identity),
607
851
  rows: () => rows,
@@ -611,15 +855,55 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
611
855
  });
612
856
  }
613
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
+
614
890
  async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
615
891
  const args = [...rawArgs];
616
892
  const feed = parseNewsFeed(takeOption(args, "--feed"));
617
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
+ }
618
904
  if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloomberb news <symbol> --exchange <code>");
619
- rejectExtraArgs(args, 1, { usage: "news [symbol] [--feed <feed>]", takes: "one symbol at most", advice: "Run it once per symbol." }, ctx);
620
905
  await withMarketData(ctx, async (market) => {
621
906
  const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
622
- const limit = ctx.cliOptions.limit ?? 20;
623
907
  const loadNews = () => market.dataProvider.getNews({
624
908
  feed: feed ?? (listing ? "ticker" : "latest"),
625
909
  // Still set for news plugins that read the deprecated scope.
@@ -633,27 +917,33 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
633
917
  listing ? loadListingQuote(market.dataProvider, listing) : null,
634
918
  ]);
635
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) : [];
636
937
  ctx.printResult({
637
938
  data: articles,
939
+ ...(read.length > 0 ? { warnings: read } : {}),
638
940
  metadata: { ticker: listing?.key ?? null, ...(identity ? listingMetadata(identity) : {}), feed: feed ?? null },
639
- freshness: rowsFreshness(newsRows(articles), {
640
- status: "not-a-feed", basis: "published stories", observedKey: "publishedAt", oldest: null,
641
- }),
941
+ freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
642
942
  }, {
643
943
  ...(identity ? { heading: listingHeading(identity) } : {}),
644
944
  rows: newsRows,
645
- columns: [
646
- // UTC with the zone named, as every CLI time prints, rather than the host zone unlabeled.
647
- {
648
- key: "publishedAt",
649
- header: "Published",
650
- format: (value) => typeof value === "string" ? formatUtcTime(value) : "",
651
- },
652
- { key: "source", header: "Source", maxWidth: 20 },
653
- { key: "title", header: "Title" },
654
- { key: "tickers", header: "Tickers", maxWidth: 16 },
655
- { key: "url", header: "URL", optional: true },
656
- ],
945
+ columns: NEWS_COLUMNS,
946
+ ...(empty ? { empty } : {}),
657
947
  });
658
948
  });
659
949
  }
@@ -674,12 +964,14 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
674
964
 
675
965
  async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
676
966
  const args = [...rawArgs];
677
- const count = parsePositiveInt(takeOption(args, "--count"), ctx.cliOptions.limit ?? 15, "Count", ctx);
967
+ // Filings come newest first, so --tail asks for the same first n as --limit.
968
+ const count = parsePositiveInt(takeOption(args, "--count"), ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 15, "Count", ctx);
678
969
  const exchangeOption = takeOption(args, "--exchange");
679
970
  const form = takeOption(args, "--form")?.trim() || null;
680
971
  const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
681
972
  await withMarketData(ctx, async (market) => {
682
973
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
974
+ const venues = loadBareVenues(listing, market);
683
975
  const { symbol, exchange } = listing.request;
684
976
  // A listing outside the US sends its company to the lookup, which needs its quote first.
685
977
  const quotePromise = loadListingQuote(market.dataProvider, listing);
@@ -696,15 +988,20 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
696
988
  quotePromise,
697
989
  ]);
698
990
  const resolved = identity ?? listingIdentity(listing, quote);
991
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
699
992
  ctx.printResult({
700
993
  data: filings,
701
994
  metadata: { ...listingMetadata(resolved), ...(form ? { form } : {}) },
702
995
  freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
996
+ ...(choice.length > 0 ? { warnings: choice } : {}),
703
997
  }, {
704
998
  heading: listingHeading(resolved),
705
999
  rows: filingRows,
706
1000
  columns: FILING_COLUMNS,
707
- 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)}.`,
708
1005
  });
709
1006
  } catch (error) {
710
1007
  if (!(error instanceof SecRegistrantMismatchError)) throw error;
@@ -722,17 +1019,50 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
722
1019
  });
723
1020
  }
724
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
+
725
1043
  /**
726
1044
  * Holder lists name institutions and funds; individual insiders rarely appear.
727
- * 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.
728
1048
  */
729
- 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 {
730
1056
  const held = data.summary?.insidersPercentHeld;
731
1057
  const share = held == null || !Number.isFinite(held)
732
1058
  ? ""
733
1059
  : `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
734
1060
  const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
735
- 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") : "";
736
1066
  return [share, transactions].filter(Boolean).join("\n");
737
1067
  }
738
1068
 
@@ -745,14 +1075,22 @@ async function printBeneficialOwners(
745
1075
  ctx: Parameters<CliCommandDef["execute"]>[1],
746
1076
  ) {
747
1077
  const { symbol, exchange } = listing.request;
748
- const [payload, holders, quote] = await Promise.all([
749
- loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, { form: beneficialRouteForm(form), history })),
1078
+ // A listing outside the US sends its company to the lookup, which needs its quote first.
1079
+ const quotePromise = loadListingQuote(market.dataProvider, listing);
1080
+ const abroad = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
1081
+ const request = { form: beneficialRouteForm(form), history, listing: { exchange, name: abroad?.name ?? undefined } };
1082
+ const [payload, holders, quote, financials] = await Promise.all([
1083
+ loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, request)),
750
1084
  market.dataProvider.getHolders?.(symbol, exchange).catch(() => null) ?? null,
751
- loadListingQuote(market.dataProvider, listing),
1085
+ quotePromise,
1086
+ // Today's share count, which the filed percents are set against.
1087
+ market.dataProvider.getTickerFinancials(symbol, exchange).catch(() => null),
752
1088
  ]);
753
1089
  const identity = listingIdentity(listing, quote);
754
- const rows = buildBeneficialReportRows(payload, { history, holders });
1090
+ const sharesOutstanding = classSharesToday(financials);
1091
+ const rows = buildBeneficialReportRows(payload, { history, holders, sharesOutstanding });
755
1092
  const notices = beneficialCoverageNotices(payload.coverage);
1093
+ const legend = beneficialReportLegend(rows, sharesOutstanding);
756
1094
  ctx.printResult({
757
1095
  data: rows,
758
1096
  ...(notices.length ? { warnings: notices } : {}),
@@ -765,12 +1103,16 @@ async function printBeneficialOwners(
765
1103
  asOf: payload.asOf,
766
1104
  coverage: payload.coverage,
767
1105
  complete: beneficialListComplete(payload),
1106
+ sharesOutstanding,
768
1107
  },
769
1108
  }, {
770
1109
  heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
771
- 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 } : {}),
772
1113
  columns: [
773
- ...BENEFICIAL_REPORT_COLUMNS,
1114
+ ...BENEFICIAL_REPORT_COLUMNS.map(({ key, header, align }) => ({ key, header, ...(align ? { align } : {}) })),
1115
+ { key: "otherPersons", header: "Other persons" },
774
1116
  { key: "status", header: "Status" },
775
1117
  { key: "filerCik", header: "Filer CIK" },
776
1118
  { key: "accessionNumber", header: "Accession" },
@@ -778,6 +1120,8 @@ async function printBeneficialOwners(
778
1120
  ],
779
1121
  empty: beneficialListUnreadable(payload)
780
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)
781
1125
  : `No 13D/13G filings in the last 4 years for ${listingTitle(identity)}.`,
782
1126
  });
783
1127
  }
@@ -799,51 +1143,80 @@ async function runHolders(
799
1143
  await withMarketData(ctx, async (market) => {
800
1144
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
801
1145
  if (form !== "13f") return printBeneficialOwners(listing, market, form, history, ctx);
802
- 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([
803
1149
  loadForListing(
804
1150
  listing, market, ctx,
805
1151
  () => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
806
1152
  (found) => found.holders.length === 0,
1153
+ { command: commandName, noun: "holder data" },
807
1154
  ),
808
1155
  loadListingQuote(market.dataProvider, listing),
1156
+ insiderCommand ? loadForm4Insiders(market, listing) : null,
809
1157
  ]);
810
1158
  const identity = listingIdentity(listing, quote);
811
- const rows = holderRows(data, ownerTypes);
812
- const limit = ctx.cliOptions.limit;
813
- const shown = limit == null ? rows.length : Math.min(limit, 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;
814
1169
  const institutional = !ownerTypes?.has("insider");
815
1170
  const total = institutional ? data.summary?.institutionsCount ?? null : null;
816
1171
  const reportDate = sharedReportDate(rows);
817
- // A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
818
- const valueCurrency = holderValueCurrency(data);
819
1172
  const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
820
1173
  const positionsBasis = rows.length > 0 ? nonUsHolderCaveat(identity.exchange || data.exchange, data.currency) : null;
821
1174
  const shareBasisNote = holderShareBasisNote(data, rows);
822
1175
  const markedRows = rows.some((row) => holderShareBasisMarker(row));
823
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.
824
1178
  const facts = holderListFacts({
825
- 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,
826
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 });
827
1194
  ctx.printResult({
828
- data,
1195
+ data: rows,
1196
+ ...(freshness ? { freshness } : {}),
829
1197
  metadata: {
830
1198
  ...listingMetadata(identity),
831
- summary: data.summary,
1199
+ summary: data.summary ?? null,
832
1200
  currency: data.currency ?? null,
833
- valueCurrency: valueCurrency ?? null,
1201
+ valueCurrency,
1202
+ units: holderReportUnits(valueCurrency),
834
1203
  asOf: data.asOf ?? null,
1204
+ // Each holder's latest period; several when the list mixes quarters.
1205
+ reportDates: holderReportDates(shownRows),
835
1206
  shown,
836
1207
  reported: rows.length,
837
1208
  total,
838
1209
  truncated: shown < (total ?? rows.length),
839
1210
  valueBasis,
840
1211
  positionsBasis,
1212
+ ...(insiderCommand ? { form4Insiders: form4 } : {}),
841
1213
  },
1214
+ ...(choice.length > 0 ? { warnings: choice } : {}),
842
1215
  }, {
843
1216
  heading: `${listingHeading(identity)}${cliStyles.muted(facts.map((fact) => ` · ${fact}`).join(""))}`,
844
- rows: () => rows,
845
- columns: [
846
- { key: "type", header: "Type" },
1217
+ columns,
1218
+ textColumns: [
1219
+ { key: "ownerType", header: "Type" },
847
1220
  { key: "name", header: "Holder" },
848
1221
  { key: "reportDate", header: "Date" },
849
1222
  { key: "shares", header: "Shares", align: "right", format: formatCountCell },
@@ -859,12 +1232,17 @@ async function runHolders(
859
1232
  key: "percentHeld",
860
1233
  header: "% Held",
861
1234
  align: "right",
862
- format: (value) => isFiniteNumber(value) ? formatHolderOwnershipPercent(value) : "",
1235
+ format: (value) => isFiniteNumber(value) ? formatPercentHeld(value) : "",
863
1236
  },
864
1237
  ],
865
- summary: (holderData: HolderData) => [
866
- [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? ""].filter(Boolean).join(" "),
867
- 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) : "",
868
1246
  ].filter(Boolean).join("\n"),
869
1247
  empty: `No holders reported for ${listingTitle(identity)}.`,
870
1248
  });
@@ -877,11 +1255,16 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
877
1255
  const raw = requireOneArg(args, "analyst <symbol>", "symbol", ctx);
878
1256
  await withMarketData(ctx, async (market) => {
879
1257
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1258
+ const venues = loadBareVenues(listing, market);
880
1259
  const [data, quote] = await Promise.all([
881
- loadForListing(listing, market, ctx, () => market.dataProvider.getAnalystResearch(listing.request.symbol, listing.request.exchange)),
1260
+ loadForListing(
1261
+ listing, market, ctx, () => market.dataProvider.getAnalystResearch(listing.request.symbol, listing.request.exchange),
1262
+ undefined, { command: "analyst", noun: "analyst research" },
1263
+ ),
882
1264
  loadListingQuote(market.dataProvider, listing),
883
1265
  ]);
884
1266
  const identity = listingIdentity(listing, quote);
1267
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
885
1268
  ctx.printResult({
886
1269
  data,
887
1270
  metadata: {
@@ -893,6 +1276,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
893
1276
  freshness: rowsFreshness(analystRows(data), {
894
1277
  ...REPORTED_DATA, basis: "analyst ratings", observedKey: "date", oldest: null,
895
1278
  }, { stale: data.stale === true }),
1279
+ ...(choice.length > 0 ? { warnings: choice } : {}),
896
1280
  }, {
897
1281
  heading: listingHeading(identity),
898
1282
  rows: analystRows,
@@ -914,17 +1298,23 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
914
1298
  const raw = requireOneArg(args, "events <symbol>", "symbol", ctx);
915
1299
  await withMarketData(ctx, async (market) => {
916
1300
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1301
+ const venues = loadBareVenues(listing, market);
917
1302
  const [data, quote] = await Promise.all([
918
- loadForListing(listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange)),
1303
+ loadForListing(
1304
+ listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
1305
+ undefined, { command: "events", noun: "corporate events" },
1306
+ ),
919
1307
  loadListingQuote(market.dataProvider, listing),
920
1308
  ]);
921
1309
  const identity = listingIdentity(listing, quote);
1310
+ const choice = await listingChoiceNotes(listing, market, quote, venues);
922
1311
  ctx.printResult({
923
1312
  data,
924
1313
  metadata: listingMetadata(identity),
925
1314
  freshness: rowsFreshness(corporateActionRows(data), {
926
1315
  ...REPORTED_DATA, basis: "corporate actions", observedKey: "date", oldest: null,
927
1316
  }),
1317
+ ...(choice.length > 0 ? { warnings: choice } : {}),
928
1318
  }, {
929
1319
  heading: listingHeading(identity),
930
1320
  rows: corporateActionRows,
@@ -939,85 +1329,177 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
939
1329
 
940
1330
  async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
941
1331
  const args = [...rawArgs];
1332
+ const leapsOptions = takeLeapsOptions(args);
942
1333
  const expiration = takeOption(args, "--expiration");
943
1334
  const exchangeOption = takeOption(args, "--exchange");
944
- const raw = requireOneArg(args, "options <symbol> [--expiration <unix>]", "symbol", ctx);
1335
+ if (leapsOptions.leaps) {
1336
+ if (expiration != null || rawArgs.includes("--expiration")) {
1337
+ ctx.fail("--leaps reads every expiry more than a year out; drop --expiration.", `Usage: gloomberb ${LEAPS_USAGE}`);
1338
+ }
1339
+ const criteria = parseLeapsCriteria(leapsOptions.raw);
1340
+ if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloomberb ${LEAPS_USAGE}`);
1341
+ const symbols = normalizeSymbols(args);
1342
+ if (symbols.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloomberb ${LEAPS_USAGE}`);
1343
+ await withMarketData(ctx, (market) => runLeapsScreen(symbols, exchangeOption, criteria, market, ctx));
1344
+ return;
1345
+ }
1346
+ if (leapsOptions.given.length > 0) {
1347
+ ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloomberb ${LEAPS_USAGE}`);
1348
+ }
1349
+ const raw = requireOneArg(args, OPTIONS_USAGE, "symbol", ctx);
1350
+ let expirationDate: number | undefined;
1351
+ if (expiration != null || rawArgs.includes("--expiration")) {
1352
+ const parsed = expiration == null ? null : parseOptionExpiration(expiration);
1353
+ if (parsed == null) {
1354
+ ctx.fail(
1355
+ expiration == null ? "--expiration needs a date." : `Invalid --expiration "${expiration}".`,
1356
+ `Use YYYY-MM-DD (2028-01-21) or Unix seconds (1832025600).\nUsage: gloomberb ${OPTIONS_USAGE}`,
1357
+ );
1358
+ }
1359
+ expirationDate = parsed;
1360
+ }
945
1361
  await withMarketData(ctx, async (market) => {
946
1362
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
947
1363
  const { symbol, exchange } = listing.request;
948
- const expirationDate = expiration == null ? undefined : Number(expiration);
1364
+ const refresh = ctx.cliOptions.refresh;
1365
+ const venues = loadBareVenues(listing, market);
949
1366
  const quotePromise = loadListingQuote(market.dataProvider, listing);
950
- const { result, chain } = await loadForListing(listing, market, ctx, async () => {
1367
+ const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1368
+ const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
951
1369
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
952
- .load({ force: ctx.cliOptions.refresh });
1370
+ .load({ force: refresh });
953
1371
  return {
954
1372
  result: cached,
955
1373
  chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expirationDate, {
956
- cacheMode: ctx.cliOptions.refresh ? "refresh" : "default",
1374
+ cacheMode: refresh ? "refresh" : "default",
957
1375
  }),
958
1376
  };
1377
+ }, undefined, { command: "options", noun: "options chain" }).catch((error: unknown) => {
1378
+ if (!isOptionsUnavailableError(error)) throw error;
1379
+ // A future or commodity with no chain of its own: name the listed fund whose options stand in.
1380
+ const alternative = listedOptionsAlternative(listing.key);
1381
+ return ctx.fail(optionsUnavailableTitle(listing.key), alternative
1382
+ ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloomberb options ${alternative.symbol}.`
1383
+ + ` Strategy payoffs: gloomberb fn OSA ${alternative.symbol}`
1384
+ : "Strategy payoffs on a listed underlying: gloomberb fn OSA <ticker>");
959
1385
  });
960
- const identity = listingIdentity(listing, await quotePromise);
1386
+ const inputs = await inputsPromise;
1387
+ if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
1388
+ // An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
1389
+ const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
1390
+ .getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
1391
+ .then((fallback) => fallback.expirationDates, () => []);
1392
+ const miss = missingExpiryMessage(listing.key, expirationDate, listed);
1393
+ ctx.fail(miss.message, miss.details);
1394
+ }
1395
+ const quote = await quotePromise;
1396
+ const identity = listingIdentity(listing, quote);
1397
+ const chain = chainWithModelFigures(loaded, inputs);
961
1398
  // A failed refresh falls back to the stored chain, which can be days old.
962
1399
  const refreshWarning = result?.refreshError == null ? null
963
1400
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
964
1401
  + (chain.asOf ? ` (last trade ${chain.asOf})` : "");
965
1402
  const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
966
- const warnings = refreshWarning ? [refreshWarning] : sessionWarning ? [sessionWarning] : undefined;
1403
+ const modelWarning = inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
1404
+ ? `IV and delta need a current ${listing.key} quote; those columns are blank`
1405
+ : null;
1406
+ const warnings = [
1407
+ ...await listingChoiceNotes(listing, market, quote, venues),
1408
+ ...[refreshWarning ?? sessionWarning, modelWarning].filter((warning): warning is string => warning != null),
1409
+ ];
967
1410
  // Dated by the chain's last trade; a chain that failed to refresh is the stored one, stale.
968
1411
  const freshness = rowsFreshness([], { asOf: chain.asOf ?? null }, {
969
1412
  ...(chain.dataSource ? { dataSource: chain.dataSource } : {}),
970
1413
  ...(chain.delayMinutes != null ? { delayMinutes: chain.delayMinutes } : {}),
971
1414
  stale: refreshWarning != null,
972
1415
  });
973
- ctx.printResult({ data: chain, metadata: { ...listingMetadata(identity), expirations: chain.expirationDates }, warnings, freshness }, {
1416
+ ctx.printResult({
1417
+ data: chain,
1418
+ metadata: {
1419
+ ...listingMetadata(identity),
1420
+ expirations: chain.expirationDates,
1421
+ // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
1422
+ model: { spot: inputs.spot ?? null, dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE },
1423
+ },
1424
+ ...(warnings.length > 0 ? { warnings } : {}),
1425
+ freshness,
1426
+ }, {
974
1427
  heading: listingHeading(identity),
975
1428
  rows: optionRows,
1429
+ // Choosing an expiry needs the list; once one is chosen the table is that expiry.
1430
+ summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
1431
+ empty: `No option contracts for ${listingTitle(identity)}.`,
976
1432
  columns: [
977
1433
  { key: "side", header: "Side" },
978
1434
  { key: "contract", header: "Contract", shrink: false },
979
- { key: "expiration", header: "Expiry" },
1435
+ // The contract symbol already carries the date, so this goes first when the table is narrow.
1436
+ { key: "expiration", header: "Expiry", optional: true, dropPriority: 2 },
980
1437
  { key: "strike", header: "Strike", align: "right" },
981
1438
  { key: "last", header: "Last", align: "right" },
982
1439
  { key: "bid", header: "Bid", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "bid") },
983
1440
  { key: "ask", header: "Ask", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "ask") },
984
- { key: "volume", header: "Vol", align: "right", format: formatCountCell },
985
- { key: "openInterest", header: "OI", align: "right", format: formatCountCell },
1441
+ { key: "iv", header: "IV", align: "right", format: formatOptionIvCell },
1442
+ { key: "delta", header: "Delta", align: "right", format: formatOptionDeltaCell },
1443
+ { key: "volume", header: "Vol", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
1444
+ { key: "openInterest", header: "OI", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
986
1445
  ],
987
1446
  });
988
1447
  });
989
1448
  }
990
1449
 
991
- const FX_USAGE = "fx <currency>";
1450
+ const FX_USAGE = "fx <currency> | fx <base>/<quote>";
992
1451
 
993
1452
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
994
- const currency = requireOneArg(rawArgs.map((arg) => arg.trim().toUpperCase()), FX_USAGE, "currency", ctx);
1453
+ const raw = requireOneArg(rawArgs, FX_USAGE, "currency or pair", ctx);
995
1454
  await withMarketData(ctx, async (market) => {
996
- const baseCurrency = market.config.baseCurrency.trim().toUpperCase();
1455
+ const request = parseFxRequest(raw, market.config.baseCurrency);
1456
+ if ("error" in request) return ctx.fail(request.error, `Usage: gloomberb ${FX_USAGE}`);
1457
+ const { currency, baseCurrency } = request;
1458
+ const pair = `${currency}/${baseCurrency}`;
1459
+ const reasons = new Map<string, string>();
997
1460
  const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
998
- .load({ force: ctx.cliOptions.refresh }).catch(() => null);
999
- const legs = currency === baseCurrency ? [] : await Promise.all([load(currency), load(baseCurrency)]);
1000
- const [from, base] = legs;
1001
- const rate = currency === baseCurrency ? 1 : from && base ? from.value / base.value : Number.NaN;
1002
- if (!Number.isFinite(rate) || rate <= 0) ctx.fail(`Exchange rate unavailable for ${currency}/${baseCurrency}`);
1461
+ .load({ force: ctx.cliOptions.refresh }).catch((error) => {
1462
+ const reason = providerMissReason(error);
1463
+ if (reason) reasons.set(code, reason);
1464
+ return null;
1465
+ });
1466
+ // Every rate is a cross of two USD legs; a code priced in itself needs none.
1467
+ const codes = currency === baseCurrency ? [] : [currency, baseCurrency];
1468
+ const legs = await Promise.all(codes.map(load));
1469
+ const missing = codes.filter((_code, index) => !isUsableRate(legs[index]?.value));
1470
+ if (missing.length > 0) {
1471
+ // A reason the data service gave for the first missing leg says more than the generic line.
1472
+ const reason = missing.map((code) => reasons.get(code)).find(Boolean);
1473
+ if (reason) ctx.fail(reason);
1474
+ ctx.fail(
1475
+ `Exchange rate unavailable for ${missing.join(" and ")}.`,
1476
+ `${pair} is crossed from each currency's USD rate, and none came back for ${missing.join(" or ")}. Check the ISO code.`,
1477
+ );
1478
+ }
1479
+ const rate = codes.length === 0 ? 1 : crossRate(legs[0]?.value, legs[1]?.value);
1003
1480
  // A cross rate is only as current as its older leg.
1004
1481
  const observed = legs.flatMap((leg) => leg?.asOf ?? []);
1005
1482
  const asOf = observed.length > 0 ? new Date(Math.min(...observed)).toISOString() : null;
1006
1483
  const stale = legs.some((leg) => leg != null && (leg.staleAt <= Date.now() || leg.refreshError != null));
1007
- ctx.printResult({ data: [{ currency, baseCurrency, rate, asOf, stale }] }, {
1008
- layout: "record",
1484
+ const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate };
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),
1009
1495
  columns: [
1010
1496
  { key: "currency", header: "Currency" },
1011
1497
  { key: "baseCurrency", header: "Base" },
1012
1498
  { key: "rate", header: "Rate", align: "right" },
1013
- ...(asOf || stale ? [{
1014
- key: "asOf",
1015
- header: "As Of",
1016
- format: (value: unknown, row: { stale: boolean }) => {
1017
- const time = typeof value === "string" ? formatUtcTime(value) : "";
1018
- return row.stale ? cliStyles.warning(`${time} stale`.trim()) : time;
1019
- },
1020
- }] : []),
1499
+ { key: "asOf", header: "As Of" },
1500
+ { key: "stale", header: "Stale" },
1501
+ { key: "pair", header: "Pair" },
1502
+ { key: "inverse", header: "Inverse", align: "right" },
1021
1503
  ],
1022
1504
  });
1023
1505
  });
@@ -1032,24 +1514,55 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
1032
1514
  const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
1033
1515
  symbol, exchangeOption, services, ctx, { ownExchangeWins: symbols.length > 1 },
1034
1516
  )));
1035
- const events = await loadForListing(
1036
- listings, services, ctx,
1037
- () => services.dataProvider.getEarningsCalendar(listings.map((listing) => listing.key)),
1038
- (found) => found.length === 0,
1039
- );
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
+ ];
1040
1550
  ctx.printResult({
1041
1551
  data: events,
1042
1552
  freshness: rowsFreshness(earningsRows(events), { status: "not-a-feed", basis: "calendar", observedKey: "date", oldest: null }),
1553
+ ...(warnings.length > 0 ? { warnings } : {}),
1043
1554
  }, {
1044
1555
  rows: earningsRows,
1045
1556
  columns: [
1046
1557
  { key: "date", header: "Date" },
1558
+ { key: "status", header: "Status" },
1047
1559
  { key: "symbol", header: "Symbol" },
1048
1560
  { key: "name", header: "Name" },
1049
1561
  { key: "timing", header: "Timing" },
1050
1562
  { key: "epsEstimate", header: "EPS Est", align: "right", format: formatEpsCell },
1051
1563
  { key: "epsActual", header: "EPS", align: "right", format: formatEpsCell },
1052
1564
  ],
1565
+ ...(noCalendar ? { empty: noCalendar, footnote: noCalendar } : {}),
1053
1566
  });
1054
1567
  });
1055
1568
  }
@@ -1090,11 +1603,11 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1090
1603
  sections: [{
1091
1604
  title: "Bars",
1092
1605
  lines: [
1093
- `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.`,
1094
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.",
1095
1608
  ],
1096
1609
  }],
1097
- examples: ["history AAPL", "history BHP:ASX --range 5Y", "history ZAR=X --range ALL", "history AAPL --range 5Y --csv > aapl.csv"],
1610
+ examples: ["history AAPL", "history BHP:ASX --range 5Y", "history ZAR=X --range ALL", "history ZAR=X --tail 5", "history AAPL --range 5Y --csv > aapl.csv"],
1098
1611
  },
1099
1612
  unknownOptionHint: (flag) => DATE_WINDOW_OPTIONS.has(flag)
1100
1613
  ? `history takes a --range instead of dates: ${TIME_RANGES.join(", ")}. ${HISTORY_ALL_RANGE}.`
@@ -1103,15 +1616,46 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1103
1616
  },
1104
1617
  {
1105
1618
  name: "options",
1106
- description: "Fetch an options chain",
1619
+ description: "Fetch an options chain, or rank LEAPS across symbols",
1107
1620
  help: {
1108
1621
  group: CLI_COMMAND_GROUPS.research,
1109
- usage: ["options <symbol> [--expiration <unix>]"],
1622
+ usage: [OPTIONS_USAGE, LEAPS_USAGE],
1110
1623
  options: [
1111
- { flags: "--expiration <unix>", description: "Expiration as Unix seconds; defaults to the nearest one" },
1624
+ {
1625
+ flags: "--expiration <YYYY-MM-DD|unix>",
1626
+ description: "Expiration as a date (2028-01-21) or Unix seconds; defaults to the nearest one, and lists the others",
1627
+ },
1112
1628
  EXCHANGE_OPTION,
1629
+ { flags: "--leaps", description: "Rank contracts more than a year out across the symbols given, as a stock replacement" },
1630
+ { flags: "--side <calls|puts>", description: `With --leaps: the side to rank (default ${DEFAULT_LEAPS_CRITERIA.side}s)` },
1631
+ {
1632
+ flags: "--delta <low-high>",
1633
+ description: `With --leaps: absolute delta band (default ${DEFAULT_LEAPS_CRITERIA.minDelta.toFixed(2)}-${DEFAULT_LEAPS_CRITERIA.maxDelta.toFixed(2)})`,
1634
+ },
1635
+ {
1636
+ flags: "--max-spread <percent>",
1637
+ description: `With --leaps: widest bid/ask spread, in percent of the midpoint (default ${DEFAULT_LEAPS_CRITERIA.maxSpreadPercent})`,
1638
+ },
1639
+ { flags: "--min-oi <contracts>", description: `With --leaps: least open interest (default ${DEFAULT_LEAPS_CRITERIA.minOpenInterest})` },
1640
+ {
1641
+ flags: "--sort <order>",
1642
+ description: "With --leaps: carry (extrinsic per year, lowest first; the default), spread, oi, delta, expiry or symbol",
1643
+ },
1644
+ ],
1645
+ sections: [{
1646
+ title: "LEAPS",
1647
+ lines: [
1648
+ "--leaps reads every expiry more than a year out for each symbol and keeps the liquid contracts in the delta band, ranked by extrinsic per year: time value (midpoint less intrinsic) as a percent of spot, per year to expiry.",
1649
+ "A symbol without LEAPS, a quote or a chain is named in a warning and the others still rank. docs/research-data.md defines each figure.",
1650
+ ],
1651
+ }],
1652
+ examples: [
1653
+ "options AAPL",
1654
+ "options AAPL --expiration 2028-01-21",
1655
+ "options AAPL:NASDAQ --json",
1656
+ "options AAPL MSFT NVDA GOOGL AMZN META --leaps",
1657
+ "options SPY QQQ --leaps --side puts --delta 0.20-0.40 --csv",
1113
1658
  ],
1114
- examples: ["options AAPL", "options AAPL:NASDAQ --json"],
1115
1659
  },
1116
1660
  execute: runOptions,
1117
1661
  },
@@ -1219,7 +1763,13 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1219
1763
  { flags: "--history", description: "With --form 13d, 13g or all, every report newest first instead of the latest per filer" },
1220
1764
  EXCHANGE_OPTION,
1221
1765
  ],
1222
- 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"],
1223
1773
  },
1224
1774
  execute: (args, ctx) => runHolders(args, ctx, "holders"),
1225
1775
  },
@@ -1266,25 +1816,38 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1266
1816
  },
1267
1817
  {
1268
1818
  name: "news",
1269
- description: "Fetch market headlines, or news for one symbol",
1819
+ description: "Fetch market headlines, news for one symbol, or stories matching text",
1270
1820
  help: {
1271
1821
  group: CLI_COMMAND_GROUPS.companyData,
1272
- usage: ["news [symbol] [--feed <feed>]"],
1822
+ usage: ["news [symbol] [--feed <feed>]", "news <query>"],
1273
1823
  options: [
1274
1824
  { flags: "--feed <feed>", description: "latest, top, or breaking for market news (default latest)" },
1275
1825
  EXCHANGE_OPTION,
1276
1826
  ],
1277
- 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"],
1278
1834
  },
1279
1835
  execute: runNews,
1280
1836
  },
1281
1837
  {
1282
1838
  name: "fx",
1283
- description: "Convert a currency into your base currency",
1839
+ description: "Convert a currency into your base currency, or quote a pair such as USD/NGN",
1284
1840
  help: {
1285
1841
  group: CLI_COMMAND_GROUPS.markets,
1286
- usage: [FX_USAGE],
1287
- examples: ["fx EUR", "fx JPY --json"],
1842
+ usage: ["fx <currency>", "fx <base>/<quote>"],
1843
+ sections: [{
1844
+ title: "Direction",
1845
+ lines: [
1846
+ "A bare code is one unit of it in your base currency. A pair reads as markets quote it: USD/NGN is how many NGN one USD buys. Two codes neither of which is USD, such as ZAR/NGN, cross through their USD rates.",
1847
+ "The answer states both directions: 1 NGN = 0.000752791 USD (USD/NGN 1328.39). --json, --csv and --ndjson carry rate (base currency per currency), pair, inverse, asOf and stale.",
1848
+ ],
1849
+ }],
1850
+ examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx JPY --json"],
1288
1851
  },
1289
1852
  execute: runFx,
1290
1853
  },