gloomberb 0.17.5 → 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 (302) hide show
  1. package/package.json +3 -1
  2. package/src/api-client/chat.ts +9 -0
  3. package/src/api-client/commodity-balances.ts +77 -0
  4. package/src/api-client/data.ts +31 -2
  5. package/src/api-client/earnings.ts +10 -0
  6. package/src/api-client/index.ts +5 -0
  7. package/src/api-client/insider-signals.ts +75 -0
  8. package/src/api-client/market-discovery.ts +1 -1
  9. package/src/api-client/normalizers.ts +1 -0
  10. package/src/api-client/paths.ts +9 -0
  11. package/src/api-client/socket.ts +10 -0
  12. package/src/api-client/types.ts +25 -1
  13. package/src/app/runtime/broker-import.ts +23 -21
  14. package/src/app/runtime/plugin-bindings.ts +13 -7
  15. package/src/app/runtime/update.ts +14 -1
  16. package/src/architecture/core-builtin-allowlist.json +2 -10
  17. package/src/brokers/broker-portfolio-sync.ts +66 -0
  18. package/src/brokers/sync-broker-instance.ts +75 -41
  19. package/src/cli/commands/market.ts +101 -34
  20. package/src/cli/commands/options-chain.ts +20 -6
  21. package/src/cli/commands/overview.ts +52 -11
  22. package/src/cli/commands/search.ts +1 -1
  23. package/src/cli/commands/ticker.ts +24 -4
  24. package/src/cli/context.ts +13 -0
  25. package/src/cli/errors.ts +15 -2
  26. package/src/cli/fx-pair.ts +77 -0
  27. package/src/cli/history-rows.ts +25 -11
  28. package/src/cli/index.ts +24 -12
  29. package/src/cli/listing-arg.ts +61 -138
  30. package/src/cli/listing-suggestion.ts +3 -21
  31. package/src/cli/pane-functions/catalog.ts +50 -0
  32. package/src/cli/pane-functions/freshness.ts +44 -9
  33. package/src/cli/pane-functions/function-help.ts +8 -8
  34. package/src/cli/pane-functions/headless.ts +19 -7
  35. package/src/cli/pane-functions/index.ts +63 -5
  36. package/src/cli/pane-functions/resolver.ts +29 -8
  37. package/src/cli/pane-functions/screenshot.ts +2 -0
  38. package/src/cli/report-tables.ts +11 -4
  39. package/src/cli/result.ts +2 -0
  40. package/src/cli/series-search.ts +6 -1
  41. package/src/cli/ticker-resolution.ts +1 -2
  42. package/src/cli/unknown-command.ts +18 -0
  43. package/src/components/chart/composite/composite-chart.tsx +80 -19
  44. package/src/components/chart/composite/format.ts +10 -0
  45. package/src/components/chart/composite/legend.tsx +18 -4
  46. package/src/components/chart/composite/panel-labels.ts +38 -0
  47. package/src/components/chart/composite/panel-series.ts +1 -0
  48. package/src/components/chart/composite/panel-surface.tsx +25 -7
  49. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  50. package/src/components/chart/composite/scene.ts +60 -2
  51. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  52. package/src/components/chart/composite/tools.ts +2 -2
  53. package/src/components/chart/composite/types.ts +28 -0
  54. package/src/components/chart/core/view-reset.ts +46 -0
  55. package/src/components/command-bar/commands/collection.ts +1 -1
  56. package/src/components/command-bar/commands/direct/index.ts +1 -1
  57. package/src/components/command-bar/list/model.ts +7 -0
  58. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  59. package/src/components/command-bar/routes/root/command-items.ts +1 -1
  60. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  61. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  62. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  63. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  64. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  65. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  66. package/src/components/command-bar/surface/index.tsx +5 -0
  67. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  68. package/src/components/command-bar/workflow/ops.ts +4 -2
  69. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  70. package/src/components/command-bar/workflow/tickers.ts +1 -1
  71. package/src/components/layout/pane/content.tsx +11 -8
  72. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  73. package/src/components/onboarding/wizard-model.ts +1 -1
  74. package/src/components/onboarding/wizard-positions.ts +18 -10
  75. package/src/components/ticker/badge/format.ts +64 -15
  76. package/src/components/ticker/badge/index.tsx +32 -11
  77. package/src/components/ticker/badge/list.tsx +4 -3
  78. package/src/components/ticker/badge/text.tsx +2 -2
  79. package/src/components/ticker/list-table-view.tsx +8 -1
  80. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  81. package/src/components/ui/data-table/opentui/model.ts +104 -0
  82. package/src/components/ui/inline-quick-add.tsx +4 -1
  83. package/src/components/ui/query-bar-keys.tsx +29 -5
  84. package/src/components/ui/query-bar.tsx +29 -4
  85. package/src/components/ui/windowed-list.tsx +27 -0
  86. package/src/docs/snapshot.json +248 -5
  87. package/src/i18n/es.ts +1 -1
  88. package/src/i18n/ja.ts +1 -1
  89. package/src/i18n/ko.ts +1 -1
  90. package/src/i18n/zh-cn.ts +1 -1
  91. package/src/i18n/zh-tw.ts +1 -1
  92. package/src/layout-marketplace/cloud.ts +16 -5
  93. package/src/layout-marketplace/gallery.tsx +4 -4
  94. package/src/layout-marketplace/linked-sync.ts +9 -3
  95. package/src/layout-marketplace/linked.ts +24 -3
  96. package/src/layout-marketplace/model.ts +39 -4
  97. package/src/market-data/coordinator/index.ts +34 -1
  98. package/src/market-data/futures-contract.ts +53 -0
  99. package/src/market-data/hooks.tsx +57 -1
  100. package/src/market-data/market/bar-period.ts +71 -0
  101. package/src/market-data/market/format.ts +14 -2
  102. package/src/market-data/market/freshness.ts +28 -1
  103. package/src/market-data/market/status.ts +152 -13
  104. package/src/market-data/model-dividend-yield.ts +76 -0
  105. package/src/market-data/quotes/freshness.ts +30 -1
  106. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  107. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  108. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  109. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  110. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  112. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  113. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  114. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  115. package/src/plugins/builtin/chart-composer/headless.ts +25 -1
  116. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  117. package/src/plugins/builtin/chart-composer/pane.tsx +21 -8
  118. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  119. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  120. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  121. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  122. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  123. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  124. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  125. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  126. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  127. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  128. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  129. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  130. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  131. package/src/plugins/builtin/chat/controller/index.ts +68 -17
  132. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  133. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  134. package/src/plugins/builtin/chat/controller/state.ts +10 -0
  135. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  136. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  137. package/src/plugins/builtin/chat/conversations.ts +12 -5
  138. package/src/plugins/builtin/chat/layout.ts +23 -5
  139. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  140. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  141. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  142. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  143. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  144. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  145. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  146. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  147. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  148. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  149. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  150. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  151. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  152. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  153. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  154. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  155. package/src/plugins/builtin/earnings/board.tsx +36 -3
  156. package/src/plugins/builtin/earnings/headless.ts +24 -5
  157. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  158. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  159. package/src/plugins/builtin/earnings/history.tsx +40 -10
  160. package/src/plugins/builtin/earnings/index.tsx +3 -1
  161. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  162. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  163. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  164. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  165. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  166. package/src/plugins/builtin/futures/model.ts +5 -4
  167. package/src/plugins/builtin/futures/series.ts +38 -1
  168. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  169. package/src/plugins/builtin/futures-curve/headless.ts +20 -2
  170. package/src/plugins/builtin/futures-curve/index.tsx +2 -2
  171. package/src/plugins/builtin/futures-curve/model.ts +45 -2
  172. package/src/plugins/builtin/futures-curve/pane.tsx +14 -10
  173. package/src/plugins/builtin/insider/headless.ts +74 -4
  174. package/src/plugins/builtin/insider/index.tsx +99 -5
  175. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  176. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  177. package/src/plugins/builtin/insider/signals.tsx +176 -0
  178. package/src/plugins/builtin/iv-history/client.ts +41 -2
  179. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  180. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  181. package/src/plugins/builtin/jodi/client.ts +42 -205
  182. package/src/plugins/builtin/jodi/headless.ts +13 -4
  183. package/src/plugins/builtin/jodi/model.ts +18 -43
  184. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  185. package/src/plugins/builtin/market-movers/client.ts +32 -6
  186. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  187. package/src/plugins/builtin/market-movers/model.ts +64 -3
  188. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  189. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  190. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  191. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  192. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  193. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  194. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  195. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  196. package/src/plugins/builtin/options-positioning/model.ts +33 -0
  197. package/src/plugins/builtin/options-positioning/pane.tsx +38 -16
  198. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  199. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  200. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  201. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  202. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  203. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  204. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  205. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  206. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  207. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  208. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  209. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  210. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  211. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  212. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  213. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  214. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  215. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  216. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  217. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  218. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  219. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  220. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  221. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  223. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  224. package/src/plugins/builtin/research/event-model.ts +41 -49
  225. package/src/plugins/builtin/research/headless.ts +1 -1
  226. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  227. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  228. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  229. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  230. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  231. package/src/plugins/builtin/shared/history-span.ts +55 -0
  232. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  233. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  234. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  235. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  236. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  237. package/src/plugins/builtin/themes/headless.ts +15 -2
  238. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  239. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  240. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  241. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  242. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  243. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  244. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  245. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  246. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  247. package/src/portfolio/valuation-basis.ts +4 -21
  248. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  249. package/src/remote/controller.ts +27 -7
  250. package/src/remote/schema.ts +25 -9
  251. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  252. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  253. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  254. package/src/renderers/dom/windowed-list.tsx +201 -0
  255. package/src/renderers/dom/windowed-rows.ts +65 -0
  256. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -9
  257. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  258. package/src/renderers/electrobun/bun/desktop/update.ts +21 -6
  259. package/src/renderers/electrobun/shared/protocol.ts +4 -2
  260. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  261. package/src/sources/listing-symbols.ts +8 -4
  262. package/src/state/app/context/index.tsx +13 -0
  263. package/src/state/app/tab-cursor.ts +85 -0
  264. package/src/sync/core-contributors.ts +206 -59
  265. package/src/test-support/config-store.ts +31 -0
  266. package/src/test-support/moved-symbols.ts +64 -0
  267. package/src/theme/colors.ts +30 -0
  268. package/src/tickers/listing-fate.ts +242 -0
  269. package/src/tickers/search/index.ts +45 -3
  270. package/src/tickers/search/ranking.ts +19 -2
  271. package/src/tickers/search/types.ts +3 -0
  272. package/src/tickers/search/upsert.ts +22 -7
  273. package/src/tickers/ticker-changes.ts +26 -9
  274. package/src/time-series/bar-spacing.ts +19 -0
  275. package/src/time-series/chart-data.ts +37 -13
  276. package/src/time-series/field-catalog.ts +5 -2
  277. package/src/time-series/forward-valuation.ts +10 -7
  278. package/src/time-series/fundamentals.ts +49 -11
  279. package/src/time-series/hooks.ts +2 -0
  280. package/src/time-series/live-bars.ts +3 -1
  281. package/src/time-series/parsed-history-cache.ts +2 -0
  282. package/src/time-series/price-units.ts +69 -0
  283. package/src/time-series/resolve.ts +50 -5
  284. package/src/time-series/session-bar.ts +138 -0
  285. package/src/time-series/spec.ts +12 -3
  286. package/src/time-series/studies.ts +211 -12
  287. package/src/time-series/transforms.ts +2 -0
  288. package/src/time-series/types.ts +11 -1
  289. package/src/time-series/valuation-price.ts +13 -0
  290. package/src/types/config.ts +10 -1
  291. package/src/types/financials.ts +2 -0
  292. package/src/types/headless.ts +27 -0
  293. package/src/types/plugin.ts +7 -0
  294. package/src/ui/host.tsx +5 -0
  295. package/src/updater/index.ts +3 -1
  296. package/src/utils/asset-category.ts +20 -0
  297. package/src/utils/decimal-comma.ts +83 -3
  298. package/src/utils/fundamentals.ts +7 -2
  299. package/src/utils/http-proxy-response.ts +66 -9
  300. package/src/utils/plugin-proxy-hosts.json +1 -5
  301. package/src/utils/typo-distance.ts +22 -0
  302. package/src/plugins/builtin/shared/zip-text.ts +0 -46
@@ -3,7 +3,6 @@
3
3
  "src/app.tsx": ["cloud"],
4
4
  "src/app/global-shortcuts.ts": ["help"],
5
5
  "src/app/pane-runtime/plugin-bindings.ts": ["broker-manager","layout-manager"],
6
- "src/app/runtime/broker-import.ts": ["portfolio-list"],
7
6
  "src/app/runtime/desktop-deeplink.ts": ["account-management","chat"],
8
7
  "src/app/runtime/startup.ts": ["chat"],
9
8
  "src/cli/commands/market.ts": ["holders","insider","research","sec"],
@@ -19,21 +18,14 @@
19
18
  "src/cli/pane-functions/resolver.ts": ["chart-composer"],
20
19
  "src/cli/pane-functions/screenshot-evidence.ts": ["attention","awards","catalysts","company-attention","company-kpis","credit-documents","exposure","market-heatmap","options-calculator","options-scenario","power","realized-vol","supply-chain","vol-surface","volatility","world-venue-map"],
21
20
  "src/cli/pane-functions/screenshot.ts": ["chart-composer","econ-statistics","market-valuation","portfolio-list","ticker-detail","time-sales"],
22
- "src/components/command-bar/commands/collection.ts": ["portfolio-list"],
23
- "src/components/command-bar/commands/direct/index.ts": ["broker-manager","portfolio-list"],
24
- "src/components/command-bar/routes/root/command-items.ts": ["portfolio-list"],
21
+ "src/components/command-bar/commands/direct/index.ts": ["broker-manager"],
25
22
  "src/components/command-bar/routes/root/help-items.ts": ["help"],
26
- "src/components/command-bar/routes/root/shortcut-items.ts": ["portfolio-list"],
27
23
  "src/components/command-bar/workflow/builtin.ts": ["portfolio-list"],
28
- "src/components/command-bar/workflow/collection-actions.ts": ["portfolio-list"],
29
24
  "src/components/command-bar/workflow/ops.ts": ["correlation","portfolio-list","ticker-detail"],
30
- "src/components/command-bar/workflow/tickers.ts": ["portfolio-list"],
31
25
  "src/components/form-modal/routes.ts": ["portfolio-list"],
32
26
  "src/components/index.ts": ["portfolio-list"],
33
27
  "src/components/layout/shell/index.tsx": ["cloud"],
34
28
  "src/components/onboarding/use-onboarding-upgrade.ts": ["chat"],
35
- "src/components/onboarding/wizard-model.ts": ["portfolio-list"],
36
- "src/components/onboarding/wizard-positions.ts": ["portfolio-list"],
37
29
  "src/layout/desks.ts": ["chart-composer"],
38
30
  "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
39
31
  "src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
@@ -43,5 +35,5 @@
43
35
  "src/renderers/electrobun/view/cli-pane-shot-entry.tsx": ["econ-statistics","market-valuation","time-sales"],
44
36
  "src/renderers/electrobun/view/main.tsx": ["notes"],
45
37
  "src/renderers/electrobun/view/remote/connection-health.ts": ["connections"],
46
- "src/sync/core-contributors.ts": ["analytics","portfolio-list"]
38
+ "src/sync/core-contributors.ts": ["analytics"]
47
39
  }
@@ -181,10 +181,17 @@ export function clearBrokerInstanceTickerData(
181
181
  brokerPortfolioIds: Set<string>,
182
182
  /** Portfolios this profile just took over: whatever another profile imported there goes too. */
183
183
  takenOverPortfolioIds: ReadonlySet<string> = new Set(),
184
+ /**
185
+ * Portfolios this sync writes. A position there without a profile came from
186
+ * another device's sync of the same account, so this one replaces it too;
187
+ * one entered by hand stays.
188
+ */
189
+ syncedPortfolioIds: ReadonlySet<string> = new Set(),
184
190
  ): TickerRecord | null {
185
191
  const positions = ticker.metadata.positions.filter((position) =>
186
192
  position.brokerInstanceId !== instanceId
187
193
  && !(takenOverPortfolioIds.has(position.portfolio) && position.brokerInstanceId)
194
+ && !(syncedPortfolioIds.has(position.portfolio) && !position.brokerInstanceId && position.broker !== "manual")
188
195
  );
189
196
  const remainingPositionPortfolios = new Set(positions.map((position) => position.portfolio));
190
197
  const portfolios = ticker.metadata.portfolios.filter((portfolioId) =>
@@ -211,6 +218,65 @@ export function clearBrokerInstanceTickerData(
211
218
  };
212
219
  }
213
220
 
221
+ /** The profile a broker portfolio id was made for: `broker:<profile>:<account>`. */
222
+ function brokerInstanceIdOfPortfolio(portfolioId: string): string | undefined {
223
+ return isBrokerPortfolioId(portfolioId) ? portfolioId.split(":")[1] || undefined : undefined;
224
+ }
225
+
226
+ /** The broker portfolios and profiles that still exist somewhere this device knows of. */
227
+ export interface KnownBrokerHoldings {
228
+ portfolioIds: ReadonlySet<string>;
229
+ instanceIds: ReadonlySet<string>;
230
+ }
231
+
232
+ export function knownBrokerHoldings(
233
+ ...sources: Array<{ portfolios?: ReadonlyArray<{ id: string }>; brokerInstances?: ReadonlyArray<{ id: string }> }>
234
+ ): KnownBrokerHoldings {
235
+ return {
236
+ portfolioIds: new Set(sources.flatMap((source) => source.portfolios?.map((portfolio) => portfolio.id) ?? [])),
237
+ instanceIds: new Set(sources.flatMap((source) => source.brokerInstances?.map((instance) => instance.id) ?? [])),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * A broker portfolio that is gone, along with the profile it was made for. One
243
+ * that still has its profile stays: that profile restores it on its next sync.
244
+ */
245
+ function isGoneBrokerPortfolio(portfolioId: string, known: KnownBrokerHoldings): boolean {
246
+ if (!isBrokerPortfolioId(portfolioId) || known.portfolioIds.has(portfolioId)) return false;
247
+ const instanceId = brokerInstanceIdOfPortfolio(portfolioId);
248
+ return !instanceId || !known.instanceIds.has(instanceId);
249
+ }
250
+
251
+ function isLeftoverBrokerPosition(position: TickerPosition, known: KnownBrokerHoldings): boolean {
252
+ return isGoneBrokerPortfolio(position.portfolio, known)
253
+ && !(position.brokerInstanceId && known.instanceIds.has(position.brokerInstanceId));
254
+ }
255
+
256
+ /**
257
+ * The ticker without what removed broker profiles left on it: positions and
258
+ * links in portfolios that are gone with their profile, and contracts of
259
+ * profiles that are gone. A pull from another device brings such positions
260
+ * without the profile that imported them, as a profile never leaves its
261
+ * device, so nothing else ever clears them. Null when there is none.
262
+ */
263
+ export function clearLeftoverBrokerHoldings(ticker: TickerRecord, known: KnownBrokerHoldings): TickerRecord | null {
264
+ const { metadata } = ticker;
265
+ const positions = metadata.positions.filter((position) => !isLeftoverBrokerPosition(position, known));
266
+ const held = new Set(positions.map((position) => position.portfolio));
267
+ const portfolios = metadata.portfolios.filter((portfolioId) => held.has(portfolioId) || !isGoneBrokerPortfolio(portfolioId, known));
268
+ const contracts = metadata.broker_contracts ?? [];
269
+ const brokerContracts = contracts.filter((contract) => !contract.brokerInstanceId || known.instanceIds.has(contract.brokerInstanceId));
270
+ if (
271
+ positions.length === metadata.positions.length
272
+ && portfolios.length === metadata.portfolios.length
273
+ && brokerContracts.length === contracts.length
274
+ ) {
275
+ return null;
276
+ }
277
+ return { ...ticker, metadata: { ...metadata, positions, portfolios, broker_contracts: brokerContracts } };
278
+ }
279
+
214
280
  function inferBrokerAccountId(position: TickerPosition, portfolioId: string, instanceId: string): string | undefined {
215
281
  if (position.brokerAccountId) return position.brokerAccountId;
216
282
  const prefix = `broker:${instanceId}:`;
@@ -7,9 +7,11 @@ import type { TickerRecord } from "../types/ticker";
7
7
  import { getBrokerInstance } from "../utils/broker-instances";
8
8
  import {
9
9
  clearBrokerInstanceTickerData,
10
+ clearLeftoverBrokerHoldings,
10
11
  ensureBrokerPortfolio,
11
12
  findReusableBrokerPortfolioId,
12
13
  isHeldByOtherSignedInProfile,
14
+ knownBrokerHoldings,
13
15
  removeStaleBrokerPortfolios,
14
16
  resolvePortfolioBrokerId,
15
17
  } from "./broker-portfolio-sync";
@@ -33,6 +35,12 @@ export interface SyncBrokerInstanceArgs {
33
35
  deferPersistence?: boolean;
34
36
  }
35
37
 
38
+ /** The app as it is when a sync commits, which may be seconds after the sync read it. */
39
+ interface BrokerSyncCommitTarget {
40
+ tickers: ReadonlyMap<string, TickerRecord>;
41
+ config: Pick<AppConfig, "portfolios" | "brokerInstances">;
42
+ }
43
+
36
44
  export interface SyncBrokerInstanceResult {
37
45
  config: AppConfig;
38
46
  tickers: Map<string, TickerRecord>;
@@ -41,6 +49,15 @@ export interface SyncBrokerInstanceResult {
41
49
  portfolioIds: string[];
42
50
  addedTickers: TickerRecord[];
43
51
  updatedTickers: TickerRecord[];
52
+ /**
53
+ * Carries the sync onto the app as it is now, which may be seconds after the
54
+ * sync read it, so what changed while the broker answered (a pull from
55
+ * another device, an edit) stays; `tickers`, `addedTickers` and
56
+ * `updatedTickers` then hold the sync over it. Hand them to the app before
57
+ * anything waits: a pull landing in a wait would be overwritten in turn.
58
+ */
59
+ rebase(onto: BrokerSyncCommitTarget): void;
60
+ /** Writes the sync's tickers and broker accounts. */
44
61
  commit(): Promise<void>;
45
62
  }
46
63
 
@@ -310,62 +327,78 @@ export async function syncBrokerInstance({
310
327
  : []);
311
328
 
312
329
  nextConfig = removeStaleBrokerPortfolios(nextConfig, instance.id, currentPortfolioIds);
313
- const cleanedTickers = new Map<string, TickerRecord>();
314
- for (const ticker of tickers.values()) {
315
- const cleanedTicker = clearBrokerInstanceTickerData(ticker, instance.id, brokerPortfolioIds, takenOverPortfolioIds);
316
- if (!cleanedTicker) continue;
317
- tickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
318
- cleanedTickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
319
- }
320
-
321
330
  nextConfig = await maybePersistResolvedBrokerConfig(nextConfig, instance, broker, persistResolvedBrokerConfig);
322
331
  throwIfBrokerImportCancelled(signal);
323
332
  nextConfig = markBrokerInstanceSynced(nextConfig, instance.id, syncedAt);
333
+ const syncedConfig = nextConfig;
334
+
335
+ // What the sync does to the tickers, given them: it runs on the tickers it
336
+ // read, and again on the live ones when it commits after the broker answered.
337
+ const writeTickers = (source: ReadonlyMap<string, TickerRecord>, liveConfig?: BrokerSyncCommitTarget["config"]) => {
338
+ const tickers = new Map(source);
339
+ // Only a portfolio and a profile gone everywhere this device knows of make a leftover.
340
+ const known = knownBrokerHoldings(config, syncedConfig, ...(liveConfig ? [liveConfig] : []));
341
+ const cleanedTickers = new Map<string, TickerRecord>();
342
+ for (const ticker of tickers.values()) {
343
+ const cleared = clearBrokerInstanceTickerData(
344
+ ticker, instance.id, brokerPortfolioIds, takenOverPortfolioIds, currentPortfolioIds,
345
+ );
346
+ const cleanedTicker = clearLeftoverBrokerHoldings(cleared ?? ticker, known) ?? cleared;
347
+ if (!cleanedTicker) continue;
348
+ tickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
349
+ cleanedTickers.set(cleanedTicker.metadata.ticker, cleanedTicker);
350
+ }
324
351
 
325
- const addedTickers = new Map<string, TickerRecord>();
326
- const updatedTickers = new Map<string, TickerRecord>();
352
+ const addedTickers = new Map<string, TickerRecord>();
353
+ const updatedTickers = new Map<string, TickerRecord>();
354
+
355
+ for (const position of positions) {
356
+ throwIfBrokerImportCancelled(signal);
357
+ const portfolioId = findReusableBrokerPortfolioId(syncedConfig, instance, position.accountId, portfolioBrokerId);
358
+ if (heldElsewhere.has(portfolioId)) continue;
359
+ const { ticker, created } = upsertBrokerPositionTicker({
360
+ tickers,
361
+ instance,
362
+ portfolioId,
363
+ position,
364
+ });
365
+ if (created) {
366
+ addedTickers.set(position.ticker, ticker);
367
+ } else {
368
+ cleanedTickers.delete(ticker.metadata.ticker);
369
+ if (!addedTickers.has(position.ticker)) {
370
+ updatedTickers.set(position.ticker, ticker);
371
+ }
372
+ }
327
373
 
328
- for (const position of positions) {
329
- throwIfBrokerImportCancelled(signal);
330
- const portfolioId = findReusableBrokerPortfolioId(nextConfig, instance, position.accountId, portfolioBrokerId);
331
- if (heldElsewhere.has(portfolioId)) continue;
332
- const { ticker, created } = upsertBrokerPositionTicker({
333
- tickers,
334
- instance,
335
- portfolioId,
336
- position,
337
- });
338
- if (created) {
339
- addedTickers.set(position.ticker, ticker);
340
- } else {
341
- cleanedTickers.delete(ticker.metadata.ticker);
342
- if (!addedTickers.has(position.ticker)) {
374
+ tickers.set(position.ticker, ticker);
375
+ if (addedTickers.has(position.ticker)) {
376
+ addedTickers.set(position.ticker, ticker);
377
+ } else {
343
378
  updatedTickers.set(position.ticker, ticker);
344
379
  }
345
380
  }
346
381
 
347
- tickers.set(position.ticker, ticker);
348
- if (addedTickers.has(position.ticker)) {
349
- addedTickers.set(position.ticker, ticker);
350
- } else {
351
- updatedTickers.set(position.ticker, ticker);
352
- }
353
- }
354
-
355
- for (const ticker of cleanedTickers.values()) {
356
- if (!addedTickers.has(ticker.metadata.ticker) && !updatedTickers.has(ticker.metadata.ticker)) {
357
- updatedTickers.set(ticker.metadata.ticker, ticker);
382
+ for (const ticker of cleanedTickers.values()) {
383
+ if (!addedTickers.has(ticker.metadata.ticker) && !updatedTickers.has(ticker.metadata.ticker)) {
384
+ updatedTickers.set(ticker.metadata.ticker, ticker);
385
+ }
358
386
  }
359
- }
387
+ return { tickers, addedTickers: [...addedTickers.values()], updatedTickers: [...updatedTickers.values()] };
388
+ };
360
389
 
390
+ let written = writeTickers(tickers);
361
391
  let committed = false;
392
+ const rebase = (onto: BrokerSyncCommitTarget) => {
393
+ if (!committed) written = writeTickers(onto.tickers, onto.config);
394
+ };
362
395
  const commit = async () => {
363
396
  if (committed) return;
364
397
  throwIfBrokerImportCancelled(signal);
365
398
  if (resources) {
366
399
  persistBrokerAccounts(resources, instance, broker, brokerAccounts);
367
400
  }
368
- await Promise.all([...addedTickers.values(), ...updatedTickers.values()].map((ticker) => (
401
+ await Promise.all([...written.addedTickers, ...written.updatedTickers].map((ticker) => (
369
402
  tickerRepository.saveTicker(ticker)
370
403
  )));
371
404
  committed = true;
@@ -377,12 +410,13 @@ export async function syncBrokerInstance({
377
410
 
378
411
  return {
379
412
  config: nextConfig,
380
- tickers,
413
+ get tickers() { return written.tickers; },
381
414
  brokerAccounts,
382
415
  positions,
383
416
  portfolioIds,
384
- addedTickers: [...addedTickers.values()],
385
- updatedTickers: [...updatedTickers.values()],
417
+ get addedTickers() { return written.addedTickers; },
418
+ get updatedTickers() { return written.updatedTickers; },
419
+ rebase,
386
420
  commit,
387
421
  };
388
422
  }
@@ -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
  },