gloomberb 0.18.3 → 0.18.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 (886) hide show
  1. package/README.md +8 -8
  2. package/README.zh-CN.md +11 -11
  3. package/bin/{gloomberb → gloom} +3 -1
  4. package/package.json +6 -2
  5. package/src/account/checkout-handoff.tsx +225 -0
  6. package/src/account/device-signin-dialog.tsx +2 -2
  7. package/src/account/device-signin.ts +2 -2
  8. package/src/account/upgrade-dialog.tsx +1 -1
  9. package/src/api-client/askg.ts +2 -2
  10. package/src/api-client/auth.ts +2 -2
  11. package/src/api-client/central-bank-rates.ts +6 -0
  12. package/src/api-client/collections.ts +5 -1
  13. package/src/api-client/cot.ts +2 -0
  14. package/src/api-client/crypto-etf-flows.ts +37 -0
  15. package/src/api-client/crypto-markets.ts +60 -0
  16. package/src/api-client/data.ts +84 -5
  17. package/src/api-client/doe.ts +15 -0
  18. package/src/api-client/equity-screener.ts +28 -0
  19. package/src/api-client/exchange-announcements.ts +38 -0
  20. package/src/api-client/fund-facts.ts +39 -0
  21. package/src/api-client/futures-curve.ts +9 -1
  22. package/src/api-client/hiring.ts +301 -0
  23. package/src/api-client/index.ts +22 -5
  24. package/src/api-client/market-discovery.ts +14 -0
  25. package/src/api-client/members.ts +6 -0
  26. package/src/api-client/mna.ts +6 -0
  27. package/src/api-client/paths.ts +18 -1
  28. package/src/api-client/request.ts +21 -4
  29. package/src/api-client/short-side-publications.ts +37 -0
  30. package/src/api-client/socket.ts +64 -4
  31. package/src/api-client/swap-curve.ts +35 -0
  32. package/src/api-client/themes.ts +7 -1
  33. package/src/api-client/types.ts +90 -2
  34. package/src/app/pane-runtime/plugin-bindings.ts +9 -2
  35. package/src/app/runtime/desktop-deeplink.ts +5 -5
  36. package/src/app/runtime/plugin-bindings.ts +2 -2
  37. package/src/app/runtime/startup.ts +13 -0
  38. package/src/app/star-prompt/status-line.tsx +2 -2
  39. package/src/architecture/core-builtin-allowlist.json +5 -2
  40. package/src/architecture/ui-literals-allowlist.json +3 -3
  41. package/src/architecture/ui-tokens-allowlist.json +1 -1
  42. package/src/brokers/broker-portfolio-sync.ts +37 -0
  43. package/src/brokers/directory.ts +11 -0
  44. package/src/brokers/logos.ts +3 -1
  45. package/src/brokers/signed-in/adapter.ts +304 -26
  46. package/src/brokers/signed-in/client.ts +14 -2
  47. package/src/brokers/signed-in/connect.ts +10 -2
  48. package/src/brokers/sync-broker-instance.ts +9 -1
  49. package/src/capabilities/chart-series.ts +22 -6
  50. package/src/cli/bank-section.ts +1 -1
  51. package/src/cli/base-converter.ts +23 -14
  52. package/src/cli/central-bank-events.ts +1 -1
  53. package/src/cli/command-options.ts +59 -4
  54. package/src/cli/commands/api.ts +9 -6
  55. package/src/cli/commands/broker.ts +2 -2
  56. package/src/cli/commands/command-utils.ts +2 -2
  57. package/src/cli/commands/crypto-hints.ts +7 -20
  58. package/src/cli/commands/fred-series.ts +4 -2
  59. package/src/cli/commands/keybindings.ts +2 -2
  60. package/src/cli/commands/market.ts +1117 -264
  61. package/src/cli/commands/news-hints.ts +2 -2
  62. package/src/cli/commands/news-search.ts +59 -6
  63. package/src/cli/commands/options-leaps.ts +41 -26
  64. package/src/cli/commands/overview.ts +121 -48
  65. package/src/cli/commands/plugins.ts +9 -9
  66. package/src/cli/commands/remote.ts +13 -10
  67. package/src/cli/commands/search.ts +64 -44
  68. package/src/cli/commands/system.ts +27 -19
  69. package/src/cli/commands/themes.ts +1 -1
  70. package/src/cli/commands/ticker.ts +192 -47
  71. package/src/cli/context.ts +3 -2
  72. package/src/cli/desktop-pane-shot-view.ts +2 -2
  73. package/src/cli/desktop-pane-shot.ts +1 -1
  74. package/src/cli/earnings-calendar.ts +4 -4
  75. package/src/cli/entry.ts +18 -1
  76. package/src/cli/errors.ts +20 -2
  77. package/src/cli/exit-codes.ts +1 -1
  78. package/src/cli/export-source.ts +10 -4
  79. package/src/cli/freshness.ts +2 -8
  80. package/src/cli/fx-forward.ts +130 -0
  81. package/src/cli/fx-pair.ts +10 -9
  82. package/src/cli/glossary.ts +64 -20
  83. package/src/cli/help.ts +19 -13
  84. package/src/cli/history-export.ts +126 -0
  85. package/src/cli/history-rows.ts +196 -43
  86. package/src/cli/index-roots.ts +5 -45
  87. package/src/cli/index.ts +24 -18
  88. package/src/cli/known-gaps.ts +118 -25
  89. package/src/cli/listing-arg.ts +300 -41
  90. package/src/cli/name-entries.ts +30 -18
  91. package/src/cli/native-smoke.ts +2 -0
  92. package/src/cli/not-a-ticker.ts +19 -9
  93. package/src/cli/opentui-libc.ts +5 -0
  94. package/src/cli/options.ts +12 -0
  95. package/src/cli/pane-functions/capabilities.ts +75 -20
  96. package/src/cli/pane-functions/catalog.ts +24 -23
  97. package/src/cli/pane-functions/discovery.ts +1 -1
  98. package/src/cli/pane-functions/dom.ts +10 -7
  99. package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
  100. package/src/cli/pane-functions/freshness.ts +174 -23
  101. package/src/cli/pane-functions/function-help.ts +66 -27
  102. package/src/cli/pane-functions/headless.ts +24 -11
  103. package/src/cli/pane-functions/index.ts +18 -16
  104. package/src/cli/pane-functions/options.ts +22 -2
  105. package/src/cli/pane-functions/report-notices.ts +12 -4
  106. package/src/cli/pane-functions/report.ts +5 -5
  107. package/src/cli/pane-functions/resolver.ts +47 -10
  108. package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
  109. package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
  110. package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
  111. package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
  112. package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
  113. package/src/cli/pane-functions/screenshot-payload.ts +453 -0
  114. package/src/cli/pane-functions/screenshot-render.ts +316 -0
  115. package/src/cli/pane-functions/screenshot-types.ts +162 -0
  116. package/src/cli/pane-functions/shot-options.ts +8 -3
  117. package/src/cli/peso-dollar-rates.ts +79 -0
  118. package/src/cli/quote-answer.ts +38 -3
  119. package/src/cli/report-tables.ts +61 -16
  120. package/src/cli/restore-plugins.ts +29 -14
  121. package/src/cli/result.ts +111 -22
  122. package/src/cli/series-search.ts +5 -5
  123. package/src/cli/statement-filings.ts +19 -0
  124. package/src/cli/suffixed-listing-rows.ts +49 -0
  125. package/src/cli/symbol-args.ts +50 -15
  126. package/src/cli/symbol-cells.ts +120 -0
  127. package/src/cli/symbol-list.ts +198 -68
  128. package/src/cli/symbol-rows.ts +131 -25
  129. package/src/cli/ticker-resolution.ts +108 -11
  130. package/src/cli/treasury-yields.ts +5 -5
  131. package/src/cli/types.ts +2 -0
  132. package/src/cli/uncovered-instrument.ts +3 -3
  133. package/src/cli/unknown-command.ts +6 -6
  134. package/src/components/chart/composite/chart-layout.ts +131 -0
  135. package/src/components/chart/composite/composite-chart.tsx +373 -16
  136. package/src/components/chart/composite/interactions.ts +7 -2
  137. package/src/components/chart/composite/legend.tsx +75 -3
  138. package/src/components/chart/composite/levels.ts +5 -0
  139. package/src/components/chart/composite/panel-gestures.ts +41 -0
  140. package/src/components/chart/composite/panel-labels.ts +2 -1
  141. package/src/components/chart/composite/panel-surface.tsx +305 -11
  142. package/src/components/chart/composite/price-axis-labels.tsx +27 -3
  143. package/src/components/chart/composite/rasterizer.ts +44 -19
  144. package/src/components/chart/composite/scene.ts +16 -4
  145. package/src/components/chart/composite/text-renderer.ts +48 -21
  146. package/src/components/chart/composite/toolbar.tsx +52 -5
  147. package/src/components/chart/composite/types.ts +13 -0
  148. package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
  149. package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
  150. package/src/components/chart/composite/use-composite-viewport.ts +12 -3
  151. package/src/components/chart/composite/value-scale.ts +149 -0
  152. package/src/components/chart/composite/volume-profile-paint.ts +6 -3
  153. package/src/components/chart/core/pointer.ts +2 -0
  154. package/src/components/chart-table/index.ts +2 -0
  155. package/src/components/chart-table/read.tsx +74 -0
  156. package/src/components/command-bar/commands/collection-list.ts +41 -19
  157. package/src/components/command-bar/commands/collection.ts +9 -3
  158. package/src/components/command-bar/commands/direct/index.ts +1 -1
  159. package/src/components/command-bar/commands/direct/runtime.ts +3 -2
  160. package/src/components/command-bar/pane-templates/items.ts +59 -6
  161. package/src/components/command-bar/pane-templates/workflow.ts +10 -1
  162. package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
  163. package/src/components/command-bar/routes/root/provider-search.ts +42 -11
  164. package/src/components/command-bar/routes/root/results.ts +18 -4
  165. package/src/components/command-bar/routes/root/runtime.ts +15 -3
  166. package/src/components/command-bar/routes/root/search-providers.ts +25 -2
  167. package/src/components/command-bar/routes/root/selection.ts +6 -1
  168. package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
  169. package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
  170. package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
  171. package/src/components/command-bar/selection-runtime.ts +50 -10
  172. package/src/components/command-bar/surface/index.tsx +24 -2
  173. package/src/components/command-bar/view-model.ts +7 -0
  174. package/src/components/command-bar/workflow/broker.ts +2 -2
  175. package/src/components/command-bar/workflow/collection-actions.ts +46 -7
  176. package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
  177. package/src/components/command-bar/workflow/fields.ts +8 -2
  178. package/src/components/command-bar/workflow/ops.ts +18 -4
  179. package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
  180. package/src/components/command-bar/workflow/submit.ts +3 -2
  181. package/src/components/command-bar/workflow/tickers.ts +5 -1
  182. package/src/components/company-logo.tsx +39 -0
  183. package/src/components/data-table/sections.ts +4 -2
  184. package/src/components/form-modal/confirm.tsx +2 -1
  185. package/src/components/form-modal/request.ts +5 -0
  186. package/src/components/form-modal/routes.ts +4 -1
  187. package/src/components/index.ts +12 -6
  188. package/src/components/layout/header.tsx +7 -6
  189. package/src/components/layout/market-summary.ts +9 -6
  190. package/src/components/layout/pane/capture-status.ts +1 -1
  191. package/src/components/layout/pane/footer/model.ts +11 -5
  192. package/src/components/layout/shell/chrome.ts +19 -6
  193. package/src/components/layout/shell/index.tsx +2 -1
  194. package/src/components/layout/shell/pane/layers.tsx +19 -1
  195. package/src/components/layout/status-bar.tsx +2 -2
  196. package/src/components/market-board/index.tsx +11 -3
  197. package/src/components/onboarding/onboarding-wizard.tsx +1 -0
  198. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
  199. package/src/components/onboarding/wizard-broker-sync.ts +4 -1
  200. package/src/components/pane-settings-dialog/desktop.tsx +20 -7
  201. package/src/components/pane-settings-dialog/tui.tsx +17 -9
  202. package/src/components/pane-settings-dialog/value.ts +8 -1
  203. package/src/components/pane-settings-dialog.tsx +44 -13
  204. package/src/components/sign-in-code-panel.tsx +27 -19
  205. package/src/components/ui/column-grid.tsx +48 -4
  206. package/src/components/ui/country-flag-art.tsx +180 -0
  207. package/src/components/ui/country-flag.tsx +118 -12
  208. package/src/components/ui/data-table/opentui/index.tsx +61 -2
  209. package/src/components/ui/data-table/opentui/model.ts +109 -2
  210. package/src/components/ui/data-table/types.ts +1 -1
  211. package/src/components/ui/display.tsx +5 -1
  212. package/src/components/ui/index.ts +3 -1
  213. package/src/components/ui/logo-mark.tsx +11 -2
  214. package/src/components/ui/query-bar.tsx +1 -1
  215. package/src/components/ui/row-notes.ts +79 -0
  216. package/src/core/state/app/layout.ts +4 -4
  217. package/src/data/cached-query.ts +8 -1
  218. package/src/data/config/chart-settings.ts +3 -3
  219. package/src/data/config/home.ts +10 -2
  220. package/src/data/config/layout.ts +4 -1
  221. package/src/data/config/store/node.ts +4 -0
  222. package/src/data/config/store/normalize.ts +13 -3
  223. package/src/data/config/store/portfolio-as-of-column.ts +2 -2
  224. package/src/data/json-storage.ts +2 -0
  225. package/src/data/saved-copies.ts +84 -0
  226. package/src/docs/snapshot.json +65 -32
  227. package/src/i18n/es.ts +29 -12
  228. package/src/i18n/ja.ts +29 -12
  229. package/src/i18n/ko.ts +29 -12
  230. package/src/i18n/zh-cn.ts +29 -12
  231. package/src/i18n/zh-tw.ts +29 -12
  232. package/src/layout/desks.ts +1 -1
  233. package/src/layout/launch-layout.ts +1 -1
  234. package/src/layout/pane-manager/floating-actions.ts +5 -3
  235. package/src/layout/pane-manager/floating.ts +43 -1
  236. package/src/layout/pane-manager/index.ts +2 -0
  237. package/src/layout/selected-ticker.ts +74 -0
  238. package/src/market-data/depositary-pair.ts +251 -0
  239. package/src/market-data/futures-board.ts +1 -1
  240. package/src/market-data/futures-contract.ts +124 -40
  241. package/src/market-data/fx-closes.ts +17 -2
  242. package/src/market-data/hooks.tsx +19 -11
  243. package/src/market-data/market/change-basis.ts +49 -7
  244. package/src/market-data/market/freshness.ts +34 -5
  245. package/src/market-data/market/period-return.ts +163 -0
  246. package/src/market-data/market/trading-sessions.ts +13 -0
  247. package/src/market-data/market/year-to-date.ts +8 -13
  248. package/src/market-data/performance.ts +20 -14
  249. package/src/market-data/perpetual-premium.ts +32 -0
  250. package/src/market-data/published-apac-sessions.ts +2 -1
  251. package/src/market-data/published-futures-sessions.ts +69 -0
  252. package/src/market-data/quotes/contributions.ts +5 -1
  253. package/src/market-data/quotes/freshness.ts +42 -0
  254. package/src/market-data/quotes/intraday-bar.ts +37 -0
  255. package/src/market-data/quotes/regular-session-valuation.ts +13 -8
  256. package/src/market-data/quotes/resolution.ts +4 -2
  257. package/src/market-data/tax-equivalent.ts +1 -1
  258. package/src/market-data/ticker-price-returns.ts +3 -1
  259. package/src/news/aggregator.ts +5 -1
  260. package/src/news/news-model.ts +4 -0
  261. package/src/news/types.ts +11 -0
  262. package/src/notifications/app-notifier.ts +1 -1
  263. package/src/plugins/auto-update.ts +2 -2
  264. package/src/plugins/builtin/account-management/index.tsx +1 -0
  265. package/src/plugins/builtin/account-management/pane.tsx +9 -8
  266. package/src/plugins/builtin/alerts/alert-engine.ts +29 -19
  267. package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
  268. package/src/plugins/builtin/alerts/cli.ts +485 -76
  269. package/src/plugins/builtin/alerts/command.ts +5 -10
  270. package/src/plugins/builtin/alerts/event-rows.ts +9 -5
  271. package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
  272. package/src/plugins/builtin/alerts/events.ts +107 -5
  273. package/src/plugins/builtin/alerts/fired-log.ts +78 -0
  274. package/src/plugins/builtin/alerts/format.ts +2 -1
  275. package/src/plugins/builtin/alerts/headless.ts +100 -39
  276. package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
  277. package/src/plugins/builtin/alerts/history-rows.ts +89 -0
  278. package/src/plugins/builtin/alerts/index.tsx +45 -18
  279. package/src/plugins/builtin/alerts/iv-level.ts +365 -0
  280. package/src/plugins/builtin/alerts/levels.ts +3 -3
  281. package/src/plugins/builtin/alerts/pane.tsx +97 -29
  282. package/src/plugins/builtin/alerts/price.ts +35 -0
  283. package/src/plugins/builtin/alerts/quotes.ts +24 -0
  284. package/src/plugins/builtin/alerts/research-builder.ts +84 -11
  285. package/src/plugins/builtin/alerts/research-rules.ts +76 -3
  286. package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
  287. package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
  288. package/src/plugins/builtin/alerts/scheduled.ts +188 -29
  289. package/src/plugins/builtin/alerts/storage.ts +13 -10
  290. package/src/plugins/builtin/alerts/targets.ts +12 -3
  291. package/src/plugins/builtin/analytics/index.tsx +8 -2
  292. package/src/plugins/builtin/analytics/metrics.ts +1 -1
  293. package/src/plugins/builtin/analytics/risk-book.ts +215 -0
  294. package/src/plugins/builtin/analytics/risk-client.ts +13 -6
  295. package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
  296. package/src/plugins/builtin/analytics/risk-math.ts +6 -3
  297. package/src/plugins/builtin/analytics/risk-model.ts +297 -104
  298. package/src/plugins/builtin/analytics/risk-options.ts +17 -2
  299. package/src/plugins/builtin/analytics/risk-pane.tsx +97 -36
  300. package/src/plugins/builtin/attention/pane.tsx +6 -2
  301. package/src/plugins/builtin/awards/headless.ts +7 -3
  302. package/src/plugins/builtin/awards/model.ts +18 -0
  303. package/src/plugins/builtin/awards/pane.tsx +6 -3
  304. package/src/plugins/builtin/backtest/headless.ts +3 -3
  305. package/src/plugins/builtin/backtest/rules.ts +41 -5
  306. package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
  307. package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
  308. package/src/plugins/builtin/broker-manager/footer.ts +27 -11
  309. package/src/plugins/builtin/broker-manager/index.tsx +21 -12
  310. package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
  311. package/src/plugins/builtin/broker-manager/model.ts +12 -2
  312. package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
  313. package/src/plugins/builtin/broker-manager/table.ts +28 -12
  314. package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
  315. package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
  316. package/src/plugins/builtin/broker-trading/controller.ts +45 -13
  317. package/src/plugins/builtin/broker-trading/dock.tsx +22 -4
  318. package/src/plugins/builtin/broker-trading/headless.ts +2 -1
  319. package/src/plugins/builtin/broker-trading/index.tsx +7 -1
  320. package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
  321. package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
  322. package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
  323. package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
  324. package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
  325. package/src/plugins/builtin/broker-trading/pane.tsx +110 -23
  326. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
  327. package/src/plugins/builtin/broker-trading/ticket.tsx +112 -31
  328. package/src/plugins/builtin/buildout/cells.tsx +4 -0
  329. package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
  330. package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
  331. package/src/plugins/builtin/buildout/pane/index.tsx +25 -12
  332. package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
  333. package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
  334. package/src/plugins/builtin/catalysts/detail.tsx +3 -2
  335. package/src/plugins/builtin/catalysts/index.tsx +1 -1
  336. package/src/plugins/builtin/catalysts/model.ts +49 -4
  337. package/src/plugins/builtin/catalysts/pane.tsx +6 -8
  338. package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
  339. package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
  340. package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
  341. package/src/plugins/builtin/changelog/index.tsx +1 -0
  342. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +1 -0
  343. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
  344. package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
  345. package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
  346. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  347. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
  348. package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
  349. package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
  350. package/src/plugins/builtin/chart-composer/headless.ts +253 -35
  351. package/src/plugins/builtin/chart-composer/index.tsx +12 -2
  352. package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
  353. package/src/plugins/builtin/chart-composer/pane.tsx +127 -28
  354. package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
  355. package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
  356. package/src/plugins/builtin/chart-composer/session-history.ts +15 -21
  357. package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
  358. package/src/plugins/builtin/chart-composer/settings.ts +31 -19
  359. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +5 -6
  360. package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
  361. package/src/plugins/builtin/chat/attachments/model.ts +3 -1
  362. package/src/plugins/builtin/chat/content/index.tsx +1 -1
  363. package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
  364. package/src/plugins/builtin/chat/people.ts +1 -1
  365. package/src/plugins/builtin/clinical-trials/client.ts +5 -2
  366. package/src/plugins/builtin/clinical-trials/model.ts +13 -4
  367. package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
  368. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
  369. package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
  370. package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
  371. package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
  372. package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
  373. package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
  374. package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
  375. package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
  376. package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
  377. package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
  378. package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
  379. package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
  380. package/src/plugins/builtin/commodity-balances/pane.tsx +11 -4
  381. package/src/plugins/builtin/company-attention/app-rank.tsx +7 -5
  382. package/src/plugins/builtin/company-attention/building-model.ts +222 -0
  383. package/src/plugins/builtin/company-attention/building.tsx +172 -0
  384. package/src/plugins/builtin/company-attention/client.ts +117 -1
  385. package/src/plugins/builtin/company-attention/evidence.ts +16 -3
  386. package/src/plugins/builtin/company-attention/headless.ts +101 -5
  387. package/src/plugins/builtin/company-attention/index.tsx +6 -3
  388. package/src/plugins/builtin/company-attention/model.ts +33 -7
  389. package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
  390. package/src/plugins/builtin/company-attention/moves.tsx +159 -0
  391. package/src/plugins/builtin/company-attention/pane.tsx +79 -23
  392. package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
  393. package/src/plugins/builtin/company-attention/signals.tsx +160 -0
  394. package/src/plugins/builtin/company-kpis/headless.ts +3 -2
  395. package/src/plugins/builtin/company-kpis/model.ts +2 -2
  396. package/src/plugins/builtin/company-kpis/pane.tsx +1 -1
  397. package/src/plugins/builtin/composite-plugins.ts +2 -1
  398. package/src/plugins/builtin/congress-trades/headless.ts +1 -1
  399. package/src/plugins/builtin/connections/index.ts +1 -0
  400. package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
  401. package/src/plugins/builtin/correlation/headless.ts +5 -3
  402. package/src/plugins/builtin/cot/headless.ts +28 -6
  403. package/src/plugins/builtin/cot/model.ts +12 -0
  404. package/src/plugins/builtin/cot/pane.tsx +3 -2
  405. package/src/plugins/builtin/cpi/headless.ts +5 -1
  406. package/src/plugins/builtin/cpi/pane.tsx +11 -9
  407. package/src/plugins/builtin/credit-boards/model.ts +2 -0
  408. package/src/plugins/builtin/credit-documents/headless.ts +1 -1
  409. package/src/plugins/builtin/crypto-board/client.ts +86 -0
  410. package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
  411. package/src/plugins/builtin/crypto-board/defi.ts +239 -0
  412. package/src/plugins/builtin/crypto-board/headless.ts +117 -6
  413. package/src/plugins/builtin/crypto-board/index.tsx +8 -3
  414. package/src/plugins/builtin/crypto-board/model.ts +40 -8
  415. package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
  416. package/src/plugins/builtin/crypto-board/series.ts +19 -1
  417. package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
  418. package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
  419. package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
  420. package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
  421. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
  422. package/src/plugins/builtin/debug/index.tsx +1 -0
  423. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  424. package/src/plugins/builtin/docs/index.tsx +1 -1
  425. package/src/plugins/builtin/docs/pane.tsx +2 -2
  426. package/src/plugins/builtin/docs/targets.ts +1 -1
  427. package/src/plugins/builtin/doe/client.ts +5 -1
  428. package/src/plugins/builtin/doe/headless.ts +12 -6
  429. package/src/plugins/builtin/doe/model.ts +43 -6
  430. package/src/plugins/builtin/doe/pane.tsx +18 -11
  431. package/src/plugins/builtin/earnings/board-model.ts +13 -6
  432. package/src/plugins/builtin/earnings/board-table.ts +4 -1
  433. package/src/plugins/builtin/earnings/board.tsx +253 -50
  434. package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
  435. package/src/plugins/builtin/earnings/calendar.tsx +338 -0
  436. package/src/plugins/builtin/earnings/client.ts +4 -3
  437. package/src/plugins/builtin/earnings/headless.ts +53 -15
  438. package/src/plugins/builtin/earnings/history-model.ts +53 -36
  439. package/src/plugins/builtin/earnings/index.tsx +15 -22
  440. package/src/plugins/builtin/earnings/model.ts +17 -12
  441. package/src/plugins/builtin/earnings/scope.ts +89 -0
  442. package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
  443. package/src/plugins/builtin/earnings-calls/data.ts +146 -2
  444. package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
  445. package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
  446. package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
  447. package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
  448. package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
  449. package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
  450. package/src/plugins/builtin/econ/calendar-model.ts +132 -20
  451. package/src/plugins/builtin/econ/detail-view.tsx +48 -10
  452. package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
  453. package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
  454. package/src/plugins/builtin/econ/index.tsx +112 -25
  455. package/src/plugins/builtin/econ/print-watch.ts +2 -1
  456. package/src/plugins/builtin/econ/types.ts +32 -1
  457. package/src/plugins/builtin/econ-statistics/client.ts +16 -3
  458. package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
  459. package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
  460. package/src/plugins/builtin/econ-statistics/headless.ts +13 -2
  461. package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
  462. package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
  463. package/src/plugins/builtin/econ-statistics/stats.ts +103 -0
  464. package/src/plugins/builtin/econ-statistics/view.ts +16 -1
  465. package/src/plugins/builtin/equity-screener/client.ts +21 -3
  466. package/src/plugins/builtin/equity-screener/headless.ts +68 -6
  467. package/src/plugins/builtin/equity-screener/live.ts +1 -1
  468. package/src/plugins/builtin/equity-screener/model.ts +6 -0
  469. package/src/plugins/builtin/equity-screener/pane.tsx +45 -10
  470. package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
  471. package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
  472. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
  473. package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
  474. package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
  475. package/src/plugins/builtin/exposure/headless.ts +51 -6
  476. package/src/plugins/builtin/exposure/index.tsx +28 -2
  477. package/src/plugins/builtin/exposure/model.ts +7 -0
  478. package/src/plugins/builtin/exposure/pane.tsx +6 -1
  479. package/src/plugins/builtin/fear-greed/index.tsx +1 -1
  480. package/src/plugins/builtin/filing-events/index.tsx +1 -1
  481. package/src/plugins/builtin/futures/model.ts +10 -9
  482. package/src/plugins/builtin/futures/returns.ts +8 -36
  483. package/src/plugins/builtin/futures/table.tsx +1 -1
  484. package/src/plugins/builtin/futures-curve/client.ts +40 -9
  485. package/src/plugins/builtin/futures-curve/headless.ts +39 -17
  486. package/src/plugins/builtin/futures-curve/model.ts +258 -69
  487. package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
  488. package/src/plugins/builtin/fx-matrix/client.ts +15 -8
  489. package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
  490. package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
  491. package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
  492. package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
  493. package/src/plugins/builtin/gpu/headless.ts +9 -4
  494. package/src/plugins/builtin/help/index.tsx +13 -3
  495. package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
  496. package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
  497. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  498. package/src/plugins/builtin/holders/client.ts +13 -1
  499. package/src/plugins/builtin/holders/format.ts +28 -17
  500. package/src/plugins/builtin/holders/headless.ts +46 -7
  501. package/src/plugins/builtin/holders/index-holders.ts +264 -0
  502. package/src/plugins/builtin/holders/pane.tsx +79 -15
  503. package/src/plugins/builtin/holders/report-rows.ts +35 -7
  504. package/src/plugins/builtin/holders/table-model.ts +2 -1
  505. package/src/plugins/builtin/holders/treemap.tsx +2 -2
  506. package/src/plugins/builtin/insider/headless.ts +1 -1
  507. package/src/plugins/builtin/insider/signals-client.ts +1 -1
  508. package/src/plugins/builtin/insider/signals.tsx +1 -1
  509. package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
  510. package/src/plugins/builtin/iv-history/client.ts +20 -0
  511. package/src/plugins/builtin/iv-history/headless.ts +4 -4
  512. package/src/plugins/builtin/iv-history/index.tsx +1 -1
  513. package/src/plugins/builtin/iv-history/model.ts +13 -4
  514. package/src/plugins/builtin/iv-history/series.ts +3 -1
  515. package/src/plugins/builtin/jobs/lock-note.ts +1 -1
  516. package/src/plugins/builtin/jobs/model.ts +4 -15
  517. package/src/plugins/builtin/jobs/pane.tsx +7 -5
  518. package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
  519. package/src/plugins/builtin/jodi/pane.tsx +10 -4
  520. package/src/plugins/builtin/layout-manager/index.tsx +1 -0
  521. package/src/plugins/builtin/macro-day/headless.ts +20 -7
  522. package/src/plugins/builtin/macro-day/index.tsx +3 -2
  523. package/src/plugins/builtin/macro-day/model.ts +29 -8
  524. package/src/plugins/builtin/macro-day/pane.tsx +46 -10
  525. package/src/plugins/builtin/macro-day/releases.ts +55 -13
  526. package/src/plugins/builtin/market-heatmap/data.ts +9 -2
  527. package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
  528. package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
  529. package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
  530. package/src/plugins/builtin/market-heatmap/model.ts +10 -7
  531. package/src/plugins/builtin/market-movers/client.ts +33 -19
  532. package/src/plugins/builtin/market-movers/headless.ts +8 -2
  533. package/src/plugins/builtin/market-movers/index.tsx +16 -10
  534. package/src/plugins/builtin/market-movers/screener.ts +29 -8
  535. package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
  536. package/src/plugins/builtin/market-movers/session-table.ts +4 -2
  537. package/src/plugins/builtin/market-movers/table.ts +4 -1
  538. package/src/plugins/builtin/market-valuation/headless.ts +2 -0
  539. package/src/plugins/builtin/market-valuation/view.ts +5 -0
  540. package/src/plugins/builtin/members/headless.ts +6 -5
  541. package/src/plugins/builtin/members/pane.tsx +11 -4
  542. package/src/plugins/builtin/mna/client.ts +1 -0
  543. package/src/plugins/builtin/mna/detail.tsx +20 -32
  544. package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
  545. package/src/plugins/builtin/mna/distress/model.ts +3 -2
  546. package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
  547. package/src/plugins/builtin/mna/headless.ts +6 -13
  548. package/src/plugins/builtin/mna/model.ts +93 -15
  549. package/src/plugins/builtin/mna/pane.tsx +5 -4
  550. package/src/plugins/builtin/news/headless.ts +12 -6
  551. package/src/plugins/builtin/news/index.tsx +10 -1
  552. package/src/plugins/builtin/news/wire/index.tsx +3 -2
  553. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
  554. package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
  555. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
  556. package/src/plugins/builtin/notes/cli.ts +49 -14
  557. package/src/plugins/builtin/openfda/pane.tsx +1 -1
  558. package/src/plugins/builtin/options/carry.ts +2 -2
  559. package/src/plugins/builtin/options/index.tsx +1 -1
  560. package/src/plugins/builtin/options/view.tsx +10 -10
  561. package/src/plugins/builtin/options-calculator/headless.ts +46 -7
  562. package/src/plugins/builtin/options-scenario/client.ts +123 -28
  563. package/src/plugins/builtin/options-scenario/collar.ts +8 -6
  564. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
  565. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
  566. package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
  567. package/src/plugins/builtin/options-scenario/csp.ts +101 -21
  568. package/src/plugins/builtin/options-scenario/headless.ts +36 -15
  569. package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
  570. package/src/plugins/builtin/options-scenario/index.tsx +28 -3
  571. package/src/plugins/builtin/options-scenario/model.ts +13 -0
  572. package/src/plugins/builtin/options-scenario/pane.tsx +9 -5
  573. package/src/plugins/builtin/perps/equity-row.tsx +11 -3
  574. package/src/plugins/builtin/perps/freshness.ts +8 -5
  575. package/src/plugins/builtin/perps/headless.ts +3 -1
  576. package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
  577. package/src/plugins/builtin/perps/live-rows.ts +161 -0
  578. package/src/plugins/builtin/perps/markets-section.tsx +154 -0
  579. package/src/plugins/builtin/perps/pane.tsx +36 -17
  580. package/src/plugins/builtin/perps/venue-markets.ts +96 -0
  581. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
  582. package/src/plugins/builtin/plugin-marketplace/pane.tsx +143 -59
  583. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +72 -24
  584. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
  585. package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
  586. package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
  587. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
  588. package/src/plugins/builtin/portfolio-list/headless.ts +6 -3
  589. package/src/plugins/builtin/portfolio-list/income.ts +13 -2
  590. package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
  591. package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
  592. package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
  593. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
  594. package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
  595. package/src/plugins/builtin/power/client.ts +4 -1
  596. package/src/plugins/builtin/power/evidence.ts +22 -7
  597. package/src/plugins/builtin/power/gas.tsx +5 -1
  598. package/src/plugins/builtin/power/pane.tsx +8 -1
  599. package/src/plugins/builtin/power/prices.tsx +10 -3
  600. package/src/plugins/builtin/research/analyst-headless.ts +25 -8
  601. package/src/plugins/builtin/research/analyst-model.ts +68 -3
  602. package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
  603. package/src/plugins/builtin/research/event-model.ts +3 -1
  604. package/src/plugins/builtin/research/event-sources.ts +12 -4
  605. package/src/plugins/builtin/research/index.tsx +2 -1
  606. package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
  607. package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
  608. package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
  609. package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
  610. package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
  611. package/src/plugins/builtin/research/relative-valuation-pane.tsx +76 -9
  612. package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
  613. package/src/plugins/builtin/reverse-dcf/model.ts +6 -2
  614. package/src/plugins/builtin/risk-factors/index.tsx +2 -2
  615. package/src/plugins/builtin/scanner/lock-note.ts +1 -1
  616. package/src/plugins/builtin/sec/forms.ts +13 -0
  617. package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
  618. package/src/plugins/builtin/sec/fund-facts.ts +68 -8
  619. package/src/plugins/builtin/sec/headless.ts +85 -8
  620. package/src/plugins/builtin/sec/index.tsx +58 -12
  621. package/src/plugins/builtin/sectors/client.ts +2 -1
  622. package/src/plugins/builtin/sectors/sector-model.ts +11 -15
  623. package/src/plugins/builtin/series-inventory.ts +2 -0
  624. package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
  625. package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
  626. package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
  627. package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
  628. package/src/plugins/builtin/shared/earnings-report.ts +3 -3
  629. package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
  630. package/src/plugins/builtin/shared/job-posting.ts +129 -0
  631. package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
  632. package/src/plugins/builtin/shared/report-freshness.ts +3 -0
  633. package/src/plugins/builtin/shared/research-trading.tsx +21 -0
  634. package/src/plugins/builtin/shared/series-cache.ts +2 -0
  635. package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
  636. package/src/plugins/builtin/short-interest/client.ts +2 -0
  637. package/src/plugins/builtin/short-interest/headless.ts +41 -12
  638. package/src/plugins/builtin/short-interest/index.tsx +23 -1
  639. package/src/plugins/builtin/short-interest/model.ts +70 -19
  640. package/src/plugins/builtin/short-interest/pane.tsx +19 -2
  641. package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
  642. package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
  643. package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
  644. package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
  645. package/src/plugins/builtin/short-interest/schedule.ts +35 -2
  646. package/src/plugins/builtin/short-interest/surface.tsx +11 -4
  647. package/src/plugins/builtin/short-interest/types.ts +9 -0
  648. package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
  649. package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
  650. package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
  651. package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
  652. package/src/plugins/builtin/supply-chain/model.ts +51 -3
  653. package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
  654. package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
  655. package/src/plugins/builtin/themes/headless.ts +8 -5
  656. package/src/plugins/builtin/themes/model.ts +14 -4
  657. package/src/plugins/builtin/themes/pane.tsx +24 -5
  658. package/src/plugins/builtin/thirteenf/headless.ts +5 -5
  659. package/src/plugins/builtin/thirteenf/signals.ts +3 -1
  660. package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
  661. package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
  662. package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
  663. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
  664. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
  665. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
  666. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
  667. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
  668. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
  669. package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
  670. package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
  671. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
  672. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
  673. package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
  674. package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
  675. package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
  676. package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
  677. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
  678. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
  679. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
  680. package/src/plugins/builtin/ticker-detail/quote-footer.ts +8 -3
  681. package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
  682. package/src/plugins/builtin/vol-surface/client.ts +5 -4
  683. package/src/plugins/builtin/vol-surface/headless.ts +12 -3
  684. package/src/plugins/builtin/vol-surface/model.ts +4 -2
  685. package/src/plugins/builtin/volatility/model.ts +5 -10
  686. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
  687. package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
  688. package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
  689. package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
  690. package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
  691. package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
  692. package/src/plugins/builtin/world-venue-map/index.ts +3 -26
  693. package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
  694. package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
  695. package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
  696. package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
  697. package/src/plugins/builtin/yield-curve/client.ts +25 -0
  698. package/src/plugins/builtin/yield-curve/curves.ts +7 -4
  699. package/src/plugins/builtin/yield-curve/headless.ts +38 -4
  700. package/src/plugins/builtin/yield-curve/history.ts +4 -3
  701. package/src/plugins/builtin/yield-curve/index.tsx +12 -6
  702. package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
  703. package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
  704. package/src/plugins/bundle.ts +1 -1
  705. package/src/plugins/catalog-browser.ts +2 -1
  706. package/src/plugins/compat.ts +3 -3
  707. package/src/plugins/dependencies.ts +75 -14
  708. package/src/plugins/extracted-plugins.ts +132 -34
  709. package/src/plugins/host-link.ts +149 -34
  710. package/src/plugins/installer.ts +35 -10
  711. package/src/plugins/loader.ts +206 -38
  712. package/src/plugins/manager-node.ts +2 -2
  713. package/src/plugins/marketplace/deactivation.ts +22 -0
  714. package/src/plugins/marketplace/model.ts +2 -2
  715. package/src/plugins/marketplace/packs.ts +1 -1
  716. package/src/plugins/ownership.ts +1 -1
  717. package/src/plugins/peer-retry.ts +155 -0
  718. package/src/plugins/registry/contributions.ts +3 -1
  719. package/src/plugins/registry/index.ts +4 -3
  720. package/src/plugins/registry/plugin-state.ts +3 -0
  721. package/src/plugins/seed-notice.ts +20 -0
  722. package/src/plugins/shortcut-argument.ts +39 -0
  723. package/src/portfolio/allocation.ts +1 -1
  724. package/src/portfolio/collection-name.ts +31 -0
  725. package/src/portfolio/column-values.ts +10 -8
  726. package/src/portfolio/cost-fx.ts +296 -19
  727. package/src/portfolio/manual-lots.ts +75 -0
  728. package/src/portfolio/mutations.ts +60 -22
  729. package/src/portfolio/suffixed-listings.ts +209 -0
  730. package/src/portfolio/totals-currency.ts +24 -0
  731. package/src/public/__snapshots__/public-api.test.ts.snap +14 -0
  732. package/src/public/time-series.ts +11 -8
  733. package/src/remote/schema.ts +1 -1
  734. package/src/renderers/browser/config-host.ts +3 -0
  735. package/src/renderers/browser/main.tsx +23 -3
  736. package/src/renderers/dom/boot-frame-page.ts +187 -0
  737. package/src/renderers/dom/boot-frame.ts +237 -0
  738. package/src/renderers/dom/build-assets.ts +46 -8
  739. package/src/renderers/dom/dom-ui-host.tsx +4 -29
  740. package/src/renderers/dom/host/ascii-text.ts +16 -2
  741. package/src/renderers/dom/host/box.tsx +43 -2
  742. package/src/renderers/dom/host/chart-surface.tsx +2 -0
  743. package/src/renderers/dom/host/hls.chunk.ts +3 -0
  744. package/src/renderers/dom/host/image-surface.tsx +49 -0
  745. package/src/renderers/dom/host/media-surface.tsx +24 -18
  746. package/src/renderers/dom/host/mouse.ts +30 -0
  747. package/src/renderers/dom/host/size-observer.ts +1 -1
  748. package/src/renderers/dom/host/style.ts +1 -1
  749. package/src/renderers/dom/styles.css +76 -0
  750. package/src/renderers/dom/test-utils.tsx +2 -1
  751. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
  752. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
  753. package/src/renderers/electrobun/bun/desktop/backend.ts +46 -3
  754. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
  755. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
  756. package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
  757. package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
  758. package/src/renderers/electrobun/bun/index.ts +11 -1
  759. package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
  760. package/src/renderers/electrobun/bun/window/create.ts +4 -1
  761. package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
  762. package/src/renderers/electrobun/shared/protocol.ts +6 -1
  763. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  764. package/src/renderers/electrobun/view/config-host.ts +3 -0
  765. package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
  766. package/src/renderers/electrobun/view/main.tsx +30 -4
  767. package/src/renderers/electrobun/view/news-source.ts +5 -5
  768. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -1
  769. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  770. package/src/renderers/opentui/browser-open.ts +88 -0
  771. package/src/renderers/opentui/host.tsx +5 -14
  772. package/src/renderers/opentui/image/loader.ts +3 -2
  773. package/src/renderers/opentui/native-smoke.ts +10 -1
  774. package/src/renderers/opentui/pane-image/index.ts +2 -1
  775. package/src/renderers/opentui/render-lib.ts +253 -0
  776. package/src/renderers/opentui/render-timing-smoke.ts +1 -1
  777. package/src/renderers/opentui/start.tsx +1 -1
  778. package/src/renderers/share/layout-view.tsx +2 -2
  779. package/src/renderers/share/main.tsx +4 -4
  780. package/src/renderers/share/view.tsx +1 -1
  781. package/src/series/resolve.ts +22 -6
  782. package/src/series/standalone.ts +30 -2
  783. package/src/series/types.ts +8 -1
  784. package/src/shares/api.ts +4 -1
  785. package/src/shares/pane.ts +1 -1
  786. package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
  787. package/src/sources/gloomberb-cloud/index.ts +32 -16
  788. package/src/sources/gloomberb-cloud/news.ts +39 -0
  789. package/src/sources/gloomberb-cloud/normalizers.ts +14 -1
  790. package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
  791. package/src/sources/listing-symbols.ts +37 -0
  792. package/src/sources/provider-errors.ts +16 -2
  793. package/src/sources/provider-router/financial-routes.ts +21 -3
  794. package/src/sources/provider-router/financials.ts +15 -2
  795. package/src/sources/provider-router/history.ts +30 -8
  796. package/src/sources/provider-router/primary.ts +1 -1
  797. package/src/state/app/bootstrap.ts +23 -0
  798. package/src/state/hooks/inline-tickers.ts +30 -5
  799. package/src/sync/controller.ts +1 -1
  800. package/src/sync/core-contributors.ts +46 -14
  801. package/src/team/collections-sync.ts +2 -1
  802. package/src/team/collections.ts +178 -17
  803. package/src/telemetry/crash-reports.ts +17 -4
  804. package/src/test-support/builtin-panes.ts +1 -1
  805. package/src/theme/colors.ts +3 -30
  806. package/src/theme/ui-tokens.ts +37 -1
  807. package/src/tickers/company-name-listing.ts +210 -33
  808. package/src/tickers/index-roots.ts +47 -0
  809. package/src/tickers/instrument-label.ts +5 -1
  810. package/src/tickers/metadata.ts +10 -0
  811. package/src/tickers/perpetual-listing.ts +5 -2
  812. package/src/tickers/search/home-listing.ts +75 -0
  813. package/src/tickers/search/index.ts +76 -27
  814. package/src/tickers/search/ranking.ts +54 -4
  815. package/src/tickers/search/upsert.ts +29 -0
  816. package/src/tickers/terminal-codes.ts +416 -40
  817. package/src/time-series/bar-aggregation.ts +243 -0
  818. package/src/time-series/base-series.ts +307 -0
  819. package/src/time-series/chart-bounds.ts +349 -0
  820. package/src/time-series/chart-seed.ts +142 -0
  821. package/src/time-series/economic.ts +5 -0
  822. package/src/time-series/fundamentals.ts +18 -3
  823. package/src/time-series/history-acquisition.ts +499 -0
  824. package/src/time-series/leverage.ts +100 -0
  825. package/src/time-series/market-resolution.ts +4 -2
  826. package/src/time-series/market.ts +2 -1
  827. package/src/time-series/price-comparison.ts +175 -30
  828. package/src/time-series/range.ts +9 -1
  829. package/src/time-series/resolution.ts +174 -16
  830. package/src/time-series/resolve-context.ts +157 -0
  831. package/src/time-series/resolve.ts +126 -1577
  832. package/src/time-series/series-name.ts +38 -0
  833. package/src/time-series/series-presentation.ts +208 -0
  834. package/src/time-series/spec.ts +11 -6
  835. package/src/time-series/studies.ts +109 -12
  836. package/src/time-series/tail-reconcile.ts +121 -0
  837. package/src/time-series/types.ts +29 -2
  838. package/src/time-series/use-chart-resolution.ts +4 -5
  839. package/src/types/broker.ts +10 -0
  840. package/src/types/config.ts +14 -0
  841. package/src/types/data-provider.ts +14 -0
  842. package/src/types/exchange-rate.ts +6 -0
  843. package/src/types/financials.ts +104 -7
  844. package/src/types/headless.ts +76 -6
  845. package/src/types/instrument.ts +3 -0
  846. package/src/types/plugin.ts +65 -8
  847. package/src/types/price-history.ts +2 -0
  848. package/src/types/ticker.ts +13 -0
  849. package/src/types/trading.ts +59 -1
  850. package/src/ui/ascii-font.ts +13 -1
  851. package/src/ui/host.tsx +6 -1
  852. package/src/updater/command-names.ts +175 -0
  853. package/src/updater/index.ts +10 -5
  854. package/src/utils/calendar-date.ts +32 -15
  855. package/src/utils/cash-runway.ts +241 -0
  856. package/src/utils/central-banks.ts +42 -0
  857. package/src/utils/cli-output.ts +259 -80
  858. package/src/utils/contract-month.ts +16 -0
  859. package/src/utils/crypto-pair.ts +61 -3
  860. package/src/utils/currency-units.ts +29 -0
  861. package/src/utils/datetime-format.ts +10 -0
  862. package/src/utils/debug-log.ts +1 -1
  863. package/src/utils/decimal-comma.ts +39 -6
  864. package/src/utils/exchange-announcements.ts +71 -0
  865. package/src/utils/exchange-rate-snapshot.ts +10 -0
  866. package/src/utils/financial-statements.ts +83 -4
  867. package/src/utils/format.ts +34 -16
  868. package/src/utils/fundamentals.ts +150 -34
  869. package/src/utils/fx-market-hours.ts +10 -1
  870. package/src/utils/latest-financial-period.ts +8 -0
  871. package/src/utils/linux-libc.ts +54 -0
  872. package/src/utils/market-capitalization.ts +111 -15
  873. package/src/utils/max-drawdown.ts +64 -0
  874. package/src/utils/network-failure.ts +83 -0
  875. package/src/utils/option-expiry.ts +139 -7
  876. package/src/utils/price-earnings.ts +11 -5
  877. package/src/utils/price-history.ts +18 -8
  878. package/src/utils/price-window.ts +131 -0
  879. package/src/utils/release-date.ts +47 -0
  880. package/src/utils/sec.ts +3 -0
  881. package/src/utils/throttled-fetch.ts +11 -4
  882. package/src/utils/trailing-eps.ts +67 -0
  883. package/src/utils/typed-text.ts +21 -0
  884. package/src/utils/utc-time.ts +24 -1
  885. package/src/version.ts +6 -0
  886. package/src/cli/pane-functions/screenshot.ts +0 -2122
@@ -14,7 +14,7 @@ import type {
14
14
  } from "../../types/financials";
15
15
  import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind, withCurrencyMinorDigits } from "../../market-data/market/format";
16
16
  import { INDEX_POINTS_EXPORT_UNIT, isIndexLevel, reportedQuoteVolume } from "../../market-data/market/index-level";
17
- import { changeBasisJson, changeBasisLabel, changeBasisNote, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
17
+ import { changeBasisJson, changeBasisLabel, changeBasisNote, quoteChangeBasis, quotePreviousCloseDate, quoteSettleText } from "../../market-data/market/change-basis";
18
18
  import {
19
19
  EXTENDED_SESSION_LABELS,
20
20
  getExtendedSessionDisplay,
@@ -25,7 +25,7 @@ import {
25
25
  quoteSessionStateLabel,
26
26
  type ExtendedSession,
27
27
  } from "../../market-data/market/status";
28
- import { formatCompact, formatCurrency, formatDistributionAmount, formatNumber, formatPercent, formatPercentRaw } from "../../utils/format";
28
+ import { formatCompact, formatCurrency, formatDistributionAmount, formatNumber, formatPercent } from "../../utils/format";
29
29
  import { withCliServices, withMarketData } from "../context";
30
30
  import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
31
31
  import type { BuiltinCliCommandDef } from "../command-options";
@@ -38,8 +38,8 @@ import {
38
38
  formatPriceRange,
39
39
  quoteChangeText,
40
40
  } from "../helpers";
41
- import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
42
- import { statementHistoryNotes } from "../../utils/financial-statements";
41
+ import { cliArrow, cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
42
+ import { filledPeriodsNote, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
43
43
  import { formatPerShareNumber } from "../../utils/reported-money";
44
44
  import {
45
45
  analystTargetCurrency,
@@ -47,21 +47,26 @@ import {
47
47
  formatRatingLabel,
48
48
  formatRecommendationMix,
49
49
  recommendationTotal,
50
- targetUpside,
50
+ analystReferenceMetadata,
51
+ analystUpside,
52
+ formatAnalystReference,
51
53
  } from "../../plugins/builtin/research/analyst-model";
52
54
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
53
- import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
55
+ import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
54
56
  import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
55
57
  import {
56
- cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions, loadReportBondFacts,
57
- renderFundamentalsReport, writesCurrencyCodes,
58
+ atReportTtmRate, cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
59
+ loadReportBondFacts, renderFundamentalsReport, writesCurrencyCodes,
58
60
  } from "./ticker";
61
+ import { createBaseConverter } from "../base-converter";
59
62
  import { loadStatementFilings } from "../statement-filings";
60
63
  import { loadBankFigures } from "../bank-section";
61
64
  import { latestFinancialPeriod } from "../../utils/latest-financial-period";
62
65
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
63
66
  import { currencyCodeLabel } from "../../utils/currency-units";
64
- import { futuresContractLines, futuresRollLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer } from "../../market-data/futures-contract";
67
+ import {
68
+ futuresContractLines, futuresPricePer, futuresRollLines, isFuturesInstrument, loadUsdPerMmbtu, usdPerMmbtuText, type UsdPerMmbtu,
69
+ } from "../../market-data/futures-contract";
65
70
  import {
66
71
  chainHasExpiry,
67
72
  formatExpiryList,
@@ -80,9 +85,14 @@ import {
80
85
  valueChainAtModelSpot,
81
86
  type OptionsChainBasis,
82
87
  } from "./options-chain";
83
- import { missingExpiryMessage, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../utils/option-expiry";
88
+ import {
89
+ expirationHint, expiredExpiryText, expiryIsoDate, isLiveOptionExpiry, liveDefaultExpiry, liveOptionExpiries, missingExpiryMessage,
90
+ OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, withLiveExpiryList,
91
+ } from "../../utils/option-expiry";
84
92
  import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
85
93
  import { apiClient } from "../../api-client";
94
+ import type { ExchangeAnnouncementsPayload } from "../../api-client/exchange-announcements";
95
+ import { exchangeAnnouncementsPage, listsExchangeAnnouncements, type ExchangeAnnouncementsPage } from "../../utils/exchange-announcements";
86
96
  import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
87
97
  import { declaredDividends } from "../../utils/declared-dividends";
88
98
  import {
@@ -91,12 +101,17 @@ import {
91
101
  import type { CliResultColumn } from "../result";
92
102
  import type { PriceHistoryResult } from "../../types/price-history";
93
103
  import {
104
+ exportNumber,
94
105
  exportRowsTable,
95
106
  reportFooterLines,
96
107
  selectReportTables,
97
108
  type CliReportTables,
98
109
  } from "../report-tables";
110
+ import { historyCoverageNotice } from "../../sources/history-coverage";
111
+ import { maxDrawdown, maxDrawdownText } from "../../utils/max-drawdown";
112
+ import { isPastPriceWindow, loadPriceWindowHistory, PRICE_WINDOW_FROM_HELP, PRICE_WINDOW_TO_HELP, priceWindowText, readPriceWindow, type PriceWindow } from "../../utils/price-window";
99
113
  import {
114
+ BAR_INTERVAL_LABELS,
100
115
  closedPeriodBarDay,
101
116
  formingPeriodBar,
102
117
  historyBarStatusNote,
@@ -109,6 +124,7 @@ import {
109
124
  historyAllRangeYears,
110
125
  historyIntervalsByRange,
111
126
  historyMissingVolumeNote,
127
+ historyVenueVolumeNote,
112
128
  historyNotes,
113
129
  historyReportsVolume,
114
130
  historyPriceDecimals,
@@ -116,20 +132,54 @@ import {
116
132
  historyUnit,
117
133
  historyVolume,
118
134
  historyVolumeText,
135
+ type HistoryRow,
136
+ type HistoryUnit,
137
+ type HistoryVolume,
119
138
  } from "../history-rows";
139
+ import {
140
+ HISTORY_EXPORT_COLUMNS,
141
+ historyExportStatus,
142
+ historyFailureRow,
143
+ oneLineFailure,
144
+ symbolListNotes,
145
+ type HistoryListRow,
146
+ } from "../history-export";
147
+ import { isCliFailure } from "../errors";
148
+ import { mapWithConcurrency } from "../../utils/concurrency";
120
149
  import { MISSING_VOLUME_TEXT } from "../../utils/price-history-integrity";
121
150
  import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
122
151
  import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures-board";
123
152
  import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
124
153
  import { newsEmptyMessage } from "./news-hints";
125
- import { commodityNewsRoute, commonWordCandidate, newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, noStoriesMessage, quotedSearchCommand, searchNewsStories, wordMeansMore } from "./news-search";
154
+ import {
155
+ commodityNewsRoute,
156
+ commonWordCandidate,
157
+ METALS_NEWS_TOPIC,
158
+ metalCodeNewsRoute,
159
+ type MetalNewsRoute,
160
+ metalNewsHeading,
161
+ metalNewsRoute,
162
+ newsFallbackText,
163
+ newsSearchHeading,
164
+ newsSearchText,
165
+ newsSearchTextProblem,
166
+ noMetalStoriesMessage,
167
+ noStoriesMessage,
168
+ quotedSearchCommand,
169
+ searchNewsStories,
170
+ wordMeansMore,
171
+ } from "./news-search";
126
172
  import { formatUtcTime } from "../../utils/utc-time";
127
173
  import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
128
174
  import { isFiniteNumber } from "../../utils/guards";
129
175
  import { exportedFundamentals } from "../../utils/price-earnings";
130
176
  import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
131
- import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
132
- import { selectMarketCapitalization } from "../../utils/market-capitalization";
177
+ import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
178
+ import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
179
+ import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
180
+ import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
181
+ import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
182
+ import { viewMarketCapitalization } from "../../utils/market-capitalization";
133
183
  import {
134
184
  barHistoryFreshness,
135
185
  barResolutionFromDates,
@@ -147,6 +197,14 @@ import {
147
197
  quarterLabel,
148
198
  showsEpsBasis,
149
199
  } from "../../plugins/builtin/shared/earnings-report";
200
+ import {
201
+ calendarEventOf,
202
+ calendarStatus,
203
+ newYorkDay,
204
+ resolveListingEvents,
205
+ withResolvedEarnings,
206
+ type ResolvedEarningsEvent,
207
+ } from "../../plugins/builtin/shared/earnings-calendar";
150
208
  import {
151
209
  EXCHANGE_OPTION,
152
210
  listingHeading,
@@ -155,12 +213,17 @@ import {
155
213
  listingVenues,
156
214
  listingChoiceNotes,
157
215
  listingReadNotes,
216
+ failUnchosenListing,
158
217
  loadBareVenues,
159
218
  loadForListing,
160
219
  loadListingQuote,
220
+ loadListingQuoteAnswer,
161
221
  listingFateLoader,
222
+ meantQuestion,
162
223
  movedSymbolLine,
224
+ ListingArgError,
163
225
  requireCliListing,
226
+ resolveCliListing,
164
227
  type CliListing,
165
228
  type ListingIdentity,
166
229
  } from "../listing-arg";
@@ -173,6 +236,8 @@ import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cu
173
236
  import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
174
237
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
175
238
  import { providerMissReason } from "../../sources/provider-errors";
239
+ import { isNetworkFailure } from "../../utils/network-failure";
240
+ import { withSavedCopy, type ReportFreshness } from "../pane-functions/freshness";
176
241
  import { isNotATickerMessage } from "../not-a-ticker";
177
242
  import { primeBoardCoins } from "../board-coins";
178
243
  import { suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
@@ -180,11 +245,12 @@ import { quotedPhrase, splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-
180
245
  import { isCompanyNameInput } from "../../tickers/company-name-listing";
181
246
  import { readNameEntries, readPhraseArgument } from "../name-entries";
182
247
  import { readSymbolList, SymbolListError } from "../symbol-list";
248
+ import { notASymbolError } from "../symbol-cells";
183
249
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
184
250
  import { knownGapMessage, subMinuteDataMessage } from "../known-gaps";
185
251
  import { phraseSearchReport } from "./search";
186
252
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
187
- import { isCryptoPairSymbol } from "../../utils/crypto-pair";
253
+ import { isCryptoPairSymbol, isRoundTheClockCoin } from "../../utils/crypto-pair";
188
254
  import { isUsListingExchange } from "../../utils/exchanges";
189
255
  import {
190
256
  describeFormerIssuer, describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type FormerIssuer, type TickerChange,
@@ -193,7 +259,21 @@ import { noSecReportsNotice, nonUsSecListingVenue, secFilingItemCodes } from "..
193
259
  import type { MarketContext } from "../types";
194
260
  import { windowRows } from "../row-window";
195
261
  import { answerQuote, errorMessage, notUsTickerSentence } from "../quote-answer";
196
- import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
262
+ import { crossRate, describeFxConversion, describeFxRate, formatFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
263
+ import {
264
+ describeShortRate,
265
+ formatForwardPoints,
266
+ formatRatePercent,
267
+ formatSignedPercent,
268
+ IMPLIED_FORWARD_LABEL,
269
+ impliedForward,
270
+ parseForwardTenor,
271
+ shortRateFor,
272
+ } from "../fx-forward";
273
+ import { unavailableOnServer } from "../../plugins/builtin/shared/cloud-resource";
274
+
275
+ /** `fx`'s forward form, for its usage line. */
276
+ const FX_FORWARD_USAGE = "fx <base>/<quote> --forward <tenor>";
197
277
  import {
198
278
  holderListFacts,
199
279
  holderListPeriod,
@@ -215,7 +295,15 @@ import {
215
295
  } from "../../plugins/builtin/holders/report-rows";
216
296
  import { fetchBeneficialOwners } from "../../plugins/builtin/holders/beneficial-client";
217
297
  import { centralBankEventsMessage } from "../central-bank-events";
218
- import { classSharesToday } from "../../plugins/builtin/holders/client";
298
+ import { classSharesAsOf, classSharesToday } from "../../plugins/builtin/holders/client";
299
+ import {
300
+ HOLDER_ROWS_MAX,
301
+ indexedHoldersPartialNote,
302
+ indexedHolderUnits,
303
+ indexHolderTicker,
304
+ loadIndexedHolders,
305
+ withIndexedHolders,
306
+ } from "../../plugins/builtin/holders/index-holders";
219
307
  import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
220
308
  import { currentReportItemsLabel } from "../../utils/sec-filings";
221
309
  import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
@@ -249,6 +337,11 @@ type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
249
337
  futuresContract?: FuturesContractTermsPayload;
250
338
  /** The quote of the root's front contract, for a futures row that prices another one (NGF27, NG2). */
251
339
  futuresFront?: Quote;
340
+ /**
341
+ * A per-MWh gas price (Dutch TTF) in dollars per MMBtu, the unit Henry Hub
342
+ * trades in, converted at the day's rate: one unit of the quote's currency in dollars.
343
+ */
344
+ usdPerMmbtu?: UsdPerMmbtu;
252
345
  /** A perpetual row's market, funding per eight hours and open interest. */
253
346
  perpetualContract?: PerpetualContractPayload;
254
347
  /** The symbol changed: its issuer moved to another symbol, or its holder had another before. */
@@ -299,10 +392,11 @@ async function readSymbols(symbols: readonly string[], dataProvider: Parameters<
299
392
  }
300
393
 
301
394
  /**
302
- * The symbols of `--file <path>` or standard input (`-`), with any typed
303
- * beside them, read by the parser `watchlist add` uses, and its notes on what
304
- * it left out. Standard input is read only when asked for, so a script that
305
- * pipes in nothing never waits on it.
395
+ * The symbols typed, of `--file <path>` and of standard input (`-`), read by
396
+ * the reader `watchlist add` uses, and its notes on what it left out (a
397
+ * stray separator, the prices beside the symbols, in one line). Standard
398
+ * input is read only when asked for, so a script that pipes in nothing never
399
+ * waits on it.
306
400
  */
307
401
  async function readListedSymbols(
308
402
  args: readonly string[],
@@ -315,7 +409,11 @@ async function readListedSymbols(
315
409
  const list = await readSymbolList({
316
410
  args, file, stdinPiped: false, readEntries: (entries) => readNameEntries(entries, market.dataProvider),
317
411
  });
318
- if (list.symbols.length === 0) ctx.fail(`No symbols in ${file && file !== "-" ? file : "standard input"}.`, usage);
412
+ // Only numbers, or nothing: the message says what was left out (`Ignored 1.899, a number, not a symbol.`).
413
+ if (list.symbols.length === 0) {
414
+ const where = file && file !== "-" ? file : args.includes("-") ? "standard input" : "what was given";
415
+ ctx.fail([`No symbols in ${where}.`, ...list.notes].join(" "), usage);
416
+ }
319
417
  return { symbols: uniqueEntries(list.symbols), notes: list.notes };
320
418
  } catch (error) {
321
419
  if (error instanceof SymbolListError) ctx.fail(error.message, usage);
@@ -323,6 +421,15 @@ async function readListedSymbols(
323
421
  }
324
422
  }
325
423
 
424
+ /** What `quote --csv` and `compare --csv` write, for `help quote` and `help compare`. */
425
+ const QUOTE_CSV_SECTION = {
426
+ title: "CSV",
427
+ lines: [
428
+ "Last, Chg%, Prev Close and each extended print are plain numbers, the currency in Cur and currency_or_unit. A pre-market, after-hours or overnight print's move is in Pre-Market Chg%, After Hours Chg% or Overnight Chg%, and compare's day range in Day Low and Day High, after the columns the export already had.",
429
+ "A row with no figure says why in its status column, never NaN; a symbol with no quote is also named on standard error.",
430
+ ],
431
+ };
432
+
326
433
  /** `--file` of quote and compare, as `watchlist add` takes it. */
327
434
  const LIST_FILE_OPTION = {
328
435
  flags: "--file <path>",
@@ -334,8 +441,8 @@ function listingMetadata(identity: ListingIdentity) {
334
441
  return { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name };
335
442
  }
336
443
 
337
- // Fitted to a narrow width, the columns marked optional go first (higher dropPriority first), then Name is cut short.
338
- // What a user types or reads the price by stays whole.
444
+ // Fitted to a narrow width, quote's Updated, Feed, Cur, Chg Basis and session prints move to labelled lines under
445
+ // their row, in that order, before Name wraps: quote never drops a column. What a user types or reads the price by stays whole.
339
446
  const QUOTE_LEAD_COLUMNS = [
340
447
  { key: "symbol", header: "Symbol", shrink: false },
341
448
  { key: "name", header: "Name" },
@@ -343,18 +450,24 @@ const QUOTE_LEAD_COLUMNS = [
343
450
  key: "price",
344
451
  header: "Last",
345
452
  align: "right" as const,
346
- format: (value: unknown, row: QuoteRow) => (
347
- row.error && !value
453
+ // An export writes the number; the row's Cur and currency_or_unit name what it is in.
454
+ value: (row: QuoteRow) => exportFigure(row.rawPrice),
455
+ format: (_value: unknown, row: QuoteRow) => (
456
+ row.error && !row.price
348
457
  // A typo is not an outage: say what is wrong with the symbol itself.
349
458
  ? isNotATickerMessage(row.error) ? cliStyles.warning("not a ticker") : cliStyles.danger("unavailable")
350
- : yieldLevelCell(value, row)
459
+ : yieldLevelCell(row.price, row)
351
460
  ),
352
461
  },
353
462
  {
354
463
  key: "changePercent",
355
464
  header: "Chg%",
356
465
  align: "right" as const,
357
- format: (value: unknown, row: QuoteRow) => row.yieldIndex ? yieldChangeCell(row) : formatChangePercentCell(value),
466
+ format: (value: unknown, row: QuoteRow) => {
467
+ const move = row.yieldIndex ? yieldChangeCell(row) : formatChangePercentCell(value);
468
+ // Keep a rolling move's short label with the value when narrow tables drop Chg Basis.
469
+ return move && row.changeBasis === "24h" ? `${move} ${cliStyles.muted("24h")}` : move;
470
+ },
358
471
  },
359
472
  // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
360
473
  extendedColumn("PRE"),
@@ -373,16 +486,21 @@ type QuoteRow = ReturnType<typeof quoteRows>[number];
373
486
  * its existing columns, so a column read by position stays where it was.
374
487
  */
375
488
  const CHANGE_BASIS_COLUMN = {
376
- key: "changeBasis", header: "Chg Basis", shrink: false, optional: true, format: (value: unknown) => cliStyles.muted(String(value ?? "")),
489
+ key: "changeBasis", header: "Chg Basis", shrink: false, stackPriority: 2,
490
+ format: (value: unknown) => value === "24h" ? "" : cliStyles.muted(String(value ?? "")),
377
491
  };
378
492
  const CHANGE_BASIS_EXPORT_COLUMNS = [
379
493
  { key: "changeBasis", header: "Chg Basis" },
380
494
  { key: "changeBasisNote", header: "Chg Basis Note" },
381
495
  ];
382
496
 
383
- /** The table's columns: Chg Basis follows Chg%, and the note is a line under the table. */
497
+ /**
498
+ * The table's columns: Chg Basis follows Chg%, and the note is a line under
499
+ * the table. The figures an export adds (an extended print's move, the day's
500
+ * low and high) are already in the table's own cells.
501
+ */
384
502
  function quoteTextColumns<Column extends { key: string }>(columns: Column[]): Array<Column | typeof CHANGE_BASIS_COLUMN> {
385
- const exported = new Set(CHANGE_BASIS_EXPORT_COLUMNS.map((column) => column.key));
503
+ const exported = new Set([...CHANGE_BASIS_EXPORT_COLUMNS, ...EXTENDED_CHANGE_EXPORT_COLUMNS, ...DAY_RANGE_EXPORT_COLUMNS].map((column) => column.key));
386
504
  const kept = columns.filter((column) => !exported.has(column.key));
387
505
  const at = kept.findIndex((column) => column.key === "changePercent") + 1;
388
506
  return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
@@ -399,28 +517,70 @@ function yieldChangeCell(row: QuoteRow): string {
399
517
  return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
400
518
  }
401
519
 
520
+ /** A figure as an export writes it: the number without float noise, nothing for none. */
521
+ function exportFigure(value: number | null | undefined): number | null {
522
+ return value == null ? null : Number.isFinite(value) ? exportNumber(value) : value;
523
+ }
524
+
525
+ const EXTENDED_SESSIONS = ["PRE", "POST", "OVERNIGHT"] as const satisfies readonly ExtendedSession[];
526
+
527
+ function extendedKey(session: ExtendedSession): string {
528
+ return session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight";
529
+ }
530
+
402
531
  /**
403
532
  * The pre-market, after-hours or overnight print and its move from the
404
533
  * regular close, in its own column; "mid" when it is a bid and ask midpoint.
534
+ * An export writes the price alone, its move going to `<session> Chg%`
535
+ * (EXTENDED_CHANGE_EXPORT_COLUMNS) and a midpoint to a closing note.
405
536
  */
406
537
  function extendedColumn(session: ExtendedSession) {
407
538
  const price = (row: QuoteRow) => row.extendedMark ? `${row.extendedPrice} mid` : row.extendedPrice;
408
539
  return {
409
- key: session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight",
540
+ key: extendedKey(session),
410
541
  header: EXTENDED_SESSION_LABELS[session],
411
542
  align: "right" as const,
412
- value: (row: QuoteRow) => row.extendedSession === session && row.extendedPrice
413
- ? [price(row), row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
414
- : "",
415
- format: (value: unknown, row: QuoteRow) => (
416
- value ? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
543
+ value: (row: QuoteRow) => row.extendedSession === session ? exportFigure(row.rawExtendedPrice) : null,
544
+ format: (_value: unknown, row: QuoteRow) => (
545
+ row.extendedSession === session && row.extendedPrice
546
+ ? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim()
547
+ : ""
417
548
  ),
418
549
  };
419
550
  }
420
551
 
421
- /** A percent to two decimals, as the table prints it. */
552
+ /**
553
+ * Each extended print's move from the regular close, as a number: the
554
+ * columns an export adds after those it had, so a column read by position
555
+ * stays where it was. The table keeps the move beside the price.
556
+ */
557
+ const EXTENDED_CHANGE_EXPORT_COLUMNS = EXTENDED_SESSIONS.map((session) => ({
558
+ key: `${extendedKey(session)}ChangePercent`,
559
+ header: `${EXTENDED_SESSION_LABELS[session]} Chg%`,
560
+ value: (row: QuoteRow) => row.extendedSession === session ? row.extendedChangePercent : null,
561
+ }));
562
+
563
+ /**
564
+ * Why an exported quote row's figures are empty, for its status column: the
565
+ * symbol's failure as the warning says it, or the figure it has no value for.
566
+ */
567
+ function quoteExportStatus(row: QuoteRow): string {
568
+ if (row.rawPrice == null || !Number.isFinite(row.rawPrice)) return row.error || "Last: no price";
569
+ if (row.changePercent == null || !Number.isFinite(row.changePercent)) return "Chg%: no previous close";
570
+ return "";
571
+ }
572
+
573
+ /** The closing note of an export with an extended price that is a bid and ask midpoint: the cell holds the bare number. */
574
+ function extendedMidNotes(data: readonly QuoteCliRecord[]): string[] {
575
+ return data.flatMap((row) => {
576
+ const extended = getExtendedSessionDisplay(row.quote);
577
+ return extended?.mark ? [`${row.target.symbol} ${EXTENDED_SESSION_LABELS[extended.session]} is the bid and ask midpoint; nothing has traded in that session`] : [];
578
+ });
579
+ }
580
+
581
+ /** A percent to two decimals, as the table prints it; none for a move that is no number (a zero close). */
422
582
  function roundedPercent(value: number | undefined): number | null {
423
- return value == null ? null : Number(value.toFixed(2));
583
+ return value == null || !Number.isFinite(value) ? null : Number(value.toFixed(2));
424
584
  }
425
585
 
426
586
  /** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
@@ -428,35 +588,54 @@ function currencyCell(value: unknown): string {
428
588
  return currencyCodeLabel(typeof value === "string" ? value : "");
429
589
  }
430
590
 
591
+ /** The session prints of a narrow quote table move under their row after its other columns, before the name. */
592
+ const EXTENDED_KEYS = new Set(["preMarket", "afterHours", "overnight"]);
593
+
431
594
  function quoteColumns() {
432
595
  return [
433
- ...QUOTE_LEAD_COLUMNS,
434
- { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1, format: currencyCell },
596
+ ...QUOTE_LEAD_COLUMNS.map((column) => EXTENDED_KEYS.has(column.key) ? { ...column, stackPriority: 1 } : column),
597
+ { key: "currency", header: "Cur", shrink: false, stackPriority: 3, format: currencyCell },
435
598
  // Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
436
599
  // row says so here, so the closing line's "N of M stale" always points at rows the table marks.
437
600
  {
438
601
  key: "feed",
439
602
  header: "Feed",
440
603
  shrink: false,
441
- optional: true,
442
- dropPriority: 2,
604
+ stackPriority: 4,
443
605
  format: (value: unknown) => value === "stale" ? cliStyles.warning(value) : String(value ?? ""),
444
606
  },
445
- // In a narrow terminal the closing line's as-of stands in for each row's.
446
- { key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
607
+ { key: "updatedAt", header: "Updated", shrink: false, stackPriority: 5 },
447
608
  ...CHANGE_BASIS_EXPORT_COLUMNS,
609
+ ...EXTENDED_CHANGE_EXPORT_COLUMNS,
448
610
  ];
449
611
  }
450
612
 
613
+ /** The day's low and high as numbers, after the columns an export already had. */
614
+ const DAY_RANGE_EXPORT_COLUMNS = [
615
+ { key: "dayLow", header: "Day Low", value: (row: QuoteRow) => exportFigure(row.rawDayLow) },
616
+ { key: "dayHigh", header: "Day High", value: (row: QuoteRow) => exportFigure(row.rawDayHigh) },
617
+ ];
618
+
451
619
  function compareColumns() {
452
620
  return [
453
621
  ...QUOTE_LEAD_COLUMNS,
454
- { key: "previousClose", header: "Prev Close", align: "right" as const, format: yieldLevelCell },
622
+ {
623
+ key: "previousClose", header: "Prev Close", align: "right" as const,
624
+ value: (row: QuoteRow) => exportFigure(row.rawPreviousClose),
625
+ format: (_value: unknown, row: QuoteRow) => yieldLevelCell(row.previousClose, row),
626
+ },
455
627
  // A narrow terminal drops these before it cuts the names short.
456
- { key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
628
+ // An export writes the range as plain numbers (12.5-13.1), and each end as a number in Day Low and Day High.
629
+ {
630
+ key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1,
631
+ value: (row: QuoteRow) => row.rawDayLow != null && row.rawDayHigh != null ? `${exportFigure(row.rawDayLow)}-${exportFigure(row.rawDayHigh)}` : "",
632
+ format: (_value: unknown, row: QuoteRow) => row.dayRange,
633
+ },
457
634
  { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
458
635
  { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
459
636
  ...CHANGE_BASIS_EXPORT_COLUMNS,
637
+ ...EXTENDED_CHANGE_EXPORT_COLUMNS,
638
+ ...DAY_RANGE_EXPORT_COLUMNS,
460
639
  ];
461
640
  }
462
641
 
@@ -510,12 +689,18 @@ function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { prev
510
689
  return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
511
690
  }
512
691
 
692
+ /** An explicit missing comparison wins over any leftover numeric move in a CLI quote. */
693
+ function quoteSessionFields(quote: Quote | null | undefined) {
694
+ const fields = getQuoteSessionFields(quote);
695
+ return quote?.changeBasis === "none" ? { ...fields, change: null, changePercent: null } : fields;
696
+ }
697
+
513
698
  function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
514
699
  return results.map((result) => {
515
700
  const quote = result.quote;
516
701
  // Last and Chg% are the regular session, as `ticker` and the quote monitor read it; a
517
702
  // pre-market or after-hours print is its own column, measured from that session's close.
518
- const display = getRegularSessionDisplay(quote);
703
+ const display = quoteSessionFields(quote);
519
704
  const extended = getExtendedSessionDisplay(quote);
520
705
  const yieldIndex = isYieldIndexSymbol(result.target.symbol);
521
706
  // An index level is in points, not in the currency its members trade in.
@@ -527,25 +712,26 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
527
712
  minimumFractionDigits: indexPoints ? 2 : currencyPriceMinDigits(quote?.currency),
528
713
  ...nearParCoin(quote) ? { fixedFractionDigits: STABLECOIN_PRICE_DIGITS } : {},
529
714
  };
530
- const price = (value: number | undefined) => (
715
+ const price = (value: number | null | undefined) => (
531
716
  quote && value != null
532
717
  ? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
533
718
  : ""
534
719
  );
535
720
  // A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
536
- const per = display?.price != null ? minorUnitPricePer(result.futuresContract, quote?.currency) : "";
721
+ const per = display.price != null ? futuresPricePer(result.futuresContract, quote?.currency) : "";
722
+ const previousClose = display.price != null && display.change != null ? display.price - display.change : quote?.previousClose;
537
723
  return {
538
724
  symbol: result.target.symbol,
539
725
  name: quote?.name || result.listing?.name || "",
540
- price: `${price(display?.price)}${per}`,
541
- rawPrice: display?.price ?? null,
726
+ price: `${price(display.price)}${per}`,
727
+ rawPrice: display.price,
542
728
  priceBasis: quote?.priceBasis ?? null,
543
729
  instrumentType: quote?.instrumentType ?? null,
544
- change: display?.change ?? null,
545
- changePercent: roundedPercent(display?.changePercent),
730
+ change: display.change,
731
+ changePercent: roundedPercent(display.changePercent ?? undefined),
546
732
  // A Cboe yield index is read as a yield in percent and moves in basis points.
547
733
  yieldIndex,
548
- changeBasisPoints: yieldIndex && display?.change != null ? toBasisPoints(display.change, 1) : null,
734
+ changeBasisPoints: yieldIndex && display.change != null ? toBasisPoints(display.change, 1) : null,
549
735
  extendedSession: extended?.session ?? null,
550
736
  extendedPrice: price(extended?.price),
551
737
  rawExtendedPrice: extended?.price ?? null,
@@ -556,7 +742,10 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
556
742
  session: quoteSessionStateLabel(quote, now) ?? "",
557
743
  sessionLabel: quoteSessionLabel(quote, now) ?? "",
558
744
  // The close the shown move is measured from: the one before the session Last is.
559
- previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
745
+ previousClose: price(previousClose),
746
+ rawPreviousClose: previousClose ?? null,
747
+ rawDayLow: quote?.low != null && quote.high != null ? quote.low : null,
748
+ rawDayHigh: quote?.low != null && quote.high != null ? quote.high : null,
560
749
  dayRange: quote?.low != null && quote.high != null
561
750
  ? indexPoints ? `${price(quote.low)}-${price(quote.high)}` : formatPriceRange(quote.low, quote.high, quote.currency, options, "-")
562
751
  : "",
@@ -625,15 +814,17 @@ function filingsWithItemLabels(filings: SecFilingItem[]) {
625
814
  });
626
815
  }
627
816
 
628
- function analystSummary(data: AnalystResearchData): string {
817
+ /** The summary over the ratings: the upside from Last, and the price and time it is measured from, as `fn ANR` gives them. */
818
+ function analystSummary(data: AnalystResearchData, quote: Quote | null): string {
629
819
  const target = data.priceTarget;
630
820
  const currency = analystTargetCurrency(data);
631
- const upside = targetUpside(target);
821
+ const { upside, reference } = analystUpside(data, quote);
632
822
  const analysts = recommendationTotal(data);
633
823
  const mix = formatRecommendationMix(data);
634
824
  const entries: Array<[string, string]> = [
635
825
  ["Average Target", formatAnalystPrice(target?.average, currency)],
636
826
  ["Upside", upside == null ? "-" : formatPercent(upside)],
827
+ ["Upside vs", upside == null ? "-" : formatAnalystReference(reference, currency)],
637
828
  ["Low / Median / High", target && [target.low, target.median, target.high].some((value) => value != null)
638
829
  ? [target.low, target.median, target.high].map((value) => formatAnalystPrice(value, currency)).join(" / ")
639
830
  : "-"],
@@ -734,13 +925,13 @@ function formatEpsCell(value: unknown): string {
734
925
  return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "";
735
926
  }
736
927
 
737
- function earningsRows(events: EarningsEvent[]) {
928
+ function earningsRows(events: ResolvedEarningsEvent[]) {
738
929
  return events.map((event) => ({
739
930
  symbol: event.symbol,
740
931
  name: event.name,
741
932
  date: isoDate(event.earningsDate).slice(0, 10),
742
- // Whether the company announced the date or it is projected from past reports.
743
- status: earningsDateLabel(event),
933
+ // Out, announced by the company, or projected from its past reports.
934
+ status: earningsDateLabel(event.dateStatus),
744
935
  timing: event.timing,
745
936
  epsEstimate: event.epsEstimate,
746
937
  epsActual: event.epsActual,
@@ -756,17 +947,17 @@ const MAX_CHOICE_LOOKUPS = 10;
756
947
  * The line `quote` prints beside a symbol that changed, as `history` and
757
948
  * `ticker` word it: a company that moved, with nothing new listed there,
758
949
  * names the quote of its new symbol ("HFC became DINO on 2022-03-15:
759
- * `gloomberb quote DINO`"); its old line's last price stays in the row.
950
+ * `gloom quote DINO`"); its old line's last price stays in the row.
760
951
  */
761
952
  function quoteChangeLine(change: TickerChange | null | undefined, formerIssuer?: FormerIssuer | null): string | null {
762
953
  const moved = movedAwayChange(change);
763
954
  // One line per symbol: a company that left it, else the company that merged away from it (GOGL).
764
- return (moved ? movedSymbolLine(moved, `gloomberb quote ${moved.tradesAs}`) : tickerChangeLine(change)) ?? formerIssuer?.note ?? null;
955
+ return (moved ? movedSymbolLine(moved, `gloom quote ${moved.tradesAs}`) : tickerChangeLine(change)) ?? formerIssuer?.note ?? null;
765
956
  }
766
957
 
767
958
  async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
768
959
  const args = [...rawArgs];
769
- const usage = `Usage: gloomberb ${commandName} <symbol...> [--file <path>]`;
960
+ const usage = `Usage: gloom ${commandName} <symbol...> [--file <path>]`;
770
961
  const exchange = takeOption(args, "--exchange") ?? "";
771
962
  // A list from a file or standard input (`-`), read as `watchlist add` reads one.
772
963
  const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
@@ -779,16 +970,33 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
779
970
  await withMarketData(ctx, async (market) => {
780
971
  const read = phrase ? await readPhraseArgument(phrase, market.dataProvider) : null;
781
972
  if (read?.kind === "phrase") return answerPhrase(read.phrase, market, ctx);
782
- // A list read from a file names what it left out: a stray separator, the cells beside a table's symbols.
783
- const list = listed && !read ? await readListedSymbols(args, file, market, ctx, usage) : null;
973
+ // Typed or from a file, one reader names what it left out: a stray separator, the prices beside the symbols.
974
+ const list = read ? null : await readListedSymbols(args, file, market, ctx, usage);
784
975
  const symbols = read?.kind === "name" ? [read.name]
785
976
  : read?.kind === "symbols" ? uniqueEntries(read.symbols)
786
- : list ? list.symbols
787
- : await readSymbols(entries, market.dataProvider);
788
- // With several symbols, --exchange is for the ones that name no exchange of their own.
789
- const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
790
- symbol, exchange, market, ctx, { ownExchangeWins: symbols.length > 1 },
791
- )));
977
+ : list!.symbols;
978
+ // What cannot be a symbol (N/A, $$$, a price alone) is never looked up; one line names it.
979
+ const refused = symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
980
+ if (refused.length === symbols.length) {
981
+ ctx.fail(refused.length === 1 ? refused[0]!.message : `Nothing to quote: ${refusedList(refused)} are not symbols.`, usage);
982
+ }
983
+ // With several symbols, --exchange is for the ones that name no exchange of their own, and one that names no
984
+ // listing (EQT:NYSE:US) is named in a line while the others are quoted.
985
+ const resolvedListings = await Promise.all(symbols.filter((symbol) => !notASymbolError(symbol)).map(async (symbol) => {
986
+ if (symbols.length === 1) return { symbol, listing: await requireCliListing(symbol, exchange, market, ctx) };
987
+ try {
988
+ return { symbol, listing: await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true }) };
989
+ } catch (error) {
990
+ if (error instanceof ListingArgError) return { symbol, error };
991
+ throw error;
992
+ }
993
+ }));
994
+ const unread = resolvedListings.flatMap((entry) => "error" in entry && entry.error ? [{ symbol: entry.symbol, error: entry.error }] : []);
995
+ const listings = resolvedListings.flatMap((entry) => "listing" in entry && entry.listing ? [entry.listing] : []);
996
+ if (listings.length === 0) {
997
+ const [first] = unread;
998
+ ctx.fail(first!.error.message, [...unread.slice(1).map(unreadLine), ...refused.length > 0 ? [refusedLine(refused)] : []].join("\n") || first!.error.details);
999
+ }
792
1000
  const targets: QuoteSubscriptionTarget[] = listings.map((listing) => ({ symbol: listing.request.symbol, exchange: listing.request.exchange }));
793
1001
  // A bare symbol's venues load beside its quote, to name the company it was read as when several list it.
794
1002
  // Each costs a search, so a long list (a portfolio check, not a lookup) names none.
@@ -798,7 +1006,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
798
1006
  const changes = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
799
1007
  : listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
800
1008
  // The service's quote of the moment, not each form's local copy from whenever it was last asked for:
801
- // BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote (GLO-735). A local copy answers only offline.
1009
+ // BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote. A local copy answers only offline.
802
1010
  const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true });
803
1011
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
804
1012
  const asked = results.map(listingOf);
@@ -819,7 +1027,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
819
1027
  const tickerChanges = symbolChanges.map(({ change }) => change);
820
1028
  const formerIssuers = symbolChanges.map(({ formerIssuer }) => formerIssuer);
821
1029
  const failed = answers[0]?.failure;
822
- if (listings.length === 1 && failed) {
1030
+ if (symbols.length === 1 && failed) {
823
1031
  ctx.fail(failed.message, [quoteChangeLine(tickerChanges[0], formerIssuers[0]), failed.details].filter(Boolean).join("\n") || undefined);
824
1032
  }
825
1033
  // A futures quote adds its dated settlement and contract terms, as `ticker` prints them; `compare` stays a table.
@@ -828,6 +1036,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
828
1036
  : null));
829
1037
  // A contract that is not its root's front (NGF27, NG2) prints the front's own price beside it, read in one batch.
830
1038
  const fronts = await loadFuturesFrontQuotes(contracts, market.dataProvider);
1039
+ // A per-MWh gas price (TTF) adds its dollars per MMBtu at the day's rate, to read beside Henry Hub.
1040
+ const gasInUsd = await Promise.all(answers.map(({ result }, index) => loadUsdPerMmbtu(
1041
+ getRegularSessionDisplay(result.quote)?.price, contracts[index], result.quote?.currency, (currency) => market.dataProvider.getExchangeRate(currency))));
831
1042
  // A perpetual adds its funding per eight hours and open interest, the figures PERP lists beside its mark.
832
1043
  const perpetualContracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
833
1044
  ? cliPerpetualContract(result.quote)
@@ -842,12 +1053,13 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
842
1053
  target: result.target,
843
1054
  listing: listingMetadata(listingIdentity(listing, result.quote)),
844
1055
  // The figures the table shows, beside the quote as the source sent it.
845
- ...getQuoteSessionFields(result.quote),
1056
+ ...quoteSessionFields(result.quote),
846
1057
  // What `change` is measured from, with the label and holiday note the table shows.
847
1058
  changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
848
1059
  quote: withPreviousCloseDate(result.quote),
849
1060
  ...contracts[index] ? { futuresContract: contracts[index] } : {},
850
1061
  ...fronts[index] ? { futuresFront: fronts[index] } : {},
1062
+ ...gasInUsd[index] ? { usdPerMmbtu: gasInUsd[index] } : {},
851
1063
  ...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
852
1064
  ...usSuggestions[index] ? { usSuggestion: usSuggestions[index] } : {},
853
1065
  error: failure?.oneLine ?? errorMessage(result.error),
@@ -857,21 +1069,34 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
857
1069
  }));
858
1070
  const notes = [
859
1071
  ...list?.notes ?? [],
1072
+ ...refused.length > 0 ? [refusedLine(refused)] : [],
1073
+ ...unread.map(unreadLine),
860
1074
  // FB now lists another security; the company that was FB trades as META. A symbol a
861
1075
  // company renamed into (META) is what was typed: its change is in the row, not here.
862
1076
  // GOGL was Golden Ocean, which merged into CMB.Tech; a fund lists there now.
863
1077
  ...tickerChanges.flatMap((change, index) => quoteChangeLine(change, formerIssuers[index]) ?? []),
864
1078
  // Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
865
1079
  ...answers.flatMap((answer) => answer.notes),
866
- // A row that stopped trading long ago reads as a quote that is merely stale.
867
- ...data.flatMap((row) => row.quote
868
- ? noLongerTradingNote(`${row.target.symbol}${row.listing.name ? ` (${row.listing.name})` : ""}`, row.quote, now) ?? []
869
- : []),
870
- // Text mode shows only "unavailable" in the cell; the JSON rows already carry each reason.
871
- ...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
1080
+ // A row that stopped trading long ago reads as a quote that is merely stale, and one with
1081
+ // nothing printed in a session the calendar has open (a feed that stalled, a typhoon day) says since when.
1082
+ ...data.flatMap((row) => {
1083
+ if (!row.quote) return [];
1084
+ const label = `${row.target.symbol}${row.listing.name ? ` (${row.listing.name})` : ""}`;
1085
+ const ended = noLongerTradingNote(label, row.quote, now);
1086
+ const quiet = ended ? null : noPrintsTodayText(row.quote, now);
1087
+ return [ended ?? (quiet ? `${label}: ${quiet}` : []), quoteBarNotes(label, row.quote, now)].flat();
1088
+ }),
1089
+ // A receipt and its home line each say the ratio, as `ticker` does.
1090
+ ...data.flatMap((row) => {
1091
+ const pair = listingDepositaryPair({ quote: row.quote });
1092
+ return pair ? [`${row.target.symbol}: ${depositaryPairText(pair, isReceiptSide({ quote: row.quote }))}.`] : [];
1093
+ }),
1094
+ // A cell shows only "unavailable", and an exported row only its status, so stderr names each failure; the JSON rows carry each reason.
1095
+ ...ctx.cliOptions.format !== "json" ? quoteNotes(data, { exchange }) : [],
872
1096
  ];
873
1097
  const columns = commandName === "compare" ? compareColumns() : quoteColumns();
874
1098
  const basisNotes = changeBasisNotes(data, now);
1099
+ const midNotes = extendedMidNotes(data);
875
1100
  if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
876
1101
  // Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
877
1102
  const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
@@ -880,7 +1105,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
880
1105
  warnings: notes.length > 0 ? notes : undefined,
881
1106
  // A Cboe yield index stops at its 15:00 ET close, before the Treasury's curve is taken: the line says so, so its
882
1107
  // close does not read as a contradiction of DGS10 or GC for the same day.
883
- freshness: quotesFreshness(data.map((row) => row.quote), yieldsOnly ? { schedule: `Cboe close ${CBOE_YIELD_INDEX_CLOSE}` } : undefined),
1108
+ // Dated by the quotes as served, which say whether one is the saved copy.
1109
+ freshness: withSavedCopy(
1110
+ quotesFreshness(data.map((row) => row.quote), yieldsOnly ? { schedule: `Cboe close ${CBOE_YIELD_INDEX_CLOSE}` } : undefined),
1111
+ answers.map(({ result }) => result.quote),
1112
+ ),
884
1113
  }, {
885
1114
  rows: quoteRows,
886
1115
  // An index row's Cur is blank, its level having no currency; the export names what it is in instead.
@@ -888,6 +1117,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
888
1117
  symbolOrSeries: row.symbol,
889
1118
  currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
890
1119
  }),
1120
+ // A row with no price or move says why in its status column, as the warnings do.
1121
+ exportStatus: quoteExportStatus,
1122
+ ...(midNotes.length > 0 ? { exportNotes: midNotes } : {}),
891
1123
  details: quoteDetailsText,
892
1124
  ...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
893
1125
  columns,
@@ -895,11 +1127,27 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
895
1127
  ? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
896
1128
  : columns),
897
1129
  });
898
- // The table names each symbol it found nothing for; with none found, the exit status says so too.
1130
+ // The table and the warnings name each symbol it found nothing for; with none found, the exit status says so too.
899
1131
  if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
900
1132
  });
901
1133
  }
902
1134
 
1135
+ /** The entries refused before any lookup, named as written: `$$$, N/A`, split by semicolons when one has a comma. */
1136
+ function refusedList(refused: ReadonlyArray<{ input: string }>): string {
1137
+ const inputs = refused.map((error) => error.input.trim());
1138
+ return inputs.join(inputs.some((input) => input.includes(",")) ? "; " : ", ");
1139
+ }
1140
+
1141
+ /** The one line a quote prints for what it did not look up: `Not a symbol, not quoted: $$$, N/A.` */
1142
+ function refusedLine(refused: ReadonlyArray<{ input: string }>): string {
1143
+ return `Not ${refused.length === 1 ? "a symbol" : "symbols"}, not quoted: ${refusedList(refused)}.`;
1144
+ }
1145
+
1146
+ /** A symbol of a list that names no listing: `Not quoted: EQT:NYSE:US (Unknown exchange US for EQT:NYSE).` */
1147
+ function unreadLine({ symbol, error }: { symbol: string; error: ListingArgError }): string {
1148
+ return `Not quoted: ${symbol} (${error.message.replace(/\.$/, "")}).`;
1149
+ }
1150
+
903
1151
  /**
904
1152
  * `quote "10 year yield"`: a phrase that is no ticker and no company's name
905
1153
  * is answered with what search finds for it and the catalog, never quoted
@@ -909,7 +1157,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
909
1157
  async function answerPhrase(phrase: string, market: MarketContext, ctx: Parameters<CliCommandDef["execute"]>[1]): Promise<void> {
910
1158
  const said = `"${phrase}" is not a ticker or a company's name.`;
911
1159
  if (ctx.cliOptions.format !== "text") {
912
- ctx.fail(said, [knownGapMessage(phrase), `Search for it: gloomberb search "${phrase}"`, `Functions and data: gloomberb catalog "${phrase}"`]
1160
+ ctx.fail(said, [knownGapMessage(phrase), `Search for it: gloom search "${phrase}"`, `Functions and data: gloom catalog "${phrase}"`]
913
1161
  .filter(Boolean).join("\n"));
914
1162
  }
915
1163
  process.exitCode = CLI_EXIT_CODES.error;
@@ -964,12 +1212,15 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
964
1212
  }
965
1213
  const terms = row.futuresContract;
966
1214
  if (!terms || !row.quote) return [];
967
- const settle: [string, string] = ["Settle", futuresSettleText(terms, row.quote)];
1215
+ const settle: [string, string] = ["Settle", quoteSettleText(row.quote, terms)];
968
1216
  // The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
969
1217
  const session = getRegularSessionDisplay(row.quote);
970
1218
  const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
971
1219
  ? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
972
1220
  : [];
1221
+ // European gas beside Henry Hub: "≈ $30.832/MMBtu at EUR/USD 1.1650", a conversion that says so.
1222
+ const inUsd = row.usdPerMmbtu;
1223
+ if (inUsd) change.push(["In USD", usdPerMmbtuText(inUsd)]);
973
1224
  const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
974
1225
  const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
975
1226
  const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
@@ -990,17 +1241,21 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
990
1241
  return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
991
1242
  }
992
1243
 
993
- /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
1244
+ /**
1245
+ * A crypto pair that will not load points at the crypto board. Structured
1246
+ * formats keep the error as thrown, and so does no connection at all, which
1247
+ * the board cannot get past either.
1248
+ */
994
1249
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
995
- if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
1250
+ if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol) || isNetworkFailure(error)) throw error;
996
1251
  return ctx.fail(errorMessage(error) ?? `No history available for ${symbol}`, CRYPTO_BOARD_HINT);
997
1252
  }
998
1253
 
999
- const HISTORY_USAGE = "history <symbol> [--range <range>]";
1254
+ const HISTORY_USAGE = "history <symbol...> [--range <range> | --from <year|date> [--to <year|date>]]";
1000
1255
  const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
1001
1256
  const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
1002
- /** Options people reach for to ask for dates, which history answers with a range. */
1003
- const DATE_WINDOW_OPTIONS = new Set(["--from", "--to", "--start", "--end", "--since", "--until", "--date", "--start-date", "--end-date"]);
1257
+ /** Options people reach for to ask for dates, which history answers with --from and --to (or --start and --end). */
1258
+ const DATE_WINDOW_OPTIONS = new Set(["--since", "--until", "--date", "--start-date", "--end-date"]);
1004
1259
  /** Options people reach for to pick a bar size, which history derives from the range. */
1005
1260
  const BAR_SIZE_OPTIONS = new Set(["--interval", "--resolution", "--bar", "--bars", "--bar-size", "--granularity", "--timeframe", "--frequency", "--freq", "--tick", "--ticks"]);
1006
1261
 
@@ -1030,16 +1285,26 @@ function financialsSymbolArg(args: readonly string[]): string | undefined {
1030
1285
  function financialsOptionHint(flag: string, args: readonly string[]): string | null {
1031
1286
  if (!QUARTERLY_OPTIONS.has(flag)) return null;
1032
1287
  const symbol = financialsSymbolArg(args) ?? "<symbol>";
1033
- return `financials lists fiscal years. For quarters: gloomberb fn GF ${symbol} --period quarterly --periods 40 (one metric at a time), `
1034
- + `or gloomberb fn FA ${symbol} --period quarterly (the statements).`;
1288
+ return `financials lists fiscal years. For quarters: gloom fn GF ${symbol} --period quarterly --periods 40 (one metric at a time), `
1289
+ + `or gloom fn FA ${symbol} --period quarterly (the statements).`;
1035
1290
  }
1036
1291
 
1037
1292
  function historyOptionHint(flag: string, args: readonly string[]): string | null {
1038
- if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${HISTORY_RANGES.join(", ")}. ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}.`;
1293
+ if (DATE_WINDOW_OPTIONS.has(flag)) {
1294
+ return `history takes dates as --from and --to (or --start and --end), each a year or a date (--from 2007 --to 2009), or a --range: ${HISTORY_RANGES.join(", ")}.`;
1295
+ }
1039
1296
  if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
1040
1297
  return null;
1041
1298
  }
1042
1299
 
1300
+ /** "Asked from 1990-01-01, data starts 1995-01-03 (first available bar)" when the bars start over a week into the window. */
1301
+ function windowHistoryNotes(window: PriceWindow, rows: readonly { date: string }[]): string[] {
1302
+ const first = rows[0]?.date.slice(0, 10);
1303
+ if (!first) return [];
1304
+ return Date.parse(`${first}T00:00:00Z`) - Date.parse(`${window.from}T00:00:00Z`) > 7 * 86_400_000
1305
+ ? [`Asked from ${window.from}, data starts ${first} (first available bar)`] : [];
1306
+ }
1307
+
1043
1308
  /** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
1044
1309
  function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
1045
1310
  const first = rows[0]?.date.slice(0, 10);
@@ -1047,116 +1312,246 @@ function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
1047
1312
  return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
1048
1313
  }
1049
1314
 
1050
- async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1051
- const args = [...rawArgs];
1052
- const range = parseRange(takeOption(args, "--range"), ctx);
1315
+ /** What `history` was asked for: a range, or a window of dates in its place. */
1316
+ interface HistoryAsk {
1317
+ range: HistoryRange;
1318
+ window: PriceWindow | null;
1319
+ }
1320
+
1321
+ /** One listing's bars as `history` prints them, with what its heading, warnings and closing line say. */
1322
+ interface ListingHistory {
1323
+ listing: CliListing;
1324
+ identity: ListingIdentity;
1325
+ unit: HistoryUnit;
1326
+ data: HistoryRow[];
1327
+ resolution: PriceHistoryResult["resolution"];
1328
+ interval: HistoryRow["interval"];
1329
+ decimals: number;
1330
+ /** Intraday bars, which print in UTC as times. */
1331
+ intraday: boolean;
1332
+ volume: HistoryVolume | null;
1333
+ freshness: ReportFreshness | undefined;
1334
+ notes: string[];
1335
+ }
1336
+
1337
+ /** The command a listing that cannot be served is pointed back at: `history`, `history --range 5Y`. */
1338
+ function historyCommand({ range, window }: HistoryAsk): string {
1339
+ // A window that runs to today retries to the day it is run on, not the one it was first run on.
1340
+ return window ? `history --from ${window.from}${isPastPriceWindow(window) ? ` --to ${window.to}` : ""}` : range === "1Y" ? "history" : `history --range ${range}`;
1341
+ }
1342
+
1343
+ /**
1344
+ * One listing's bars, dated and checked as `history` prints them. Without
1345
+ * `lookups` it does not look up the other venues of a bare symbol or whether
1346
+ * its company moved, which a long list skips, as `quote` does.
1347
+ */
1348
+ async function loadListingHistory(
1349
+ listing: CliListing,
1350
+ market: MarketContext,
1351
+ ctx: Parameters<CliCommandDef["execute"]>[1],
1352
+ ask: HistoryAsk,
1353
+ { lookups }: { lookups: boolean },
1354
+ ): Promise<ListingHistory> {
1355
+ const { range, window } = ask;
1053
1356
  // YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
1054
1357
  const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
1358
+ const venues = lookups ? loadBareVenues(listing, market) : null;
1359
+ const { symbol, exchange } = listing.request;
1360
+ const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
1361
+ const load = () => window ? loadPriceWindowHistory(market.dataProvider, symbol, exchange, window, context)
1362
+ : market.dataProvider.getPriceHistoryWithMetadata
1363
+ ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
1364
+ : market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
1365
+ const quoteAnswer = loadListingQuoteAnswer(market.dataProvider, listing);
1366
+ const quoted = quoteAnswer.then((answer) => answer.quote);
1367
+ const fate = lookups ? listingFateLoader(listing, market, quoted) : null;
1368
+ const request = { command: historyCommand(ask), noun: "history", ...(fate ? { fate } : {}) };
1369
+ const [loaded, quote] = await Promise.all([
1370
+ loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, request)
1371
+ .catch((error) => failHistory(error, listing.key, ctx)),
1372
+ quoted,
1373
+ ]);
1374
+ // A bare symbol several companies list is never served from the one the data service picked, in a range or a window:
1375
+ // it asks for an exchange as `quote` does, and in a list that is the symbol's row alone.
1376
+ await failUnchosenListing(listing, market, ctx, request, (await quoteAnswer).error);
1377
+ const { resolution } = loaded;
1378
+ // While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
1379
+ // A window that ended before today has no session in progress.
1380
+ const points = window && isPastPriceWindow(window) ? loaded.points
1381
+ : withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
1382
+ const fromQuote = points !== loaded.points;
1383
+ const identity = listingIdentity(listing, quote);
1384
+ // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
1385
+ const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
1386
+ const unit = historyUnit(listing.key, listed ?? quote);
1387
+ const traded = {
1388
+ listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
1389
+ };
1390
+ // A coin's volume is traded value in its quote currency: said in every row and on the status line.
1391
+ const volume = historyVolume(unit, traded);
1392
+ const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
1393
+ // Bars stop arriving on the listing's own trading days, not New York's.
1394
+ const venue = quote?.listingExchangeName || exchange || undefined;
1395
+ const served = range === "YTD" && !window ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval, venue))) : rows;
1396
+ // October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
1397
+ const forming = formingPeriodBar(served, venue);
1398
+ // A complete one is dated by the day its last session closed, the day its close is from.
1399
+ const closedDay = forming ? null : closedPeriodBarDay(served, venue);
1400
+ const data = forming?.rows ?? served;
1401
+ const interval = data[0]?.interval ?? null;
1402
+ // Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
1403
+ const intraday = data.some((row) => row.date.length > 10);
1404
+ // A bar history: dated by its last bar, stale once bars of its size stop arriving.
1405
+ // The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
1406
+ const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
1407
+ // The source that served the bars, a broker or another source when Gloom Cloud could not.
1408
+ const source = loaded.sourceName ? { source: loaded.sourceName } : {};
1409
+ // A window that closed before today is the history asked for: no later bar makes it stale.
1410
+ const freshness = withSavedCopy(window && isPastPriceWindow(window) ? rowsFreshness(data, {
1411
+ ...source,
1412
+ status: "not-a-feed", basis: `${interval ? BAR_INTERVAL_LABELS[interval] : "price"} bars, ${priceWindowText(window)}`, observedKey: "date", oldest: null,
1413
+ }) : rowsFreshness(data, {
1414
+ ...source,
1415
+ ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
1416
+ observedKey: "date",
1417
+ oldest: null,
1418
+ cadenceExchange: venue,
1419
+ ...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
1420
+ ...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
1421
+ ...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
1422
+ // The rows are drawn from the bars served, which say whether they are the saved copy.
1423
+ }), loaded);
1424
+ const gaps = historyGapNote(data, traded);
1425
+ const coverage = window ? historyCoverageNotice(loaded.coverageStart, Date.parse(`${window.from}T00:00:00Z`), loaded.coverageNote)
1426
+ : range === "YTD" ? null : historyCoverageWarning(range, loaded);
1427
+ const notes = [
1428
+ // A window the source has no bar in (a typo, a weekend, a listing that had not started) is said, not an empty table alone.
1429
+ ...data.length === 0 ? [emptyHistoryNote(listing.key, ask)] : [],
1430
+ // FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
1431
+ ...fate ? listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }) : [],
1432
+ ...lookups ? await listingChoiceNotes(listing, market, quote, venues) : [],
1433
+ // ALL says where the source's bars start (BTC-USD's in 2014-09), unless the coverage line does.
1434
+ ...window ? [...windowHistoryNotes(window, data), coverage ?? []]
1435
+ : range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date, Date.now(), coverage != null), coverage ?? []],
1436
+ // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1437
+ gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1438
+ historyVenueVolumeNote(data) ?? [],
1439
+ historyBarStatusNote(forming ? served : data) ?? [],
1440
+ ].flat();
1441
+ return {
1442
+ listing, identity, unit, data, resolution, interval, intraday, volume, freshness, notes,
1443
+ decimals: historyPriceDecimals(data, listing.saved?.metadata.assetCategory),
1444
+ };
1445
+ }
1446
+
1447
+ /** What a listing with no bar for what was asked says, in a warning and in a list's status column. */
1448
+ function emptyHistoryNote(symbol: string, { range, window }: HistoryAsk): string {
1449
+ return window ? `No bars for ${symbol} from ${priceWindowText(window)}.` : `No bars for ${symbol} in ${range}.`;
1450
+ }
1451
+
1452
+ /** A bar's prices to the decimals its history prints them to. */
1453
+ function historyPrice(decimals: number) {
1454
+ return (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
1455
+ }
1456
+
1457
+ /** The columns a history table prints: Time in UTC for intraday bars, prices to `price`'s decimals. */
1458
+ function historyTextColumns(intraday: boolean, price: (value: unknown, row: Record<string, unknown>) => string): CliResultColumn[] {
1459
+ return [
1460
+ intraday
1461
+ ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
1462
+ : { key: "date", header: "Date" },
1463
+ { key: "open", header: "Open", align: "right", format: price },
1464
+ { key: "high", header: "High", align: "right", format: price },
1465
+ { key: "low", header: "Low", align: "right", format: price },
1466
+ { key: "close", header: "Close", align: "right", format: price },
1467
+ { key: "volume", header: "Volume", align: "right", format: formatCountCell },
1468
+ ];
1469
+ }
1470
+
1471
+ /**
1472
+ * The volume a table prints: a bar with one venue's count in place of the
1473
+ * market's shows it beside the venue, never as its volume, and a bar without
1474
+ * volume in a history that reports it prints as missing, never as 0 or blank.
1475
+ */
1476
+ function historyVolumeCell(missingVolume: (row: Record<string, unknown>) => string) {
1477
+ return (value: unknown, row: Record<string, unknown>) => formatCountCell(value)
1478
+ || (typeof row.venueVolume === "number"
1479
+ ? cliStyles.muted(`${formatCountCell(row.venueVolume)} ${typeof row.volumeVenue === "string" ? row.volumeVenue : ""}`.trim())
1480
+ : missingVolume(row));
1481
+ }
1482
+
1483
+ /** Text marks a bar still forming, rebuilt from hourly bars or closing before its settlement beside its flag; exports keep them as columns. */
1484
+ const HISTORY_FLAG_TEXT_COLUMN: CliResultColumn = {
1485
+ key: "flag",
1486
+ header: "Flag",
1487
+ format: (value, row) => [
1488
+ typeof value === "string" ? cliStyles.warning(value) : "",
1489
+ row.partial ? cliStyles.muted("partial") : "",
1490
+ row.filled ? cliStyles.muted("filled") : "",
1491
+ row.unsettled ? cliStyles.muted("unsettled") : "",
1492
+ typeof row.status === "string" ? cliStyles.warning(row.status) : "",
1493
+ ].filter(Boolean).join(" "),
1494
+ };
1495
+
1496
+ /** The dates `--from` and `--to` (or `--start` and `--end`) name, each given once. */
1497
+ function historyWindowOption(args: string[], rangeOption: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): PriceWindow | null {
1498
+ const bound = (flag: string, alias: string) => {
1499
+ const value = takeOption(args, flag);
1500
+ const aliased = takeOption(args, alias);
1501
+ if (value != null && aliased != null) ctx.fail(`${alias} is ${flag}; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
1502
+ return { value: value ?? aliased, flag: aliased != null ? alias : flag };
1503
+ };
1504
+ const typedFrom = bound("--from", "--start");
1505
+ const typedTo = bound("--to", "--end");
1506
+ // Every message names the options as typed: an --end alone asks for a --start, not a --from.
1507
+ const from = { ...typedFrom, flag: typedFrom.value == null && typedTo.flag === "--end" ? "--start" : typedFrom.flag };
1508
+ const to = { ...typedTo, flag: typedTo.value == null && from.flag === "--start" ? "--end" : typedTo.flag };
1509
+ let window: PriceWindow | null;
1510
+ try {
1511
+ window = readPriceWindow(from.value, to.value, new Date(), undefined, { from: from.flag, to: to.flag });
1512
+ } catch (error) {
1513
+ return ctx.fail(errorMessage(error) ?? `Invalid ${from.flag} or ${to.flag}.`, `Usage: gloom ${HISTORY_USAGE}`);
1514
+ }
1515
+ if (window && rangeOption != null) ctx.fail(`${from.flag} replaces --range; give one of them.`, `Usage: gloom ${HISTORY_USAGE}`);
1516
+ return window;
1517
+ }
1518
+
1519
+ /** How many symbols of a list load at once: enough that a few hundred take a minute or two, not one call after another. */
1520
+ const HISTORY_LIST_CONCURRENCY = 8;
1521
+
1522
+ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1523
+ const args = [...rawArgs];
1524
+ const rangeOption = takeOption(args, "--range");
1525
+ // --from 2007 --to 2009: those years' bars, daily up to five years, in place of a range ending now.
1526
+ const window = historyWindowOption(args, rangeOption, ctx);
1527
+ const ask: HistoryAsk = { range: parseRange(rangeOption, ctx), window };
1055
1528
  const requestedExchange = takeOption(args, "--exchange");
1529
+ // A list from a file or standard input (`-`), or several symbols typed (AAPL MSFT, AAPL,MSFT), is one long table.
1530
+ // One argument with spaces stays one, as before: it may be a company's name.
1531
+ const file = args.some((arg) => arg === "--file" || arg.startsWith("--file=")) ? takeOption(args, "--file") ?? "" : undefined;
1532
+ if (file != null || args.includes("-") || uniqueEntries(args).length > 1 || args.some((arg) => /[,;]/.test(arg))) {
1533
+ return runHistoryList(args, file, requestedExchange, ask, ctx);
1534
+ }
1056
1535
  const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
1057
1536
  await withMarketData(ctx, async (market) => {
1058
1537
  const listing = await requireCliListing(raw, requestedExchange, market, ctx);
1059
- const venues = loadBareVenues(listing, market);
1060
- const { symbol, exchange } = listing.request;
1061
- const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
1062
- const load = () => market.dataProvider.getPriceHistoryWithMetadata
1063
- ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
1064
- : market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
1065
- const quoted = loadListingQuote(market.dataProvider, listing);
1066
- const fate = listingFateLoader(listing, market, quoted);
1067
- const [loaded, quote] = await Promise.all([
1068
- loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
1069
- command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history", fate,
1070
- })
1071
- .catch((error) => failHistory(error, listing.key, ctx)),
1072
- quoted,
1073
- ]);
1074
- const { resolution } = loaded;
1075
- // While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
1076
- const points = withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
1077
- const fromQuote = points !== loaded.points;
1078
- const identity = listingIdentity(listing, quote);
1079
- // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
1080
- const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(logFailure("cli.commands-market", "Read quote metadata for history failed", null));
1081
- const unit = historyUnit(listing.key, listed ?? quote);
1082
- const traded = {
1083
- listing: listing.key, exchange: quote?.listingExchangeName || exchange || undefined, instrumentType: quote?.instrumentType,
1084
- };
1085
- // A coin's volume is traded value in its quote currency: said in every row and on the status line.
1086
- const volume = historyVolume(unit, traded);
1087
- const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
1088
- const served = range === "YTD" ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval))) : rows;
1089
- // Bars stop arriving on the listing's own trading days, not New York's.
1090
- const venue = quote?.listingExchangeName || exchange || undefined;
1091
- // October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
1092
- const forming = formingPeriodBar(served, venue);
1093
- // A complete one is dated by the day its last session closed, the day its close is from.
1094
- const closedDay = forming ? null : closedPeriodBarDay(served, venue);
1095
- const data = forming?.rows ?? served;
1096
- const interval = data[0]?.interval ?? null;
1097
- const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
1098
- const price = (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
1099
- // Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
1100
- const intraday = data.some((row) => row.date.length > 10);
1101
- // A bar history: dated by its last bar, stale once bars of its size stop arriving.
1102
- // The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
1103
- const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
1104
- const freshness = rowsFreshness(data, {
1105
- ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
1106
- observedKey: "date",
1107
- oldest: null,
1108
- cadenceExchange: venue,
1109
- ...(forming ? { periodToDate: forming.periodToDate } : closedDay ? { asOf: closedDay } : {}),
1110
- ...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
1111
- ...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
1112
- });
1113
- const gaps = historyGapNote(data, traded);
1114
- const notes = [
1115
- // FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
1116
- ...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
1117
- ...await listingChoiceNotes(listing, market, quote, venues),
1118
- ...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
1119
- // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1120
- gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1121
- historyBarStatusNote(forming ? served : data) ?? [],
1122
- ].flat();
1123
- const priceColumns: CliResultColumn[] = [
1124
- intraday
1125
- ? { key: "date", header: "Time", format: (value) => typeof value === "string" ? formatUtcTime(value) : "" }
1126
- : { key: "date", header: "Date" },
1127
- { key: "open", header: "Open", align: "right", format: price },
1128
- { key: "high", header: "High", align: "right", format: price },
1129
- { key: "low", header: "Low", align: "right", format: price },
1130
- { key: "close", header: "Close", align: "right", format: price },
1131
- { key: "volume", header: "Volume", align: "right", format: formatCountCell },
1132
- ];
1133
- // A bar without volume in a history that reports it prints as missing, never as 0 or blank beside the others.
1538
+ const history = await loadListingHistory(listing, market, ctx, ask, { lookups: true });
1539
+ const { identity, unit, data, interval, intraday, volume, freshness, notes } = history;
1540
+ const price = historyPrice(history.decimals);
1541
+ // A bar without volume in a history that reports it prints as missing.
1134
1542
  const missingVolume = historyReportsVolume(data) ? MISSING_VOLUME_TEXT : "";
1135
- const textPriceColumns = priceColumns.map((column) => column.key !== "volume" ? column
1136
- : { ...column, format: (value: unknown) => formatCountCell(value) || missingVolume });
1137
- // Text shows a flag only on a bar that has one; the column is dropped when none does.
1138
- const flagColumn: CliResultColumn = {
1139
- key: "flag", header: "Flag", format: (value) => typeof value === "string" ? cliStyles.warning(value) : "",
1140
- };
1141
- // Text also marks a bar still forming, rebuilt from hourly bars or closing before its settlement; exports keep them as fields.
1142
- const flagTextColumn: CliResultColumn = {
1143
- ...flagColumn,
1144
- format: (value, row) => [
1145
- typeof value === "string" ? cliStyles.warning(value) : "",
1146
- row.partial ? cliStyles.muted("partial") : "",
1147
- row.filled ? cliStyles.muted("filled") : "",
1148
- row.unsettled ? cliStyles.muted("unsettled") : "",
1149
- ].filter(Boolean).join(" "),
1150
- };
1151
- const marked = data.some((row) => row.partial || row.filled);
1152
- const unsettled = data.some((row) => row.unsettled);
1543
+ const textColumns = historyTextColumns(intraday, price).map((column) => column.key !== "volume" ? column
1544
+ : { ...column, format: historyVolumeCell(() => missingVolume) });
1545
+ // The largest fall from a close to a later one in the bars shown, said under the heading.
1546
+ const drawdown = maxDrawdown(data);
1153
1547
  ctx.printResult({
1154
1548
  data,
1155
1549
  metadata: {
1156
1550
  ...listingMetadata(identity),
1157
- range,
1158
- resolution,
1159
- requestedRange: range,
1551
+ range: window ? null : ask.range,
1552
+ resolution: history.resolution,
1553
+ requestedRange: window ? null : ask.range,
1554
+ ...(window ? { requestedWindow: window } : {}),
1160
1555
  currency: unit.currency,
1161
1556
  unit: unit.unit,
1162
1557
  interval,
@@ -1165,28 +1560,144 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1165
1560
  // The first bar after the last gap or jump; the first bar when there is none.
1166
1561
  unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
1167
1562
  bars: data.length,
1563
+ maxDrawdown: drawdown,
1168
1564
  asOf: freshness?.asOf ?? null,
1169
1565
  ...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
1170
1566
  },
1171
1567
  ...(notes.length > 0 ? { warnings: notes } : {}),
1172
1568
  freshness,
1173
1569
  }, {
1174
- heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
1570
+ heading: [listingHeading(identity), cliStyles.muted(historyFacts(unit, data).join(" · ")),
1571
+ ...drawdown && interval ? [cliStyles.muted(maxDrawdownText(drawdown, { basis: `${BAR_INTERVAL_LABELS[interval]} closes`, price,
1572
+ date: (text) => intraday ? formatUtcTime(text) : text.slice(0, 10) }))] : []].join("\n"),
1175
1573
  dateKey: "date",
1176
- textColumns: [...textPriceColumns, flagTextColumn],
1177
- columns: [
1178
- ...priceColumns,
1179
- flagColumn,
1180
- ...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
1181
- ...unsettled ? [{ key: "unsettled", header: "Unsettled" }] : [],
1182
- ...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
1183
- ],
1574
+ textColumns: [...textColumns, HISTORY_FLAG_TEXT_COLUMN],
1575
+ // One header whatever the market is doing, for one symbol as for a list (HISTORY_EXPORT_COLUMNS).
1576
+ columns: HISTORY_EXPORT_COLUMNS,
1577
+ exportStatus: historyExportStatus,
1184
1578
  // Every exported row says what it is a bar of, the currency a price is in and its bar size, so it survives head and concatenation.
1185
- exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
1579
+ exportIdentity: { symbolOrSeries: history.listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
1186
1580
  });
1187
1581
  });
1188
1582
  }
1189
1583
 
1584
+ /** A symbol of a list: its bars, or why it has none. */
1585
+ type ListedHistory = { symbol: string; history: ListingHistory } | { symbol: string; error: string };
1586
+
1587
+ /**
1588
+ * `history --file holdings.txt` (or several symbols, or `-` for standard
1589
+ * input): every symbol's bars in one long table, `HISTORY_LIST_CONCURRENCY`
1590
+ * symbols loading at once. A symbol that serves nothing is one row with its
1591
+ * reason in `status`, and a warning; the command fails only when none served.
1592
+ */
1593
+ async function runHistoryList(
1594
+ args: string[],
1595
+ file: string | undefined,
1596
+ exchange: string | undefined,
1597
+ ask: HistoryAsk,
1598
+ ctx: Parameters<CliCommandDef["execute"]>[1],
1599
+ ) {
1600
+ const usage = `Usage: gloom ${HISTORY_USAGE}`;
1601
+ await withMarketData(ctx, async (market) => {
1602
+ const list = await readListedSymbols(args, file, market, ctx, usage);
1603
+ const refused = list.symbols.flatMap((symbol) => notASymbolError(symbol) ?? []);
1604
+ const symbols = list.symbols.filter((symbol) => !notASymbolError(symbol));
1605
+ if (symbols.length === 0) ctx.fail(refused.length === 1 ? refused[0]!.message : `No history to read: ${refusedList(refused)} are not symbols.`, usage);
1606
+ // A long list is a batch, not a lookup: it skips the per-symbol venue and moved-company lookups, as `quote` does.
1607
+ const lookups = symbols.length <= MAX_CHOICE_LOOKUPS;
1608
+ const listed = await mapWithConcurrency(symbols, HISTORY_LIST_CONCURRENCY, async (symbol): Promise<ListedHistory> => {
1609
+ try {
1610
+ const listing = await resolveCliListing(symbol, exchange, market, { ownExchangeWins: true });
1611
+ return { symbol, history: await loadListingHistory(listing, market, ctx, ask, { lookups }) };
1612
+ } catch (error) {
1613
+ const message = errorMessage(error) ?? `No history for ${symbol}`;
1614
+ return { symbol, error: isCliFailure(error) ? oneLineFailure(message, error.details) : message };
1615
+ }
1616
+ });
1617
+ const served = listed.flatMap((entry) => "history" in entry && entry.history.data.length > 0 ? [entry.history] : []);
1618
+ // A symbol with no bars, whether its request failed or came back empty, is one row with the reason.
1619
+ const rows: HistoryListRow[] = listed.flatMap((entry) => "history" in entry
1620
+ ? entry.history.data.length > 0
1621
+ ? entry.history.data.map((row) => ({ symbol: entry.history.listing.key, ...row }))
1622
+ : [historyFailureRow(entry.history.listing.key, emptyHistoryNote(entry.history.listing.key, ask))]
1623
+ : [historyFailureRow(entry.symbol, entry.error)]);
1624
+ const bySymbol = new Map(served.map((history) => [history.listing.key, history] as const));
1625
+ const notes = [
1626
+ ...list.notes,
1627
+ ...refused.length > 0 ? [`Not ${refused.length === 1 ? "a symbol" : "symbols"}, no history read: ${refusedList(refused)}.`] : [],
1628
+ ...symbolListNotes(listed.map((entry) => ({
1629
+ symbol: "history" in entry ? entry.history.listing.key : entry.symbol,
1630
+ notes: "history" in entry ? entry.history.notes : [entry.error],
1631
+ }))),
1632
+ ];
1633
+ // The closing lines date the list by its newest bar; each row's as_of is its own symbol's.
1634
+ const lastBars = served.flatMap((history) => history.data.at(-1) ?? []);
1635
+ const pastWindow = ask.window && isPastPriceWindow(ask.window);
1636
+ const listFreshness = served.length === 0 ? undefined : rowsFreshness(lastBars, pastWindow && ask.window ? {
1637
+ status: "not-a-feed", basis: `price bars, ${priceWindowText(ask.window)}`, observedKey: "date", oldest: null,
1638
+ } : {
1639
+ ...barHistoryFreshness(served.some((history) => history.intraday) ? null : served[0]!.interval ?? "1d"),
1640
+ observedKey: "date",
1641
+ oldest: null,
1642
+ });
1643
+ // A saved copy is marked on the bars a symbol loaded, not on the rows drawn from them (loadListingHistory
1644
+ // reads it there): the list ends "couldn't refresh; saved <time>" for the oldest copy among its symbols.
1645
+ const savedAt = served.flatMap((history) => history.freshness?.savedAt ?? []).sort()[0];
1646
+ const freshness = listFreshness && savedAt && !listFreshness.savedAt ? { ...listFreshness, savedAt } : listFreshness;
1647
+ const decimals = (row: Record<string, unknown>) => bySymbol.get(String(row.symbol))?.decimals ?? 2;
1648
+ const intraday = served.some((history) => history.intraday);
1649
+ const textColumns = historyTextColumns(intraday, (value, row) => historyPrice(decimals(row))(value)).map((column) => column.key !== "volume" ? column
1650
+ : { ...column, format: historyVolumeCell((row) => {
1651
+ const history = bySymbol.get(String(row.symbol));
1652
+ return history && row.date && historyReportsVolume(history.data) ? MISSING_VOLUME_TEXT : "";
1653
+ }) });
1654
+ ctx.printResult({
1655
+ data: rows,
1656
+ metadata: {
1657
+ range: ask.window ? null : ask.range,
1658
+ requestedRange: ask.window ? null : ask.range,
1659
+ ...(ask.window ? { requestedWindow: ask.window } : {}),
1660
+ // Each symbol as a single `history` describes it, or the reason it has no bars.
1661
+ symbols: listed.map((entry) => "history" in entry ? {
1662
+ ...listingMetadata(entry.history.identity),
1663
+ currency: entry.history.unit.currency,
1664
+ unit: entry.history.unit.unit,
1665
+ interval: entry.history.interval,
1666
+ firstDate: entry.history.data[0]?.date ?? null,
1667
+ lastDate: entry.history.data.at(-1)?.date ?? null,
1668
+ bars: entry.history.data.length,
1669
+ asOf: entry.history.freshness?.asOf ?? null,
1670
+ } : { symbol: entry.symbol, error: entry.error }),
1671
+ },
1672
+ ...(notes.length > 0 ? { warnings: notes } : {}),
1673
+ ...(served.length === 0 ? { failure: { code: "cli_error", message: `No history for ${symbols.join(", ")}.` } } : {}),
1674
+ freshness,
1675
+ // With no bars to date, an export still ends its rows with what each is of, under the same header.
1676
+ ...(freshness ? {} : { exportFreshness: rowsFreshness([], { status: "not-a-feed", basis: "price bars" }) }),
1677
+ }, {
1678
+ seriesKey: "symbol",
1679
+ dateKey: "date",
1680
+ textColumns: [{ key: "symbol", header: "Symbol", shrink: false }, ...textColumns,
1681
+ { key: "currency", header: "Cur", shrink: false, format: currencyCell }, HISTORY_FLAG_TEXT_COLUMN],
1682
+ columns: HISTORY_EXPORT_COLUMNS,
1683
+ exportStatus: historyExportStatus,
1684
+ // Every row names its symbol, the currency its prices are in, its bar size and the as-of of its own symbol's bars.
1685
+ exportIdentity: (row) => {
1686
+ const history = bySymbol.get(String(row.symbol));
1687
+ return {
1688
+ symbolOrSeries: String(row.symbol),
1689
+ currencyOrUnit: history ? history.unit.currency ?? history.unit.unit : null,
1690
+ interval: history?.interval ?? null,
1691
+ // A symbol with no bars has no as-of of its own.
1692
+ asOf: history ? history.freshness?.asOf ?? undefined : null,
1693
+ };
1694
+ },
1695
+ });
1696
+ // The table and the warnings name each symbol with no bars; with none served, the exit status says so too.
1697
+ if (served.length === 0) process.exitCode = CLI_EXIT_CODES.error;
1698
+ });
1699
+ }
1700
+
1190
1701
  type FinancialsView = "statements" | "fundamentals" | "valuation";
1191
1702
 
1192
1703
  const STATEMENT_COLUMNS: CliResultColumn<ReturnType<typeof financialStatementRows>[number]>[] = [
@@ -1234,9 +1745,12 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
1234
1745
  await withMarketData(ctx, async (market) => {
1235
1746
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1236
1747
  const venues = loadBareVenues(listing, market);
1237
- const financials = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
1748
+ const loaded = await loadForListing(listing, market, ctx, () => market.dataProvider.getTickerFinancials(
1238
1749
  listing.request.symbol, listing.request.exchange, { cacheMode: ctx.cliOptions.refresh ? "refresh" : "default" },
1239
1750
  ), undefined, { command: commandName, noun: commandName === "valuation" ? "valuation data" : commandName });
1751
+ // A converted TTM block at the rate `fx` reports, as `ticker` shows it; only the US dollar legs are read.
1752
+ const financials = view === "statements" ? loaded
1753
+ : await atReportTtmRate(loaded, createBaseConverter(market.dataProvider, "USD"));
1240
1754
  // A trailing yield is measured from the cash history as DVD measures it, a forward yield dated ahead names the
1241
1755
  // declared distribution it annualizes (not in JSON), and one whose dividend stopped says so; read beside the listing notes.
1242
1756
  const distributions = view === "statements" ? null : loadDividendDistributions(
@@ -1259,7 +1773,8 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
1259
1773
  const freshness = view === "valuation" ? valuationFreshness(financials) : fundamentalsFreshness(financials);
1260
1774
  // Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
1261
1775
  const priced = atRegularSessionPrice(financials);
1262
- const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
1776
+ // One market cap per view: the share count times the price where the provider's sits more than 2% from it.
1777
+ const capitalization = viewMarketCapitalization(priced.quote, priced.fundamentals, regularSessionValuationPrice(priced.quote));
1263
1778
  // The filings behind the latest statements, and for a bank its deposits, uninsured share and CRE.
1264
1779
  const [statementFilings, bank] = view === "fundamentals"
1265
1780
  ? await Promise.all([
@@ -1307,8 +1822,13 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
1307
1822
  const freshness = rowsFreshness(rows, {
1308
1823
  ...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
1309
1824
  });
1310
- // The rows are the annual years served; older ones are not, and the closing lines say so.
1311
- const historyNotes = statementHistoryNotes("annual", rows.map((row) => row.date), listing.key);
1825
+ // The rows are the annual years served; older ones are not, and the closing lines say so, after how old
1826
+ // the latest year is when it ended over 15 months ago.
1827
+ const stale = stalePeriodNote(rows.map((row) => ({ date: row.date, lead: row.revenue != null })));
1828
+ // Years filled from the listing's exchange, for the revenue and net income columns.
1829
+ const filled = filledPeriodsNote(financials.annualStatements.filter((statement) => rows.some((row) => row.date === statement.date)),
1830
+ new Set(["totalRevenue", "netIncome"]));
1831
+ const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", rows.map((row) => row.date), listing.key)];
1312
1832
  // NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
1313
1833
  const tables = tabular
1314
1834
  ? selectTables({
@@ -1348,9 +1868,9 @@ const NEWS_COLUMNS = [
1348
1868
  header: "Published",
1349
1869
  format: (value: unknown) => typeof value === "string" ? formatUtcTime(value) : "",
1350
1870
  },
1351
- { key: "source", header: "Source", maxWidth: 20 },
1871
+ { key: "source", header: "Source" },
1352
1872
  { key: "title", header: "Title" },
1353
- { key: "tickers", header: "Tickers", maxWidth: 16 },
1873
+ { key: "tickers", header: "Tickers" },
1354
1874
  { key: "url", header: "URL", optional: true },
1355
1875
  ];
1356
1876
 
@@ -1365,7 +1885,14 @@ const NEWS_COLUMNS = [
1365
1885
  async function printNewsSearch(
1366
1886
  text: string,
1367
1887
  articles: NewsArticle[],
1368
- { reason, empty, commodity = true, failure }: { reason?: string; empty?: string; commodity?: boolean; failure?: string },
1888
+ { reason, empty, commodity = true, failure, ticker = null }: {
1889
+ reason?: string;
1890
+ empty?: string;
1891
+ commodity?: boolean;
1892
+ failure?: string;
1893
+ /** The listing a coin pair's search stands in for. */
1894
+ ticker?: string | null;
1895
+ },
1369
1896
  ctx: Parameters<CliCommandDef["execute"]>[1],
1370
1897
  ) {
1371
1898
  const searched = failure === undefined;
@@ -1374,7 +1901,7 @@ async function printNewsSearch(
1374
1901
  ctx.printResult({
1375
1902
  data: articles,
1376
1903
  ...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
1377
- metadata: { ticker: null, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
1904
+ metadata: { ticker, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
1378
1905
  freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
1379
1906
  }, {
1380
1907
  heading: newsSearchHeading(text, reason),
@@ -1384,6 +1911,50 @@ async function printNewsSearch(
1384
1911
  });
1385
1912
  }
1386
1913
 
1914
+ /** The ticker a metal's name is in capitals, when one answers to exactly that: GOLD, Gold.com Inc., for "gold". */
1915
+ async function tickerOfWord(word: string, market: MarketContext): Promise<{ symbol: string; name: string | null } | null> {
1916
+ const symbol = word.toUpperCase();
1917
+ const listing = await resolveCliListing(symbol, null, market).catch(() => {
1918
+ // Intentional ignore: a word no listing answers to offers no ticker under the metal's news.
1919
+ return null;
1920
+ });
1921
+ if (!listing || listing.symbol.toUpperCase() !== symbol) return null;
1922
+ const quote = await loadListingQuote(market.dataProvider, listing);
1923
+ const identity = listingIdentity(listing, quote);
1924
+ return quote !== null || identity.name != null ? { symbol: listing.symbol, name: identity.name ?? null } : null;
1925
+ }
1926
+
1927
+ /**
1928
+ * A metal's market news: the stories tagged METALS that mention
1929
+ * it, for its future, its board code or its name. Under them, the ticker its
1930
+ * name is in capitals, whose company news `news GOLD` keeps.
1931
+ */
1932
+ async function printMetalNews(route: MetalNewsRoute, limit: number, ctx: Parameters<CliCommandDef["execute"]>[1], market?: MarketContext) {
1933
+ const search = searchNewsStories(route.metal, limit, [METALS_NEWS_TOPIC]).then((found) => ({ found, failure: undefined }), (error: unknown) => {
1934
+ logFailure("cli.commands-market", "Search metals news failed")(error);
1935
+ return { found: [] as NewsArticle[], failure: errorMessage(error) ?? "no answer" };
1936
+ });
1937
+ const word = route.word;
1938
+ const ticker = word
1939
+ ? await (market ? tickerOfWord(word, market) : withMarketData(ctx, (context) => tickerOfWord(word, context)))
1940
+ .catch(logFailure("cli.commands-market", "Read the ticker a metal's name is failed", null))
1941
+ : null;
1942
+ const { found, failure } = await search;
1943
+ const companyNews = ticker ? `gloom news ${ticker.symbol}` : null;
1944
+ ctx.printResult({
1945
+ data: found,
1946
+ ...(failure === undefined ? {} : { warnings: [`Searching metals stories for "${route.metal}" failed: ${failure}`] }),
1947
+ metadata: { ticker: null, search: route.metal, topics: [METALS_NEWS_TOPIC], feed: null, ...(companyNews ? { companyNews } : {}) },
1948
+ freshness: rowsFreshness(newsRows(found), cloudNewsFreshness(await cloudRealtimeAccess())),
1949
+ }, {
1950
+ heading: metalNewsHeading(route),
1951
+ rows: newsRows,
1952
+ columns: NEWS_COLUMNS,
1953
+ empty: failure === undefined ? noMetalStoriesMessage(route) : "No stories to show.",
1954
+ ...(ticker ? { footnote: `${word} ${cliArrow("→")} ${route.metal}'s market news. For ${ticker.name ?? ticker.symbol} (ticker ${ticker.symbol}), try: ${companyNews}` } : {}),
1955
+ });
1956
+ }
1957
+
1387
1958
  async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1388
1959
  const args = [...rawArgs];
1389
1960
  const feed = parseNewsFeed(takeOption(args, "--feed"));
@@ -1392,27 +1963,42 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
1392
1963
  const limit = ctx.cliOptions.tail ?? ctx.cliOptions.limit ?? 20;
1393
1964
  const text = newsSearchText(args);
1394
1965
  if (text != null) {
1395
- if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloomberb news <query>");
1966
+ if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloom news <query>");
1396
1967
  const problem = newsSearchTextProblem(text);
1397
1968
  if (problem) ctx.fail(problem);
1398
1969
  await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
1399
1970
  return;
1400
1971
  }
1401
- if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloomberb news <symbol> --exchange <code>");
1972
+ // A metal's future or name is its market's news, not a company's: GOLD in capitals stays Gold.com's.
1973
+ const metal = args.length === 1 && !exchangeOption && !feed ? metalNewsRoute(args[0]!) : null;
1974
+ if (metal) {
1975
+ await printMetalNews(metal, limit, ctx);
1976
+ return;
1977
+ }
1978
+ if (exchangeOption && !args[0]) ctx.fail("--exchange needs a symbol: gloom news <symbol> --exchange <code>");
1402
1979
  await withMarketData(ctx, async (market) => {
1403
1980
  const listing = args[0] ? await requireCliListing(args[0], exchangeOption, market, ctx) : null;
1404
- const loadNews = () => market.dataProvider.getNews({
1981
+ const quoteLoad = listing ? loadListingQuote(market.dataProvider, listing) : Promise.resolve(null);
1982
+ // A coin pair's feed searches for the coin when no story is filed under the pair, by the name its quote gives.
1983
+ const coin = !!listing && !feed && isRoundTheClockCoin(listing.request.symbol, listing.request.exchange);
1984
+ const loadNews = async () => market.dataProvider.getNews({
1405
1985
  feed: feed ?? (listing ? "ticker" : "latest"),
1406
1986
  // Still set for news plugins that read the deprecated scope.
1407
1987
  scope: listing ? "ticker" : "global",
1408
1988
  ticker: listing?.request.symbol,
1409
1989
  exchange: listing?.request.exchange || undefined,
1410
1990
  limit,
1991
+ ...(coin ? { listingName: (await quoteLoad)?.name || listing!.saved?.metadata.name || undefined } : {}),
1411
1992
  });
1412
1993
  const [articles, quote] = await Promise.all([
1413
1994
  listing ? loadForListing(listing, market, ctx, loadNews, (found) => found.length === 0) : loadNews(),
1414
- listing ? loadListingQuote(market.dataProvider, listing) : null,
1995
+ quoteLoad,
1415
1996
  ]);
1997
+ const searchedFor = listing ? articles[0]?.searchedFor : undefined;
1998
+ if (listing && searchedFor) {
1999
+ await printNewsSearch(searchedFor, articles, { reason: `no stories filed under ${listing.symbol}`, ticker: listing.key }, ctx);
2000
+ return;
2001
+ }
1416
2002
  const identity = listing ? listingIdentity(listing, quote) : null;
1417
2003
  const resolved = quote !== null || identity?.name != null;
1418
2004
  const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
@@ -1430,6 +2016,12 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
1430
2016
  const emptyMessage = (searched: boolean) => listing && identity
1431
2017
  ? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
1432
2018
  : undefined;
2019
+ // A metal's board code no listing answers to (GC, SI) is the metal's market news.
2020
+ const metalCode = !resolved && unfiled && !exchangeOption ? metalCodeNewsRoute(args[0]!) : null;
2021
+ if (metalCode) {
2022
+ await printMetalNews(metalCode, limit, ctx, market);
2023
+ return;
2024
+ }
1433
2025
  if (word && reason) {
1434
2026
  // The lookup that found no ticker already answered; a search that fails leaves that answer.
1435
2027
  const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
@@ -1447,7 +2039,7 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
1447
2039
  const typed = args[0]?.trim() ?? "";
1448
2040
  const wordHint = listing && resolved && !listing.exchange && !feed && !exchangeOption && commonWordCandidate(typed, identity?.name)
1449
2041
  && wordMeansMore(await searchNewsStories(typed, 20).catch(logFailure("cli.commands-market", "Search news stories for a ticker word failed", [])), listing.symbol)
1450
- ? `${typed} → ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
2042
+ ? `${typed} ${cliArrow("→")} ${identity?.name ?? listing.symbol} (ticker ${listing.symbol}). For stories that mention ${typed.toLowerCase()}, try: ${quotedSearchCommand(typed.toLowerCase())}`
1451
2043
  : null;
1452
2044
  ctx.printResult({
1453
2045
  data: articles,
@@ -1490,6 +2082,27 @@ async function registrantUsExchange(market: MarketContext, symbol: string, regis
1490
2082
  return (venues.find((venue) => isUsListingExchange(venue.exchange)) ?? venues[0])?.exchange ?? null;
1491
2083
  }
1492
2084
 
2085
+ /** ASX announcements as `filings` lists them for a listing that files nothing with the SEC. */
2086
+ const ANNOUNCEMENT_COLUMNS = [
2087
+ { key: "date", header: "Released", format: (value: unknown) => typeof value === "string" ? `${value.slice(0, 16).replace("T", " ")} UTC` : "" },
2088
+ { key: "headline", header: "Headline", maxWidth: 60 },
2089
+ { key: "type", header: "Type", maxWidth: 36 },
2090
+ { key: "priceSensitive", header: "Price sensitive", format: (value: unknown) => value === true ? "yes" : "" },
2091
+ { key: "url", header: "URL", optional: true },
2092
+ ];
2093
+
2094
+ const ASX_ANNOUNCEMENTS = { source: "ASX company announcements", status: "not-a-feed", basis: "exchange announcements", oldest: null } as const;
2095
+
2096
+ function announcementRows(payload: ExchangeAnnouncementsPayload) {
2097
+ return payload.announcements.map((item) => ({
2098
+ date: item.date,
2099
+ headline: item.headline,
2100
+ type: item.types.join("; "),
2101
+ priceSensitive: item.priceSensitive,
2102
+ url: item.url,
2103
+ }));
2104
+ }
2105
+
1493
2106
  async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1494
2107
  const args = [...rawArgs];
1495
2108
  // Filings come newest first, so --tail asks for the same first n as --limit.
@@ -1498,12 +2111,16 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1498
2111
  const form = takeOption(args, "--form")?.trim() || null;
1499
2112
  const raw = requireOneArg(args, "filings <symbol> [--count <n>] [--form <form>]", "symbol", ctx);
1500
2113
  await withMarketData(ctx, async (market) => {
1501
- const listing = await requireCliListing(raw, exchangeOption, market, ctx);
2114
+ // NST.AX is NST:ASX, as quote, valuation and the pane functions read it: the SEC is asked by venue.
2115
+ const listing = await requireCliListing(raw, exchangeOption, market, ctx, { venueSuffix: true });
1502
2116
  const venues = loadBareVenues(listing, market);
1503
2117
  const { symbol, exchange } = listing.request;
1504
2118
  // A listing outside the US sends its company to the lookup, which needs its quote first.
1505
2119
  const quotePromise = loadListingQuote(market.dataProvider, listing);
1506
2120
  const identity = nonUsSecListingVenue(symbol, exchange) ? listingIdentity(listing, await quotePromise) : null;
2121
+ // Where a listing abroad publishes its announcements, whatever the SEC holds for it.
2122
+ const page = identity ? exchangeAnnouncementsPage(identity.symbol, identity.exchange) : null;
2123
+ const pageLine = page ? `${page.name}: ${page.url}` : null;
1507
2124
  try {
1508
2125
  const context = identity?.name ? { listingName: identity.name } : undefined;
1509
2126
  const [filings, quote] = await Promise.all([
@@ -1517,9 +2134,17 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1517
2134
  ]);
1518
2135
  const resolved = identity ?? listingIdentity(listing, quote);
1519
2136
  const choice = await listingChoiceNotes(listing, market, quote, venues);
2137
+ // A company on ASX that files nothing with the SEC lists its ASX announcements instead.
2138
+ if (identity && filings.length === 0 && !form && listsExchangeAnnouncements(identity.exchange)) {
2139
+ await printExchangeAnnouncements(ctx, { resolved, count, choice, page: page! });
2140
+ return;
2141
+ }
1520
2142
  ctx.printResult({
1521
2143
  data: filingsWithItemLabels(filings),
1522
- metadata: { ...listingMetadata(resolved), ...(form ? { form } : {}) },
2144
+ metadata: {
2145
+ ...listingMetadata(resolved), ...(form ? { form } : {}),
2146
+ ...(page ? { announcementsPage: page.url } : {}),
2147
+ },
1523
2148
  freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
1524
2149
  ...(choice.length > 0 ? { warnings: choice } : {}),
1525
2150
  }, {
@@ -1527,10 +2152,12 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1527
2152
  rows: filingRows,
1528
2153
  columns: FILING_COLUMNS,
1529
2154
  textColumns: FILING_TEXT_COLUMNS,
1530
- // A listing abroad is looked up by its company: none found means the company files nothing with the SEC.
1531
- empty: form ? `No ${form} filings found for ${listingTitle(resolved)}.`
2155
+ // A listing abroad is looked up by its company: none found means the company files nothing with the SEC,
2156
+ // and the page its own exchange publishes its announcements on is where to read them.
2157
+ empty: [form ? `No ${form} filings found for ${listingTitle(resolved)}.`
1532
2158
  : identity ? noSecReportsNotice(resolved)
1533
- : `No SEC filings found for ${listingTitle(resolved)}.`,
2159
+ : `No SEC filings found for ${listingTitle(resolved)}.`, pageLine].filter(Boolean).join("\n"),
2160
+ ...(pageLine ? { footnote: pageLine, exportNotes: [pageLine] } : {}),
1534
2161
  });
1535
2162
  } catch (error) {
1536
2163
  if (!(error instanceof SecRegistrantMismatchError)) throw error;
@@ -1541,13 +2168,59 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1541
2168
  const resolved = identity ?? listingIdentity(listing, await quotePromise);
1542
2169
  ctx.printResult({
1543
2170
  data: [] as SecFilingItem[],
1544
- metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName },
2171
+ metadata: { ...listingMetadata(resolved), secRegistrant: error.registrantName, ...(page ? { announcementsPage: page.url } : {}) },
1545
2172
  warnings: ctx.cliOptions.format === "text" ? undefined : [message],
1546
- }, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: message });
2173
+ }, { rows: filingRows, columns: FILING_COLUMNS, textColumns: FILING_TEXT_COLUMNS, empty: [message, pageLine].filter(Boolean).join("\n") });
1547
2174
  }
1548
2175
  });
1549
2176
  }
1550
2177
 
2178
+ /**
2179
+ * `filings` for a company on ASX that files nothing with the SEC: its latest
2180
+ * ASX announcements from Gloom Cloud, newest first, under the line saying it
2181
+ * files no SEC reports, with its ASX announcements page. A failed read still
2182
+ * ends on that page, with the reason.
2183
+ */
2184
+ async function printExchangeAnnouncements(
2185
+ ctx: Parameters<CliCommandDef["execute"]>[1],
2186
+ input: { resolved: ListingIdentity; count: number; choice: string[]; page: ExchangeAnnouncementsPage },
2187
+ ): Promise<void> {
2188
+ const { resolved, count, choice, page } = input;
2189
+ const notice = noSecReportsNotice(resolved);
2190
+ const pageLine = `${page.name}: ${page.url}`;
2191
+ let payload: ExchangeAnnouncementsPayload | null = null;
2192
+ let failure: string | null = null;
2193
+ try {
2194
+ payload = await apiClient.getCloudExchangeAnnouncements({ symbol: resolved.symbol, exchange: resolved.exchange, limit: Math.min(count, 50) });
2195
+ } catch (error) {
2196
+ failure = `ASX announcements unavailable: ${error instanceof Error ? error.message : String(error)}`;
2197
+ }
2198
+ const rows = payload ? announcementRows(payload) : [];
2199
+ const warnings = [
2200
+ ...choice,
2201
+ ...payload?.stale ? ["ASX could not be reached; these are the announcements Gloom Cloud read last."] : [],
2202
+ ...failure ? [failure] : [],
2203
+ ];
2204
+ ctx.printResult({
2205
+ data: rows,
2206
+ metadata: {
2207
+ ...listingMetadata(resolved), secFilings: 0, announcementsPage: payload?.page ?? page.url,
2208
+ ...(payload ? { announcementsTotal: payload.total, announcementsRetrievedAt: payload.retrievedAt } : {}),
2209
+ },
2210
+ freshness: rowsFreshness(rows, { ...ASX_ANNOUNCEMENTS, observedKey: "date" }),
2211
+ ...(warnings.length > 0 ? { warnings } : {}),
2212
+ }, {
2213
+ heading: listingHeading(resolved),
2214
+ ...(rows.length > 0 ? { summary: () => `${notice} Its latest ASX announcements:` } : {}),
2215
+ rows: () => rows,
2216
+ columns: ANNOUNCEMENT_COLUMNS,
2217
+ empty: `${failure ? notice : `${notice} ASX lists no announcements for it.`}\n${pageLine}`,
2218
+ footnote: pageLine,
2219
+ exportNotes: [notice, pageLine],
2220
+ exportIdentity: { symbolOrSeries: `${resolved.symbol}:${resolved.exchange}` },
2221
+ });
2222
+ }
2223
+
1551
2224
  type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
1552
2225
 
1553
2226
  /** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
@@ -1620,7 +2293,7 @@ function insiderSummary(
1620
2293
  const transactions = form4?.status === "available"
1621
2294
  ? [
1622
2295
  ...form4.summary ? [form4.summary] : [],
1623
- `${form4.lines} Form 4 ${form4.lines === 1 ? "line" : "lines"} in the latest ${form4.filings} ${form4.filings === 1 ? "filing" : "filings"}${form4.newestFiled ? `, newest filed ${form4.newestFiled}` : ""}: gloomberb fn INS ${listing.key}`,
2296
+ `${form4.lines} Form 4 ${form4.lines === 1 ? "line" : "lines"} in the latest ${form4.filings} ${form4.filings === 1 ? "filing" : "filings"}${form4.newestFiled ? `, newest filed ${form4.newestFiled}` : ""}: gloom fn INS ${listing.key}`,
1624
2297
  ].join("\n")
1625
2298
  : form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
1626
2299
  : form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
@@ -1651,7 +2324,8 @@ async function printBeneficialOwners(
1651
2324
  const sharesOutstanding = classSharesToday(financials);
1652
2325
  const rows = buildBeneficialReportRows(payload, { history, holders, sharesOutstanding });
1653
2326
  const notices = beneficialCoverageNotices(payload.coverage);
1654
- const legend = beneficialReportLegend(rows, sharesOutstanding);
2327
+ const sharesOutstandingAsOf = classSharesAsOf(financials);
2328
+ const legend = beneficialReportLegend(rows, sharesOutstanding, sharesOutstandingAsOf);
1655
2329
  ctx.printResult({
1656
2330
  data: rows,
1657
2331
  ...(notices.length ? { warnings: notices } : {}),
@@ -1665,6 +2339,7 @@ async function printBeneficialOwners(
1665
2339
  coverage: payload.coverage,
1666
2340
  complete: beneficialListComplete(payload),
1667
2341
  sharesOutstanding,
2342
+ ...(sharesOutstandingAsOf ? { sharesOutstandingAsOf } : {}),
1668
2343
  },
1669
2344
  }, {
1670
2345
  heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
@@ -1687,6 +2362,9 @@ async function printBeneficialOwners(
1687
2362
  });
1688
2363
  }
1689
2364
 
2365
+ /** Holder rows `holders` and `13f` print without `--limit`, as fn HDS does. */
2366
+ const HOLDERS_DEFAULT_ROWS = 25;
2367
+
1690
2368
  async function runHolders(
1691
2369
  rawArgs: string[],
1692
2370
  ctx: Parameters<CliCommandDef["execute"]>[1],
@@ -1705,8 +2383,15 @@ async function runHolders(
1705
2383
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1706
2384
  if (form !== "13f") return printBeneficialOwners(listing, market, form, history, ctx);
1707
2385
  const insiderCommand = commandName === "insider" && ownerTypes != null;
2386
+ const institutional = !ownerTypes?.has("insider");
1708
2387
  const venues = loadBareVenues(listing, market);
1709
- const [data, quote, form4] = await Promise.all([
2388
+ // A US line's institutions are every 13F filer, from the SEC holder index as fn 13F lists them,
2389
+ // as many as the limit asks (HDS's 25 by default); elsewhere, or when the index cannot be read, the source's few.
2390
+ const indexTicker = institutional ? indexHolderTicker(listing.request.symbol, listing.request.exchange) : null;
2391
+ // --tail counts back from the end of the rows loaded, so it loads at least as many as it asks for.
2392
+ const tail = ctx.cliOptions.tail;
2393
+ const indexLimit = Math.min(ctx.cliOptions.limit ?? Math.max(tail ?? 0, HOLDERS_DEFAULT_ROWS), HOLDER_ROWS_MAX);
2394
+ const [source, quote, form4, index] = await Promise.all([
1710
2395
  loadForListing(
1711
2396
  listing, market, ctx,
1712
2397
  () => market.dataProvider.getHolders(listing.request.symbol, listing.request.exchange),
@@ -1715,7 +2400,12 @@ async function runHolders(
1715
2400
  ),
1716
2401
  loadListingQuote(market.dataProvider, listing),
1717
2402
  insiderCommand ? loadForm4Report(market, listing) : null,
2403
+ indexTicker ? loadIndexedHolders(indexTicker, indexLimit).catch(logFailure("cli.commands-market", "Read the 13F holder index failed", null)) : null,
1718
2404
  ]);
2405
+ const indexed = !!index?.rows.length && indexHolderTicker(listing.request.symbol, listing.request.exchange, source) != null;
2406
+ const data = indexed
2407
+ ? withIndexedHolders(source, indexTicker!, { ...index!, rows: index!.rows.slice(0, indexLimit) }, indexedHolderUnits(source, quote?.price))
2408
+ : source;
1719
2409
  const identity = listingIdentity(listing, quote);
1720
2410
  const choice = await listingChoiceNotes(listing, market, quote, venues);
1721
2411
  // A London line's values can be dollars from the 13F filings; the listing's currency is not their unit.
@@ -1727,7 +2417,6 @@ async function runHolders(
1727
2417
  });
1728
2418
  const shownRows = windowRows(rows, ctx.cliOptions).rows;
1729
2419
  const shown = shownRows.length;
1730
- const institutional = !ownerTypes?.has("insider");
1731
2420
  const total = institutional ? data.summary?.institutionsCount ?? null : null;
1732
2421
  const reportDate = sharedReportDate(rows);
1733
2422
  const valueBasis = rows.length > 0 ? holderValueBasis(reportDate, data.valueBasis, valueCurrency) : null;
@@ -1755,7 +2444,13 @@ async function runHolders(
1755
2444
  : rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
1756
2445
  // Named as fn HDS takes it, so the closing notes point to the same listing.
1757
2446
  const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
1758
- const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
2447
+ const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total, indexed });
2448
+ // The index's smallest filers are pages past what was loaded: --tail is the end of the loaded rows, not of every filer.
2449
+ const tailNote = indexed && tail != null && total != null && rows.length < total
2450
+ ? `--tail ${tail} is the last ${shown} of the largest ${rows.length} loaded, not the smallest of all ${total.toLocaleString("en-US")} holders.`
2451
+ : null;
2452
+ const partialNote = indexed ? indexedHoldersPartialNote(index, indexLimit) : null;
2453
+ const warnings = [...choice, ...(partialNote ? [partialNote] : []), ...(tailNote ? [tailNote] : [])];
1759
2454
  ctx.printResult({
1760
2455
  data: rows,
1761
2456
  ...(freshness ? { freshness } : {}),
@@ -1780,7 +2475,7 @@ async function runHolders(
1780
2475
  form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
1781
2476
  } : {}),
1782
2477
  },
1783
- ...(choice.length > 0 ? { warnings: choice } : {}),
2478
+ ...(warnings.length > 0 ? { warnings } : {}),
1784
2479
  }, {
1785
2480
  heading: `${listingHeading(identity)}${cliStyles.muted(facts.map((fact) => ` · ${fact}`).join(""))}`,
1786
2481
  columns,
@@ -1835,6 +2530,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1835
2530
  ]);
1836
2531
  const identity = listingIdentity(listing, quote);
1837
2532
  const choice = await listingChoiceNotes(listing, market, quote, venues);
2533
+ const { upside, reference } = analystUpside(data, quote);
1838
2534
  ctx.printResult({
1839
2535
  data,
1840
2536
  metadata: {
@@ -1842,6 +2538,8 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1842
2538
  recommendationRating: data.recommendationRating,
1843
2539
  priceTarget: data.priceTarget,
1844
2540
  recommendations: data.recommendations,
2541
+ upside: upside ?? null,
2542
+ upsideReference: analystReferenceMetadata(reference),
1845
2543
  },
1846
2544
  freshness: rowsFreshness(analystRows(data), {
1847
2545
  ...REPORTED_DATA, basis: "analyst ratings", observedKey: "date", oldest: null,
@@ -1850,7 +2548,7 @@ async function runAnalyst(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1850
2548
  }, {
1851
2549
  heading: listingHeading(identity),
1852
2550
  rows: analystRows,
1853
- summary: analystSummary,
2551
+ summary: (research) => analystSummary(research, quote),
1854
2552
  columns: [
1855
2553
  { key: "date", header: "Date" },
1856
2554
  { key: "firm", header: "Firm" },
@@ -1872,13 +2570,18 @@ async function runEvents(rawArgs: string[], ctx: Parameters<CliCommandDef["execu
1872
2570
  await withMarketData(ctx, async (market) => {
1873
2571
  const listing = await requireCliListing(raw, exchangeOption, market, ctx);
1874
2572
  const venues = loadBareVenues(listing, market);
1875
- const [data, quote] = await Promise.all([
2573
+ // The earnings rows are resolved against the calendar `earnings` reads, so the next report is the same in both.
2574
+ const calendar = market.dataProvider.getEarningsCalendar?.([listing.key])
2575
+ .catch(logFailure("cli.commands-market", "Read the earnings calendar for events failed", [] as EarningsEvent[]));
2576
+ const [recorded, quote, events] = await Promise.all([
1876
2577
  loadForListing(
1877
2578
  listing, market, ctx, () => market.dataProvider.getCorporateActions(listing.request.symbol, listing.request.exchange),
1878
2579
  undefined, { command: "events", noun: "corporate events" },
1879
2580
  ),
1880
2581
  loadListingQuote(market.dataProvider, listing),
2582
+ calendar,
1881
2583
  ]);
2584
+ const data = withResolvedEarnings(recorded, calendarEventOf(events, listing.key), newYorkDay());
1882
2585
  const identity = listingIdentity(listing, quote);
1883
2586
  const choice = await listingChoiceNotes(listing, market, quote, venues);
1884
2587
  ctx.printResult({
@@ -1909,14 +2612,14 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1909
2612
  // With --leaps, --expiration is a window of one expiry.
1910
2613
  const expirationGiven = expiration != null || rawArgs.some((arg) => arg === "--expiration" || arg.startsWith("--expiration="));
1911
2614
  const criteria = parseLeapsCriteria({ ...leapsOptions.raw, ...expirationGiven ? { "--expiration": expiration ?? "" } : {} });
1912
- if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloomberb ${LEAPS_USAGE}`);
2615
+ if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloom ${LEAPS_USAGE}`);
1913
2616
  const entries = normalizeSymbols(args);
1914
- if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloomberb ${LEAPS_USAGE}`);
2617
+ if (entries.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloom ${LEAPS_USAGE}`);
1915
2618
  await withMarketData(ctx, async (market) => runLeapsScreen(await readSymbols(entries, market.dataProvider), exchangeOption, criteria, market, ctx));
1916
2619
  return;
1917
2620
  }
1918
2621
  if (leapsOptions.given.length > 0) {
1919
- ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloomberb ${LEAPS_USAGE}`);
2622
+ ctx.fail(`${leapsOptions.given.join(", ")} only ${leapsOptions.given.length > 1 ? "apply" : "applies"} with --leaps.`, `Usage: gloom ${LEAPS_USAGE}`);
1920
2623
  }
1921
2624
  const raw = requireOneArg(args, OPTIONS_USAGE, "symbol", ctx);
1922
2625
  let expirationDate: number | undefined;
@@ -1925,7 +2628,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1925
2628
  if (parsed == null) {
1926
2629
  ctx.fail(
1927
2630
  expiration == null ? "--expiration needs a date." : `Invalid --expiration "${expiration}".`,
1928
- `Use ${OPTION_EXPIRATION_FORMAT}.\nUsage: gloomberb ${OPTIONS_USAGE}`,
2631
+ `Use ${OPTION_EXPIRATION_FORMAT}.${expiration == null ? "" : expirationHint(expiration)}\nUsage: gloom ${OPTIONS_USAGE}`,
1929
2632
  );
1930
2633
  }
1931
2634
  expirationDate = parsed;
@@ -1938,30 +2641,54 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1938
2641
  const quotePromise = loadListingQuote(market.dataProvider, listing);
1939
2642
  const fate = listingFateLoader(listing, market, quotePromise);
1940
2643
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1941
- const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
1942
- const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
2644
+ const now = Date.now();
2645
+ const readChain = async (expiry: number | undefined) => {
2646
+ const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expiry, undefined])
1943
2647
  .load({ force: refresh });
1944
2648
  return {
1945
2649
  result: cached,
1946
- chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expirationDate, {
2650
+ chain: cached?.value ?? await market.dataProvider.getOptionsChain(symbol, exchange, expiry, {
1947
2651
  cacheMode: refresh ? "refresh" : "default",
1948
2652
  }),
1949
2653
  };
2654
+ };
2655
+ // An expiry whose last session has closed has no chain to show: it says so, and names the nearest live one.
2656
+ if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, exchange)) {
2657
+ const listed = await readChain(undefined).then(({ chain }) => chain.expirationDates,
2658
+ logFailure("cli-market", "Read available option expirations failed", []));
2659
+ ctx.fail(`expiry ${expiryIsoDate(expirationDate)} for ${listing.key} `
2660
+ + expiredExpiryText(expirationDate, listed, now, { exchange, flag: "expiration" }),
2661
+ `Run gloom options ${listing.key} to list the live expiries.`);
2662
+ }
2663
+ const { result, chain: read } = await loadForListing(listing, market, ctx, async () => {
2664
+ const first = await readChain(expirationDate);
2665
+ // A source can still default to the expiry that lapsed at Friday's close; the nearest live one is shown instead.
2666
+ const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, exchange) : undefined;
2667
+ if (live === null) return { ...first, chain: { ...first.chain, calls: [], puts: [] } };
2668
+ if (live == null) return first;
2669
+ // The default's list stays the catalogue: a dated chain can list fewer expiries.
2670
+ const shown = await readChain(live);
2671
+ return { ...shown, chain: { ...shown.chain,
2672
+ expirationDates: [...new Set([...first.chain.expirationDates, ...shown.chain.expirationDates])].sort((a, b) => a - b) } };
1950
2673
  }, undefined, { command: "options", noun: "options chain", fate }).catch((error: unknown) => {
1951
2674
  if (!isOptionsUnavailableError(error)) throw error;
1952
2675
  // A future or commodity with no chain of its own: name the listed fund whose options stand in.
1953
2676
  const alternative = listedOptionsAlternative(listing.key);
1954
- return ctx.fail(optionsUnavailableTitle(listing.key), alternative
1955
- ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloomberb options ${alternative.symbol}.`
1956
- + ` Strategy payoffs: gloomberb fn OSA ${alternative.symbol}`
1957
- : "Strategy payoffs on a listed underlying: gloomberb fn OSA <ticker>");
2677
+ // A name read as a fund or an index asks about the company's stock it means, whose options are listed.
2678
+ const meant = meantQuestion(listing);
2679
+ return ctx.fail(meant ? `${optionsUnavailableTitle(listing.key)} ${meant}` : optionsUnavailableTitle(listing.key), alternative
2680
+ ? `Nearest listed alternative: ${alternative.symbol} (${alternative.name}), gloom options ${alternative.symbol}.`
2681
+ + ` Strategy payoffs: gloom fn OSA ${alternative.symbol}`
2682
+ : "Strategy payoffs on a listed underlying: gloom fn OSA <ticker>");
1958
2683
  });
2684
+ const loaded = withLiveExpiryList(read, now, exchange);
1959
2685
  const inputs = await inputsPromise;
1960
2686
  if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
1961
2687
  // An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
1962
2688
  const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
1963
2689
  .getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
1964
- .then((fallback) => fallback.expirationDates, logFailure("cli-market", "Read available option expirations failed", []));
2690
+ .then((fallback) => liveOptionExpiries(fallback.expirationDates, now, exchange),
2691
+ logFailure("cli-market", "Read available option expirations failed", []));
1965
2692
  const miss = missingExpiryMessage(listing.key, expirationDate, listed);
1966
2693
  ctx.fail(miss.message, miss.details);
1967
2694
  }
@@ -2162,15 +2889,35 @@ async function runFxOnDate(
2162
2889
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
2163
2890
  const args = [...rawArgs];
2164
2891
  const dateOption = takeOption(args, "--date");
2165
- if (args.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
2892
+ const forwardOption = takeOption(args, "--forward");
2893
+ if (args.length === 0) ctx.fail(`Usage: gloom ${FX_USAGE}`);
2894
+ // A forward is implied from today's spot and the latest short rates, which are all Gloom Cloud serves.
2895
+ if (dateOption != null && forwardOption != null) {
2896
+ ctx.fail(
2897
+ "fx --forward does not take --date: the forward is implied from today's spot and the latest short rates, and past short rates are not served.",
2898
+ `For that day's close: gloom fx ${args.join(" ")} --date ${dateOption}. For today's forward, drop --date.`,
2899
+ );
2900
+ }
2166
2901
  const date = dateOption == null ? null : readFxDate(dateOption, ctx);
2902
+ const tenor = forwardOption == null ? null : parseForwardTenor(forwardOption);
2903
+ if (tenor && "error" in tenor) ctx.fail(tenor.error, `Usage: gloom ${FX_FORWARD_USAGE}`);
2167
2904
  await withMarketData(ctx, async (market) => {
2168
2905
  const parsed = parseFxArgs(args, market.config.baseCurrency);
2169
- if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloomberb ${FX_USAGE}`);
2906
+ if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloom ${forwardOption == null ? FX_USAGE : FX_FORWARD_USAGE}`);
2170
2907
  if (date) return runFxOnDate(parsed, date, args, market, ctx);
2171
2908
  const { request, amount, toUsdByDefault } = parsed;
2172
2909
  const { currency, baseCurrency } = request;
2173
2910
  const pair = `${currency}/${baseCurrency}`;
2911
+ if (tenor) {
2912
+ if (amount != null) ctx.fail("--forward prices a pair, not an amount.", `Usage: gloom ${FX_FORWARD_USAGE}`);
2913
+ if (currency === baseCurrency) ctx.fail(`${pair} has no forward: both sides are ${currency}.`, `Usage: gloom ${FX_FORWARD_USAGE}`);
2914
+ }
2915
+ // The short rates are read alongside spot. A failed read is held as a value, not left to reject
2916
+ // unobserved while spot loads, and is raised where the forward needs the rates.
2917
+ const shortRates = tenor && !("error" in tenor) ? apiClient.getCloudShortRates().then(
2918
+ (payload) => ({ payload }),
2919
+ (error: unknown) => ({ error: unavailableOnServer(error, "Forwards are not available from Gloom Cloud yet: it serves no short rates.") }),
2920
+ ) : null;
2174
2921
  const reasons = new Map<string, string>();
2175
2922
  const load = (code: string) => market.dataProvider.getCachedQuery("getExchangeRate", [code])
2176
2923
  .load({ force: ctx.cliOptions.refresh }).catch((error) => {
@@ -2199,13 +2946,68 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
2199
2946
  const converted = amount == null ? null : amount * rate;
2200
2947
  const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate, ...(amount == null ? {} : { amount, converted }) };
2201
2948
  // The closing line dates the rate, and says when it is stale, as `fn FXC` does.
2202
- const freshness = fxPairFreshness(new Map(codes.flatMap((code, index) => {
2949
+ const loaded = new Map(codes.flatMap((code, index) => {
2203
2950
  const leg = legs[index];
2204
2951
  return leg ? [[code, {
2205
2952
  rate: leg.value,
2206
- timing: { asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes },
2953
+ timing: {
2954
+ asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes,
2955
+ intradayBar: leg.intradayBar, staleReason: leg.staleReason,
2956
+ },
2207
2957
  }] as const] : [];
2208
- })));
2958
+ }));
2959
+ const freshness = fxPairFreshness(loaded);
2960
+ // A rate that is its pair's bar says which bar, and a stale one why it stands.
2961
+ const rateNotes = fxRateNotes(new Map([...loaded].map(([code, leg]) => [code, leg.timing])));
2962
+ if (tenor && shortRates && !("error" in tenor)) {
2963
+ const read = await shortRates;
2964
+ if ("error" in read) throw read.error;
2965
+ const { rates } = read.payload;
2966
+ // BASE/QUOTE: the forward needs the rate of each; a missing one is named, never filled in.
2967
+ const base = shortRateFor(currency, rates);
2968
+ const quote = shortRateFor(baseCurrency, rates);
2969
+ const missing = [base, quote].flatMap((lookup) => "error" in lookup ? [lookup.error] : []);
2970
+ if ("error" in base || "error" in quote) {
2971
+ return ctx.fail(`No ${tenor.label} forward for ${pair}. ${missing.join(" ")}`, "The forward is implied from both currencies' short rates by covered interest parity.");
2972
+ }
2973
+ const implied = impliedForward(rate, tenor.days, base.rate, quote.rate);
2974
+ const forwardDate = new Date(Math.floor(Date.now() / 86_400_000) * 86_400_000 + tenor.days * 86_400_000).toISOString().slice(0, 10);
2975
+ const rateFields = (short: typeof base.rate) => ({
2976
+ currency: short.currency, name: short.name, tenor: short.tenor, value: short.value, asOf: short.asOf,
2977
+ dayCountBasis: short.dayCountBasis, publisher: short.publisher, sourceUrl: short.sourceUrl, stale: short.stale,
2978
+ });
2979
+ const staleRates = [base.rate, quote.rate].filter((short) => short.stale)
2980
+ .map((short) => `${short.name} (${short.currency}) is the last copy read, dated ${short.asOf ?? "undated"}; its source could not be read since.`);
2981
+ const forwardRow = {
2982
+ pair, tenor: tenor.label, days: tenor.days, forwardDate, spot: rate, spotAsOf: asOf, forward: implied.forward,
2983
+ points: implied.points, pointSize: implied.pointSize, premiumPercent: implied.premiumPercent, annualizedPercent: implied.annualizedPercent,
2984
+ baseRate: rateFields(base.rate), quoteRate: rateFields(quote.rate), method: "covered interest parity", label: IMPLIED_FORWARD_LABEL,
2985
+ };
2986
+ const growth = (short: typeof base.rate) => `(1 + ${formatRatePercent(short.value)} × ${tenor.days}/${short.dayCountBasis})`;
2987
+ // A currency typed as its name (yen) says which code it was read as, as the spot answer does.
2988
+ const forwardWarnings = [...request.readAs ?? [], ...rateNotes, ...staleRates];
2989
+ return ctx.printResult({ data: [forwardRow], freshness, ...(forwardWarnings.length > 0 ? { warnings: forwardWarnings } : {}) }, {
2990
+ text: () => [
2991
+ `${pair} ${tenor.label} forward ${formatFxRate(implied.forward)}, ${IMPLIED_FORWARD_LABEL}`,
2992
+ `Spot ${formatFxRate(rate)} · forward points ${formatForwardPoints(implied.points)} (pips of ${implied.pointSize}) · ${implied.premiumPercent >= 0 ? "premium" : "discount"} ${formatSignedPercent(implied.premiumPercent)} over ${tenor.days} days to ${forwardDate}, ${formatSignedPercent(implied.annualizedPercent, 2)} a year (×360/${tenor.days})`,
2993
+ `${currency} rate: ${describeShortRate(base.rate)}`,
2994
+ `${baseCurrency} rate: ${describeShortRate(quote.rate)}`,
2995
+ `Covered interest parity: ${formatFxRate(rate)} × ${growth(quote.rate)} / ${growth(base.rate)}; each rate earns simple interest over the ${tenor.days} days, standing in for a ${tenor.label} term rate.`,
2996
+ ].join("\n"),
2997
+ exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: `forward ${tenor.label}` },
2998
+ columns: [
2999
+ { key: "pair", header: "Pair" },
3000
+ { key: "tenor", header: "Tenor" },
3001
+ { key: "days", header: "Days", align: "right" },
3002
+ { key: "forwardDate", header: "Forward date" },
3003
+ { key: "spot", header: "Spot", align: "right" },
3004
+ { key: "forward", header: "Forward", align: "right" },
3005
+ { key: "points", header: "Points", align: "right" },
3006
+ { key: "premiumPercent", header: "Premium %", align: "right" },
3007
+ { key: "annualizedPercent", header: "Annualized %", align: "right" },
3008
+ ],
3009
+ });
3010
+ }
2209
3011
  // An amount with one currency names no target; the answer says it went to USD.
2210
3012
  const warnings = [
2211
3013
  // A currency typed as its name (yen) says which code it was read as.
@@ -2213,10 +3015,21 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
2213
3015
  ...toUsdByDefault && amount != null
2214
3016
  ? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
2215
3017
  : [],
3018
+ ...rateNotes,
2216
3019
  ];
2217
- ctx.printResult({ data: [row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
3020
+ // The peso's dollar rate is the official one; the market rates Buenos Aires prices imply follow it, each with its inputs.
3021
+ const peso = isPesoDollarPair(currency, baseCurrency)
3022
+ ? await loadPesoDollarRates((targets) => market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh }))
3023
+ : null;
3024
+ if (peso && peso.missing.length > 0) warnings.push(`No price for ${peso.missing.join(", ")}, so the rates from ${peso.missing.length === 1 ? "it are" : "them are"} left out.`);
3025
+ const rateText = peso ? `${describeFxRate(request, rate)} · ${peso.spotKind} rate` : describeFxRate(request, rate);
3026
+ ctx.printResult({ data: [peso ? { ...row, ...pesoDollarRatesJson(peso) } : row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
2218
3027
  // The conversion, then the rate it used; the source line under it dates that rate.
2219
- text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
3028
+ text: () => [
3029
+ ...amount == null ? [] : [describeFxConversion(request, amount, rate)],
3030
+ rateText,
3031
+ ...peso ? pesoDollarRateLines(peso) : [],
3032
+ ].join("\n"),
2220
3033
  // The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
2221
3034
  exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
2222
3035
  columns: [
@@ -2240,7 +3053,7 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
2240
3053
  const args = [...rawArgs];
2241
3054
  const exchangeOption = takeOption(args, "--exchange");
2242
3055
  const entries = normalizeSymbols(args);
2243
- if (entries.length === 0) ctx.fail("Usage: gloomberb earnings <symbol...>");
3056
+ if (entries.length === 0) ctx.fail("Usage: gloom earnings <symbol...>");
2244
3057
  await withCliServices(ctx, async (services) => {
2245
3058
  const symbols = await readSymbols(entries, services.dataProvider);
2246
3059
  const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
@@ -2250,9 +3063,9 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
2250
3063
  // Each bare symbol's venues cost a search, so a long list (a portfolio check, not a lookup) names none.
2251
3064
  const venues = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
2252
3065
  : listings.map((listing) => [listing, loadBareVenues(listing, services)] as const));
2253
- let events: EarningsEvent[];
3066
+ let calendar: EarningsEvent[];
2254
3067
  try {
2255
- events = await loadForListing(
3068
+ calendar = await loadForListing(
2256
3069
  listings, services, ctx,
2257
3070
  () => services.dataProvider.getEarningsCalendar(keys),
2258
3071
  (found) => found.length === 0,
@@ -2262,6 +3075,15 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
2262
3075
  if (failure) ctx.fail(failure);
2263
3076
  throw error;
2264
3077
  }
3078
+ // Each date is resolved as events, EE, ERN and OSA resolve it: one that has come is read against the
3079
+ // company's quarters on record, for its actual and the next report on record (shared/earnings-calendar).
3080
+ const today = newYorkDay();
3081
+ const resolved = await resolveListingEvents(services.dataProvider, listings.map((listing) => ({
3082
+ key: listing.key, symbol: listing.request.symbol, exchange: listing.request.exchange,
3083
+ })), calendar, today);
3084
+ const events: ResolvedEarningsEvent[] = calendar
3085
+ .map((event) => resolved.get(event.symbol.toUpperCase()) ?? { ...event, dateStatus: calendarStatus(event, today) })
3086
+ .sort((left, right) => left.earningsDate.getTime() - right.earningsDate.getTime());
2265
3087
  const eventOf = (listing: CliListing) => events.find((event) => event.symbol.toUpperCase() === listing.key.toUpperCase());
2266
3088
  const [abroadNotes, choiceNotes] = await Promise.all([
2267
3089
  // A listing abroad with no date of its own says so, with its US line's date when that line is the same company.
@@ -2281,7 +3103,8 @@ async function runEarnings(rawArgs: string[], ctx: Parameters<CliCommandDef["exe
2281
3103
  ];
2282
3104
  ctx.printResult({
2283
3105
  data: events,
2284
- freshness: rowsFreshness(earningsRows(events), { status: "not-a-feed", basis: "calendar", observedKey: "date", oldest: null }),
3106
+ // Dated by when it was read: a row's date is when the report is, not how old the calendar is.
3107
+ freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", oldest: null }),
2285
3108
  ...(warnings.length > 0 ? { warnings } : {}),
2286
3109
  }, {
2287
3110
  rows: earningsRows,
@@ -2307,7 +3130,9 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2307
3130
  group: CLI_COMMAND_GROUPS.research,
2308
3131
  usage: ["quote <symbol...>", "quote --file <path>"],
2309
3132
  options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
2310
- examples: ["quote AAPL MSFT NVDA", "quote SAN:EPA BHP:ASX", "quote BTC-USD EURUSD=X", "quote binance:BZUSDT", "quote AAPL --json", "quote --file holdings.csv"],
3133
+ sections: [QUOTE_CSV_SECTION],
3134
+ examples: ["quote AAPL MSFT NVDA", "quote SAN:EPA BHP:ASX", "quote BTC-USD EURUSD=X", "quote binance:BZUSDT", "quote AAPL --json", "quote --file holdings.csv",
3135
+ "quote --file holdings.csv --csv > quotes.csv"],
2311
3136
  },
2312
3137
  execute: (args, ctx) => runQuote(args, ctx, "quote"),
2313
3138
  },
@@ -2318,6 +3143,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2318
3143
  group: CLI_COMMAND_GROUPS.research,
2319
3144
  usage: ["compare <symbol...>", "compare --file <path>"],
2320
3145
  options: [EXCHANGE_OPTION, LIST_FILE_OPTION],
3146
+ sections: [QUOTE_CSV_SECTION],
2321
3147
  examples: ["compare KO PEP", "compare SAN:EPA SAN:NYSE", "compare SPY QQQ IWM --csv", "compare - < peers.txt"],
2322
3148
  },
2323
3149
  execute: (args, ctx) => runQuote(args, ctx, "compare"),
@@ -2327,26 +3153,45 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2327
3153
  description: "Fetch open, high, low, close, and volume over a range",
2328
3154
  help: {
2329
3155
  group: CLI_COMMAND_GROUPS.research,
2330
- usage: [HISTORY_USAGE],
3156
+ usage: [HISTORY_USAGE, "history --file <path> [--range <range> | --from <year|date> [--to <year|date>]]"],
2331
3157
  options: [
2332
3158
  { flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
3159
+ { flags: "--from <year|date>, --start <year|date>", description: `${PRICE_WINDOW_FROM_HELP}. Up to five years come in daily bars, up to twenty weekly, longer monthly` },
3160
+ { flags: "--to <year|date>, --end <year|date>", description: PRICE_WINDOW_TO_HELP },
3161
+ { flags: "--file <path>", description: "Read the symbols from a text or CSV file, one per line (- reads standard input), and write every symbol's bars in one table" },
2333
3162
  EXCHANGE_OPTION,
2334
3163
  ],
2335
3164
  sections: [{
2336
3165
  title: "Bars",
2337
3166
  lines: [
2338
3167
  `Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for, and for a coin, which trades every day, names each day it has no bar for.`,
2339
- "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. Every --csv and --ndjson row carries its flag and ends with symbol_or_series, currency_or_unit, interval, source and as_of; --json rows carry currency, unit, interval and flag.",
3168
+ "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. --json rows carry currency, unit, interval and flag.",
2340
3169
  "A break in the series flags the bar after it, which keeps its prices: gap after more than three months with no bar (weeks for weekly, daily and intraday bars), jump when the close is over 100 times the one before or under a hundredth of it. A warning names each break, and the heading says the date the series runs unbroken from.",
3170
+ "The heading's last line is the max drawdown of the bars shown: the largest fall from a close to a later close, with the peak and the trough, their dates, and the date a close got back to the peak, if one did (maxDrawdown in --json).",
3171
+ ],
3172
+ }, {
3173
+ title: "CSV",
3174
+ lines: [
3175
+ "Every --csv has one header, whatever the symbol, range or market state: Date, Open, High, Low, Close, Volume, Flag, Partial, Filled, Unsettled, Volume unit, Volume note, Venue volume, Volume venue, status, then symbol_or_series, currency_or_unit, interval, source and as_of. Prices are plain numbers in the currency currency_or_unit names. Partial, Filled and Unsettled are true or false, and status says why a row has no prices.",
3176
+ ],
3177
+ }, {
3178
+ title: "Several symbols",
3179
+ lines: [
3180
+ `history AAPL MSFT 000660.KS, or --file with one symbol a line (- reads standard input), writes every symbol's bars in one long table, ${HISTORY_LIST_CONCURRENCY} symbols loading at once. Each row names its symbol, its currency and the as-of of that symbol's bars in symbol_or_series, currency_or_unit and as_of.`,
3181
+ "A symbol with no history is one row with its reason in status, and a warning names it; the command fails only when no symbol has any. --tail 5 keeps each symbol's newest five bars.",
2341
3182
  ],
2342
3183
  }, {
2343
3184
  title: "Long history",
2344
3185
  lines: [
2345
- "ZAR has daily FRED data from 1980: gloomberb fred DEXSFUS --start 1980 --sort asc --csv",
2346
- "NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (gloomberb fn G USDNGN=X --rangePreset 5Y --chartResolution 1d --csv); before that there are monthly bars only (gloomberb history USDNGN=X --range ALL).",
3186
+ "ZAR has daily FRED data from 1980: gloom fred DEXSFUS --start 1980 --sort asc --csv",
3187
+ "NGN, SAR and ILS have no FRED series. Their daily bars cover the last five years (gloom fn G USDNGN=X --range-preset 5Y --chart-resolution 1d --csv); before that there are monthly bars only (gloom history USDNGN=X --range ALL).",
2347
3188
  ],
2348
3189
  }],
2349
- 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"],
3190
+ examples: [
3191
+ "history AAPL", "history BHP:ASX --range 5Y", "history JPM --from 2007 --to 2009", "history ZAR=X --range ALL", "history ZAR=X --tail 5",
3192
+ "history AAPL --range 5Y --csv > aapl.csv", "history AAPL MSFT 000660.KS --range 1M --csv",
3193
+ "history --file holdings.txt --start 2024-01-01 --end 2024-12-31 --csv > bars.csv",
3194
+ ],
2350
3195
  },
2351
3196
  unknownOptionHint: historyOptionHint,
2352
3197
  execute: runHistory,
@@ -2422,7 +3267,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2422
3267
  },
2423
3268
  execute: async (args, ctx) => {
2424
3269
  const query = args.join(" ");
2425
- if (!query) ctx.fail("Usage: gloomberb provider-search <query>");
3270
+ if (!query) ctx.fail("Usage: gloom provider-search <query>");
2426
3271
  await withMarketData(ctx, async (market) => {
2427
3272
  const results = await market.dataProvider.search(query);
2428
3273
  ctx.printResult({ data: results.slice(0, ctx.cliOptions.limit ?? results.length) }, {
@@ -2522,6 +3367,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2522
3367
  title: "Output",
2523
3368
  lines: [
2524
3369
  "Holders run largest value first, as in fn HDS. --csv and --json carry the HDS report's columns: values in the currency the header names, shares, and the quarter's change and the stake in percent (0.59 is 0.59%).",
3370
+ "A US listing lists every 13F filer, as fn 13F does: the largest 25, or as many as --limit asks up to 200; --tail counts back from the end of those, not from the smallest filer. A listing abroad has only the source's largest few.",
2525
3371
  ],
2526
3372
  }],
2527
3373
  examples: ["holders AAPL", "holders SAN:EPA", "holders SAN --csv", "holders CAR --form all", "holders CAR --form 13g --history --json"],
@@ -2548,7 +3394,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2548
3394
  options: [EXCHANGE_OPTION],
2549
3395
  sections: [{
2550
3396
  title: "13F filings",
2551
- lines: ["For each fund's reported position and its change over the quarter, run gloomberb fn 13F <symbol>."],
3397
+ lines: ["For each fund's reported position and its change over the quarter, run gloom fn 13F <symbol>."],
2552
3398
  }],
2553
3399
  examples: ["13f AAPL", "13f BP:NYSE"],
2554
3400
  },
@@ -2556,7 +3402,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2556
3402
  },
2557
3403
  {
2558
3404
  name: "filings",
2559
- description: "Fetch recent SEC filings",
3405
+ description: "Fetch recent SEC filings; a listing abroad gets its exchange's announcements page, and ASX's list",
2560
3406
  help: {
2561
3407
  group: CLI_COMMAND_GROUPS.companyData,
2562
3408
  usage: ["filings <symbol> [--count <n>] [--form <form>]"],
@@ -2565,7 +3411,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2565
3411
  { flags: "--form <form>", description: "Only this form and its amendments, such as 10-K or 13D" },
2566
3412
  EXCHANGE_OPTION,
2567
3413
  ],
2568
- examples: ["filings AAPL", "filings BHP:ASX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
3414
+ examples: ["filings AAPL", "filings BHP:ASX", "filings NST.AX", "filings AAPL --count 40 --json", "filings CAR --form 13G"],
2569
3415
  },
2570
3416
  execute: runFilings,
2571
3417
  },
@@ -2595,9 +3441,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2595
3441
  description: "Convert a currency into your base currency, quote a pair such as USD/NGN, or convert an amount such as fx 1234.5 EUR/USD",
2596
3442
  help: {
2597
3443
  group: CLI_COMMAND_GROUPS.markets,
2598
- usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
3444
+ usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>", FX_FORWARD_USAGE],
2599
3445
  options: [
2600
3446
  { flags: "--date <day>", description: `The rate at that day's daily FX close, such as a transaction date (${DATE_OPTION_EXAMPLES}); a weekend or holiday reads the trading day before it` },
3447
+ { flags: "--forward <tenor>", description: "The forward implied by both currencies' short rates (covered interest parity), not a dealer quote: 30d, 2w, 3m, up to 1y; today's only, so not with --date" },
2601
3448
  ],
2602
3449
  sections: [{
2603
3450
  title: "Direction",
@@ -2616,8 +3463,14 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2616
3463
  lines: [
2617
3464
  "--date gives the rate at that day's daily FX close instead of the latest: fx USD/CAD --date 2026-09-02, or with an amount, fx 1250 USD/CAD --date 2026-09-02. Each currency's USD pair is read at its close of that day and crossed through USD, as the latest rate is. A weekend or market holiday has no close: the answer then gives the trading day before it and says so. The source line dates the close; --json adds date (the day asked for) and asOf (the close used).",
2618
3465
  ],
3466
+ }, {
3467
+ title: "Forwards",
3468
+ lines: [
3469
+ "--forward <tenor> gives the forward implied by covered interest parity: spot × (1 + quote rate × days/basis) / (1 + base rate × days/basis), with the forward points (pips of 0.0001, 0.01 against JPY) and the premium over spot for the period and a year. It is implied by rates, not a dealer quote.",
3470
+ "The rates are SOFR (USD), €STR (EUR), SONIA (GBP) and TIIE 28d (MXN), each with its day and publisher. A pair whose rate is missing says which one. Tenors count calendar days from today: 30d, 2w, 3m, 1y. The forward is today's: it does not take --date, as only the latest short rates are served.",
3471
+ ],
2619
3472
  }],
2620
- examples: ["fx EUR", "fx NGN", "fx yen", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx USD/CAD --date 2026-09-02", "fx JPY --json"],
3473
+ examples: ["fx EUR", "fx NGN", "fx yen", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx USD/CAD --date 2026-09-02", "fx JPY --json", "fx USD/MXN --forward 30d", "fx EUR/USD --forward 3m"],
2621
3474
  },
2622
3475
  execute: runFx,
2623
3476
  },