gloomberb 0.16.3 → 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 (451) 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/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -7,11 +7,19 @@ 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
24
  import { isoDate, parsePositiveInt, rejectExtraArgs, requireOneArg, takeFlag, takeOption } from "./command-utils";
17
25
  import type { BuiltinCliCommandDef } from "../command-options";
@@ -32,9 +40,25 @@ import {
32
40
  recommendationTotal,
33
41
  targetUpside,
34
42
  } from "../../plugins/builtin/research/analyst-model";
35
- import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
36
43
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
44
+ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
45
+ import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
37
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";
38
62
  import type { CliResultColumn } from "../result";
39
63
  import {
40
64
  exportRowsTable,
@@ -60,11 +84,15 @@ import { fundamentalsFreshness, quotesFreshness, rowsFreshness } from "../freshn
60
84
  import {
61
85
  barHistoryFreshness,
62
86
  barResolutionFromDates,
87
+ cloudNewsFreshness,
88
+ cloudRealtimeAccess,
63
89
  REPORTED_DATA,
64
90
  SEC_FILINGS,
65
91
  } from "../../plugins/builtin/shared/report-freshness";
66
92
  import {
93
+ bareListingNote,
67
94
  EXCHANGE_OPTION,
95
+ isNoProviderError,
68
96
  listingHeading,
69
97
  listingIdentity,
70
98
  listingTitle,
@@ -72,15 +100,22 @@ import {
72
100
  loadForListing,
73
101
  loadListingQuote,
74
102
  notTradedMessage,
103
+ quoteBareListing,
75
104
  requireCliListing,
76
105
  type CliListing,
106
+ type ListingDataRequest,
77
107
  type ListingIdentity,
78
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";
79
112
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
80
113
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
81
114
  import { isUsListingExchange } from "../../utils/exchanges";
82
115
  import { nonUsSecListingVenue } from "../../utils/sec";
83
116
  import type { MarketContext } from "../types";
117
+ import { windowRows } from "../row-window";
118
+ import { crossRate, describeFxRate, isUsableRate, parseFxRequest } from "../fx-pair";
84
119
  import {
85
120
  holderListFacts,
86
121
  holderShareBasisMarker,
@@ -138,28 +173,67 @@ function listingMetadata(identity: ListingIdentity) {
138
173
  return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
139
174
  }
140
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.
141
178
  const QUOTE_LEAD_COLUMNS = [
142
- { key: "symbol", header: "Symbol" },
179
+ { key: "symbol", header: "Symbol", shrink: false },
143
180
  { key: "name", header: "Name" },
144
181
  {
145
182
  key: "price",
146
183
  header: "Last",
147
184
  align: "right" as const,
148
- format: (value: unknown, row: ReturnType<typeof quoteRows>[number]) => (
149
- 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 ?? "")
150
190
  ),
151
191
  },
152
192
  { key: "changePercent", header: "Chg%", align: "right" as const, format: formatChangePercentCell },
153
- { 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 },
154
197
  ];
155
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
+
156
221
  function quoteColumns() {
157
222
  return [
158
223
  ...QUOTE_LEAD_COLUMNS,
159
- { key: "currency", header: "Cur" },
160
- // Whether the price is real-time or delayed: a feed state, not where it came from.
161
- { key: "source", header: "Feed", format: (value: unknown) => value === "live" || value === "delayed" ? value : "" },
162
- { key: "updatedAt", header: "Updated" },
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 },
163
237
  ];
164
238
  }
165
239
 
@@ -167,9 +241,10 @@ function compareColumns() {
167
241
  return [
168
242
  ...QUOTE_LEAD_COLUMNS,
169
243
  { key: "previousClose", header: "Prev Close", align: "right" as const },
170
- { key: "dayRange", header: "Day Range", align: "right" as const },
171
- { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell },
172
- { key: "currency", header: "Cur" },
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 },
173
248
  ];
174
249
  }
175
250
 
@@ -178,11 +253,20 @@ function errorMessage(error: unknown): string | null {
178
253
  return error instanceof Error ? error.message : String(error);
179
254
  }
180
255
 
181
- 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()) {
182
264
  return results.map((result) => {
183
265
  const quote = result.quote;
184
- // Same price and move as the quote monitor: the live session's print against the daily reference.
185
- const display = getActiveQuoteDisplay(quote);
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);
186
270
  // An index level is in points, not in the currency its members trade in.
187
271
  const indexPoints = quote?.instrumentType?.trim().toUpperCase() === "INDEX";
188
272
  // Pad to two decimals so a column lines up, but never past the currency's minor unit (JPY has none).
@@ -204,9 +288,14 @@ function quoteRows(results: QuoteCliRecord[]) {
204
288
  priceBasis: quote?.priceBasis ?? null,
205
289
  instrumentType: quote?.instrumentType ?? null,
206
290
  change: display?.change ?? null,
207
- 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),
208
297
  session: quote?.marketState ? marketStateLabel(quote.marketState) : "",
209
- // 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.
210
299
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
211
300
  dayRange: quote?.low != null && quote.high != null
212
301
  ? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
@@ -215,6 +304,7 @@ function quoteRows(results: QuoteCliRecord[]) {
215
304
  currency: quote?.currency ?? "",
216
305
  providerId: quote?.providerId ?? "",
217
306
  source: quote?.dataSource ?? quote?.providerId ?? "",
307
+ feed: quoteFeedState(quote, now),
218
308
  updatedAt: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : "",
219
309
  error: result.error ?? "",
220
310
  };
@@ -345,22 +435,6 @@ function corporateActionRows(data: CorporateActionsData) {
345
435
  ].sort((left, right) => right.date.localeCompare(left.date));
346
436
  }
347
437
 
348
- function optionRows(chain: OptionsChain) {
349
- return [...chain.calls.map((contract) => ({ side: "call", ...contract })), ...chain.puts.map((contract) => ({ side: "put", ...contract }))]
350
- .map((contract) => ({
351
- side: contract.side,
352
- contract: contract.contractSymbol,
353
- strike: contract.strike,
354
- last: contract.lastPrice,
355
- bid: contract.bid,
356
- ask: contract.ask,
357
- volume: contract.volume,
358
- openInterest: contract.openInterest,
359
- iv: contract.impliedVolatility,
360
- expiration: new Date(contract.expiration * 1000).toISOString().slice(0, 10),
361
- }));
362
- }
363
-
364
438
  /**
365
439
  * After a US open, a chain whose latest trade predates it still carries the
366
440
  * prior session's quotes and volume. The delayed feed lags the open by about
@@ -375,11 +449,6 @@ function priorSessionChainWarning(chain: OptionsChain, exchange: string, now: nu
375
449
  return `No option trades this session yet (last trade ${chain.asOf})`;
376
450
  }
377
451
 
378
- function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid" | "ask"): string {
379
- const quote = optionQuoteSide({ bid: Number(row.bid), ask: Number(row.ask) }, side);
380
- return quote == null ? "—" : String(quote);
381
- }
382
-
383
452
  /** Per-share earnings to the cent, as reported; consensus averages carry more digits. */
384
453
  function formatEpsCell(value: unknown): string {
385
454
  return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
@@ -412,20 +481,25 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
412
481
  const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
413
482
  const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh });
414
483
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
415
- // A listing with no quote on an exchange its symbol is not listed on says so; venues are only looked up for those.
416
- const notTraded = await Promise.all(results.map((result, index) => (
417
- result.quote ? null : notTradedMessage(listingOf(result, index), market)
418
- )));
419
- if (listings.length === 1 && notTraded[0]) ctx.fail(notTraded[0]);
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);
420
488
  // Each row names its listing by key (SAN:EPA) and company, so the table needs no line above it.
421
- const data = results.map((result, index) => ({
489
+ const data = answers.map(({ result, listing, failure }) => ({
422
490
  target: result.target,
423
- 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),
424
494
  quote: result.quote,
425
- error: notTraded[index] ?? errorMessage(result.error),
495
+ error: failure?.oneLine ?? errorMessage(result.error),
426
496
  }));
427
- // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
428
- const notes = ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [];
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
+ ];
429
503
  ctx.printResult({
430
504
  data,
431
505
  warnings: notes.length > 0 ? notes : undefined,
@@ -437,6 +511,58 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
437
511
  });
438
512
  }
439
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
+
440
566
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
441
567
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
442
568
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
@@ -461,7 +587,9 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
461
587
  ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
462
588
  : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points) => ({ points, resolution: null }));
463
589
  const [{ points, resolution }, quote] = await Promise.all([
464
- 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
+ })
465
593
  .catch((error) => failHistory(error, listing.key, ctx)),
466
594
  loadListingQuote(market.dataProvider, listing),
467
595
  ]);
@@ -515,6 +643,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
515
643
  freshness,
516
644
  }, {
517
645
  heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
646
+ dateKey: "date",
518
647
  textColumns: [...priceColumns, flagColumn],
519
648
  // Every exported row says its currency and bar size, so it survives head and concatenation.
520
649
  columns: [
@@ -560,7 +689,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
560
689
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
561
690
  const financials = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
562
691
  listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
563
- ));
692
+ ), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
564
693
  const identity = listingIdentity(listing, financials.quote);
565
694
  const data: FinancialsCliData = {
566
695
  symbol: listing.key,
@@ -619,7 +748,8 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
619
748
  rejectExtraArgs(args, 1, { usage: "news [symbol] [--feed <feed>]", takes: "one symbol at most", advice: "Run it once per symbol." }, ctx);
620
749
  await withMarketData(ctx, async (market) => {
621
750
  const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
622
- 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;
623
753
  const loadNews = () => market.dataProvider.getNews({
624
754
  feed: feed ?? (listing ? "ticker" : "latest"),
625
755
  // Still set for news plugins that read the deprecated scope.
@@ -636,9 +766,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
636
766
  ctx.printResult({
637
767
  data: articles,
638
768
  metadata: { ticker: listing?.key ?? null, ...(identity ? listingMetadata(identity) : {}), feed: feed ?? null },
639
- freshness: rowsFreshness(newsRows(articles), {
640
- status: "not-a-feed", basis: "published stories", observedKey: "publishedAt", oldest: null,
641
- }),
769
+ freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
642
770
  }, {
643
771
  ...(identity ? { heading: listingHeading(identity) } : {}),
644
772
  rows: newsRows,
@@ -674,7 +802,8 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
674
802
 
675
803
  async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
676
804
  const args = [...rawArgs];
677
- 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);
678
807
  const exchangeOption = takeOption(args, "--exchange");
679
808
  const form = takeOption(args, "--form")?.trim() || null;
680
809
  const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
@@ -745,10 +874,14 @@ async function printBeneficialOwners(
745
874
  ctx: Parameters<CliCommandDef["execute"]>[1],
746
875
  ) {
747
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 } };
748
881
  const [payload, holders, quote] = await Promise.all([
749
- loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, { form: beneficialRouteForm(form), history })),
882
+ loadForListing(listing, market, ctx, () => fetchBeneficialOwners(symbol, request)),
750
883
  market.dataProvider.getHolders?.(symbol, exchange).catch(() => null) ?? null,
751
- loadListingQuote(market.dataProvider, listing),
884
+ quotePromise,
752
885
  ]);
753
886
  const identity = listingIdentity(listing, quote);
754
887
  const rows = buildBeneficialReportRows(payload, { history, holders });
@@ -804,13 +937,13 @@ async function runHolders(
804
937
  listing, market, ctx,
805
938
  () => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
806
939
  (found) => found.holders.length === 0,
940
+ { command: commandName, noun: "holder data" },
807
941
  ),
808
942
  loadListingQuote(market.dataProvider, listing),
809
943
  ]);
810
944
  const identity = listingIdentity(listing, quote);
811
945
  const rows = holderRows(data, ownerTypes);
812
- const limit = ctx.cliOptions.limit;
813
- const shown = limit == null ? rows.length : Math.min(limit, rows.length);
946
+ const shown = windowRows(rows, ctx.cliOptions).rows.length;
814
947
  const institutional = !ownerTypes?.has("insider");
815
948
  const total = institutional ? data.summary?.institutionsCount ?? null : null;
816
949
  const reportDate = sharedReportDate(rows);
@@ -878,7 +1011,10 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
878
1011
  await withMarketData(ctx, async (market) => {
879
1012
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
880
1013
  const [data, quote] = await Promise.all([
881
- 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
+ ),
882
1018
  loadListingQuote(market.dataProvider, listing),
883
1019
  ]);
884
1020
  const identity = listingIdentity(listing, quote);
@@ -915,7 +1051,10 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
915
1051
  await withMarketData(ctx, async (market) => {
916
1052
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
917
1053
  const [data, quote] = await Promise.all([
918
- 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
+ ),
919
1058
  loadListingQuote(market.dataProvider, listing),
920
1059
  ]);
921
1060
  const identity = listingIdentity(listing, quote);
@@ -939,85 +1078,168 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
939
1078
 
940
1079
  async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
941
1080
  const args = [...rawArgs];
1081
+ const leapsOptions = takeLeapsOptions(args);
942
1082
  const expiration = takeOption(args, "--expiration");
943
1083
  const exchangeOption = takeOption(args, "--exchange");
944
- const raw = requireOneArg(args, "options <symbol> [--expiration <unix>]", "symbol", 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
+ }
945
1110
  await withMarketData(ctx, async (market) => {
946
1111
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
947
1112
  const { symbol, exchange } = listing.request;
948
- const expirationDate = expiration == null ? undefined : Number(expiration);
1113
+ const refresh = ctx.cliOptions.refresh;
949
1114
  const quotePromise = loadListingQuote(market.dataProvider, listing);
950
- 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 () => {
951
1117
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
952
- .load({ force: ctx.cliOptions.refresh });
1118
+ .load({ force: refresh });
953
1119
  return {
954
1120
  result: cached,
955
1121
  chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expirationDate, {
956
- cacheMode: ctx.cliOptions.refresh ? "refresh" : "default",
1122
+ cacheMode: refresh ? "refresh" : "default",
957
1123
  }),
958
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>");
959
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
+ }
960
1143
  const identity = listingIdentity(listing, await quotePromise);
1144
+ const chain = chainWithModelFigures(loaded, inputs);
961
1145
  // A failed refresh falls back to the stored chain, which can be days old.
962
1146
  const refreshWarning = result?.refreshError == null ? null
963
1147
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
964
1148
  + (chain.asOf ? ` (last trade ${chain.asOf})` : "");
965
1149
  const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
966
- 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);
967
1154
  // Dated by the chain's last trade; a chain that failed to refresh is the stored one, stale.
968
1155
  const freshness = rowsFreshness([], { asOf: chain.asOf ?? null }, {
969
1156
  ...(chain.dataSource ? { dataSource: chain.dataSource } : {}),
970
1157
  ...(chain.delayMinutes != null ? { delayMinutes: chain.delayMinutes } : {}),
971
1158
  stale: refreshWarning != null,
972
1159
  });
973
- 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
+ }, {
974
1171
  heading: listingHeading(identity),
975
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)}.`,
976
1176
  columns: [
977
1177
  { key: "side", header: "Side" },
978
1178
  { key: "contract", header: "Contract", shrink: false },
979
- { 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 },
980
1181
  { key: "strike", header: "Strike", align: "right" },
981
1182
  { key: "last", header: "Last", align: "right" },
982
1183
  { key: "bid", header: "Bid", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "bid") },
983
1184
  { key: "ask", header: "Ask", align: "right", format: (_value, row) => formatOptionQuoteCell(row, "ask") },
984
- { key: "volume", header: "Vol", align: "right", format: formatCountCell },
985
- { key: "openInterest", header: "OI", align: "right", format: formatCountCell },
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 },
986
1189
  ],
987
1190
  });
988
1191
  });
989
1192
  }
990
1193
 
991
- const FX_USAGE = "fx <currency>";
1194
+ const FX_USAGE = "fx <currency> | fx <base>/<quote>";
992
1195
 
993
1196
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
994
- const currency = requireOneArg(rawArgs.map((arg) => arg.trim().toUpperCase()), FX_USAGE, "currency", ctx);
1197
+ const raw = requireOneArg(rawArgs, FX_USAGE, "currency or pair", ctx);
995
1198
  await withMarketData(ctx, async (market) => {
996
- 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>();
997
1204
  const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
998
- .load({ force: ctx.cliOptions.refresh }).catch(() => null);
999
- const legs = currency === baseCurrency ? [] : await Promise.all([load(currency), load(baseCurrency)]);
1000
- const [from, base] = legs;
1001
- const rate = currency === baseCurrency ? 1 : from && base ? from.value / base.value : Number.NaN;
1002
- if (!Number.isFinite(rate) || rate <= 0) ctx.fail(`Exchange rate unavailable for ${currency}/${baseCurrency}`);
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);
1003
1224
  // A cross rate is only as current as its older leg.
1004
1225
  const observed = legs.flatMap((leg) => leg?.asOf ?? []);
1005
1226
  const asOf = observed.length > 0 ? new Date(Math.min(...observed)).toISOString() : null;
1006
1227
  const stale = legs.some((leg) => leg != null && (leg.staleAt <= Date.now() || leg.refreshError != null));
1007
- ctx.printResult({ data: [{ currency, baseCurrency, rate, asOf, stale }] }, {
1008
- layout: "record",
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
+ },
1009
1235
  columns: [
1010
1236
  { key: "currency", header: "Currency" },
1011
1237
  { key: "baseCurrency", header: "Base" },
1012
1238
  { key: "rate", header: "Rate", align: "right" },
1013
- ...(asOf || stale ? [{
1014
- key: "asOf",
1015
- header: "As Of",
1016
- format: (value: unknown, row: { stale: boolean }) => {
1017
- const time = typeof value === "string" ? formatUtcTime(value) : "";
1018
- return row.stale ? cliStyles.warning(`${time} stale`.trim()) : time;
1019
- },
1020
- }] : []),
1239
+ { key: "asOf", header: "As Of" },
1240
+ { key: "stale", header: "Stale" },
1241
+ { key: "pair", header: "Pair" },
1242
+ { key: "inverse", header: "Inverse", align: "right" },
1021
1243
  ],
1022
1244
  });
1023
1245
  });
@@ -1094,7 +1316,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1094
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.",
1095
1317
  ],
1096
1318
  }],
1097
- examples: ["history AAPL", "history BHP:ASX --range 5Y", "history ZAR=X --range ALL", "history AAPL --range 5Y --csv > aapl.csv"],
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"],
1098
1320
  },
1099
1321
  unknownOptionHint: (flag) => DATE_WINDOW_OPTIONS.has(flag)
1100
1322
  ? `history takes a --range instead of dates: ${TIME_RANGES.join(", ")}. ${HISTORY_ALL_RANGE}.`
@@ -1103,15 +1325,46 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1103
1325
  },
1104
1326
  {
1105
1327
  name: "options",
1106
- description: "Fetch an options chain",
1328
+ description: "Fetch an options chain, or rank LEAPS across symbols",
1107
1329
  help: {
1108
1330
  group: CLI_COMMAND_GROUPS.research,
1109
- usage: ["options <symbol> [--expiration <unix>]"],
1331
+ usage: [OPTIONS_USAGE, LEAPS_USAGE],
1110
1332
  options: [
1111
- { 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
+ },
1112
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",
1113
1367
  ],
1114
- examples: ["options AAPL", "options AAPL:NASDAQ --json"],
1115
1368
  },
1116
1369
  execute: runOptions,
1117
1370
  },
@@ -1280,11 +1533,18 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1280
1533
  },
1281
1534
  {
1282
1535
  name: "fx",
1283
- 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",
1284
1537
  help: {
1285
1538
  group: CLI_COMMAND_GROUPS.markets,
1286
- usage: [FX_USAGE],
1287
- 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"],
1288
1548
  },
1289
1549
  execute: runFx,
1290
1550
  },