gloomberb 0.16.2 → 0.16.4

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 (527) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -7,14 +7,23 @@ import type {
7
7
  CorporateActionsData,
8
8
  HolderData,
9
9
  OptionsChain,
10
+ Quote,
10
11
  TickerFinancials,
11
12
  } from "../../types/financials";
12
13
  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";
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, requireArg, takeFlag, takeOption } from "./command-utils";
17
- import { CLI_COMMAND_GROUPS } from "../help";
24
+ import { isoDate, parsePositiveInt, rejectExtraArgs, requireOneArg, takeFlag, takeOption } from "./command-utils";
25
+ import type { BuiltinCliCommandDef } from "../command-options";
26
+ import { CLI_COMMAND_GROUPS, TABLE_SECTION_OPTION } from "../help";
18
27
  import {
19
28
  formatChangePercentCell,
20
29
  formatCountCell,
@@ -31,22 +40,59 @@ import {
31
40
  recommendationTotal,
32
41
  targetUpside,
33
42
  } from "../../plugins/builtin/research/analyst-model";
34
- import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
35
43
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
36
- import { renderFundamentalsReport } from "./ticker";
37
- import { historyPriceDecimals, historyRows } from "../history-rows";
44
+ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
45
+ import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
46
+ import { fundamentalsReportTables, renderFundamentalsReport } from "./ticker";
47
+ import {
48
+ chainHasExpiry,
49
+ chainWithModelFigures,
50
+ formatExpiryList,
51
+ loadOptionModelInputs,
52
+ formatOptionDeltaCell,
53
+ formatOptionIvCell,
54
+ formatOptionQuoteCell,
55
+ missingExpiryMessage,
56
+ OPTION_MODEL_RATE,
57
+ OPTIONS_USAGE,
58
+ optionRows,
59
+ parseOptionExpiration,
60
+ } from "./options-chain";
61
+ import { DEFAULT_LEAPS_CRITERIA, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions } from "./options-leaps";
62
+ import type { CliResultColumn } from "../result";
63
+ import {
64
+ exportRowsTable,
65
+ reportFooterLines,
66
+ selectReportTables,
67
+ type CliReportTables,
68
+ } from "../report-tables";
69
+ import {
70
+ historyFacts,
71
+ historyFlagNote,
72
+ historyAllRangeYears,
73
+ historyIntervalsByRange,
74
+ historyNotes,
75
+ historyPriceDecimals,
76
+ historyRows,
77
+ historyUnit,
78
+ } from "../history-rows";
38
79
  import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
39
80
  import { formatUtcTime } from "../../utils/utc-time";
81
+ import { isFiniteNumber } from "../../utils/guards";
40
82
  import { exportedFundamentals } from "../../utils/price-earnings";
41
83
  import { fundamentalsFreshness, quotesFreshness, rowsFreshness } from "../freshness";
42
84
  import {
43
85
  barHistoryFreshness,
44
86
  barResolutionFromDates,
87
+ cloudNewsFreshness,
88
+ cloudRealtimeAccess,
45
89
  REPORTED_DATA,
46
90
  SEC_FILINGS,
47
91
  } from "../../plugins/builtin/shared/report-freshness";
48
92
  import {
93
+ bareListingNote,
49
94
  EXCHANGE_OPTION,
95
+ isNoProviderError,
50
96
  listingHeading,
51
97
  listingIdentity,
52
98
  listingTitle,
@@ -54,22 +100,32 @@ import {
54
100
  loadForListing,
55
101
  loadListingQuote,
56
102
  notTradedMessage,
103
+ quoteBareListing,
57
104
  requireCliListing,
58
105
  type CliListing,
106
+ type ListingDataRequest,
59
107
  type ListingIdentity,
60
108
  } from "../listing-arg";
109
+ import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "../index-roots";
110
+ import { providerMissReason } from "../../sources/provider-errors";
111
+ import { isNotATickerMessage } from "../not-a-ticker";
61
112
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
62
113
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
63
114
  import { isUsListingExchange } from "../../utils/exchanges";
64
115
  import { nonUsSecListingVenue } from "../../utils/sec";
65
116
  import type { MarketContext } from "../types";
117
+ import { windowRows } from "../row-window";
118
+ import { crossRate, describeFxRate, isUsableRate, parseFxRequest } from "../fx-pair";
66
119
  import {
67
120
  holderListFacts,
121
+ holderShareBasisMarker,
122
+ holderShareBasisNote,
68
123
  holderValueBasis,
69
124
  moneyColumnHeader,
70
125
  nonUsHolderCaveat,
71
126
  sharedReportDate,
72
127
  } from "../../plugins/builtin/holders/report-header";
128
+ import { formatHolderOwnershipPercent, holderValueCurrency } from "../../plugins/builtin/holders/format";
73
129
  import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
74
130
  import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
75
131
  import {
@@ -117,28 +173,67 @@ function listingMetadata(identity: ListingIdentity) {
117
173
  return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
118
174
  }
119
175
 
176
+ // Fitted to a narrow width, the columns marked optional go first (higher dropPriority first), then Name is cut short.
177
+ // What a user types or reads the price by stays whole.
120
178
  const QUOTE_LEAD_COLUMNS = [
121
- { key: "symbol", header: "Symbol" },
179
+ { key: "symbol", header: "Symbol", shrink: false },
122
180
  { key: "name", header: "Name" },
123
181
  {
124
182
  key: "price",
125
183
  header: "Last",
126
184
  align: "right" as const,
127
- format: (value: unknown, row: ReturnType<typeof quoteRows>[number]) => (
128
- row.error && !value ? cliStyles.danger("unavailable") : String(value ?? "")
185
+ format: (value: unknown, row: QuoteRow) => (
186
+ row.error && !value
187
+ // A typo is not an outage: say what is wrong with the symbol itself.
188
+ ? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
189
+ : String(value ?? "")
129
190
  ),
130
191
  },
131
192
  { key: "changePercent", header: "Chg%", align: "right" as const, format: formatChangePercentCell },
132
- { key: "session", header: "Session" },
193
+ // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
194
+ extendedColumn("PRE"),
195
+ extendedColumn("POST"),
196
+ { key: "session", header: "Session", shrink: false },
133
197
  ];
134
198
 
199
+ type QuoteRow = ReturnType<typeof quoteRows>[number];
200
+
201
+ /** The pre-market or after-hours print and its move from the regular close, in its own column. */
202
+ function extendedColumn(session: ExtendedSession) {
203
+ return {
204
+ key: session === "PRE" ? "preMarket" : "afterHours",
205
+ header: EXTENDED_SESSION_LABELS[session],
206
+ align: "right" as const,
207
+ value: (row: QuoteRow) => row.extendedSession === session && row.extendedPrice
208
+ ? [row.extendedPrice, row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
209
+ : "",
210
+ format: (value: unknown, row: QuoteRow) => (
211
+ value ? [row.extendedPrice, formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
212
+ ),
213
+ };
214
+ }
215
+
216
+ /** A percent to two decimals, as the table prints it. */
217
+ function roundedPercent(value: number | undefined): number | null {
218
+ return value == null ? null : Number(value.toFixed(2));
219
+ }
220
+
135
221
  function quoteColumns() {
136
222
  return [
137
223
  ...QUOTE_LEAD_COLUMNS,
138
- { key: "currency", header: "Cur" },
139
- // Whether the price is real-time or delayed: a feed state, not where it came from.
140
- { key: "source", header: "Feed", format: (value: unknown) => value === "live" || value === "delayed" ? value : "" },
141
- { key: "updatedAt", header: "Updated" },
224
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1 },
225
+ // Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
226
+ // row says so here, so the closing line's "N of M stale" always points at rows the table marks.
227
+ {
228
+ key: "feed",
229
+ header: "Feed",
230
+ shrink: false,
231
+ optional: true,
232
+ dropPriority: 2,
233
+ format: (value: unknown) => value === "stale" ? cliStyles.warning(value) : String(value ?? ""),
234
+ },
235
+ // In a narrow terminal the closing line's as-of stands in for each row's.
236
+ { key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
142
237
  ];
143
238
  }
144
239
 
@@ -146,9 +241,10 @@ function compareColumns() {
146
241
  return [
147
242
  ...QUOTE_LEAD_COLUMNS,
148
243
  { key: "previousClose", header: "Prev Close", align: "right" as const },
149
- { key: "dayRange", header: "Day Range", align: "right" as const },
150
- { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell },
151
- { key: "currency", header: "Cur" },
244
+ // A narrow terminal drops these before it cuts the names short.
245
+ { key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
246
+ { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
247
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3 },
152
248
  ];
153
249
  }
154
250
 
@@ -157,11 +253,20 @@ function errorMessage(error: unknown): string | null {
157
253
  return error instanceof Error ? error.message : String(error);
158
254
  }
159
255
 
160
- function quoteRows(results: QuoteCliRecord[]) {
256
+ /** What the Feed cell says: stale wins over the feed's own live or delayed, as the closing line counts it. */
257
+ function quoteFeedState(quote: QuoteCliRecord["quote"], now: number): "stale" | "live" | "delayed" | "" {
258
+ if (!quote) return "";
259
+ if (isQuoteStaleForCurrentSession(quote, now)) return "stale";
260
+ return quote.dataSource === "live" || quote.dataSource === "delayed" ? quote.dataSource : "";
261
+ }
262
+
263
+ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
161
264
  return results.map((result) => {
162
265
  const quote = result.quote;
163
- // Same price and move as the quote monitor: the live session's print against the daily reference.
164
- const display = getActiveQuoteDisplay(quote);
266
+ // Last and Chg% are the regular session, as `ticker` and the quote monitor read it; a
267
+ // pre-market or after-hours print is its own column, measured from that session's close.
268
+ const display = getRegularSessionDisplay(quote);
269
+ const extended = getExtendedSessionDisplay(quote);
165
270
  // An index level is in points, not in the currency its members trade in.
166
271
  const indexPoints = quote?.instrumentType?.trim().toUpperCase() === "INDEX";
167
272
  // Pad to two decimals so a column lines up, but never past the currency's minor unit (JPY has none).
@@ -183,9 +288,14 @@ function quoteRows(results: QuoteCliRecord[]) {
183
288
  priceBasis: quote?.priceBasis ?? null,
184
289
  instrumentType: quote?.instrumentType ?? null,
185
290
  change: display?.change ?? null,
186
- changePercent: display?.changePercent == null ? null : Number(display.changePercent.toFixed(2)),
291
+ changePercent: roundedPercent(display?.changePercent),
292
+ extendedSession: extended?.session ?? null,
293
+ extendedPrice: price(extended?.price),
294
+ rawExtendedPrice: extended?.price ?? null,
295
+ extendedChange: extended?.change ?? null,
296
+ extendedChangePercent: roundedPercent(extended?.changePercent),
187
297
  session: quote?.marketState ? marketStateLabel(quote.marketState) : "",
188
- // The close the shown move is measured from; a pre-market move starts at the last close.
298
+ // The close the shown move is measured from: the one before the session Last is.
189
299
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
190
300
  dayRange: quote?.low != null && quote.high != null
191
301
  ? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
@@ -194,6 +304,7 @@ function quoteRows(results: QuoteCliRecord[]) {
194
304
  currency: quote?.currency ?? "",
195
305
  providerId: quote?.providerId ?? "",
196
306
  source: quote?.dataSource ?? quote?.providerId ?? "",
307
+ feed: quoteFeedState(quote, now),
197
308
  updatedAt: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : "",
198
309
  error: result.error ?? "",
199
310
  };
@@ -250,6 +361,7 @@ function holderRows(data: HolderData, ownerTypes?: Set<string>) {
250
361
  value: holder.value ?? null,
251
362
  percentHeld: holder.percentHeld ?? null,
252
363
  changeShares: holder.changeShares ?? null,
364
+ shareBasis: holder.shareBasis ?? null,
253
365
  }));
254
366
  }
255
367
 
@@ -323,22 +435,6 @@ function corporateActionRows(data: CorporateActionsData) {
323
435
  ].sort((left, right) => right.date.localeCompare(left.date));
324
436
  }
325
437
 
326
- function optionRows(chain: OptionsChain) {
327
- return [...chain.calls.map((contract) => ({ side: "call", ...contract })), ...chain.puts.map((contract) => ({ side: "put", ...contract }))]
328
- .map((contract) => ({
329
- side: contract.side,
330
- contract: contract.contractSymbol,
331
- strike: contract.strike,
332
- last: contract.lastPrice,
333
- bid: contract.bid,
334
- ask: contract.ask,
335
- volume: contract.volume,
336
- openInterest: contract.openInterest,
337
- iv: contract.impliedVolatility,
338
- expiration: new Date(contract.expiration * 1000).toISOString().slice(0, 10),
339
- }));
340
- }
341
-
342
438
  /**
343
439
  * After a US open, a chain whose latest trade predates it still carries the
344
440
  * prior session's quotes and volume. The delayed feed lags the open by about
@@ -353,11 +449,6 @@ function priorSessionChainWarning(chain: OptionsChain, exchange: string, now: nu
353
449
  return `No option trades this session yet (last trade ${chain.asOf})`;
354
450
  }
355
451
 
356
- function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid" | "ask"): string {
357
- const quote = optionQuoteSide({ bid: Number(row.bid), ask: Number(row.ask) }, side);
358
- return quote == null ? "—" : String(quote);
359
- }
360
-
361
452
  /** Per-share earnings to the cent, as reported; consensus averages carry more digits. */
362
453
  function formatEpsCell(value: unknown): string {
363
454
  return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
@@ -390,20 +481,25 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
390
481
  const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
391
482
  const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh });
392
483
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
393
- // A listing with no quote on an exchange its symbol is not listed on says so; venues are only looked up for those.
394
- const notTraded = await Promise.all(results.map((result, index) => (
395
- result.quote ? null : notTradedMessage(listingOf(result, index), market)
396
- )));
397
- if (listings.length === 1 && notTraded[0]) ctx.fail(notTraded[0]);
484
+ const request = { command: commandName, noun: "quote" };
485
+ const answers = await Promise.all(results.map((result, index) => answerQuote(result, listingOf(result, index), market, request, ctx.cliOptions.refresh)));
486
+ const failed = answers[0]?.failure;
487
+ if (listings.length === 1 && failed) ctx.fail(failed.message, failed.details);
398
488
  // Each row names its listing by key (SAN:EPA) and company, so the table needs no line above it.
399
- const data = results.map((result, index) => ({
489
+ const data = answers.map(({ result, listing, failure }) => ({
400
490
  target: result.target,
401
- listing: listingMetadata(listingIdentity(listingOf(result, index), result.quote)),
491
+ listing: listingMetadata(listingIdentity(listing, result.quote)),
492
+ // The figures the table shows, beside the quote as the source sent it.
493
+ ...getQuoteSessionFields(result.quote),
402
494
  quote: result.quote,
403
- error: notTraded[index] ?? errorMessage(result.error),
495
+ error: failure?.oneLine ?? errorMessage(result.error),
404
496
  }));
405
- // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
406
- const notes = ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [];
497
+ const notes = [
498
+ // Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
499
+ ...answers.flatMap((answer) => answer.notes),
500
+ // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
501
+ ...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
502
+ ];
407
503
  ctx.printResult({
408
504
  data,
409
505
  warnings: notes.length > 0 ? notes : undefined,
@@ -415,17 +511,74 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
415
511
  });
416
512
  }
417
513
 
514
+ interface QuoteAnswer {
515
+ result: QuoteBatchResult;
516
+ /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
517
+ listing: CliListing;
518
+ /** Why there is no quote, when there is more to say than the source's own error. */
519
+ failure?: { message: string; details?: string; oneLine: string };
520
+ notes: string[];
521
+ }
522
+
523
+ /**
524
+ * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
525
+ * is not listed on says so. A bare symbol the data service read on its home
526
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
527
+ * quoted on its listings (`quoteBareListing`), else names them; a bare index
528
+ * root (VIX) also names its index (^VIX). Venues are only looked up for a
529
+ * symbol with no quote, so a request that succeeds costs no search.
530
+ */
531
+ async function answerQuote(
532
+ result: QuoteBatchResult,
533
+ listing: CliListing,
534
+ market: MarketContext,
535
+ request: ListingDataRequest,
536
+ refresh: boolean,
537
+ ): Promise<QuoteAnswer> {
538
+ const root = listing.exchange ? null : indexRootFor(listing.symbol);
539
+ // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
540
+ const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
541
+ ? [indexRootAlsoLine(root, request.command)] : [];
542
+ if (result.quote) return { result, listing, notes: [bareListingNote(listing, result.quote) ?? [], also(result.quote)].flat() };
543
+ const notTraded = await notTradedMessage(listing, market);
544
+ if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
545
+ if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
546
+ const other = await quoteBareListing(listing, market, request, { refresh });
547
+ if (other?.kind === "quoted") {
548
+ return {
549
+ result: { target: other.listing.request, quote: other.quote },
550
+ listing: other.listing,
551
+ notes: [other.note, ...also(other.quote)],
552
+ };
553
+ }
554
+ const indexLine = root ? indexRootTryLine(root, request.command) : null;
555
+ if (!other && !indexLine) return { result, listing, notes: [] };
556
+ const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
557
+ const details = [other?.details, indexLine].filter(Boolean).join("\n");
558
+ const oneLine = [
559
+ message,
560
+ other ? `Other listings: ${other.others.join(", ")}.` : "",
561
+ indexLine ?? "",
562
+ ].filter(Boolean).join(" ");
563
+ return { result, listing, failure: { message, details, oneLine }, notes: [] };
564
+ }
565
+
418
566
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
419
567
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
420
568
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
421
569
  return ctx.fail(errorMessage(error) ?? `No history available for ${symbol}`, CRYPTO_BOARD_HINT);
422
570
  }
423
571
 
572
+ const HISTORY_USAGE = "history <symbol> [--range <range>]";
573
+ const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
574
+ /** Options people reach for to ask for dates, which history answers with a range. */
575
+ const DATE_WINDOW_OPTIONS = new Set(["--from", "--to", "--start", "--end", "--since", "--until", "--date", "--start-date", "--end-date"]);
576
+
424
577
  async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
425
578
  const args = [...rawArgs];
426
579
  const range = parseRange(takeOption(args, "--range"), ctx);
427
580
  const requestedExchange = takeOption(args, "--exchange");
428
- const raw = requireArg(args[0], "Usage: gloomberb history <symbol> [--range <range>]", ctx);
581
+ const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
429
582
  await withMarketData(ctx, async (market) => {
430
583
  const listing = await requireCliListing(raw, requestedExchange, market, ctx);
431
584
  const { symbol, exchange } = listing.request;
@@ -434,12 +587,18 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
434
587
  ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
435
588
  : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points) => ({ points, resolution: null }));
436
589
  const [{ points, resolution }, quote] = await Promise.all([
437
- loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0)
590
+ loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
591
+ command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history",
592
+ })
438
593
  .catch((error) => failHistory(error, listing.key, ctx)),
439
594
  loadListingQuote(market.dataProvider, listing),
440
595
  ]);
441
596
  const identity = listingIdentity(listing, quote);
442
- const data = historyRows(points, resolution);
597
+ // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
598
+ const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
599
+ const unit = historyUnit(listing.symbol, listed ?? quote);
600
+ const data = historyRows(points, resolution, unit.currency);
601
+ const interval = data[0]?.interval ?? null;
443
602
  const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
444
603
  const price = (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
445
604
  // Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
@@ -450,17 +609,48 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
450
609
  observedKey: "date",
451
610
  oldest: null,
452
611
  });
453
- ctx.printResult({ data, metadata: { ...listingMetadata(identity), range, resolution }, freshness }, {
454
- heading: listingHeading(identity),
612
+ const notes = [...historyNotes(range, interval, data[0]?.date), historyFlagNote(data) ?? []].flat();
613
+ const priceColumns: CliResultColumn[] = [
614
+ intraday
615
+ ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
616
+ : { key: "date", header: "Date" },
617
+ { key: "open", header: "Open", align: "right", format: price },
618
+ { key: "high", header: "High", align: "right", format: price },
619
+ { key: "low", header: "Low", align: "right", format: price },
620
+ { key: "close", header: "Close", align: "right", format: price },
621
+ { key: "volume", header: "Volume", align: "right", format: formatCountCell },
622
+ ];
623
+ // Text shows a flag only on a bar that has one; the column is dropped when none does.
624
+ const flagColumn: CliResultColumn = {
625
+ key: "flag", header: "Flag", format: (value) => typeof value === "string" ? cliStyles.warning(value) : "",
626
+ };
627
+ ctx.printResult({
628
+ data,
629
+ metadata: {
630
+ ...listingMetadata(identity),
631
+ range,
632
+ resolution,
633
+ requestedRange: range,
634
+ currency: unit.currency,
635
+ unit: unit.unit,
636
+ interval,
637
+ firstDate: data[0]?.date ?? null,
638
+ lastDate: data.at(-1)?.date ?? null,
639
+ bars: data.length,
640
+ asOf: freshness?.asOf ?? null,
641
+ },
642
+ ...(notes.length > 0 ? { warnings: notes } : {}),
643
+ freshness,
644
+ }, {
645
+ heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
646
+ dateKey: "date",
647
+ textColumns: [...priceColumns, flagColumn],
648
+ // Every exported row says its currency and bar size, so it survives head and concatenation.
455
649
  columns: [
456
- intraday
457
- ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
458
- : { key: "date", header: "Date" },
459
- { key: "open", header: "Open", align: "right", format: price },
460
- { key: "high", header: "High", align: "right", format: price },
461
- { key: "low", header: "Low", align: "right", format: price },
462
- { key: "close", header: "Close", align: "right", format: price },
463
- { key: "volume", header: "Volume", align: "right", format: formatCountCell },
650
+ ...priceColumns,
651
+ { key: "currency", header: "Currency" },
652
+ { key: "interval", header: "Interval" },
653
+ flagColumn,
464
654
  ],
465
655
  });
466
656
  });
@@ -468,16 +658,38 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
468
658
 
469
659
  type FinancialsView = "statements" | "fundamentals" | "valuation";
470
660
 
661
+ const STATEMENT_COLUMNS: CliResultColumn<ReturnType<typeof financialStatementRows>[number]>[] = [
662
+ { key: "date", header: "Date" },
663
+ { key: "revenue", header: "Revenue", align: "right", format: (value) => value == null ? "" : formatCompact(Number(value)) },
664
+ { key: "grossProfit", header: "Gross", align: "right", format: (value) => value == null ? "" : formatCompact(Number(value)) },
665
+ { key: "operatingIncome", header: "Op Inc", align: "right", format: (value) => value == null ? "" : formatCompact(Number(value)) },
666
+ { key: "netIncome", header: "Net Inc", align: "right", format: (value) => value == null ? "" : formatCompact(Number(value)) },
667
+ { key: "eps", header: "EPS", align: "right", format: (value) => value == null ? "" : formatPerShareNumber(Number(value)) },
668
+ { key: "currency", header: "Cur" },
669
+ ];
670
+
471
671
  async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], view: FinancialsView) {
472
672
  const args = [...rawArgs];
473
673
  const exchangeOption = takeOption(args, "--exchange");
674
+ const sectionFlag = args.some((arg) => arg === "--section" || arg.startsWith("--section="));
675
+ const section = takeOption(args, "--section");
474
676
  const commandName = view === "statements" ? "financials" : view;
475
- const raw = requireArg(args[0], `Usage: gloomberb ${commandName} <symbol>`, ctx);
677
+ const tabular = ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson";
678
+ if (sectionFlag && !tabular) ctx.fail("--section picks one table of --csv or --ndjson output.");
679
+ if (sectionFlag && !section?.trim()) ctx.fail("--section needs a section title or number.");
680
+ const raw = requireOneArg(args, `${commandName} <symbol>`, "symbol", ctx);
681
+ const selectTables = (tables: CliReportTables) => {
682
+ try {
683
+ return selectReportTables(tables, section);
684
+ } catch (error) {
685
+ return ctx.fail(error instanceof Error ? error.message : String(error));
686
+ }
687
+ };
476
688
  await withMarketData(ctx, async (market) => {
477
689
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
478
690
  const financials = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
479
691
  listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
480
- ));
692
+ ), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
481
693
  const identity = listingIdentity(listing, financials.quote);
482
694
  const data: FinancialsCliData = {
483
695
  symbol: listing.key,
@@ -486,13 +698,28 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
486
698
  ...financials,
487
699
  };
488
700
  if (view !== "statements") {
701
+ const freshness = fundamentalsFreshness(financials);
489
702
  ctx.printResult({
490
703
  // JSON reads a multiple over a loss as null with its reason, never as a number.
491
704
  data: { ...data, fundamentals: exportedFundamentals(data.fundamentals) },
492
- freshness: fundamentalsFreshness(financials),
493
- }, { text: () => renderFundamentalsReport(data, view) });
705
+ freshness,
706
+ }, {
707
+ text: () => renderFundamentalsReport(data, view),
708
+ ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
709
+ });
494
710
  return;
495
711
  }
712
+ const rows = financialStatementRows(data);
713
+ const freshness = rowsFreshness(rows, {
714
+ ...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
715
+ });
716
+ // NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
717
+ const tables = tabular
718
+ ? selectTables({
719
+ tables: [exportRowsTable("Annual statements", STATEMENT_COLUMNS, rows)],
720
+ footer: reportFooterLines({ freshness }),
721
+ })
722
+ : undefined;
496
723
  ctx.printResult({
497
724
  data,
498
725
  metadata: {
@@ -503,21 +730,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
503
730
  fundamentals: exportedFundamentals(financials.fundamentals),
504
731
  profile: financials.profile,
505
732
  },
506
- freshness: rowsFreshness(financialStatementRows(data), {
507
- ...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
508
- }),
733
+ freshness,
509
734
  }, {
510
735
  heading: listingHeading(identity),
511
- rows: financialStatementRows,
512
- columns: [
513
- { key: "date", header: "Date" },
514
- { key: "revenue", header: "Revenue", align: "right", value: (row) => row.revenue == null ? "" : formatCompact(Number(row.revenue)) },
515
- { key: "grossProfit", header: "Gross", align: "right", value: (row) => row.grossProfit == null ? "" : formatCompact(Number(row.grossProfit)) },
516
- { key: "operatingIncome", header: "Op Inc", align: "right", value: (row) => row.operatingIncome == null ? "" : formatCompact(Number(row.operatingIncome)) },
517
- { key: "netIncome", header: "Net Inc", align: "right", value: (row) => row.netIncome == null ? "" : formatCompact(Number(row.netIncome)) },
518
- { key: "eps", header: "EPS", align: "right", format: (value) => value == null ? "" : formatPerShareNumber(Number(value)) },
519
- { key: "currency", header: "Cur" },
520
- ],
736
+ rows: () => rows,
737
+ columns: STATEMENT_COLUMNS,
738
+ ...(tables && ctx.cliOptions.format === "csv" ? { tables } : {}),
521
739
  });
522
740
  });
523
741
  }
@@ -527,9 +745,11 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
527
745
  const feed = parseNewsFeed(takeOption(args, "--feed"));
528
746
  const exchangeOption = takeOption(args, "--exchange");
529
747
  if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloomberb news <symbol> --exchange <code>");
748
+ rejectExtraArgs(args, 1, { usage: "news [symbol] [--feed <feed>]", takes: "one symbol at most", advice: "Run it once per symbol." }, ctx);
530
749
  await withMarketData(ctx, async (market) => {
531
750
  const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
532
- const limit = ctx.cliOptions.limit ?? 20;
751
+ // Stories come newest first, so the newest n are the first n either way.
752
+ const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
533
753
  const loadNews = () => market.dataProvider.getNews({
534
754
  feed: feed ?? (listing ? "ticker" : "latest"),
535
755
  // Still set for news plugins that read the deprecated scope.
@@ -546,9 +766,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
546
766
  ctx.printResult({
547
767
  data: articles,
548
768
  metadata: { ticker: listing?.key ?? null, ...(identity ? listingMetadata(identity) : {}), feed: feed ?? null },
549
- freshness: rowsFreshness(newsRows(articles), {
550
- status: "not-a-feed", basis: "published stories", observedKey: "publishedAt", oldest: null,
551
- }),
769
+ freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
552
770
  }, {
553
771
  ...(identity ? { heading: listingHeading(identity) } : {}),
554
772
  rows: newsRows,
@@ -584,10 +802,11 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
584
802
 
585
803
  async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
586
804
  const args = [...rawArgs];
587
- const count = parsePositiveInt(takeOption(args, "--count"), ctx.cliOptions.limit ?? 15, "Count", ctx);
805
+ // Filings come newest first, so --tail asks for the same first n as --limit.
806
+ const count = parsePositiveInt(takeOption(args, "--count"), ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 15, "Count", ctx);
588
807
  const exchangeOption = takeOption(args, "--exchange");
589
808
  const form = takeOption(args, "--form")?.trim() || null;
590
- const raw = requireArg(args[0], "Usage: gloomberb filings <symbol>", ctx);
809
+ const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
591
810
  await withMarketData(ctx, async (market) => {
592
811
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
593
812
  const { symbol, exchange } = listing.request;
@@ -655,10 +874,14 @@ async function printBeneficialOwners(
655
874
  ctx: Parameters<CliCommandDef["execute"]>[1],
656
875
  ) {
657
876
  const { symbol, exchange } = listing.request;
877
+ // A listing outside the US sends its company to the lookup, which needs its quote first.
878
+ const quotePromise = loadListingQuote(market.dataProvider, listing);
879
+ const abroad = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
880
+ const request = { form: beneficialRouteForm(form), history, listing: { exchange, name: abroad?.name ?? undefined } };
658
881
  const [payload, holders, quote] = await Promise.all([
659
- loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, { form: beneficialRouteForm(form), history })),
882
+ loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, request)),
660
883
  market.dataProvider.getHolders?.(symbol, exchange).catch(() => null) ?? null,
661
- loadListingQuote(market.dataProvider, listing),
884
+ quotePromise,
662
885
  ]);
663
886
  const identity = listingIdentity(listing, quote);
664
887
  const rows = buildBeneficialReportRows(payload, { history, holders });
@@ -702,7 +925,7 @@ async function runHolders(
702
925
  const exchangeOption = takeOption(args, "--exchange");
703
926
  const formOption = commandName === "holders" ? takeOption(args, "--form") : undefined;
704
927
  const history = commandName === "holders" && takeFlag(args, "--history");
705
- const raw = requireArg(args[0], `Usage: gloomberb ${commandName} <symbol>`, ctx);
928
+ const raw = requireOneArg(args, `${commandName} <symbol>`, "symbol", ctx);
706
929
  const form = formOption == null ? "13f" : parseHolderForm(formOption);
707
930
  if (!form) ctx.fail(`Unknown form "${formOption}".`, `Use one of ${HOLDER_FORMS.join(", ")}.`);
708
931
  if (form === "13f" && history) ctx.fail("--history lists 13D/13G reports.", "Add --form 13d, 13g or all.");
@@ -714,26 +937,33 @@ async function runHolders(
714
937
  listing, market, ctx,
715
938
  () => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
716
939
  (found) => found.holders.length === 0,
940
+ { command: commandName, noun: "holder data" },
717
941
  ),
718
942
  loadListingQuote(market.dataProvider, listing),
719
943
  ]);
720
944
  const identity = listingIdentity(listing, quote);
721
945
  const rows = holderRows(data, ownerTypes);
722
- const limit = ctx.cliOptions.limit;
723
- const shown = limit == null ? rows.length : Math.min(limit, rows.length);
946
+ const shown = windowRows(rows, ctx.cliOptions).rows.length;
724
947
  const institutional = !ownerTypes?.has("insider");
725
948
  const total = institutional ? data.summary?.institutionsCount ?? null : null;
726
949
  const reportDate = sharedReportDate(rows);
727
- const valueBasis = rows.length > 0 ? holderValueBasis(reportDate) : null;
950
+ // A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
951
+ const valueCurrency = holderValueCurrency(data);
952
+ const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
728
953
  const positionsBasis = rows.length > 0 ? nonUsHolderCaveat(identity.exchange || data.exchange, data.currency) : null;
954
+ const shareBasisNote = holderShareBasisNote(data, rows);
955
+ const markedRows = rows.some((row) => holderShareBasisMarker(row));
729
956
  // The heading names the listing; its unit, date and how much of the list follows on the same line.
730
- const facts = holderListFacts({ currency: data.currency, asOf: data.asOf, shown, reported: rows.length, total });
957
+ const facts = holderListFacts({
958
+ currency: valueCurrency, listingCurrency: data.currency, asOf: data.asOf, shown, reported: rows.length, total,
959
+ });
731
960
  ctx.printResult({
732
961
  data,
733
962
  metadata: {
734
963
  ...listingMetadata(identity),
735
964
  summary: data.summary,
736
965
  currency: data.currency ?? null,
966
+ valueCurrency: valueCurrency ?? null,
737
967
  asOf: data.asOf ?? null,
738
968
  shown,
739
969
  reported: rows.length,
@@ -750,16 +980,23 @@ async function runHolders(
750
980
  { key: "name", header: "Holder" },
751
981
  { key: "reportDate", header: "Date" },
752
982
  { key: "shares", header: "Shares", align: "right", format: formatCountCell },
983
+ // Marks the rows of a home line held as receipts; absent when no row is.
984
+ ...(markedRows ? [{ key: "shareBasis", header: "Basis", value: (row: Record<string, unknown>) => holderShareBasisMarker(row) }] : []),
753
985
  {
754
986
  key: "value",
755
- header: moneyColumnHeader("Value", data.currency),
987
+ header: moneyColumnHeader("Value", valueCurrency),
756
988
  align: "right",
757
989
  value: (row) => row.value == null ? "" : formatCompact(Number(row.value)),
758
990
  },
759
- { key: "percentHeld", header: "% Held", align: "right", format: formatFractionPercentCell },
991
+ {
992
+ key: "percentHeld",
993
+ header: "% Held",
994
+ align: "right",
995
+ format: (value) => isFiniteNumber(value) ? formatHolderOwnershipPercent(value) : "",
996
+ },
760
997
  ],
761
998
  summary: (holderData: HolderData) => [
762
- [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? ""].filter(Boolean).join(" "),
999
+ [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? ""].filter(Boolean).join(" "),
763
1000
  commandName === "insider" && ownerTypes ? insiderSummary(holderData, ownerTypes) : "",
764
1001
  ].filter(Boolean).join("\n"),
765
1002
  empty: `No holders reported for ${listingTitle(identity)}.`,
@@ -770,11 +1007,14 @@ async function runHolders(
770
1007
  async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
771
1008
  const args = [...rawArgs];
772
1009
  const exchangeOption = takeOption(args, "--exchange");
773
- const raw = requireArg(args[0], "Usage: gloomberb analyst <symbol>", ctx);
1010
+ const raw = requireOneArg(args, "analyst <symbol>", "symbol", ctx);
774
1011
  await withMarketData(ctx, async (market) => {
775
1012
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
776
1013
  const [data, quote] = await Promise.all([
777
- loadForListing(listing, market, ctx, () => market.dataProvider.getAnalystResearch(listing.request.symbol, listing.request.exchange)),
1014
+ loadForListing(
1015
+ listing, market, ctx, () => market.dataProvider.getAnalystResearch(listing.request.symbol, listing.request.exchange),
1016
+ undefined, { command: "analyst", noun: "analyst research" },
1017
+ ),
778
1018
  loadListingQuote(market.dataProvider, listing),
779
1019
  ]);
780
1020
  const identity = listingIdentity(listing, quote);
@@ -807,11 +1047,14 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
807
1047
  async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
808
1048
  const args = [...rawArgs];
809
1049
  const exchangeOption = takeOption(args, "--exchange");
810
- const raw = requireArg(args[0], "Usage: gloomberb events <symbol>", ctx);
1050
+ const raw = requireOneArg(args, "events <symbol>", "symbol", ctx);
811
1051
  await withMarketData(ctx, async (market) => {
812
1052
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
813
1053
  const [data, quote] = await Promise.all([
814
- loadForListing(listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange)),
1054
+ loadForListing(
1055
+ listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
1056
+ undefined, { command: "events", noun: "corporate events" },
1057
+ ),
815
1058
  loadListingQuote(market.dataProvider, listing),
816
1059
  ]);
817
1060
  const identity = listingIdentity(listing, quote);
@@ -835,83 +1078,168 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
835
1078
 
836
1079
  async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
837
1080
  const args = [...rawArgs];
1081
+ const leapsOptions = takeLeapsOptions(args);
838
1082
  const expiration = takeOption(args, "--expiration");
839
1083
  const exchangeOption = takeOption(args, "--exchange");
840
- const raw = requireArg(args[0], "Usage: gloomberb options <symbol> [--expiration <unix>]", ctx);
1084
+ if (leapsOptions.leaps) {
1085
+ if (expiration != null || rawArgs.includes("--expiration")) {
1086
+ ctx.fail("--leaps reads every expiry more than a year out; drop --expiration.", `Usage: gloomberb ${LEAPS_USAGE}`);
1087
+ }
1088
+ const criteria = parseLeapsCriteria(leapsOptions.raw);
1089
+ if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloomberb ${LEAPS_USAGE}`);
1090
+ const symbols = normalizeSymbols(args);
1091
+ if (symbols.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloomberb ${LEAPS_USAGE}`);
1092
+ await withMarketData(ctx, (market) => runLeapsScreen(symbols, exchangeOption, criteria, market, ctx));
1093
+ return;
1094
+ }
1095
+ if (leapsOptions.given.length > 0) {
1096
+ ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloomberb ${LEAPS_USAGE}`);
1097
+ }
1098
+ const raw = requireOneArg(args, OPTIONS_USAGE, "symbol", ctx);
1099
+ let expirationDate: number | undefined;
1100
+ if (expiration != null || rawArgs.includes("--expiration")) {
1101
+ const parsed = expiration == null ? null : parseOptionExpiration(expiration);
1102
+ if (parsed == null) {
1103
+ ctx.fail(
1104
+ expiration == null ? "--expiration needs a date." : `Invalid --expiration "${expiration}".`,
1105
+ `Use YYYY-MM-DD (2028-01-21) or Unix seconds (1832025600).\nUsage: gloomberb ${OPTIONS_USAGE}`,
1106
+ );
1107
+ }
1108
+ expirationDate = parsed;
1109
+ }
841
1110
  await withMarketData(ctx, async (market) => {
842
1111
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
843
1112
  const { symbol, exchange } = listing.request;
844
- const expirationDate = expiration == null ? undefined : Number(expiration);
1113
+ const refresh = ctx.cliOptions.refresh;
845
1114
  const quotePromise = loadListingQuote(market.dataProvider, listing);
846
- const { result, chain } = await loadForListing(listing, market, ctx, async () => {
1115
+ const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1116
+ const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
847
1117
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
848
- .load({ force: ctx.cliOptions.refresh });
1118
+ .load({ force: refresh });
849
1119
  return {
850
1120
  result: cached,
851
1121
  chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expirationDate, {
852
- cacheMode: ctx.cliOptions.refresh ? "refresh" : "default",
1122
+ cacheMode: refresh ? "refresh" : "default",
853
1123
  }),
854
1124
  };
1125
+ }, undefined, { command: "options", noun: "options chain" }).catch((error: unknown) => {
1126
+ if (!isOptionsUnavailableError(error)) throw error;
1127
+ // A future or commodity with no chain of its own: name the listed fund whose options stand in.
1128
+ const alternative = listedOptionsAlternative(listing.key);
1129
+ return ctx.fail(optionsUnavailableTitle(listing.key), alternative
1130
+ ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloomberb options ${alternative.symbol}.`
1131
+ + ` Strategy payoffs: gloomberb fn OSA ${alternative.symbol}`
1132
+ : "Strategy payoffs on a listed underlying: gloomberb fn OSA <ticker>");
855
1133
  });
1134
+ const inputs = await inputsPromise;
1135
+ if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
1136
+ // An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
1137
+ const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
1138
+ .getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
1139
+ .then((fallback) => fallback.expirationDates, () => []);
1140
+ const miss = missingExpiryMessage(listing.key, expirationDate, listed);
1141
+ ctx.fail(miss.message, miss.details);
1142
+ }
856
1143
  const identity = listingIdentity(listing, await quotePromise);
1144
+ const chain = chainWithModelFigures(loaded, inputs);
857
1145
  // A failed refresh falls back to the stored chain, which can be days old.
858
1146
  const refreshWarning = result?.refreshError == null ? null
859
1147
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
860
1148
  + (chain.asOf ? ` (last trade ${chain.asOf})` : "");
861
1149
  const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
862
- const warnings = refreshWarning ? [refreshWarning] : sessionWarning ? [sessionWarning] : undefined;
1150
+ const modelWarning = inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
1151
+ ? `IV and delta need a current ${listing.key} quote; those columns are blank`
1152
+ : null;
1153
+ const warnings = [refreshWarning ?? sessionWarning, modelWarning].filter((warning): warning is string => warning != null);
863
1154
  // Dated by the chain's last trade; a chain that failed to refresh is the stored one, stale.
864
1155
  const freshness = rowsFreshness([], { asOf: chain.asOf ?? null }, {
865
1156
  ...(chain.dataSource ? { dataSource: chain.dataSource } : {}),
866
1157
  ...(chain.delayMinutes != null ? { delayMinutes: chain.delayMinutes } : {}),
867
1158
  stale: refreshWarning != null,
868
1159
  });
869
- ctx.printResult({ data: chain, metadata: { ...listingMetadata(identity), expirations: chain.expirationDates }, warnings, freshness }, {
1160
+ ctx.printResult({
1161
+ data: chain,
1162
+ metadata: {
1163
+ ...listingMetadata(identity),
1164
+ expirations: chain.expirationDates,
1165
+ // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
1166
+ model: { spot: inputs.spot ?? null, dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE },
1167
+ },
1168
+ ...(warnings.length > 0 ? { warnings } : {}),
1169
+ freshness,
1170
+ }, {
870
1171
  heading: listingHeading(identity),
871
1172
  rows: optionRows,
1173
+ // Choosing an expiry needs the list; once one is chosen the table is that expiry.
1174
+ summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
1175
+ empty: `No option contracts for ${listingTitle(identity)}.`,
872
1176
  columns: [
873
1177
  { key: "side", header: "Side" },
874
1178
  { key: "contract", header: "Contract", shrink: false },
875
- { key: "expiration", header: "Expiry" },
1179
+ // The contract symbol already carries the date, so this goes first when the table is narrow.
1180
+ { key: "expiration", header: "Expiry", optional: true, dropPriority: 2 },
876
1181
  { key: "strike", header: "Strike", align: "right" },
877
1182
  { key: "last", header: "Last", align: "right" },
878
1183
  { key: "bid", header: "Bid", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "bid") },
879
1184
  { key: "ask", header: "Ask", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "ask") },
880
- { key: "volume", header: "Vol", align: "right", format: formatCountCell },
881
- { key: "openInterest", header: "OI", align: "right", format: formatCountCell },
1185
+ { key: "iv", header: "IV", align: "right", format: formatOptionIvCell },
1186
+ { key: "delta", header: "Delta", align: "right", format: formatOptionDeltaCell },
1187
+ { key: "volume", header: "Vol", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
1188
+ { key: "openInterest", header: "OI", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
882
1189
  ],
883
1190
  });
884
1191
  });
885
1192
  }
886
1193
 
1194
+ const FX_USAGE = "fx <currency> | fx <base>/<quote>";
1195
+
887
1196
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
888
- const currency = requireArg(rawArgs[0]?.trim().toUpperCase(), "Usage: gloomberb fx <currency>", ctx);
1197
+ const raw = requireOneArg(rawArgs, FX_USAGE, "currency or pair", ctx);
889
1198
  await withMarketData(ctx, async (market) => {
890
- const baseCurrency = market.config.baseCurrency.trim().toUpperCase();
1199
+ const request = parseFxRequest(raw, market.config.baseCurrency);
1200
+ if ("error" in request) return ctx.fail(request.error, `Usage: gloomberb ${FX_USAGE}`);
1201
+ const { currency, baseCurrency } = request;
1202
+ const pair = `${currency}/${baseCurrency}`;
1203
+ const reasons = new Map<string, string>();
891
1204
  const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
892
- .load({ force: ctx.cliOptions.refresh }).catch(() => null);
893
- const legs = currency === baseCurrency ? [] : await Promise.all([load(currency), load(baseCurrency)]);
894
- const [from, base] = legs;
895
- const rate = currency === baseCurrency ? 1 : from && base ? from.value / base.value : Number.NaN;
896
- if (!Number.isFinite(rate) || rate <= 0) ctx.fail(`Exchange rate unavailable for ${currency}/${baseCurrency}`);
1205
+ .load({ force: ctx.cliOptions.refresh }).catch((error) => {
1206
+ const reason = providerMissReason(error);
1207
+ if (reason) reasons.set(code, reason);
1208
+ return null;
1209
+ });
1210
+ // Every rate is a cross of two USD legs; a code priced in itself needs none.
1211
+ const codes = currency === baseCurrency ? [] : [currency, baseCurrency];
1212
+ const legs = await Promise.all(codes.map(load));
1213
+ const missing = codes.filter((_code, index) => !isUsableRate(legs[index]?.value));
1214
+ if (missing.length > 0) {
1215
+ // A reason the data service gave for the first missing leg says more than the generic line.
1216
+ const reason = missing.map((code) => reasons.get(code)).find(Boolean);
1217
+ if (reason) ctx.fail(reason);
1218
+ ctx.fail(
1219
+ `Exchange rate unavailable for ${missing.join(" and ")}.`,
1220
+ `${pair} is crossed from each currency's USD rate, and none came back for ${missing.join(" or ")}. Check the ISO code.`,
1221
+ );
1222
+ }
1223
+ const rate = codes.length === 0 ? 1 : crossRate(legs[0]?.value, legs[1]?.value);
897
1224
  // A cross rate is only as current as its older leg.
898
1225
  const observed = legs.flatMap((leg) => leg?.asOf ?? []);
899
1226
  const asOf = observed.length > 0 ? new Date(Math.min(...observed)).toISOString() : null;
900
1227
  const stale = legs.some((leg) => leg != null && (leg.staleAt <= Date.now() || leg.refreshError != null));
901
- ctx.printResult({ data: [{ currency, baseCurrency, rate, asOf, stale }] }, {
902
- layout: "record",
1228
+ const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate };
1229
+ ctx.printResult({ data: [row] }, {
1230
+ text: () => {
1231
+ const time = asOf ? `As of ${formatUtcTime(asOf)}` : "";
1232
+ const when = stale ? cliStyles.warning(time ? `${time}, stale` : "Stale") : time && cliStyles.muted(time);
1233
+ return [describeFxRate(request, rate), when].filter(Boolean).join("\n");
1234
+ },
903
1235
  columns: [
904
1236
  { key: "currency", header: "Currency" },
905
1237
  { key: "baseCurrency", header: "Base" },
906
1238
  { key: "rate", header: "Rate", align: "right" },
907
- ...(asOf || stale ? [{
908
- key: "asOf",
909
- header: "As Of",
910
- format: (value: unknown, row: { stale: boolean }) => {
911
- const time = typeof value === "string" ? formatUtcTime(value) : "";
912
- return row.stale ? cliStyles.warning(`${time} stale`.trim()) : time;
913
- },
914
- }] : []),
1239
+ { key: "asOf", header: "As Of" },
1240
+ { key: "stale", header: "Stale" },
1241
+ { key: "pair", header: "Pair" },
1242
+ { key: "inverse", header: "Inverse", align: "right" },
915
1243
  ],
916
1244
  });
917
1245
  });
@@ -948,7 +1276,7 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
948
1276
  });
949
1277
  }
950
1278
 
951
- export const marketDataCliCommands: CliCommandDef[] = [
1279
+ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
952
1280
  {
953
1281
  name: "quote",
954
1282
  description: "Show the latest price for one or more symbols",
@@ -976,26 +1304,67 @@ export const marketDataCliCommands: CliCommandDef[] = [
976
1304
  description: "Fetch open, high, low, close, and volume over a range",
977
1305
  help: {
978
1306
  group: CLI_COMMAND_GROUPS.research,
979
- usage: ["history <symbol> [--range <range>]"],
1307
+ usage: [HISTORY_USAGE],
980
1308
  options: [
981
- { flags: "--range <range>", description: `${TIME_RANGES.join(", ")} (default 1Y)` },
1309
+ { flags: "--range <range>", description: `${TIME_RANGES.join(", ")} (default 1Y); ${HISTORY_ALL_RANGE}` },
982
1310
  EXCHANGE_OPTION,
983
1311
  ],
984
- examples: ["history AAPL", "history BHP:ASX --range 5Y", "history AAPL --range 5Y --csv > aapl.csv"],
1312
+ sections: [{
1313
+ title: "Bars",
1314
+ lines: [
1315
+ `Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for.`,
1316
+ "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. Every exported row (--csv, --ndjson, --json) carries currency, interval and flag.",
1317
+ ],
1318
+ }],
1319
+ 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"],
985
1320
  },
1321
+ unknownOptionHint: (flag) => DATE_WINDOW_OPTIONS.has(flag)
1322
+ ? `history takes a --range instead of dates: ${TIME_RANGES.join(", ")}. ${HISTORY_ALL_RANGE}.`
1323
+ : null,
986
1324
  execute: runHistory,
987
1325
  },
988
1326
  {
989
1327
  name: "options",
990
- description: "Fetch an options chain",
1328
+ description: "Fetch an options chain, or rank LEAPS across symbols",
991
1329
  help: {
992
1330
  group: CLI_COMMAND_GROUPS.research,
993
- usage: ["options <symbol> [--expiration <unix>]"],
1331
+ usage: [OPTIONS_USAGE, LEAPS_USAGE],
994
1332
  options: [
995
- { flags: "--expiration <unix>", description: "Expiration as Unix seconds; defaults to the nearest one" },
1333
+ {
1334
+ flags: "--expiration <YYYY-MM-DD|unix>",
1335
+ description: "Expiration as a date (2028-01-21) or Unix seconds; defaults to the nearest one, and lists the others",
1336
+ },
996
1337
  EXCHANGE_OPTION,
1338
+ { flags: "--leaps", description: "Rank contracts more than a year out across the symbols given, as a stock replacement" },
1339
+ { flags: "--side <calls|puts>", description: `With --leaps: the side to rank (default ${DEFAULT_LEAPS_CRITERIA.side}s)` },
1340
+ {
1341
+ flags: "--delta <low-high>",
1342
+ description: `With --leaps: absolute delta band (default ${DEFAULT_LEAPS_CRITERIA.minDelta.toFixed(2)}-${DEFAULT_LEAPS_CRITERIA.maxDelta.toFixed(2)})`,
1343
+ },
1344
+ {
1345
+ flags: "--max-spread <percent>",
1346
+ description: `With --leaps: widest bid/ask spread, in percent of the midpoint (default ${DEFAULT_LEAPS_CRITERIA.maxSpreadPercent})`,
1347
+ },
1348
+ { flags: "--min-oi <contracts>", description: `With --leaps: least open interest (default ${DEFAULT_LEAPS_CRITERIA.minOpenInterest})` },
1349
+ {
1350
+ flags: "--sort <order>",
1351
+ description: "With --leaps: carry (extrinsic per year, lowest first; the default), spread, oi, delta, expiry or symbol",
1352
+ },
1353
+ ],
1354
+ sections: [{
1355
+ title: "LEAPS",
1356
+ lines: [
1357
+ "--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.",
1358
+ "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.",
1359
+ ],
1360
+ }],
1361
+ examples: [
1362
+ "options AAPL",
1363
+ "options AAPL --expiration 2028-01-21",
1364
+ "options AAPL:NASDAQ --json",
1365
+ "options AAPL MSFT NVDA GOOGL AMZN META --leaps",
1366
+ "options SPY QQQ --leaps --side puts --delta 0.20-0.40 --csv",
997
1367
  ],
998
- examples: ["options AAPL", "options AAPL:NASDAQ --json"],
999
1368
  },
1000
1369
  execute: runOptions,
1001
1370
  },
@@ -1032,8 +1401,8 @@ export const marketDataCliCommands: CliCommandDef[] = [
1032
1401
  help: {
1033
1402
  group: CLI_COMMAND_GROUPS.companyData,
1034
1403
  usage: ["financials <symbol>"],
1035
- options: [EXCHANGE_OPTION],
1036
- examples: ["financials MSFT", "financials SAN:EPA", "financials MSFT --json"],
1404
+ options: [EXCHANGE_OPTION, TABLE_SECTION_OPTION],
1405
+ examples: ["financials MSFT", "financials SAN:EPA", "financials MSFT --json", "financials MSFT --csv > msft.csv"],
1037
1406
  },
1038
1407
  execute: (args, ctx) => runFinancials(args, ctx, "statements"),
1039
1408
  },
@@ -1043,8 +1412,8 @@ export const marketDataCliCommands: CliCommandDef[] = [
1043
1412
  help: {
1044
1413
  group: CLI_COMMAND_GROUPS.companyData,
1045
1414
  usage: ["fundamentals <symbol>"],
1046
- options: [EXCHANGE_OPTION],
1047
- examples: ["fundamentals NVDA", "fundamentals ASML:AMS"],
1415
+ options: [EXCHANGE_OPTION, TABLE_SECTION_OPTION],
1416
+ examples: ["fundamentals NVDA", "fundamentals ASML:AMS", "fundamentals NVDA --csv --section fundamentals"],
1048
1417
  },
1049
1418
  execute: (args, ctx) => runFinancials(args, ctx, "fundamentals"),
1050
1419
  },
@@ -1054,8 +1423,8 @@ export const marketDataCliCommands: CliCommandDef[] = [
1054
1423
  help: {
1055
1424
  group: CLI_COMMAND_GROUPS.companyData,
1056
1425
  usage: ["valuation <symbol>"],
1057
- options: [EXCHANGE_OPTION],
1058
- examples: ["valuation NVDA", "valuation BP:LSE"],
1426
+ options: [EXCHANGE_OPTION, TABLE_SECTION_OPTION],
1427
+ examples: ["valuation NVDA", "valuation BP:LSE", "valuation NVDA --csv"],
1059
1428
  },
1060
1429
  execute: (args, ctx) => runFinancials(args, ctx, "valuation"),
1061
1430
  },
@@ -1164,11 +1533,18 @@ export const marketDataCliCommands: CliCommandDef[] = [
1164
1533
  },
1165
1534
  {
1166
1535
  name: "fx",
1167
- description: "Convert a currency into your base currency",
1536
+ description: "Convert a currency into your base currency, or quote a pair such as USD/NGN",
1168
1537
  help: {
1169
1538
  group: CLI_COMMAND_GROUPS.markets,
1170
- usage: ["fx <currency>"],
1171
- examples: ["fx EUR", "fx JPY --json"],
1539
+ usage: ["fx <currency>", "fx <base>/<quote>"],
1540
+ sections: [{
1541
+ title: "Direction",
1542
+ lines: [
1543
+ "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.",
1544
+ "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.",
1545
+ ],
1546
+ }],
1547
+ examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx JPY --json"],
1172
1548
  },
1173
1549
  execute: runFx,
1174
1550
  },