gloomberb 0.17.4 → 0.17.6

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 (384) hide show
  1. package/package.json +3 -1
  2. package/patches/electrobun@1.18.1.patch +80 -0
  3. package/src/api-client/chat.ts +9 -0
  4. package/src/api-client/commodity-balances.ts +77 -0
  5. package/src/api-client/data.ts +35 -3
  6. package/src/api-client/earnings.ts +10 -0
  7. package/src/api-client/index.ts +5 -0
  8. package/src/api-client/insider-signals.ts +75 -0
  9. package/src/api-client/market-discovery.ts +1 -1
  10. package/src/api-client/normalizers.ts +1 -0
  11. package/src/api-client/paths.ts +9 -0
  12. package/src/api-client/research-activity.ts +10 -3
  13. package/src/api-client/socket.ts +10 -0
  14. package/src/api-client/types.ts +25 -1
  15. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
  16. package/src/app/runtime/broker-import.ts +23 -21
  17. package/src/app/runtime/plugin-bindings.ts +13 -7
  18. package/src/app/runtime/update.ts +64 -4
  19. package/src/app.tsx +2 -0
  20. package/src/architecture/core-builtin-allowlist.json +2 -10
  21. package/src/ask-gloom/pane-context.ts +11 -1
  22. package/src/brokers/broker-portfolio-sync.ts +66 -0
  23. package/src/brokers/sync-broker-instance.ts +75 -41
  24. package/src/cli/commands/market.ts +101 -34
  25. package/src/cli/commands/options-chain.ts +20 -6
  26. package/src/cli/commands/overview.ts +54 -13
  27. package/src/cli/commands/search.ts +1 -1
  28. package/src/cli/commands/system.ts +19 -1
  29. package/src/cli/commands/ticker.ts +24 -4
  30. package/src/cli/context.ts +13 -0
  31. package/src/cli/errors.ts +15 -2
  32. package/src/cli/fx-pair.ts +77 -0
  33. package/src/cli/glossary.ts +34 -4
  34. package/src/cli/history-rows.ts +25 -11
  35. package/src/cli/index.ts +30 -12
  36. package/src/cli/listing-arg.ts +61 -138
  37. package/src/cli/listing-suggestion.ts +3 -21
  38. package/src/cli/pane-functions/catalog.ts +50 -0
  39. package/src/cli/pane-functions/freshness.ts +44 -9
  40. package/src/cli/pane-functions/function-help.ts +9 -9
  41. package/src/cli/pane-functions/headless.ts +19 -7
  42. package/src/cli/pane-functions/index.ts +63 -5
  43. package/src/cli/pane-functions/market-session.ts +18 -3
  44. package/src/cli/pane-functions/resolver.ts +29 -8
  45. package/src/cli/pane-functions/screenshot.ts +2 -0
  46. package/src/cli/report-tables.ts +11 -4
  47. package/src/cli/result.ts +2 -0
  48. package/src/cli/series-search.ts +6 -1
  49. package/src/cli/ticker-resolution.ts +1 -2
  50. package/src/cli/unknown-command.ts +18 -0
  51. package/src/components/chart/composite/composite-chart.tsx +80 -19
  52. package/src/components/chart/composite/format.ts +17 -0
  53. package/src/components/chart/composite/legend.tsx +26 -15
  54. package/src/components/chart/composite/panel-labels.ts +38 -0
  55. package/src/components/chart/composite/panel-series.ts +1 -0
  56. package/src/components/chart/composite/panel-surface.tsx +25 -7
  57. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  58. package/src/components/chart/composite/scene.ts +60 -2
  59. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  60. package/src/components/chart/composite/tools.ts +2 -2
  61. package/src/components/chart/composite/types.ts +28 -0
  62. package/src/components/chart/core/view-reset.ts +46 -0
  63. package/src/components/command-bar/commands/collection.ts +1 -1
  64. package/src/components/command-bar/commands/direct/index.ts +48 -1
  65. package/src/components/command-bar/commands/registry.ts +8 -0
  66. package/src/components/command-bar/list/model.ts +7 -0
  67. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  68. package/src/components/command-bar/panel/prompt-binding.ts +8 -0
  69. package/src/components/command-bar/routes/root/command-items.ts +17 -2
  70. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  71. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  72. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  73. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  74. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  75. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  76. package/src/components/command-bar/surface/index.tsx +7 -1
  77. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  78. package/src/components/command-bar/workflow/ops.ts +8 -5
  79. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  80. package/src/components/command-bar/workflow/tickers.ts +1 -1
  81. package/src/components/form-modal/confirm.tsx +1 -0
  82. package/src/components/form-modal/request.ts +2 -0
  83. package/src/components/layout/detached-pane-shell.tsx +6 -1
  84. package/src/components/layout/pane/content.tsx +11 -8
  85. package/src/components/layout/pane/sidebar.tsx +24 -11
  86. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  87. package/src/components/layout/shell/index.tsx +5 -1
  88. package/src/components/layout/shell/menu.ts +8 -0
  89. package/src/components/layout/shell/pane/capture.ts +29 -12
  90. package/src/components/onboarding/wizard-model.ts +1 -1
  91. package/src/components/onboarding/wizard-positions.ts +18 -10
  92. package/src/components/ticker/badge/format.ts +64 -15
  93. package/src/components/ticker/badge/index.tsx +32 -11
  94. package/src/components/ticker/badge/list.tsx +4 -3
  95. package/src/components/ticker/badge/text.tsx +2 -2
  96. package/src/components/ticker/list-table-view.tsx +8 -1
  97. package/src/components/ui/confirm-dialog.tsx +30 -4
  98. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  99. package/src/components/ui/data-table/opentui/model.ts +104 -0
  100. package/src/components/ui/fields.tsx +5 -0
  101. package/src/components/ui/inline-quick-add.tsx +4 -1
  102. package/src/components/ui/query-bar-keys.tsx +29 -5
  103. package/src/components/ui/query-bar.tsx +29 -4
  104. package/src/components/ui/windowed-list.tsx +27 -0
  105. package/src/data/config/store/normalize.ts +8 -0
  106. package/src/docs/snapshot.json +248 -5
  107. package/src/i18n/es.ts +3 -1
  108. package/src/i18n/ja.ts +3 -1
  109. package/src/i18n/ko.ts +3 -1
  110. package/src/i18n/zh-cn.ts +3 -1
  111. package/src/i18n/zh-tw.ts +3 -1
  112. package/src/layout-marketplace/cloud.ts +16 -5
  113. package/src/layout-marketplace/gallery.tsx +4 -4
  114. package/src/layout-marketplace/linked-sync.ts +9 -3
  115. package/src/layout-marketplace/linked.ts +24 -3
  116. package/src/layout-marketplace/model.ts +39 -4
  117. package/src/market-data/coordinator/events.ts +5 -1
  118. package/src/market-data/coordinator/index.ts +38 -1
  119. package/src/market-data/coordinator/quotes.ts +4 -0
  120. package/src/market-data/futures-contract.ts +53 -0
  121. package/src/market-data/hooks.tsx +225 -30
  122. package/src/market-data/market/bar-period.ts +71 -0
  123. package/src/market-data/market/format.ts +14 -2
  124. package/src/market-data/market/freshness.ts +28 -1
  125. package/src/market-data/market/status.ts +152 -13
  126. package/src/market-data/model-dividend-yield.ts +76 -0
  127. package/src/market-data/quotes/freshness.ts +30 -1
  128. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  129. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  130. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  131. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  132. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  134. package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
  135. package/src/plugins/builtin/broker-manager/model.ts +2 -2
  136. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  137. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  138. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  139. package/src/plugins/builtin/chart-composer/headless.ts +27 -2
  140. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  141. package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
  142. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  143. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  144. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  145. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  146. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  147. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  148. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  149. package/src/plugins/builtin/chat/content/footer.tsx +5 -2
  150. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  151. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  152. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  153. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  154. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  155. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  156. package/src/plugins/builtin/chat/controller/index.ts +88 -17
  157. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  158. package/src/plugins/builtin/chat/controller/messages.ts +14 -2
  159. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  160. package/src/plugins/builtin/chat/controller/state.ts +25 -0
  161. package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
  162. package/src/plugins/builtin/chat/controller/view.ts +4 -0
  163. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  164. package/src/plugins/builtin/chat/conversations.ts +12 -5
  165. package/src/plugins/builtin/chat/index.tsx +1 -1
  166. package/src/plugins/builtin/chat/layout.ts +23 -5
  167. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  168. package/src/plugins/builtin/chat/share/flow.tsx +18 -9
  169. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  170. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  171. package/src/plugins/builtin/chat/unread-badge.ts +38 -7
  172. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  173. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
  174. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  175. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  176. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  177. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  178. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  179. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  180. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  181. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  182. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  183. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  184. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  185. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  186. package/src/plugins/builtin/earnings/board.tsx +36 -3
  187. package/src/plugins/builtin/earnings/headless.ts +24 -5
  188. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  189. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  190. package/src/plugins/builtin/earnings/history.tsx +40 -10
  191. package/src/plugins/builtin/earnings/index.tsx +3 -1
  192. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  193. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  194. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  195. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  196. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  197. package/src/plugins/builtin/futures/model.ts +5 -4
  198. package/src/plugins/builtin/futures/series.ts +38 -1
  199. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  200. package/src/plugins/builtin/futures-curve/headless.ts +33 -8
  201. package/src/plugins/builtin/futures-curve/index.tsx +4 -4
  202. package/src/plugins/builtin/futures-curve/model.ts +109 -6
  203. package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
  204. package/src/plugins/builtin/insider/headless.ts +74 -4
  205. package/src/plugins/builtin/insider/index.tsx +99 -5
  206. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  207. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  208. package/src/plugins/builtin/insider/signals.tsx +176 -0
  209. package/src/plugins/builtin/iv-history/client.ts +41 -2
  210. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  211. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  212. package/src/plugins/builtin/jodi/client.ts +42 -205
  213. package/src/plugins/builtin/jodi/headless.ts +13 -4
  214. package/src/plugins/builtin/jodi/model.ts +18 -43
  215. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  216. package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
  217. package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
  218. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  219. package/src/plugins/builtin/macro-day/release-table.ts +3 -3
  220. package/src/plugins/builtin/market-movers/client.ts +32 -6
  221. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  222. package/src/plugins/builtin/market-movers/model.ts +64 -3
  223. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  224. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  225. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  226. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  227. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  228. package/src/plugins/builtin/mna/model.ts +2 -1
  229. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  230. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  231. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  232. package/src/plugins/builtin/options-positioning/model.ts +47 -0
  233. package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
  234. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  235. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  236. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  237. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  238. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  239. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  240. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  241. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  242. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  243. package/src/plugins/builtin/perps/client.ts +77 -6
  244. package/src/plugins/builtin/perps/headless.ts +25 -14
  245. package/src/plugins/builtin/perps/index.tsx +1 -1
  246. package/src/plugins/builtin/perps/model.ts +32 -0
  247. package/src/plugins/builtin/perps/pane.tsx +5 -4
  248. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  249. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  250. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  251. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  252. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  253. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  254. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  255. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  256. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  257. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  258. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  259. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  260. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  261. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  262. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  263. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  264. package/src/plugins/builtin/rate-path/model.ts +1 -1
  265. package/src/plugins/builtin/rate-path/pane.tsx +2 -1
  266. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  267. package/src/plugins/builtin/research/event-model.ts +41 -49
  268. package/src/plugins/builtin/research/event-sources.ts +1 -1
  269. package/src/plugins/builtin/research/headless.ts +1 -1
  270. package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
  271. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  272. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  273. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  274. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  275. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  276. package/src/plugins/builtin/sectors/client.ts +46 -19
  277. package/src/plugins/builtin/sectors/sector-model.ts +22 -10
  278. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  279. package/src/plugins/builtin/shared/history-span.ts +55 -0
  280. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  281. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  282. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  283. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  284. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  285. package/src/plugins/builtin/themes/headless.ts +15 -2
  286. package/src/plugins/builtin/thirteenf/api.ts +60 -43
  287. package/src/plugins/builtin/thirteenf/data.ts +76 -35
  288. package/src/plugins/builtin/thirteenf/headless.ts +2 -0
  289. package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
  290. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  291. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  292. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  293. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  294. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  295. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  296. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  297. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  298. package/src/plugins/builtin/yield-curve/curves.ts +15 -1
  299. package/src/plugins/builtin/yield-curve/headless.ts +17 -7
  300. package/src/plugins/builtin/yield-curve/history.ts +2 -0
  301. package/src/plugins/builtin/yield-curve/index.tsx +23 -18
  302. package/src/plugins/builtin/yield-curve/series.ts +19 -9
  303. package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
  304. package/src/plugins/builtin/yield-curve/world.ts +4 -0
  305. package/src/plugins/registry/host-actions.ts +3 -0
  306. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  307. package/src/portfolio/valuation-basis.ts +4 -21
  308. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  309. package/src/remote/controller.ts +27 -7
  310. package/src/remote/schema.ts +25 -9
  311. package/src/renderers/browser/app-services.ts +7 -1
  312. package/src/renderers/browser/config-host.ts +55 -2
  313. package/src/renderers/browser/first-screen.ts +193 -0
  314. package/src/renderers/browser/main.tsx +59 -7
  315. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  316. package/src/renderers/dom/desktop/controls.tsx +2 -0
  317. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  318. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  319. package/src/renderers/dom/windowed-list.tsx +201 -0
  320. package/src/renderers/dom/windowed-rows.ts +65 -0
  321. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
  322. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
  323. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  324. package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
  325. package/src/renderers/electrobun/shared/protocol.ts +7 -4
  326. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  327. package/src/renderers/electrobun/view/update-host.ts +2 -2
  328. package/src/shares/bug-report-dialog.tsx +162 -0
  329. package/src/shares/bug-report-flow.tsx +84 -0
  330. package/src/shares/bug-report.ts +42 -0
  331. package/src/shares/capture-flow.ts +19 -0
  332. package/src/shares/capture-preview.tsx +129 -0
  333. package/src/shares/chat.ts +10 -1
  334. package/src/shares/dialog.tsx +16 -91
  335. package/src/shares/flow.tsx +12 -16
  336. package/src/shares/personal-pane.ts +35 -1
  337. package/src/sources/listing-symbols.ts +8 -4
  338. package/src/state/app/context/index.tsx +13 -0
  339. package/src/state/app/tab-cursor.ts +85 -0
  340. package/src/sync/core-contributors.ts +206 -59
  341. package/src/telemetry/usage-counts.ts +16 -0
  342. package/src/test-support/config-store.ts +31 -0
  343. package/src/test-support/moved-symbols.ts +64 -0
  344. package/src/theme/colors.ts +30 -0
  345. package/src/tickers/listing-fate.ts +242 -0
  346. package/src/tickers/open-target.ts +61 -11
  347. package/src/tickers/search/index.ts +45 -3
  348. package/src/tickers/search/ranking.ts +19 -2
  349. package/src/tickers/search/types.ts +3 -0
  350. package/src/tickers/search/upsert.ts +22 -7
  351. package/src/tickers/ticker-changes.ts +26 -9
  352. package/src/time-series/bar-spacing.ts +19 -0
  353. package/src/time-series/chart-data.ts +37 -13
  354. package/src/time-series/field-catalog.ts +5 -2
  355. package/src/time-series/forward-valuation.ts +10 -7
  356. package/src/time-series/fundamentals.ts +49 -11
  357. package/src/time-series/hooks.ts +2 -0
  358. package/src/time-series/live-bars.ts +3 -1
  359. package/src/time-series/parsed-history-cache.ts +2 -0
  360. package/src/time-series/price-units.ts +69 -0
  361. package/src/time-series/resolve.ts +71 -11
  362. package/src/time-series/session-bar.ts +138 -0
  363. package/src/time-series/spec.ts +20 -3
  364. package/src/time-series/studies.ts +211 -12
  365. package/src/time-series/transforms.ts +2 -0
  366. package/src/time-series/types.ts +11 -1
  367. package/src/time-series/valuation-price.ts +13 -0
  368. package/src/types/config.ts +24 -1
  369. package/src/types/financials.ts +2 -0
  370. package/src/types/headless.ts +27 -0
  371. package/src/types/plugin.ts +7 -0
  372. package/src/ui/host.tsx +5 -0
  373. package/src/updater/channel.ts +51 -0
  374. package/src/updater/github-releases.ts +85 -16
  375. package/src/updater/index.ts +128 -17
  376. package/src/utils/asset-category.ts +20 -0
  377. package/src/utils/datetime-format.ts +7 -1
  378. package/src/utils/decimal-comma.ts +83 -3
  379. package/src/utils/fundamentals.ts +7 -2
  380. package/src/utils/http-proxy-response.ts +66 -9
  381. package/src/utils/plugin-proxy-hosts.json +1 -5
  382. package/src/utils/semver.ts +47 -2
  383. package/src/utils/typo-distance.ts +22 -0
  384. package/src/plugins/builtin/shared/zip-text.ts +0 -46
@@ -16,10 +16,12 @@ import {
16
16
  getExtendedSessionDisplay,
17
17
  getQuoteSessionFields,
18
18
  getRegularSessionDisplay,
19
+ quoteHeadlineTime,
19
20
  quoteSessionLabel,
21
+ quoteSessionStateLabel,
20
22
  type ExtendedSession,
21
23
  } from "../../market-data/market/status";
22
- import { formatCompact, formatDistributionAmount, formatPercent, formatPercentRaw } from "../../utils/format";
24
+ import { formatCompact, formatCurrency, formatDistributionAmount, formatNumber, formatPercent, formatPercentRaw } from "../../utils/format";
23
25
  import { withCliServices, withMarketData } from "../context";
24
26
  import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
25
27
  import type { BuiltinCliCommandDef } from "../command-options";
@@ -59,6 +61,7 @@ import {
59
61
  formatOptionIvCell,
60
62
  formatOptionQuoteCell,
61
63
  OPTION_MODEL_RATE,
64
+ OPTION_VOLATILITY_FIELDS,
62
65
  OPTIONS_USAGE,
63
66
  optionRows,
64
67
  optionsBasisFields,
@@ -81,6 +84,7 @@ import {
81
84
  type CliReportTables,
82
85
  } from "../report-tables";
83
86
  import {
87
+ formingPeriodBar,
84
88
  historyBarStatusNote,
85
89
  historyCoverageWarning,
86
90
  historyFacts,
@@ -102,6 +106,7 @@ import { bareFuturesCode } from "../../market-data/futures-board";
102
106
  import { newsEmptyMessage } from "./news-hints";
103
107
  import { newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, searchNewsStories } from "./news-search";
104
108
  import { formatUtcTime } from "../../utils/utc-time";
109
+ import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
105
110
  import { isFiniteNumber } from "../../utils/guards";
106
111
  import { exportedFundamentals } from "../../utils/price-earnings";
107
112
  import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
@@ -116,7 +121,15 @@ import {
116
121
  SEC_FILINGS,
117
122
  } from "../../plugins/builtin/shared/report-freshness";
118
123
  import {
119
- bareListingNote,
124
+ earningsDateStatus,
125
+ earningsDetailParts,
126
+ earningsEpsBasis,
127
+ epsBasisName,
128
+ isProvisionalActual,
129
+ quarterLabel,
130
+ showsEpsBasis,
131
+ } from "../../plugins/builtin/shared/earnings-report";
132
+ import {
120
133
  tradingBoardNote,
121
134
  EXCHANGE_OPTION,
122
135
  futuresTryLine,
@@ -132,16 +145,15 @@ import {
132
145
  loadForListing,
133
146
  loadListingQuote,
134
147
  listingFateLoader,
135
- listingFateNotes,
136
- noLongerTradingNote,
148
+ movedSymbolLine,
137
149
  notTradedMessage,
138
150
  quoteBareListing,
139
151
  requireCliListing,
140
- tradingListing,
141
152
  type CliListing,
142
153
  type ListingDataRequest,
143
154
  type ListingIdentity,
144
155
  } from "../listing-arg";
156
+ import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
145
157
  import { indexRootAlsoLine, indexRootFor, indexRootTryLine, isYieldIndexSymbol } from "../index-roots";
146
158
  import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
147
159
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
@@ -155,11 +167,11 @@ import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentC
155
167
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
156
168
  import { looksLikeCurrencyPair } from "../../utils/currency-pair";
157
169
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
158
- import { describeTickerChange, startTickerChangeLookup, tickerChangeLine, type TickerChange } from "../../tickers/ticker-changes";
170
+ import { describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type TickerChange } from "../../tickers/ticker-changes";
159
171
  import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
160
172
  import type { MarketContext } from "../types";
161
173
  import { windowRows } from "../row-window";
162
- import { crossRate, describeFxRate, isUsableRate, parseFxRequest } from "../fx-pair";
174
+ import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
163
175
  import {
164
176
  holderListFacts,
165
177
  holderListPeriod,
@@ -271,7 +283,8 @@ const QUOTE_LEAD_COLUMNS = [
271
283
  extendedColumn("PRE"),
272
284
  extendedColumn("POST"),
273
285
  extendedColumn("OVERNIGHT"),
274
- { key: "session", header: "Session", shrink: false },
286
+ // An export keeps the market state (PRE-MKT); the table names the close a pre-market with no trade stands at (Thu close).
287
+ { key: "session", header: "Session", shrink: false, format: (_value: unknown, row: QuoteRow) => row.sessionLabel },
275
288
  ];
276
289
 
277
290
  type QuoteRow = ReturnType<typeof quoteRows>[number];
@@ -393,7 +406,8 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
393
406
  extendedChangePercent: roundedPercent(extended?.changePercent),
394
407
  // The extended price is the session's bid and ask midpoint, nothing having traded in it.
395
408
  extendedMark: extended?.mark === true,
396
- session: quoteSessionLabel(quote) ?? "",
409
+ session: quoteSessionStateLabel(quote, now) ?? "",
410
+ sessionLabel: quoteSessionLabel(quote, now) ?? "",
397
411
  // The close the shown move is measured from: the one before the session Last is.
398
412
  previousClose: price(display?.change != null ? display.price - display.change : quote?.previousClose),
399
413
  dayRange: quote?.low != null && quote.high != null
@@ -404,7 +418,8 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
404
418
  providerId: quote?.providerId ?? "",
405
419
  source: quote?.dataSource ?? quote?.providerId ?? "",
406
420
  feed: quoteFeedState(quote, now),
407
- updatedAt: quote?.lastUpdated ? new Date(quote.lastUpdated).toISOString() : "",
421
+ // When the price Last shows was observed: in a pre-market nothing has traded in, the close it stands at.
422
+ updatedAt: quote?.lastUpdated ? new Date(quoteHeadlineTime(quote)).toISOString() : "",
408
423
  error: result.error ?? "",
409
424
  };
410
425
  });
@@ -490,19 +505,32 @@ function corporateActionRows(data: CorporateActionsData) {
490
505
  // An upcoming estimate carries no unit of its own; it shares the one every reported quarter states.
491
506
  const reportedCurrencies = new Set(data.earnings.map((event) => event.currency?.trim()).filter(Boolean));
492
507
  const earningsCurrency = reportedCurrencies.size === 1 ? [...reportedCurrencies][0] : undefined;
508
+ // To the cent, as ERN and EE show it; a consensus average carries more digits.
493
509
  const eps = (value: number | undefined, unit: string | undefined) => (
494
- value == null || !unit ? cleanDecimal(value)
495
- : /^[A-Z]{3}$/.test(unit) ? formatDistributionAmount(value, unit) : `${cleanDecimal(value)} ${unit}`
510
+ value == null || !Number.isFinite(value) ? "-"
511
+ : !unit ? formatNumber(value, 2)
512
+ : /^[A-Z]{3}$/.test(unit) ? formatCurrency(value, unit, 2) : `${formatNumber(value, 2)} ${unit}`
496
513
  );
497
514
  return [
498
515
  ...data.earnings.map((event) => {
499
516
  const unit = event.currency?.trim() || earningsCurrency;
517
+ const quarter = quarterLabel(event.fiscalPeriod);
518
+ const basis = earningsEpsBasis(event.epsBasis);
519
+ // History rows are keyed by fiscal quarter end, upcoming ones by announcement date.
520
+ const note = [event.dateType === "fiscal-period-end" ? "period end" : null, quarter === "-" ? null : quarter].filter(Boolean).join(", ");
500
521
  return {
501
522
  type: "earnings",
502
523
  date: event.date,
503
- // History rows are keyed by fiscal quarter end, upcoming ones by announcement date.
504
- detail: (event.epsActual == null ? `est ${eps(event.epsEstimate, unit)}` : `eps ${eps(event.epsActual, unit)}`)
505
- + (event.dateType === "fiscal-period-end" ? " (period end)" : ""),
524
+ // The basis, beat, date status and analysts are the words EE uses for the same row.
525
+ detail: [event.epsActual == null ? `est ${eps(event.epsEstimate, unit)}` : `eps ${eps(event.epsActual, unit)}`,
526
+ ...earningsDetailParts(event)].join(", ") + (note ? ` (${note})` : ""),
527
+ epsActual: event.epsActual,
528
+ epsEstimate: event.epsEstimate,
529
+ epsBasis: showsEpsBasis({ epsActual: event.epsActual, epsBasis: basis }) ? epsBasisName(basis) : undefined,
530
+ ...(isProvisionalActual(event) ? { epsProvisional: true } : {}),
531
+ epsAnalysts: event.epsAnalysts,
532
+ dateStatus: earningsDateStatus(event.dateStatus) ?? undefined,
533
+ fiscalPeriod: event.fiscalPeriod,
506
534
  };
507
535
  }),
508
536
  ...data.dividends.map((event) => ({
@@ -562,6 +590,17 @@ function earningsRows(events: EarningsEvent[]) {
562
590
  /** Symbols at most whose venues a command looks up to name the company each bare one went to. */
563
591
  const MAX_CHOICE_LOOKUPS = 10;
564
592
 
593
+ /**
594
+ * The line `quote` prints beside a symbol that changed, as `history` and
595
+ * `ticker` word it: a company that moved, with nothing new listed there,
596
+ * names the quote of its new symbol ("HFC became DINO on 2022-03-15:
597
+ * `gloomberb quote DINO`"); its old line's last price stays in the row.
598
+ */
599
+ function quoteChangeLine(change: TickerChange | null | undefined): string | null {
600
+ const moved = movedAwayChange(change);
601
+ return moved ? movedSymbolLine(moved, `gloomberb quote ${moved.tradesAs}`) : tickerChangeLine(change);
602
+ }
603
+
565
604
  async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1], commandName: "quote" | "compare") {
566
605
  const args = [...rawArgs];
567
606
  const exchange = takeOption(args, "--exchange") ?? "";
@@ -600,7 +639,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
600
639
  }));
601
640
  const failed = answers[0]?.failure;
602
641
  if (listings.length === 1 && failed) {
603
- ctx.fail(failed.message, [tickerChangeLine(tickerChanges[0]), failed.details].filter(Boolean).join("\n") || undefined);
642
+ ctx.fail(failed.message, [quoteChangeLine(tickerChanges[0]), failed.details].filter(Boolean).join("\n") || undefined);
604
643
  }
605
644
  // A futures quote adds its dated settlement and contract terms, as `ticker` prints them; `compare` stays a table.
606
645
  const contracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
@@ -632,7 +671,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
632
671
  const notes = [
633
672
  // FB now lists another security; the company that was FB trades as META. A symbol a
634
673
  // company renamed into (META) is what was typed: its change is in the row, not here.
635
- ...tickerChanges.flatMap((change) => tickerChangeLine(change) ?? []),
674
+ ...tickerChanges.flatMap((change) => quoteChangeLine(change) ?? []),
636
675
  // Which listing a bare symbol was read as, and the index a bare root is not, are in no row.
637
676
  ...answers.flatMap((answer) => answer.notes),
638
677
  // A row that stopped trading long ago reads as a quote that is merely stale.
@@ -839,7 +878,10 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
839
878
  .catch((error) => failHistory(error, listing.key, ctx)),
840
879
  quoted,
841
880
  ]);
842
- const { points, resolution } = loaded;
881
+ const { resolution } = loaded;
882
+ // While the session trades the day's bar is the quote's, whatever range the history was cut for (withSessionBar).
883
+ const points = withSessionBar(loaded.points, quote, { now: Date.now(), venue: quote?.listingExchangeName || exchange || undefined });
884
+ const fromQuote = points !== loaded.points;
843
885
  const identity = listingIdentity(listing, quote);
844
886
  // Without a quote currency, the listing metadata research reads (as `ticker` does) may still state it.
845
887
  const listed = quote?.currency ? quote : await market.dataProvider.getQuoteMetadata?.(symbol, exchange).catch(() => null);
@@ -849,20 +891,28 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
849
891
  };
850
892
  // A coin's volume is traded value in its quote currency: said in every row and on the status line.
851
893
  const volume = historyVolume(unit, traded);
852
- const data = historyRows(points, resolution, unit.currency, unit.unit, volume);
894
+ const served = historyRows(points, resolution, unit.currency, unit.unit, volume);
895
+ // Bars stop arriving on the listing's own trading days, not New York's.
896
+ const venue = quote?.listingExchangeName || exchange || undefined;
897
+ // October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
898
+ const forming = formingPeriodBar(served, venue);
899
+ const data = forming?.rows ?? served;
853
900
  const interval = data[0]?.interval ?? null;
854
901
  const decimals = historyPriceDecimals(data, listing.saved?.metadata.assetCategory);
855
902
  const price = (value: unknown) => typeof value === "number" ? value.toFixed(decimals) : "";
856
903
  // Intraday bars print in UTC, as the charts and time and sales label them, not the host zone.
857
904
  const intraday = data.some((row) => row.date.length > 10);
858
905
  // A bar history: dated by its last bar, stale once bars of its size stop arriving.
906
+ // The day's own bar, while its session trades, is not a close: dated by the quote it was read from.
907
+ const dayToDate = fromQuote && interval === "1d" && data.at(-1)?.partial === true;
859
908
  const freshness = rowsFreshness(data, {
860
909
  ...barHistoryFreshness(intraday ? null : barResolutionFromDates(data.map((row) => row.date))),
861
910
  observedKey: "date",
862
911
  oldest: null,
863
- // Bars stop arriving on the listing's own trading days, not New York's.
864
- cadenceExchange: quote?.listingExchangeName || exchange || undefined,
865
- ...(volume ? { note: historyVolumeText(volume, interval) } : {}),
912
+ cadenceExchange: venue,
913
+ ...(forming ? { periodToDate: forming.periodToDate } : {}),
914
+ ...(dayToDate ? { basis: "daily bars", ...(quote && Number.isFinite(quote.lastUpdated) ? { asOf: new Date(quote.lastUpdated).toISOString() } : {}) } : {}),
915
+ ...(volume || dayToDate ? { note: [volume ? historyVolumeText(volume, interval) : null, dayToDate ? `the newest bar is ${DAY_SO_FAR_TEXT}` : null].filter(Boolean).join("; ") } : {}),
866
916
  });
867
917
  const gaps = historyGapNote(data, traded);
868
918
  const notes = [
@@ -870,7 +920,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
870
920
  ...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
871
921
  ...await listingChoiceNotes(listing, market, quote, venues),
872
922
  ...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
873
- gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(data) ?? [],
923
+ // The status line says how far a bar still forming runs; the note is for the bars it does not name.
924
+ gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyBarStatusNote(forming ? served : data) ?? [],
874
925
  ].flat();
875
926
  const priceColumns: CliResultColumn[] = [
876
927
  intraday
@@ -1627,6 +1678,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1627
1678
  interval: OPTIONS_EXPORT_INTERVAL,
1628
1679
  // `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
1629
1680
  units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
1681
+ // What each contract's `iv` and `impliedVolatility` are, so neither is read as the other.
1682
+ fields: OPTION_VOLATILITY_FIELDS,
1630
1683
  // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
1631
1684
  model: {
1632
1685
  spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
@@ -1685,13 +1738,12 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1685
1738
  /** What an options chain export's `interval` says: one quote snapshot, not bars. */
1686
1739
  const OPTIONS_EXPORT_INTERVAL = "snapshot";
1687
1740
 
1688
- const FX_USAGE = "fx <currency> | fx <base>/<quote>";
1689
-
1690
1741
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1691
- const raw = requireOneArg(rawArgs, FX_USAGE, "currency or pair", ctx);
1742
+ if (rawArgs.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
1692
1743
  await withMarketData(ctx, async (market) => {
1693
- const request = parseFxRequest(raw, market.config.baseCurrency);
1694
- if ("error" in request) return ctx.fail(request.error, `Usage: gloomberb ${FX_USAGE}`);
1744
+ const parsed = parseFxArgs(rawArgs, market.config.baseCurrency);
1745
+ if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloomberb ${FX_USAGE}`);
1746
+ const { request, amount, toUsdByDefault } = parsed;
1695
1747
  const { currency, baseCurrency } = request;
1696
1748
  const pair = `${currency}/${baseCurrency}`;
1697
1749
  const reasons = new Map<string, string>();
@@ -1719,7 +1771,8 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1719
1771
  const observed = legs.flatMap((leg) => leg?.asOf ?? []);
1720
1772
  const asOf = observed.length > 0 ? new Date(Math.min(...observed)).toISOString() : null;
1721
1773
  const stale = legs.some((leg) => leg != null && (leg.staleAt <= Date.now() || leg.refreshError != null));
1722
- const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate };
1774
+ const converted = amount == null ? null : amount * rate;
1775
+ const row = { currency, baseCurrency, rate, asOf, stale, pair, inverse: 1 / rate, ...(amount == null ? {} : { amount, converted }) };
1723
1776
  // The closing line dates the rate, and says when it is stale, as `fn FXC` does.
1724
1777
  const freshness = fxPairFreshness(new Map(codes.flatMap((code, index) => {
1725
1778
  const leg = legs[index];
@@ -1728,8 +1781,13 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1728
1781
  timing: { asOf: leg.asOf, staleAt: leg.staleAt, error: leg.refreshError, delayMinutes: leg.delayMinutes },
1729
1782
  }] as const] : [];
1730
1783
  })));
1731
- ctx.printResult({ data: [row], freshness }, {
1732
- text: () => describeFxRate(request, rate),
1784
+ // An amount with one currency names no target; the answer says it went to USD.
1785
+ const warnings = toUsdByDefault && amount != null
1786
+ ? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${rawArgs[0]!.trim()} ${currency}/<to>.`]
1787
+ : undefined;
1788
+ ctx.printResult({ data: [row], freshness, warnings }, {
1789
+ // The conversion, then the rate it used; the source line under it dates that rate.
1790
+ text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
1733
1791
  // The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
1734
1792
  exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "spot" },
1735
1793
  columns: [
@@ -1740,6 +1798,10 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
1740
1798
  { key: "stale", header: "Stale" },
1741
1799
  { key: "pair", header: "Pair" },
1742
1800
  { key: "inverse", header: "Inverse", align: "right" },
1801
+ ...(amount == null ? [] : [
1802
+ { key: "amount", header: "Amount", align: "right" as const },
1803
+ { key: "converted", header: "Converted", align: "right" as const },
1804
+ ]),
1743
1805
  ],
1744
1806
  });
1745
1807
  });
@@ -2097,18 +2159,23 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2097
2159
  },
2098
2160
  {
2099
2161
  name: "fx",
2100
- description: "Convert a currency into your base currency, or quote a pair such as USD/NGN",
2162
+ 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",
2101
2163
  help: {
2102
2164
  group: CLI_COMMAND_GROUPS.markets,
2103
- usage: ["fx <currency>", "fx <base>/<quote>"],
2165
+ usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>"],
2104
2166
  sections: [{
2105
2167
  title: "Direction",
2106
2168
  lines: [
2107
2169
  "A bare code is one unit of it in your base currency. A pair reads as markets quote it: USD/NGN is how many NGN one USD buys. Two codes neither of which is USD, such as ZAR/NGN, cross through their USD rates.",
2108
2170
  "The answer states both directions: 1 NGN = 0.000752791 USD (USD/NGN 1328.39). --json, --csv and --ndjson carry rate (base currency per currency), pair, inverse, asOf and stale.",
2109
2171
  ],
2172
+ }, {
2173
+ title: "Amounts",
2174
+ lines: [
2175
+ "An amount in front converts it from the first currency to the second: fx 1234.5 EUR/USD, or fx 1234,5 EUR USD. It takes either decimal mark (1.234,5 and 1,234.5 are 1234.5) and reads as an amount of the first currency: 1,234 or 1.234 EUR are thousands, a third decimal being below a cent, while in a currency with three decimals (KWD) fx asks which. An amount and one currency converts to USD. The answer gives the converted amount, then the rate it used, dated by the source line; --json adds amount and converted.",
2176
+ ],
2110
2177
  }],
2111
- examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx JPY --json"],
2178
+ examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx JPY --json"],
2112
2179
  },
2113
2180
  execute: runFx,
2114
2181
  },
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../plugins/builtin/options/open-interest-as-of";
14
14
  import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
15
15
  import { expiryIsoDate, parseOptionExpiration } from "../../utils/option-expiry";
16
+ import { loadModelDividendYield } from "../../market-data/model-dividend-yield";
16
17
 
17
18
  export const OPTIONS_USAGE = "options <symbol> [--expiration <YYYY-MM-DD|unix>]";
18
19
 
@@ -66,6 +67,14 @@ interface OptionModelFigures {
66
67
  /** The risk-free rate every options valuation in the app assumes. */
67
68
  export const OPTION_MODEL_RATE = DEFAULT_OPTION_CALC_DRAFT.rate;
68
69
 
70
+ /** What each contract's two volatilities are, stated in the chain's JSON metadata so neither is read as the other. */
71
+ export const OPTION_VOLATILITY_FIELDS = {
72
+ iv: "Annualized volatility (decimal) solved from the quote midpoint, one per strike from its out-of-the-money side, at the "
73
+ + "spot, valuation time and rate in metadata.model. Delta here and dealer gamma (GEX) are valued with it.",
74
+ impliedVolatility: "The data provider's own reported implied volatility, passed through unchanged. Delta does not use it; "
75
+ + "GEX falls back to it only at a strike with no two-sided quote on either side.",
76
+ } as const;
77
+
69
78
  /**
70
79
  * IV and delta per contract, from the same solver and Greeks as the options
71
80
  * pane (put-call parity forward, one volatility per strike, the pane's rate).
@@ -110,11 +119,13 @@ export function loadUnderlyingFinancials(
110
119
 
111
120
  /**
112
121
  * Spot and dividend yield for IV and delta, read from the underlying the way the
113
- * options pane does; missing ones stay missing. `financials` shares one read of
114
- * them with a caller that needs more of them.
122
+ * options pane does; missing ones stay missing. The dividend yield is any the
123
+ * fundamentals state, stale or not, else, as OSA reads it
124
+ * (`loadModelDividendYield`), 0 for a history with no dividend paid.
125
+ * `financials` shares one read of them with a caller that needs more of them.
115
126
  */
116
127
  export async function loadOptionModelInputs(
117
- dataProvider: Pick<DataProvider, "getTickerFinancials">,
128
+ dataProvider: Pick<DataProvider, "getTickerFinancials" | "getCorporateActions">,
118
129
  listing: CliListing,
119
130
  quote: Promise<Quote | null>,
120
131
  refresh: boolean,
@@ -123,10 +134,12 @@ export async function loadOptionModelInputs(
123
134
  const financials = await underlying;
124
135
  const current = [await quote, financials?.quote]
125
136
  .find((candidate) => candidate && candidate.stale !== true && isFiniteNumber(candidate.price) && candidate.price > 0);
126
- const dividendYield = financials?.fundamentals?.dividendYield;
137
+ const { symbol, exchange } = listing.request;
138
+ const dividend = await loadModelDividendYield(financials?.fundamentals?.dividendYield, symbol,
139
+ dataProvider.getCorporateActions ? () => dataProvider.getCorporateActions!(symbol, exchange) : null);
127
140
  return {
128
141
  ...(current ? { spot: current.price, quote: current } : {}),
129
- ...(isFiniteNumber(dividendYield) && dividendYield >= 0 ? { dividendYield } : {}),
142
+ ...(dividend ? { dividendYield: dividend.value } : {}),
130
143
  };
131
144
  }
132
145
 
@@ -180,7 +193,8 @@ export function optionsBasisFootnote(basis: OptionsChainBasis): string {
180
193
  const parts: string[] = [];
181
194
  if (basis.spot) {
182
195
  parts.push(`Greeks at spot ${formatModelSpotPrice(basis.spot.price)} (${modelSpotLabel(basis.spot)}), `
183
- + `dividend yield ${basis.dividendYield == null ? "0%" : `${Number((basis.dividendYield * 100).toFixed(2))}%`}, `
196
+ // An unknown yield is valued as none, and says so rather than read as a stated 0%.
197
+ + `dividend yield ${basis.dividendYield == null ? "unknown (valued at 0%)" : `${Number((basis.dividendYield * 100).toFixed(2))}%`}, `
184
198
  + `rate ${Number((OPTION_MODEL_RATE * 100).toFixed(2))}%`);
185
199
  }
186
200
  const oi = basis.openInterest;
@@ -7,10 +7,18 @@ import {
7
7
  fetchScreenerResult,
8
8
  fetchTrending,
9
9
  MARKET_SUMMARY_SYMBOLS,
10
- rankScreenerQuotes,
11
10
  type ScreenerCategory,
12
11
  } from "../../plugins/builtin/market-movers/screener";
13
- import { formatMoverPrice, moverReferencePrice, moverRegularSession } from "../../plugins/builtin/market-movers/model";
12
+ import {
13
+ formatMoverPrice,
14
+ MOVER_RANK_BASES,
15
+ MOVER_RANKING_LABELS,
16
+ moverReferencePrice,
17
+ moverRegularSession,
18
+ rankMoverQuotes,
19
+ type MoverRankBasis,
20
+ } from "../../plugins/builtin/market-movers/model";
21
+ import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
14
22
  import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
15
23
  import {
16
24
  COUNTRY_CYCLE,
@@ -111,7 +119,16 @@ const MOVER_LISTS: Record<string, ScreenerCategory | "trending"> = {
111
119
  "most-active": "most_actives",
112
120
  trending: "trending",
113
121
  };
114
- const MOVERS_USAGE = "movers [gainers|losers|active|trending]";
122
+ const MOVERS_USAGE = "movers [gainers|losers|active|trending] [--session regular|extended]";
123
+ /** The most rows the movers list serves in one request. */
124
+ const MOVER_REQUEST_CAP = 100;
125
+
126
+ function moverRankBasis(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): MoverRankBasis {
127
+ if (value == null) return "regular";
128
+ const basis = MOVER_RANK_BASES.find((candidate) => candidate === value.trim().toLowerCase());
129
+ if (!basis) ctx.fail(`Unknown --session "${value}".`, "Use regular or extended.");
130
+ return basis;
131
+ }
115
132
 
116
133
  function screenerCategory(args: readonly string[], ctx: Parameters<CliCommandDef["execute"]>[1]): ScreenerCategory | "trending" {
117
134
  rejectExtraArgs(args, 1, { usage: MOVERS_USAGE, takes: "one list" }, ctx);
@@ -154,9 +171,16 @@ function quoteRows(results: Awaited<ReturnType<NonNullable<import("../../types/d
154
171
  });
155
172
  }
156
173
 
157
- async function runMoverCommand(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
174
+ async function runMoverCommand(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
175
+ const args = [...rawArgs];
176
+ const sessionOption = takeOption(args, "--session");
177
+ const basis = moverRankBasis(sessionOption, ctx);
158
178
  const category = screenerCategory(args, ctx);
159
179
  const limit = ctx.cliOptions.limit ?? 25;
180
+ if (sessionOption != null && (category === "trending" || category === "most_actives")) {
181
+ ctx.fail("--session ranks Gainers and Losers.", category === "trending"
182
+ ? "Trending keeps its source's order." : "Most active ranks by volume.");
183
+ }
160
184
  if (category === "trending") {
161
185
  await withCliServices(ctx, async (services) => {
162
186
  const trending = await fetchTrending(limit, undefined, { forceRefresh: ctx.cliOptions.refresh });
@@ -171,11 +195,17 @@ async function runMoverCommand(args: string[], ctx: Parameters<CliCommandDef["ex
171
195
  return;
172
196
  }
173
197
 
174
- const screener = await fetchScreenerResult(category, limit, undefined, { forceRefresh: ctx.cliOptions.refresh });
175
- // Ranked by the live print, as the list is; then Last and Chg% are the regular
176
- // session, as `ticker` reads it, and a pre-market or after-hours print is its own column.
177
- const rows = rankScreenerQuotes(category, screener.data).map((quote) => {
178
- const { extended, ...row } = moverRegularSession(quote);
198
+ // After the close a price list keeps only the rows moving its way on the basis
199
+ // asked for, so it reads more candidates than it shows.
200
+ const count = category === "most_actives" ? limit : Math.min(MOVER_REQUEST_CAP, Math.max(limit, MOVER_CANDIDATES));
201
+ const screener = await fetchScreenerResult(category, count, undefined, { forceRefresh: ctx.cliOptions.refresh, session: basis });
202
+ // Last and Chg% are the regular session, as `ticker` reads it, and a
203
+ // pre-market or after-hours print is its own column. The list is ranked by
204
+ // the regular session's move (its close's after the close), or with
205
+ // --session extended by the extended print's move from that close.
206
+ const { quotes, rankedBy } = rankMoverQuotes(category, screener.data.map(moverRegularSession), basis);
207
+ const rows = quotes.slice(0, limit).map((quote) => {
208
+ const { extended, ...row } = quote;
179
209
  return {
180
210
  ...row,
181
211
  extendedSession: extended?.session ?? null,
@@ -184,8 +214,15 @@ async function runMoverCommand(args: string[], ctx: Parameters<CliCommandDef["ex
184
214
  extendedChangePercent: extended?.changePercent ?? null,
185
215
  };
186
216
  });
217
+ const fellBack = basis === "extended" && rankedBy === "regular";
187
218
  // A screener snapshot, as the MOST report reads it: delayed, each row dated by its own last price.
188
- ctx.printResult({ data: rows, freshness: rowsFreshness(rows, { status: "delayed" }, { stale: screener.stale === true }) }, {
219
+ ctx.printResult({
220
+ data: rows,
221
+ metadata: { category, rankedBy },
222
+ ...(fellBack ? { warnings: ["No pre-market or after-hours prints now, so the list is ranked by the regular-session move."] } : {}),
223
+ freshness: rowsFreshness(rows, { status: "delayed" }, { stale: screener.stale === true }),
224
+ }, {
225
+ heading: `Ranked by ${MOVER_RANKING_LABELS[rankedBy]}`,
189
226
  columns: [
190
227
  ...MOVER_COLUMNS.slice(0, 4),
191
228
  ...EXTENDED_COLUMNS,
@@ -428,7 +465,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
428
465
  }),
429
466
  }, {
430
467
  dateKey: "date",
431
- empty: fredEmptyMessage(startDate, latest),
468
+ empty: fredEmptyMessage(startDate, latest == null ? null : exportRow(latest)),
432
469
  ...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
433
470
  exportIdentity: { symbolOrSeries: seriesId, currencyOrUnit: unit, interval },
434
471
  textColumns: [
@@ -486,7 +523,7 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
486
523
  key: "changeBasisPoints",
487
524
  header: "1D bp",
488
525
  align: "right",
489
- format: (value) => typeof value === "number" ? formatBasisPoints(value / 100) : "-",
526
+ format: (value) => typeof value === "number" ? formatBasisPoints(value / 100, 1) : "-",
490
527
  },
491
528
  { key: "date", header: "Date" },
492
529
  { key: "seriesId", header: "Series" },
@@ -557,7 +594,11 @@ export const overviewCliCommands: CliCommandDef[] = [
557
594
  help: {
558
595
  group: CLI_COMMAND_GROUPS.markets,
559
596
  usage: [MOVERS_USAGE],
560
- examples: ["movers", "movers losers --limit 10", "movers trending"],
597
+ options: [{
598
+ flags: "--session <basis>",
599
+ description: "Rank gainers and losers by the regular-session move (regular, the default) or by the pre-market or after-hours move from the close (extended)",
600
+ }],
601
+ examples: ["movers", "movers losers --limit 10", "movers gainers --session extended", "movers trending"],
561
602
  },
562
603
  execute: runMoverCommand,
563
604
  },
@@ -16,7 +16,7 @@ import {
16
16
  type TickerSearchCandidate,
17
17
  } from "../../tickers/search";
18
18
  import { initMarketData, withMarketData } from "../context";
19
- import { loadListingFate } from "../listing-arg";
19
+ import { loadListingFate } from "../../tickers/listing-fate";
20
20
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
21
21
  import { startTickerChangeLookup, tickerChangeLine, type TickerChangeLookup } from "../../tickers/ticker-changes";
22
22
  import { fail } from "../errors";
@@ -4,6 +4,7 @@ import { saveConfig } from "../../data/config/store";
4
4
  import { builtinPluginGroupMembers } from "../../plugins/ownership";
5
5
  import type { AppConfig, TelemetryConfig } from "../../types/config";
6
6
  import { canonicalTimeZone } from "../../utils/utc-time";
7
+ import { NIGHTLY_CHANNEL_SUMMARY, parseUpdateChannel, withUpdateChannel } from "../../updater/channel";
7
8
  import type { CliCommandDef } from "../../types/plugin";
8
9
  import { resolveCliDataDir, withCliServices, withConfigData } from "../context";
9
10
  import { CLI_COMMAND_GROUPS } from "../help";
@@ -33,6 +34,8 @@ const STAR_PROMPT_CONFIG_KEY = "starPrompt.enabled";
33
34
  const TIMEZONE_CONFIG_KEY = "timezone";
34
35
  /** What `config set timezone` takes to go back to UTC alone. */
35
36
  const NO_TIMEZONE = "none";
37
+ /** Stable releases, or nightly builds as well. */
38
+ const UPDATE_CHANNEL_CONFIG_KEY = "updates.channel";
36
39
  const EDITABLE_CONFIG_KEYS = [
37
40
  "baseCurrency",
38
41
  "refreshIntervalMinutes",
@@ -41,6 +44,7 @@ const EDITABLE_CONFIG_KEYS = [
41
44
  ...Object.keys(TELEMETRY_CONFIG_KEYS),
42
45
  STAR_PROMPT_CONFIG_KEY,
43
46
  TIMEZONE_CONFIG_KEY,
47
+ UPDATE_CHANNEL_CONFIG_KEY,
44
48
  ];
45
49
 
46
50
  function isTelemetryConfigKey(key: string): key is TelemetryConfigKey {
@@ -219,6 +223,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
219
223
  "config set telemetry.usage false",
220
224
  "config set starPrompt.enabled false",
221
225
  "config set timezone <Region/City>|none",
226
+ "config set updates.channel stable|nightly",
222
227
  "config get keybindings",
223
228
  "config set keybindings.actions.<action> <keys>|null|default",
224
229
  "config set keybindings.commands.<keys> <command>|null",
@@ -236,6 +241,11 @@ export function createSystemCliCommands(): CliCommandDef[] {
236
241
  lines: [
237
242
  `theme takes one of these ids, or its name as config themes lists it: ${getThemeIds().join(", ")}.`,
238
243
  ],
244
+ }, {
245
+ title: "Release channel",
246
+ lines: [
247
+ `The app updates itself from stable releases. With updates.channel nightly (Release Channel in the command bar) it also installs the nightly build: ${NIGHTLY_CHANNEL_SUMMARY}. Back on stable, a nightly build stays until a newer stable release is out. GLOOMBERB_UPDATE_CHANNEL overrides the setting for one run; npm and Bun installs always follow stable.`,
248
+ ],
239
249
  }],
240
250
  examples: [
241
251
  "config",
@@ -287,6 +297,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
287
297
  "telemetry.attention": context.config.telemetry?.attention === true,
288
298
  [STAR_PROMPT_CONFIG_KEY]: context.config.starPrompt?.enabled !== false,
289
299
  [TIMEZONE_CONFIG_KEY]: context.config.timezone ?? null,
300
+ [UPDATE_CHANNEL_CONFIG_KEY]: context.config.updates?.channel ?? "stable",
290
301
  };
291
302
 
292
303
  if (action === "list") {
@@ -347,11 +358,16 @@ export function createSystemCliCommands(): CliCommandDef[] {
347
358
  if ((isTelemetryConfigKey(key) || key === STAR_PROMPT_CONFIG_KEY) && value !== "true" && value !== "false") {
348
359
  ctx.fail(`Usage: gloomberb config set ${key} true|false`);
349
360
  }
361
+ if (key === UPDATE_CHANNEL_CONFIG_KEY && !parseUpdateChannel(value)) {
362
+ ctx.fail(`Usage: gloomberb config set ${key} stable|nightly`);
363
+ }
350
364
  // A display name may come unquoted: config set theme White Phosphor.
351
365
  const parsedValue = key === "theme"
352
366
  ? requireThemeId(args.slice(2).join(" "))
353
367
  : key === TIMEZONE_CONFIG_KEY
354
368
  ? parseTimezoneSetting(value)
369
+ : key === UPDATE_CHANNEL_CONFIG_KEY
370
+ ? parseUpdateChannel(value)
355
371
  : key === "refreshIntervalMinutes"
356
372
  ? Number(value)
357
373
  : isBooleanConfigKey(key)
@@ -363,7 +379,9 @@ export function createSystemCliCommands(): CliCommandDef[] {
363
379
  ? { ...context.config, starPrompt: { ...context.config.starPrompt, enabled: parsedValue as boolean } }
364
380
  : key === TIMEZONE_CONFIG_KEY
365
381
  ? withTimezone(context.config, parsedValue as string | null)
366
- : { ...context.config, [key]: parsedValue };
382
+ : key === UPDATE_CHANNEL_CONFIG_KEY
383
+ ? withUpdateChannel(context.config, parsedValue as "stable" | "nightly")
384
+ : { ...context.config, [key]: parsedValue };
367
385
  if (!ctx.cliOptions.dryRun) await saveConfig(nextConfig);
368
386
  ctx.printResult({
369
387
  data: {