gloomberb 0.16.6 → 0.17.1

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 (429) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +42 -2
  5. package/src/api-client/doe.ts +19 -0
  6. package/src/api-client/equity-screener.ts +3 -0
  7. package/src/api-client/europe-energy.ts +91 -0
  8. package/src/api-client/index.ts +7 -0
  9. package/src/api-client/letters.ts +41 -0
  10. package/src/api-client/normalizers.ts +36 -6
  11. package/src/api-client/organization-tickers.ts +44 -0
  12. package/src/api-client/request.ts +3 -1
  13. package/src/api-client/types.ts +27 -1
  14. package/src/app/runtime/desktop-deeplink.ts +48 -15
  15. package/src/app/runtime/update.ts +16 -0
  16. package/src/architecture/core-builtin-allowlist.json +1 -1
  17. package/src/architecture/dom-cell-metrics-allowlist.json +102 -0
  18. package/src/ask-gloom/pane-context.ts +23 -19
  19. package/src/ask-gloom/pane-request.ts +2 -2
  20. package/src/cli/commands/fred-series.ts +77 -11
  21. package/src/cli/commands/market.ts +134 -25
  22. package/src/cli/commands/options-chain.ts +75 -1
  23. package/src/cli/commands/overview.ts +29 -20
  24. package/src/cli/commands/ticker.ts +82 -20
  25. package/src/cli/desktop-pane-shot-view.ts +114 -0
  26. package/src/cli/desktop-pane-shot.ts +30 -31
  27. package/src/cli/entry.ts +7 -0
  28. package/src/cli/export-source.ts +43 -0
  29. package/src/cli/freshness.ts +18 -3
  30. package/src/cli/glossary.ts +17 -0
  31. package/src/cli/history-rows.ts +174 -13
  32. package/src/cli/index.ts +14 -6
  33. package/src/cli/known-gaps.ts +14 -3
  34. package/src/cli/listing-arg.ts +20 -2
  35. package/src/cli/listing-suggestion.ts +159 -0
  36. package/src/cli/native-smoke.ts +13 -0
  37. package/src/cli/not-a-ticker.ts +13 -1
  38. package/src/cli/pane-functions/capabilities.ts +16 -8
  39. package/src/cli/pane-functions/catalog.ts +39 -7
  40. package/src/cli/pane-functions/dom.ts +39 -13
  41. package/src/cli/pane-functions/function-help.ts +54 -7
  42. package/src/cli/pane-functions/headless.ts +38 -8
  43. package/src/cli/pane-functions/index.ts +85 -3
  44. package/src/cli/pane-functions/lock-note.ts +56 -0
  45. package/src/cli/pane-functions/report-notices.ts +6 -3
  46. package/src/cli/pane-functions/report.ts +16 -2
  47. package/src/cli/pane-functions/resolver.ts +7 -4
  48. package/src/cli/report-tables.ts +31 -7
  49. package/src/cli/result.ts +54 -11
  50. package/src/cli/symbol-rows.ts +18 -1
  51. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  52. package/src/components/chart/composite/composite-chart.tsx +4 -21
  53. package/src/components/chart/composite/legend.tsx +10 -6
  54. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  55. package/src/components/command-bar/list/model.ts +2 -0
  56. package/src/components/command-bar/list/view.tsx +2 -1
  57. package/src/components/command-bar/pane-templates/items.ts +1 -1
  58. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  59. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  60. package/src/components/command-bar/workflow/ops.ts +8 -97
  61. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  62. package/src/components/data-table/view.tsx +1 -0
  63. package/src/components/feed-data-table/stack-view.tsx +18 -42
  64. package/src/components/index.ts +3 -2
  65. package/src/components/layout/header.tsx +58 -14
  66. package/src/components/layout/missing-pane.tsx +2 -0
  67. package/src/components/layout/shell/chrome.ts +10 -4
  68. package/src/components/layout/shell/index.tsx +1 -0
  69. package/src/components/layout/shell/pane/ask.ts +12 -7
  70. package/src/components/layout/shell/pane/capture.ts +8 -34
  71. package/src/components/markdown-text.tsx +37 -6
  72. package/src/components/reader/article-reader.tsx +91 -266
  73. package/src/components/reader/detail-reader.tsx +210 -0
  74. package/src/components/reader/reader-text.tsx +239 -0
  75. package/src/components/table-view-shared.tsx +8 -4
  76. package/src/components/ticker/badge/list.tsx +15 -4
  77. package/src/components/ui/choice-dialog.tsx +3 -0
  78. package/src/components/ui/column-grid.tsx +206 -0
  79. package/src/components/ui/data-table/types.ts +7 -0
  80. package/src/components/ui/detail-scroll-body.tsx +31 -4
  81. package/src/components/ui/display.tsx +86 -4
  82. package/src/components/ui/fill.ts +8 -0
  83. package/src/components/ui/icon.tsx +11 -1
  84. package/src/components/ui/index.ts +2 -0
  85. package/src/components/ui/list-view.tsx +3 -1
  86. package/src/components/ui/query-bar-keys.tsx +2 -0
  87. package/src/components/ui/query-bar.tsx +9 -1
  88. package/src/components/ui/select-button.tsx +3 -0
  89. package/src/data/config/store/fold-panes.ts +202 -11
  90. package/src/data/config/store/migrations.ts +13 -1
  91. package/src/docs/snapshot.json +29 -0
  92. package/src/i18n/es.ts +14 -0
  93. package/src/i18n/ja.ts +14 -0
  94. package/src/i18n/ko.ts +14 -0
  95. package/src/i18n/zh-cn.ts +14 -0
  96. package/src/i18n/zh-tw.ts +14 -0
  97. package/src/layout/desk-keys.ts +7 -0
  98. package/src/layout/desks.ts +2 -2
  99. package/src/market-data/market/status.ts +67 -6
  100. package/src/market-data/quotes/contributions.ts +15 -0
  101. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  102. package/src/market-data/quotes/resolution.ts +4 -0
  103. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  104. package/src/market-data/snapshot-provider.ts +6 -3
  105. package/src/notifications/app-badge.ts +30 -0
  106. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  107. package/src/plugins/builtin/account-management/form-components.tsx +57 -8
  108. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  109. package/src/plugins/builtin/account-management/model.ts +27 -0
  110. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  111. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  112. package/src/plugins/builtin/alerts/cli.ts +6 -4
  113. package/src/plugins/builtin/analytics/index.tsx +1 -1
  114. package/src/plugins/builtin/awards/pane.tsx +29 -18
  115. package/src/plugins/builtin/backtest/pane.tsx +11 -4
  116. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  117. package/src/plugins/builtin/buildout/detail/company.tsx +11 -7
  118. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  119. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  120. package/src/plugins/builtin/catalysts/model.ts +92 -10
  121. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  122. package/src/plugins/builtin/cds/index.tsx +21 -30
  123. package/src/plugins/builtin/changelog/index.tsx +11 -12
  124. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  125. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  126. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  127. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  128. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  129. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  130. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  131. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  132. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  133. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  134. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  135. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  136. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  137. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  138. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  139. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  140. package/src/plugins/builtin/chat/index.tsx +44 -21
  141. package/src/plugins/builtin/chat/layout.ts +7 -14
  142. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  143. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  144. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  145. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  146. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  147. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  148. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  149. package/src/plugins/builtin/chat/message/types.ts +3 -0
  150. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  151. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  152. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  153. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  154. package/src/plugins/builtin/chat/people.ts +202 -0
  155. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  156. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  157. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  158. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  159. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  160. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  161. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  162. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  163. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  164. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  165. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  166. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  167. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  168. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  169. package/src/plugins/builtin/cloud/thesis/detail.tsx +11 -9
  170. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  171. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  172. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  173. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  174. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  175. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  176. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  177. package/src/plugins/builtin/composite-plugins.ts +26 -1
  178. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  179. package/src/plugins/builtin/connections/pane.tsx +16 -5
  180. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  181. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  182. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  183. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  184. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  185. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  186. package/src/plugins/builtin/debug/index.tsx +2 -2
  187. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  188. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  189. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  190. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  191. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  192. package/src/plugins/builtin/docs/pane.tsx +16 -8
  193. package/src/plugins/builtin/doe/index.tsx +11 -4
  194. package/src/plugins/builtin/doe/pane.tsx +5 -4
  195. package/src/plugins/builtin/doe/prices.ts +84 -0
  196. package/src/plugins/builtin/doe/series.ts +4 -1
  197. package/src/plugins/builtin/earnings/index.tsx +2 -0
  198. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  199. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  200. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  201. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  202. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  203. package/src/plugins/builtin/equity-screener/headless.ts +95 -3
  204. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  205. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  206. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  207. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  208. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  209. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  210. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  211. package/src/plugins/builtin/executives/index.tsx +18 -0
  212. package/src/plugins/builtin/executives/pane.tsx +3 -1
  213. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  214. package/src/plugins/builtin/filing-events/index.tsx +18 -0
  215. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  216. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  217. package/src/plugins/builtin/fixed-income/pane.tsx +163 -0
  218. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  219. package/src/plugins/builtin/futures/index.tsx +24 -7
  220. package/src/plugins/builtin/futures/series.ts +23 -2
  221. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  222. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  223. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  224. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  225. package/src/plugins/builtin/insider/detail.tsx +20 -12
  226. package/src/plugins/builtin/insider/index.tsx +2 -2
  227. package/src/plugins/builtin/iv-history/format.ts +3 -0
  228. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  229. package/src/plugins/builtin/iv-history/index.tsx +6 -1
  230. package/src/plugins/builtin/iv-history/model.ts +3 -1
  231. package/src/plugins/builtin/iv-history/screen-pane.tsx +16 -6
  232. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  233. package/src/plugins/builtin/jobs/headless.ts +6 -20
  234. package/src/plugins/builtin/jobs/index.tsx +2 -0
  235. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  236. package/src/plugins/builtin/jodi/client.ts +220 -0
  237. package/src/plugins/builtin/jodi/headless.ts +63 -0
  238. package/src/plugins/builtin/jodi/index.tsx +32 -0
  239. package/src/plugins/builtin/jodi/model.ts +75 -0
  240. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  241. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  242. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  243. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  244. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  245. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  246. package/src/plugins/builtin/market-movers/table.ts +2 -2
  247. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  248. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  249. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  250. package/src/plugins/builtin/mna/index.tsx +2 -0
  251. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  252. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  253. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  254. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  255. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  256. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  257. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  258. package/src/plugins/builtin/openfda/client.ts +123 -15
  259. package/src/plugins/builtin/openfda/model.ts +163 -0
  260. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  261. package/src/plugins/builtin/openfda/types.ts +25 -0
  262. package/src/plugins/builtin/options/index.tsx +2 -1
  263. package/src/plugins/builtin/options/model-spot.ts +94 -0
  264. package/src/plugins/builtin/options/open-interest-as-of.ts +63 -0
  265. package/src/plugins/builtin/options/view.tsx +56 -10
  266. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  267. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  268. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  269. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  270. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  271. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  272. package/src/plugins/builtin/pe-band/model.ts +3 -2
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  274. package/src/plugins/builtin/plugin-module.ts +3 -1
  275. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  276. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  277. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  278. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  279. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  280. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  281. package/src/plugins/builtin/power/evidence.ts +32 -3
  282. package/src/plugins/builtin/power/gas-client.ts +110 -0
  283. package/src/plugins/builtin/power/gas-model.ts +194 -0
  284. package/src/plugins/builtin/power/gas.tsx +145 -0
  285. package/src/plugins/builtin/power/headless.ts +4 -1
  286. package/src/plugins/builtin/power/index.tsx +19 -9
  287. package/src/plugins/builtin/power/pane.tsx +2 -3
  288. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  289. package/src/plugins/builtin/power/prices.tsx +36 -13
  290. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  291. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  292. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  293. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  294. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  295. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  296. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  297. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  298. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  299. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  300. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  301. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  302. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  303. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  304. package/src/plugins/builtin/scanner/headless.ts +9 -2
  305. package/src/plugins/builtin/scanner/index.ts +3 -0
  306. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  307. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  308. package/src/plugins/builtin/sec/index.tsx +131 -43
  309. package/src/plugins/builtin/sec/model.ts +148 -2
  310. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  311. package/src/plugins/builtin/sectors/index.tsx +2 -0
  312. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  313. package/src/plugins/builtin/series-inventory.ts +2 -1
  314. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  315. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  316. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  317. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  318. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  319. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  320. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  321. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  322. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  323. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  324. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  325. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  326. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  327. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  328. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  329. package/src/plugins/builtin/themes/model.ts +2 -2
  330. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +25 -1
  331. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +87 -23
  332. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  333. package/src/plugins/builtin/ticker-detail/headless.ts +6 -3
  334. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  335. package/src/plugins/builtin/ticker-detail/overview/model.ts +12 -3
  336. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -13
  337. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  338. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  339. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  340. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  341. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  342. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  343. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  344. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  345. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  346. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  347. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  348. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  349. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  350. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  351. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  352. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  353. package/src/plugins/builtin-editorial.ts +1 -1
  354. package/src/plugins/catalog-browser.ts +25 -2
  355. package/src/plugins/marketplace/model.ts +57 -21
  356. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  357. package/src/renderers/dom/build-assets.ts +14 -5
  358. package/src/renderers/dom/data-table/index.tsx +53 -18
  359. package/src/renderers/dom/data-table/row.tsx +1 -1
  360. package/src/renderers/dom/desktop/controls.tsx +2 -1
  361. package/src/renderers/dom/desktop/icons.tsx +25 -0
  362. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  363. package/src/renderers/dom/desktop/menu.tsx +1 -1
  364. package/src/renderers/dom/desktop/popover.tsx +6 -15
  365. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  366. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  367. package/src/renderers/dom/host/input.tsx +27 -3
  368. package/src/renderers/dom/host/resize-rows.ts +111 -0
  369. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  370. package/src/renderers/dom/host/style.ts +14 -0
  371. package/src/renderers/dom/host/surface-3d.tsx +8 -5
  372. package/src/renderers/dom/host/surface-fit.ts +10 -6
  373. package/src/renderers/dom/host/text.tsx +2 -2
  374. package/src/renderers/dom/styles.css +85 -10
  375. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  376. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  377. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  378. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  379. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  380. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  381. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  382. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  383. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  384. package/src/renderers/electrobun/view/app-services.ts +2 -22
  385. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  386. package/src/renderers/electrobun/view/main.tsx +2 -0
  387. package/src/renderers/electrobun/view/news-source.ts +42 -0
  388. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  389. package/src/series/validate.ts +6 -1
  390. package/src/shares/api.ts +5 -1
  391. package/src/shares/dialog.tsx +2 -1
  392. package/src/shares/location.ts +31 -0
  393. package/src/shares/open.ts +26 -0
  394. package/src/shares/pane-image.ts +18 -6
  395. package/src/shares/personal-pane.ts +42 -54
  396. package/src/sources/gloomberb-cloud/index.ts +7 -2
  397. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  398. package/src/sources/history-coverage.ts +22 -4
  399. package/src/sources/history-result.ts +3 -1
  400. package/src/sources/provider-errors.ts +2 -1
  401. package/src/sources/provider-router/batches.ts +18 -2
  402. package/src/sources/provider-router/financials.ts +5 -0
  403. package/src/sources/provider-router/index.ts +2 -1
  404. package/src/state/app/context/index.tsx +3 -2
  405. package/src/test-support/builtin-panes.ts +31 -0
  406. package/src/tickers/listing-venues.ts +22 -0
  407. package/src/tickers/search/index.ts +7 -2
  408. package/src/tickers/search/ranking.ts +19 -0
  409. package/src/time-series/chart-data.ts +2 -0
  410. package/src/time-series/parsed-history-cache.ts +41 -11
  411. package/src/time-series/resolve.ts +47 -15
  412. package/src/types/config.ts +1 -1
  413. package/src/types/data-provider.ts +13 -1
  414. package/src/types/financials.ts +35 -0
  415. package/src/types/headless.ts +22 -0
  416. package/src/types/plugin.ts +46 -6
  417. package/src/types/price-history.ts +2 -0
  418. package/src/ui/host.tsx +6 -0
  419. package/src/utils/crypto-pair.ts +1 -1
  420. package/src/utils/currency-pair.ts +66 -0
  421. package/src/utils/decimal-comma.ts +57 -0
  422. package/src/utils/financial-statements.ts +25 -0
  423. package/src/utils/fundamentals.ts +41 -1
  424. package/src/utils/guards.ts +25 -0
  425. package/src/utils/local-utc-offset.ts +37 -0
  426. package/src/utils/plugin-proxy-hosts.json +8 -1
  427. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  428. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  429. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -16,7 +16,7 @@ import {
16
16
  getExtendedSessionDisplay,
17
17
  getQuoteSessionFields,
18
18
  getRegularSessionDisplay,
19
- marketStateLabel,
19
+ quoteSessionLabel,
20
20
  type ExtendedSession,
21
21
  } from "../../market-data/market/status";
22
22
  import { formatCompact, formatDistributionAmount, formatPercent, formatPercentRaw } from "../../utils/format";
@@ -31,6 +31,7 @@ import {
31
31
  formatPriceRange,
32
32
  } from "../helpers";
33
33
  import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
34
+ import { statementHistoryNotes } from "../../utils/financial-statements";
34
35
  import { formatPerShareNumber } from "../../utils/reported-money";
35
36
  import {
36
37
  analystTargetCurrency,
@@ -48,9 +49,9 @@ import type { FuturesContractTermsPayload } from "../../api-client/futures-curve
48
49
  import { futuresSettleText, isFuturesInstrument } from "../../market-data/futures-contract";
49
50
  import {
50
51
  chainHasExpiry,
51
- chainWithModelFigures,
52
52
  formatExpiryList,
53
53
  loadOptionModelInputs,
54
+ loadOptionsChainOpenInterest,
54
55
  formatOptionDeltaCell,
55
56
  formatOptionIvCell,
56
57
  formatOptionQuoteCell,
@@ -58,10 +59,16 @@ import {
58
59
  OPTION_MODEL_RATE,
59
60
  OPTIONS_USAGE,
60
61
  optionRows,
62
+ optionsBasisFields,
63
+ optionsBasisFootnote,
61
64
  parseOptionExpiration,
65
+ requestListedOpenInterest,
66
+ valueChainAtModelSpot,
67
+ type OptionsChainBasis,
62
68
  } from "./options-chain";
63
69
  import { DEFAULT_LEAPS_CRITERIA, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions } from "./options-leaps";
64
70
  import type { CliResultColumn } from "../result";
71
+ import type { PriceHistoryResult } from "../../types/price-history";
65
72
  import {
66
73
  exportRowsTable,
67
74
  reportFooterLines,
@@ -70,9 +77,12 @@ import {
70
77
  } from "../report-tables";
71
78
  import {
72
79
  historyBarStatusNote,
80
+ historyCoverageWarning,
73
81
  historyFacts,
74
82
  historyFlagNote,
75
83
  historyGapNote,
84
+ historyJumpNote,
85
+ historyUnbrokenFrom,
76
86
  historyAllRangeYears,
77
87
  historyIntervalsByRange,
78
88
  historyNotes,
@@ -89,7 +99,9 @@ import { newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProb
89
99
  import { formatUtcTime } from "../../utils/utc-time";
90
100
  import { isFiniteNumber } from "../../utils/guards";
91
101
  import { exportedFundamentals } from "../../utils/price-earnings";
92
- import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness } from "../freshness";
102
+ import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
103
+ import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
104
+ import { selectMarketCapitalization } from "../../utils/market-capitalization";
93
105
  import {
94
106
  barHistoryFreshness,
95
107
  barResolutionFromDates,
@@ -128,13 +140,15 @@ import {
128
140
  import { indexRootAlsoLine, indexRootFor, indexRootTryLine, isYieldIndexSymbol } from "../index-roots";
129
141
  import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
130
142
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
131
- import { providerMissReason } from "../../sources/provider-errors";
132
- import { isNotATickerMessage } from "../not-a-ticker";
143
+ import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
144
+ import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
145
+ import { describeSuggestion, suggestListing } from "../listing-suggestion";
133
146
  import { splitSymbolArgs } from "../symbol-args";
134
147
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
135
148
  import { subMinuteDataMessage } from "../known-gaps";
136
149
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
137
150
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
151
+ import { looksLikeCurrencyPair } from "../../utils/currency-pair";
138
152
  import { isUsListingExchange } from "../../utils/exchanges";
139
153
  import { describeTickerChange, startTickerChangeLookup, tickerChangeLine, type TickerChange } from "../../tickers/ticker-changes";
140
154
  import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
@@ -245,6 +259,7 @@ const QUOTE_LEAD_COLUMNS = [
245
259
  // Text drops a column no row fills, so a table without an extended print stays as narrow as before.
246
260
  extendedColumn("PRE"),
247
261
  extendedColumn("POST"),
262
+ extendedColumn("OVERNIGHT"),
248
263
  { key: "session", header: "Session", shrink: false },
249
264
  ];
250
265
 
@@ -261,17 +276,21 @@ function yieldChangeCell(row: QuoteRow): string {
261
276
  return row.changeBasisPoints == null ? "" : colorBySign(formatBasisPoints(row.changeBasisPoints / 100, 1), row.changeBasisPoints);
262
277
  }
263
278
 
264
- /** The pre-market or after-hours print and its move from the regular close, in its own column. */
279
+ /**
280
+ * The pre-market, after-hours or overnight print and its move from the
281
+ * regular close, in its own column; "mid" when it is a bid and ask midpoint.
282
+ */
265
283
  function extendedColumn(session: ExtendedSession) {
284
+ const price = (row: QuoteRow) => row.extendedMark ? `${row.extendedPrice} mid` : row.extendedPrice;
266
285
  return {
267
- key: session === "PRE" ? "preMarket" : "afterHours",
286
+ key: session === "PRE" ? "preMarket" : session === "POST" ? "afterHours" : "overnight",
268
287
  header: EXTENDED_SESSION_LABELS[session],
269
288
  align: "right" as const,
270
289
  value: (row: QuoteRow) => row.extendedSession === session && row.extendedPrice
271
- ? [row.extendedPrice, row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
290
+ ? [price(row), row.extendedChangePercent == null ? "" : formatPercentRaw(row.extendedChangePercent)].join(" ").trim()
272
291
  : "",
273
292
  format: (value: unknown, row: QuoteRow) => (
274
- value ? [row.extendedPrice, formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
293
+ value ? [price(row), formatChangePercentCell(row.extendedChangePercent)].join(" ").trim() : ""
275
294
  ),
276
295
  };
277
296
  }
@@ -361,7 +380,9 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
361
380
  rawExtendedPrice: extended?.price ?? null,
362
381
  extendedChange: extended?.change ?? null,
363
382
  extendedChangePercent: roundedPercent(extended?.changePercent),
364
- session: quote?.marketState ? marketStateLabel(quote.marketState) : "",
383
+ // The extended price is the session's bid and ask midpoint, nothing having traded in it.
384
+ extendedMark: extended?.mark === true,
385
+ session: quoteSessionLabel(quote) ?? "",
365
386
  // The close the shown move is measured from: the one before the session Last is.
366
387
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
367
388
  dayRange: quote?.low != null && quote.high != null
@@ -696,7 +717,7 @@ async function answerQuote(
696
717
  root ? indexRootTryLine(root, request.command) : null,
697
718
  other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
698
719
  ].filter((line): line is string => !!line);
699
- if (!other && tryLines.length === 0) return { result, listing, notes: [] };
720
+ if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
700
721
  const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
701
722
  const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
702
723
  const oneLine = [
@@ -707,6 +728,21 @@ async function answerQuote(
707
728
  return { result, listing, failure: { message, details, oneLine }, notes: [] };
708
729
  }
709
730
 
731
+ /**
732
+ * A symbol written like a currency pair that is not a ticker (USD_NGN, as some
733
+ * FX feeds write a pair) names the listing `gloomberb search` finds nearest to
734
+ * it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
735
+ * Naira)?". When search finds nothing near, the row keeps its own error and its
736
+ * search hint.
737
+ */
738
+ async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
739
+ const message = errorMessage(result.error);
740
+ const symbol = message ? notATickerSymbol(message) : null;
741
+ if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
742
+ const suggestion = await suggestListing(symbol, market);
743
+ return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
744
+ }
745
+
710
746
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
711
747
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
712
748
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
@@ -744,16 +780,17 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
744
780
  const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
745
781
  const load = () => market.dataProvider.getPriceHistoryWithMetadata
746
782
  ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
747
- : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points) => ({ points, resolution: null }));
783
+ : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
748
784
  const quoted = loadListingQuote(market.dataProvider, listing);
749
785
  const fate = listingFateLoader(listing, market, quoted);
750
- const [{ points, resolution }, quote] = await Promise.all([
786
+ const [loaded, quote] = await Promise.all([
751
787
  loadForListing(listing, market, ctx, load, (loaded) => loaded.points.length === 0, {
752
788
  command: range === "1Y" ? "history" : `history --range ${range}`, noun: "history", fate,
753
789
  })
754
790
  .catch((error) => failHistory(error, listing.key, ctx)),
755
791
  quoted,
756
792
  ]);
793
+ const { points, resolution } = loaded;
757
794
  const identity = listingIdentity(listing, quote);
758
795
  // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
759
796
  const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
@@ -783,7 +820,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
783
820
  // FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
784
821
  ...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
785
822
  ...await listingChoiceNotes(listing, market, quote, venues),
786
- ...historyNotes(range, interval, data[0]?.date), gaps ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(data) ?? [],
823
+ ...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
824
+ gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(data) ?? [],
787
825
  ].flat();
788
826
  const priceColumns: CliResultColumn[] = [
789
827
  intraday
@@ -821,6 +859,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
821
859
  interval,
822
860
  firstDate: data[0]?.date ?? null,
823
861
  lastDate: data.at(-1)?.date ?? null,
862
+ // The first bar after the last gap or jump; the first bar when there is none.
863
+ unbrokenFrom: historyUnbrokenFrom(data) ?? data[0]?.date ?? null,
824
864
  bars: data.length,
825
865
  asOf: freshness?.asOf ?? null,
826
866
  ...(volume ? { volumeUnit: volume.unit, volumePeriod: interval === "1d" ? "UTC day" : interval } : {}),
@@ -831,16 +871,14 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
831
871
  heading: `${listingHeading(identity)}\n${cliStyles.muted(historyFacts(unit, data).join(" · "))}`,
832
872
  dateKey: "date",
833
873
  textColumns: [...priceColumns, flagTextColumn],
834
- // Every exported row says its currency, what a price is in and its bar size, so it survives head and concatenation.
835
874
  columns: [
836
875
  ...priceColumns,
837
- { key: "currency", header: "Currency" },
838
- { key: "unit", header: "Unit" },
839
- { key: "interval", header: "Interval" },
840
876
  flagColumn,
841
877
  ...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
842
878
  ...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
843
879
  ],
880
+ // 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.
881
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: unit.currency ?? unit.unit, interval },
844
882
  });
845
883
  });
846
884
  }
@@ -889,15 +927,28 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
889
927
  ...financials,
890
928
  };
891
929
  if (view !== "statements") {
892
- const freshness = fundamentalsFreshness(financials);
930
+ // A valuation is measured at the quote's regular session price, so the quote dates it.
931
+ const freshness = view === "valuation" ? valuationFreshness(financials) : fundamentalsFreshness(financials);
932
+ // Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
933
+ const priced = atRegularSessionPrice(financials);
934
+ const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
893
935
  ctx.printResult({
894
936
  // JSON reads a multiple over a loss as null with its reason, never as a number.
895
- data: { ...data, fundamentals: exportedFundamentals(data.fundamentals) },
937
+ data: {
938
+ ...data,
939
+ ...view === "valuation" ? {
940
+ ...getQuoteSessionFields(financials.quote),
941
+ marketCap: capitalization?.value ?? null,
942
+ marketCapCurrency: capitalization?.currency ?? null,
943
+ } : {},
944
+ fundamentals: exportedFundamentals(priced.fundamentals),
945
+ },
896
946
  freshness,
897
947
  ...(choice.length > 0 ? { warnings: choice } : {}),
898
948
  }, {
899
949
  text: () => renderFundamentalsReport(data, view),
900
950
  ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
951
+ exportIdentity: { symbolOrSeries: listing.key },
901
952
  });
902
953
  return;
903
954
  }
@@ -905,11 +956,13 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
905
956
  const freshness = rowsFreshness(rows, {
906
957
  ...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
907
958
  });
959
+ // The rows are the annual years served; older ones are not, and the closing lines say so.
960
+ const historyNotes = statementHistoryNotes("annual", rows.map((row) => row.date), listing.key);
908
961
  // NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
909
962
  const tables = tabular
910
963
  ? selectTables({
911
964
  tables: [exportRowsTable("Annual statements", STATEMENT_COLUMNS, rows)],
912
- footer: reportFooterLines({ freshness }),
965
+ footer: reportFooterLines({ freshness, notes: historyNotes }),
913
966
  })
914
967
  : undefined;
915
968
  ctx.printResult({
@@ -921,6 +974,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
921
974
  quarterlyStatements: financials.quarterlyStatements.length,
922
975
  fundamentals: exportedFundamentals(financials.fundamentals),
923
976
  profile: financials.profile,
977
+ ...(historyNotes.length > 0 ? { notes: historyNotes } : {}),
924
978
  },
925
979
  freshness,
926
980
  ...(choice.length > 0 ? { warnings: choice } : {}),
@@ -928,7 +982,10 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
928
982
  heading: listingHeading(identity),
929
983
  rows: () => rows,
930
984
  columns: STATEMENT_COLUMNS,
985
+ ...(historyNotes.length > 0 ? { footnote: historyNotes.join("\n"), exportNotes: historyNotes } : {}),
931
986
  ...(tables && ctx.cliOptions.format === "csv" ? { tables } : {}),
987
+ // Annual statements in the company's reporting currency.
988
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: rows.find((row) => row.currency)?.currency ?? null, interval: "annual" },
932
989
  });
933
990
  });
934
991
  }
@@ -1318,6 +1375,7 @@ async function runHolders(
1318
1375
  tables: [exportRowsTable("Holders", columns, shownRows)],
1319
1376
  footer: reportFooterLines({ freshness, notes: notices }),
1320
1377
  },
1378
+ exportIdentity: { symbolOrSeries: listing.key },
1321
1379
  summary: () => [
1322
1380
  [valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
1323
1381
  insiderCommand ? insiderSummary(data, ownerTypes, listing, identity, form4) : "",
@@ -1447,6 +1505,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1447
1505
  const quotePromise = loadListingQuote(market.dataProvider, listing);
1448
1506
  const fate = listingFateLoader(listing, market, quotePromise);
1449
1507
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1508
+ // OPX's listing dates the chain's open interest; asked for alongside the chain when the expiry is known.
1509
+ const listedOpenInterest = expirationDate != null ? requestListedOpenInterest(symbol, expirationDate) : null;
1450
1510
  const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
1451
1511
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
1452
1512
  .load({ force: refresh });
@@ -1476,7 +1536,13 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1476
1536
  }
1477
1537
  const quote = await quotePromise;
1478
1538
  const identity = listingIdentity(listing, quote);
1479
- const chain = chainWithModelFigures(loaded, inputs);
1539
+ // IV and delta are valued at one spot and instant, named under the table and on every exported row.
1540
+ const { chain, spot: modelSpot } = valueChainAtModelSpot(loaded, inputs, Date.now(), identity.exchange);
1541
+ const basis: OptionsChainBasis = { spot: modelSpot, dividendYield: inputs.dividendYield ?? null,
1542
+ openInterest: await loadOptionsChainOpenInterest(symbol, chain, listedOpenInterest) };
1543
+ const basisFields = optionsBasisFields(basis);
1544
+ const currency = [...chain.calls, ...chain.puts].find((contract) => contract.currency?.trim())?.currency.trim().toUpperCase()
1545
+ ?? quote?.currency ?? null;
1480
1546
  // A failed refresh falls back to the stored chain, which can be days old.
1481
1547
  const refreshWarning = result?.refreshError == null ? null
1482
1548
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
@@ -1498,23 +1564,54 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1498
1564
  ...(chain.delayMinutes != null ? { delayMinutes: chain.delayMinutes } : {}),
1499
1565
  stale: refreshWarning != null,
1500
1566
  });
1567
+ const contractCount = chain.calls.length + chain.puts.length;
1501
1568
  ctx.printResult({
1502
1569
  data: chain,
1503
1570
  metadata: {
1504
1571
  ...listingMetadata(identity),
1505
1572
  expirations: chain.expirationDates,
1573
+ currency,
1574
+ interval: OPTIONS_EXPORT_INTERVAL,
1575
+ // `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
1576
+ units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
1506
1577
  // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
1507
- model: { spot: inputs.spot ?? null, dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE },
1578
+ model: {
1579
+ spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
1580
+ valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
1581
+ dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
1582
+ },
1583
+ openInterest: { asOf: basisFields.openInterestAsOf, listedAsOf: basis.openInterest?.listedDate ?? null,
1584
+ compared: basis.openInterest?.compared ?? 0, matched: basis.openInterest?.matched ?? 0 },
1508
1585
  },
1509
1586
  ...(warnings.length > 0 ? { warnings } : {}),
1510
1587
  freshness,
1511
1588
  }, {
1512
1589
  heading: listingHeading(identity),
1513
- rows: optionRows,
1590
+ rows: (data: OptionsChain) => optionRows(data).map((row) => ({ ...row, ...basisFields })),
1514
1591
  // Choosing an expiry needs the list; once one is chosen the table is that expiry.
1515
1592
  summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
1516
1593
  empty: `No option contracts for ${listingTitle(identity)}.`,
1594
+ ...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis) } : {}),
1595
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: currency, interval: OPTIONS_EXPORT_INTERVAL },
1596
+ // Exports state each decimal's unit in its header and end every row with the spot and dates it is as of.
1517
1597
  columns: [
1598
+ { key: "side", header: "Side" },
1599
+ { key: "contract", header: "Contract" },
1600
+ { key: "expiration", header: "Expiry" },
1601
+ { key: "strike", header: "Strike" },
1602
+ { key: "last", header: "Last" },
1603
+ { key: "bid", header: "Bid" },
1604
+ { key: "ask", header: "Ask" },
1605
+ { key: "iv", header: "IV (annualized decimal)" },
1606
+ { key: "delta", header: "Delta (decimal)" },
1607
+ { key: "volume", header: "Vol" },
1608
+ { key: "openInterest", header: "OI" },
1609
+ { key: "spot", header: "Spot" },
1610
+ { key: "spotBasis", header: "Spot basis" },
1611
+ { key: "spotAsOf", header: "Spot as of" },
1612
+ { key: "openInterestAsOf", header: "OI as of" },
1613
+ ],
1614
+ textColumns: [
1518
1615
  { key: "side", header: "Side" },
1519
1616
  { key: "contract", header: "Contract", shrink: false },
1520
1617
  // The contract symbol already carries the date, so this goes first when the table is narrow.
@@ -1532,6 +1629,9 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1532
1629
  });
1533
1630
  }
1534
1631
 
1632
+ /** What an options chain export's `interval` says: one quote snapshot, not bars. */
1633
+ const OPTIONS_EXPORT_INTERVAL = "snapshot";
1634
+
1535
1635
  const FX_USAGE = "fx <currency> | fx <base>/<quote>";
1536
1636
 
1537
1637
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
@@ -1577,6 +1677,8 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1577
1677
  })));
1578
1678
  ctx.printResult({ data: [row], freshness }, {
1579
1679
  text: () => describeFxRate(request, rate),
1680
+ // The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
1681
+ exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
1580
1682
  columns: [
1581
1683
  { key: "currency", header: "Currency" },
1582
1684
  { key: "baseCurrency", header: "Base" },
@@ -1689,7 +1791,14 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1689
1791
  title: "Bars",
1690
1792
  lines: [
1691
1793
  `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.`,
1692
- "A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. Every exported row (--csv, --ndjson, --json) carries currency, interval and flag.",
1794
+ "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.",
1795
+ "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.",
1796
+ ],
1797
+ }, {
1798
+ title: "Long history",
1799
+ lines: [
1800
+ "ZAR has daily FRED data from 1980: gloomberb fred DEXSFUS --start 1980 --sort asc --csv",
1801
+ "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).",
1693
1802
  ],
1694
1803
  }],
1695
1804
  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"],
@@ -5,6 +5,12 @@ import { cliStyles, cliTerminalWidth, wrapText } from "../../utils/cli-output";
5
5
  import { isFiniteNumber } from "../../utils/guards";
6
6
  import { calculateOptionGreeks, solveChainVolatilities } from "../../plugins/builtin/options/analytics";
7
7
  import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
8
+ import {
9
+ formatModelSpotPrice, modelSpotLabel, optionModelSpot, type OptionModelSpot,
10
+ } from "../../plugins/builtin/options/model-spot";
11
+ import {
12
+ chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf,
13
+ } from "../../plugins/builtin/options/open-interest-as-of";
8
14
  import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
9
15
  import { expiryIsoDate, missingExpiryText, parseOptionExpiration } from "../../utils/option-expiry";
10
16
 
@@ -61,6 +67,8 @@ export function formatExpiryList(expirations: readonly number[], width = cliTerm
61
67
  export interface OptionModelInputs {
62
68
  /** The underlying's current price; absent when the quote is missing or stale. */
63
69
  spot?: number;
70
+ /** The quote `spot` is read from, for the spot the chain is valued at (`optionModelSpot`). */
71
+ quote?: Quote;
64
72
  dividendYield?: number;
65
73
  now?: number;
66
74
  }
@@ -119,7 +127,7 @@ export async function loadOptionModelInputs(
119
127
  .find((candidate) => candidate && candidate.stale !== true && isFiniteNumber(candidate.price) && candidate.price > 0);
120
128
  const dividendYield = financials?.fundamentals?.dividendYield;
121
129
  return {
122
- ...(current ? { spot: current.price } : {}),
130
+ ...(current ? { spot: current.price, quote: current } : {}),
123
131
  ...(isFiniteNumber(dividendYield) && dividendYield >= 0 ? { dividendYield } : {}),
124
132
  };
125
133
  }
@@ -131,6 +139,72 @@ export function chainWithModelFigures(chain: OptionsChain, inputs: OptionModelIn
131
139
  return { ...chain, calls: chain.calls.map(add), puts: chain.puts.map(add) };
132
140
  }
133
141
 
142
+ /**
143
+ * What a chain's IV, delta and open interest are as of: the spot and instant
144
+ * they are valued at, and the session the open interest counts. The rows and
145
+ * the closing lines of every format say it.
146
+ */
147
+ export interface OptionsChainBasis {
148
+ spot: OptionModelSpot | null;
149
+ dividendYield: number | null;
150
+ openInterest: OpenInterestAsOf | null;
151
+ }
152
+
153
+ /** The chain with `iv` and `delta` valued at the spot `optionModelSpot` picks for it, and that spot. */
154
+ export function valueChainAtModelSpot(
155
+ chain: OptionsChain, inputs: OptionModelInputs, now: number, exchange?: string,
156
+ ): { chain: OptionsChain; spot: OptionModelSpot | null } {
157
+ const spot = optionModelSpot(inputs.quote, chain, now, exchange);
158
+ return {
159
+ chain: chainWithModelFigures(chain, spot ? { spot: spot.price, dividendYield: inputs.dividendYield, now: spot.valuationTime } : inputs),
160
+ spot,
161
+ };
162
+ }
163
+
164
+ /** The open-interest date of the chain's expiry, from OPX's listing of it; null when the listing cannot be read. */
165
+ export async function loadOptionsChainOpenInterest(
166
+ symbol: string, chain: OptionsChain, listed?: ReturnType<typeof loadListedOpenInterest> | null,
167
+ ): Promise<OpenInterestAsOf | null> {
168
+ const expiration = [...chain.calls, ...chain.puts][0]?.expiration;
169
+ if (expiration == null) return null;
170
+ const payload = await (listed ?? loadListedOpenInterest(symbol, expiryIsoDate(expiration)));
171
+ return payload ? chainOpenInterestAsOf(chain, payload) : null;
172
+ }
173
+
174
+ /** OPX's listing of an expiry, requested while the chain loads. */
175
+ export function requestListedOpenInterest(symbol: string, expiration: number) {
176
+ return loadListedOpenInterest(symbol, expiryIsoDate(expiration));
177
+ }
178
+
179
+ /**
180
+ * The line under the chain: `Greeks at spot 89.79 (close Mon 12 Oct 20:00 UTC), dividend yield 2.34%, rate 4% · OI as of Fri 9 Oct`.
181
+ * An open interest whose date cannot be established says why.
182
+ */
183
+ export function optionsBasisFootnote(basis: OptionsChainBasis): string {
184
+ const parts: string[] = [];
185
+ if (basis.spot) {
186
+ parts.push(`Greeks at spot ${formatModelSpotPrice(basis.spot.price)} (${modelSpotLabel(basis.spot)}), `
187
+ + `dividend yield ${basis.dividendYield == null ? "0%" : `${Number((basis.dividendYield * 100).toFixed(2))}%`}, `
188
+ + `rate ${Number((OPTION_MODEL_RATE * 100).toFixed(2))}%`);
189
+ }
190
+ const oi = basis.openInterest;
191
+ parts.push(oi?.date ? openInterestAsOfLabel(oi)
192
+ : oi?.listedDate && oi.compared > 0
193
+ ? `OI date not published: the counts differ from OPX's ${oi.listedDate} counts on ${oi.compared - oi.matched} of ${oi.compared} contracts`
194
+ : openInterestAsOfLabel(null));
195
+ return parts.join(" · ");
196
+ }
197
+
198
+ /** The spot and open-interest fields every exported row carries. */
199
+ export function optionsBasisFields(basis: OptionsChainBasis) {
200
+ return {
201
+ spot: basis.spot?.price ?? null,
202
+ spotBasis: basis.spot ? basis.spot.basis === "close" ? "close" : basis.spot.session ?? "last" : null,
203
+ spotAsOf: basis.spot ? new Date(basis.spot.observedAt).toISOString() : null,
204
+ openInterestAsOf: basis.openInterest?.date ?? null,
205
+ };
206
+ }
207
+
134
208
  export function optionRows(chain: OptionsChain) {
135
209
  const contracts = [
136
210
  ...chain.calls.map((contract) => ({ side: "call", contract })),
@@ -40,13 +40,17 @@ import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
40
40
  import {
41
41
  basisPointsCell,
42
42
  DEFAULT_FRED_START,
43
+ filterFredSeries,
44
+ fredEmptyAdvice,
43
45
  fredEmptyMessage,
44
46
  fredExportValue,
45
47
  fredHistoryNotes,
48
+ fredInterval,
49
+ fredObservationsFromStart,
46
50
  fredProvenance,
47
51
  fredPublicationTime,
52
+ fredUnit,
48
53
  fredValueHeader,
49
- fredValueKey,
50
54
  isSpreadSeries,
51
55
  laggingSeriesNote,
52
56
  } from "./fred-series";
@@ -346,13 +350,10 @@ async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execut
346
350
  if (error instanceof ApiRequestError && error.status === 404) ctx.fail("The list of supported FRED series is not available yet.");
347
351
  throw error;
348
352
  }
349
- const terms = filter.toLowerCase().split(/\s+/).filter(Boolean);
350
- const rows = catalog.series
351
- .map((series) => ({ id: series.id, title: series.title?.trim() ?? "", group: series.group?.trim() ?? "" }))
352
- .filter((series) => {
353
- const text = `${series.id} ${series.title} ${series.group}`.toLowerCase();
354
- return terms.every((term) => text.includes(term));
355
- });
353
+ const rows = filterFredSeries(
354
+ catalog.series.map((series) => ({ id: series.id, title: series.title?.trim() ?? "", group: series.group?.trim() ?? "" })),
355
+ filter,
356
+ );
356
357
  ctx.printResult({ data: rows, metadata: { filter: filter || null, total: catalog.series.length } }, {
357
358
  columns: [
358
359
  { key: "id", header: "ID", shrink: false },
@@ -381,15 +382,17 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
381
382
  if (isUnsupportedSeries(error)) ctx.fail(`Unsupported FRED series ${seriesId}. List supported series with: gloomberb fred --list`);
382
383
  throw error;
383
384
  });
384
- // Each observation exports under its unit (`value_bp`, `value_pct`), a spread in basis points.
385
- const valueKey = fredValueKey(seriesId, data.info?.units);
385
+ // Every observation exports as `value`, a spread in basis points; its unit goes in the `currency_or_unit` column.
386
+ const unit = fredUnit(seriesId, data.info);
387
+ const interval = fredInterval(data.info?.frequency);
386
388
  // A value FRED does not list yet carries its publisher and when it published, in every export.
387
389
  const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string }) => ({
388
390
  date: observation.date,
389
- [valueKey]: fredExportValue(seriesId, observation.value),
391
+ value: fredExportValue(seriesId, observation.value),
390
392
  ...(observation.publisher?.trim() ? { publisher: observation.publisher.trim(), ...(observation.publishedAt ? { publishedAt: observation.publishedAt } : {}) } : {}),
391
393
  });
392
- const rows = data.observations.map(exportRow);
394
+ // A --start before the series begins comes back padded with blank days: the rows start at its first observation.
395
+ const rows = fredObservationsFromStart(data.observations, data.info?.observationStart).map(exportRow);
393
396
  // The default start hides older history and a start before the series begins is moved up silently: say where it really starts.
394
397
  const notes = [
395
398
  ...fredHistoryNotes({ startDate, startGiven: start != null, seriesStart: data.info?.observationStart }),
@@ -398,6 +401,10 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
398
401
  // Nothing on or after the start: name where the series ends, so an empty answer is not a dead end.
399
402
  const latest = rows.length > 0 ? null : await apiClient.getCloudFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
400
403
  .then((payload) => payload.observations[0] ?? null).catch(() => null);
404
+ // CSV and NDJSON are rows alone: an empty one would read as a series with no data, so it fails and says where the series ends.
405
+ if (rows.length === 0 && (ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson")) {
406
+ ctx.fail(`No ${seriesId} observations on or after ${startDate}.`, fredEmptyAdvice(seriesId, latest));
407
+ }
401
408
  const periodicity = periodicityOf(data.info?.frequency);
402
409
  const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
403
410
  // A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
@@ -408,7 +415,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
408
415
  ctx.printResult({
409
416
  data: rows,
410
417
  metadata: {
411
- info: data.info, seriesId, startDate, sortOrder,
418
+ info: data.info, seriesId, startDate, sortOrder, unit, interval,
412
419
  ...(notes.length ? { notes } : {}),
413
420
  ...(latest ? { latestObservation: exportRow(latest) } : {}),
414
421
  },
@@ -422,12 +429,13 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
422
429
  }, {
423
430
  dateKey: "date",
424
431
  empty: fredEmptyMessage(startDate, latest),
425
- ...(notes.length ? { footnote: notes.join("\n") } : {}),
432
+ ...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
433
+ exportIdentity: { symbolOrSeries: seriesId, currencyOrUnit: unit, interval },
426
434
  textColumns: [
427
435
  { key: "date", header: "Date" },
428
436
  isSpreadSeries(seriesId)
429
- ? { key: valueKey, header: "Value (bp)", align: "right", format: basisPointsCell }
430
- : { key: valueKey, header: fredValueHeader(data.info), align: "right" },
437
+ ? { key: "value", header: "Value (bp)", align: "right", format: basisPointsCell }
438
+ : { key: "value", header: fredValueHeader(data.info), align: "right" },
431
439
  ],
432
440
  });
433
441
  }
@@ -445,12 +453,12 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
445
453
  return {
446
454
  seriesId,
447
455
  date: latest?.date ?? "",
448
- // Yields in percent, named so in every export, as `fred` names a percent series.
449
- value_pct: latest?.value ?? null,
456
+ // Yields in percent, which the exports' `currency_or_unit` column says (`pct`), as `fred` does.
457
+ value: latest?.value ?? null,
450
458
  title: data.info?.title ?? "",
451
459
  // The move since the previous observation FRED has, not calendar yesterday: a Monday reads against Friday.
452
460
  previousDate: previous?.date ?? null,
453
- previousValue_pct: previous?.value ?? null,
461
+ previousValue: previous?.value ?? null,
454
462
  changeBasisPoints: latest?.value != null && previous?.value != null ? toBasisPoints(latest.value - previous.value, 1) : null,
455
463
  latest,
456
464
  publishedAt: latest?.publisher ? null : fredPublicationTime(data.info, latest?.date),
@@ -470,9 +478,10 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
470
478
  ...(publishedAt ? { publishedAt } : {}),
471
479
  }),
472
480
  }, {
481
+ exportIdentity: (row) => ({ symbolOrSeries: String(row.seriesId), currencyOrUnit: "pct", interval: "daily" }),
473
482
  textColumns: [
474
483
  { key: "tenor", header: "Tenor", value: (row) => YIELD_TENORS[String(row.seriesId)] ?? row.seriesId },
475
- { key: "value_pct", header: "Yield %", align: "right", format: (value) => typeof value === "number" ? value.toFixed(2) : "-" },
484
+ { key: "value", header: "Yield %", align: "right", format: (value) => typeof value === "number" ? value.toFixed(2) : "-" },
476
485
  {
477
486
  key: "changeBasisPoints",
478
487
  header: "1D bp",