gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -8,11 +8,12 @@ import { describeFundamentalMarketCap, marketCapShareBasisNote, selectMarketCapi
8
8
  import {
9
9
  formatCompact,
10
10
  formatCurrency,
11
+ formatCurrencyCode,
11
12
  formatNumber,
12
13
  formatPercent,
13
14
  formatPercentRaw,
14
15
  } from "../../utils/format";
15
- import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity, formatMarketChangeWithCurrency, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../market-data/market/format";
16
+ import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../market-data/market/format";
16
17
  import {
17
18
  cliStyles,
18
19
  cliTerminalWidth,
@@ -28,14 +29,17 @@ import {
28
29
  getExtendedSessionDisplay,
29
30
  getQuoteSessionFields,
30
31
  getRegularSessionDisplay,
32
+ quoteBookSessionLabel,
31
33
  quoteHeadlineTime,
32
34
  quoteMarketStateLabel,
33
35
  } from "../../market-data/market/status";
34
36
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
35
- import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms } from "../../market-data/futures-contract";
37
+ import { changeBasisJson, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
38
+ import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms, minorUnitPricePer } from "../../market-data/futures-contract";
39
+ import { currencyCodeLabel, heldCurrencyUnit } from "../../utils/currency-units";
40
+ import { quotedFxPair } from "../../utils/currency-pair";
36
41
  import type { AppConfig } from "../../types/config";
37
42
  import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
38
- import { quoteTradingDay } from "../../market-data/quotes/day-range";
39
43
  import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
40
44
  import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
41
45
  import { instrumentTypeLabel, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
@@ -46,7 +50,8 @@ import type { TickerRecord } from "../../types/ticker";
46
50
  import type { CliCommandContext } from "../../types/plugin";
47
51
  import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
48
52
  import { isFuturesTicker } from "../../portfolio/futures-positions";
49
- import { createBaseConverter } from "../base-converter";
53
+ import { createBaseConverter, type BaseConversion } from "../base-converter";
54
+ import { formatFxRate } from "../fx-pair";
50
55
  import { initMarketData, withMarketData } from "../context";
51
56
  import { fail } from "../errors";
52
57
  import type { MarketContext } from "../types";
@@ -58,6 +63,7 @@ import {
58
63
  formatSignedCurrency,
59
64
  formatTimestamp,
60
65
  formatWatchlistNames,
66
+ quoteChangeText,
61
67
  } from "../helpers";
62
68
  import { NotesFiles } from "../../plugins/builtin/notes/files";
63
69
  import { isKnownNonUsListing, secFilingItemCodes } from "../../utils/sec";
@@ -68,6 +74,7 @@ import {
68
74
  resolveCliListing, type CliListing, type ListingDataRequest,
69
75
  } from "../listing-arg";
70
76
  import { movedAwayChange } from "../../tickers/ticker-changes";
77
+ import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
71
78
  import { isNotATickerMessage } from "../not-a-ticker";
72
79
  import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
73
80
  import { cliFreshnessFooter } from "../result";
@@ -106,10 +113,6 @@ async function loadRecentSecFilings(
106
113
  // Prose such as a company description stays readable on wide terminals.
107
114
  const MAX_PROSE_WIDTH = 100;
108
115
  const METADATA_SEPARATOR = " · ";
109
- const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
110
- const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
111
- /** The states the header reads as CLOSED: the venue is between sessions, so nobody is quoting. */
112
- const CLOSED_MARKET_STATES = new Set<string>(["CLOSED", "PREPRE", "POSTPOST"]);
113
116
 
114
117
  interface TickerCommandDependencies {
115
118
  /** `--exchange`: the listing, for a symbol that trades in several places. */
@@ -149,25 +152,22 @@ function appendMetricSection(lines: string[], title: string, metrics: Array<[str
149
152
  }
150
153
 
151
154
  /**
152
- * A closed market's bid and ask are the last quote of its session, not an open book (ITA's $202.00 against
153
- * $208.74 around a $206.65 close): say so, with the day the quote is from.
155
+ * Out of the regular session the bid and ask say whose book they are: the pre-market or after-hours book beside
156
+ * Last and the extended print, or a closed market's last quote with its day (ITA's $202.00 against $208.74
157
+ * around a $206.65 close is no open book).
154
158
  */
155
159
  function bidAskText(quote: Quote, options: MarketFormatOptions): string {
156
160
  const text = formatBidAsk(quote.bid, quote.ask, quote.bidSize, quote.askSize, quote.currency, options);
157
- if (text === "—" || !quote.marketState || !CLOSED_MARKET_STATES.has(quote.marketState)) return text;
158
- const day = quoteTradingDay(quote);
159
- const date = day ? new Date(`${day}T00:00:00Z`) : null;
160
- const label = date && Number.isFinite(date.getTime())
161
- ? `, ${WEEKDAYS[date.getUTCDay()]} ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]}` : "";
162
- return `${text} ${cliStyles.muted(`(last quote${label})`)}`;
161
+ const label = text === "—" ? null : quoteBookSessionLabel(quote);
162
+ return label ? `${text} ${cliStyles.muted(`(${label})`)}` : text;
163
163
  }
164
164
 
165
165
  function wrapProse(text: string): string {
166
166
  return wrapText(text, Math.min(cliTerminalWidth() ?? MAX_PROSE_WIDTH, MAX_PROSE_WIDTH)).join("\n");
167
167
  }
168
168
 
169
- function buildStatementMetrics(statement: FinancialStatement, currency?: string): Array<[string, string]> {
170
- const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare);
169
+ function buildStatementMetrics(statement: FinancialStatement, currency?: string, codes = false): Array<[string, string]> {
170
+ const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare, codes);
171
171
  return [
172
172
  ["Revenue", money(statement.totalRevenue)],
173
173
  ["Gross Profit", money(statement.grossProfit)],
@@ -303,8 +303,11 @@ function shouldFetchSecFilings(
303
303
  }
304
304
 
305
305
  async function appendTickerPositions(lines: string[], tickerFile: TickerRecord | null, quote: TickerFinancials["quote"],
306
- config: AppConfig, toBase: (value: number, currency: string) => Promise<number>): Promise<void> {
306
+ config: AppConfig, toBase: ReportBaseConverter, rates: ReportRateNotes): Promise<void> {
307
307
  const quoteCurrency = quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "";
308
+ const codes = writesCurrencyCodes(config);
309
+ const money = codes ? formatCurrencyCode : formatCurrency;
310
+ const converted = new Set<string>();
308
311
  if (tickerFile && tickerFile.metadata.positions.length > 0) {
309
312
  lines.push("");
310
313
  lines.push(renderSection("Positions"));
@@ -319,11 +322,14 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
319
322
  const currentPrice = getPortfolioQuoteDisplay(metrics, quote)?.price ?? null;
320
323
  const currentPriceBase = currentPrice != null && quoteCurrency ? await toBase(currentPrice, quoteCurrency) : null;
321
324
  const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
325
+ if (positionCurrency) converted.add(positionCurrency.toUpperCase());
326
+ if (quoteCurrency && currentPrice != null) converted.add(quoteCurrency.toUpperCase());
322
327
  const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
323
328
  { currency: config.baseCurrency, convert: value => value * positionRate }, quote);
324
- // A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
329
+ // A cost bought at an entered rate into the base currency converts at it, as its P&L does, and says so; any other at today's.
330
+ const boughtRate = positionCurrency ? lotCostFxRate(position, positionCurrency, config.baseCurrency) : null;
325
331
  const costBasisBase = !positionCurrency ? Number.NaN
326
- : lotCostFxRate(position, positionCurrency, config.baseCurrency) !== null ? baseMetrics.signedCost
332
+ : boughtRate !== null ? baseMetrics.signedCost
327
333
  : await toBase(metrics.signedCost, positionCurrency);
328
334
  const marketValueBase = resolvePortfolioMarketValue(baseMetrics, currentPriceBase)?.net ?? Number.NaN;
329
335
  const selectedPnl = resolvePortfolioPositionPnl(baseMetrics, currentPriceBase);
@@ -334,26 +340,79 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
334
340
  const stats: CliStatEntry[] = [
335
341
  [
336
342
  "Position",
337
- `${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"}`,
343
+ `${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"}`,
338
344
  ],
339
- ["Cost Basis", formatCurrency(costBasisBase, config.baseCurrency)],
340
- ["Market Value", formatCurrency(marketValueBase, config.baseCurrency)],
345
+ ["Cost Basis", `${money(costBasisBase, config.baseCurrency)}${boughtRate !== null
346
+ ? ` (${formatFxRate(boughtRate)} ${config.baseCurrency.trim().toUpperCase()} per ${positionCurrency} when bought)` : ""}`],
347
+ ["Market Value", money(marketValueBase, config.baseCurrency)],
341
348
  [
342
349
  selectedPnl.basis === "broker-snapshot" ? "Broker P&L" : "P&L",
343
- pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency), pnl),
350
+ pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency, codes), pnl),
344
351
  ],
345
352
  ];
346
353
  if (position.markPrice != null) {
347
- stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"]);
354
+ stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"]);
348
355
  }
349
356
  lines.push(renderStats(stats));
350
357
  if (index < positions.length - 1) {
351
358
  lines.push("");
352
359
  }
353
360
  }
361
+ for (const note of conversionNotes(toBase, converted, rates)) lines.push(cliStyles.muted(note));
354
362
  }
355
363
  }
356
364
 
365
+ /** The ticker report's converter: what `createBaseConverter` makes, whose rates a report states; a plain function in tests. */
366
+ type ReportBaseConverter = ((value: number, fromCurrency: string) => Promise<number>) & { conversions?(): BaseConversion[] };
367
+
368
+ /**
369
+ * A base currency other than USD reads every amount with its code (259.36
370
+ * USD, 878.15B CAD): $ alone is also the Canadian, Australian and other
371
+ * dollars. A USD base keeps the signs it always had.
372
+ */
373
+ export function writesCurrencyCodes(config: Pick<AppConfig, "baseCurrency"> | undefined): boolean {
374
+ const base = config?.baseCurrency?.trim().toUpperCase();
375
+ return !!base && base !== "USD";
376
+ }
377
+
378
+ /**
379
+ * What the report's rate notes have said and must say: each currency's rate
380
+ * is stated once, under the first section that converted it, and a lot whose
381
+ * cost converts at the rate entered when it was bought is left out of it.
382
+ */
383
+ interface ReportRateNotes {
384
+ /** Currencies whose rate a note already gave. */
385
+ noted: Set<string>;
386
+ /** Currencies of lots whose Cost Basis is at their own entered rate. */
387
+ boughtAt: ReadonlySet<string>;
388
+ }
389
+
390
+ /** The currencies of the shown lots whose cost converts into the base currency at the rate they were bought at. */
391
+ function boughtRateCurrencies(tickerFile: TickerRecord | null, quoteCurrency: string, baseCurrency: string): Set<string> {
392
+ return new Set((tickerFile?.metadata.positions ?? []).flatMap((position) => {
393
+ const currency = (position.currency || quoteCurrency).trim();
394
+ return position.shares !== 0 && currency && lotCostFxRate(position, currency, baseCurrency) !== null ? [currency.toUpperCase()] : [];
395
+ }));
396
+ }
397
+
398
+ /**
399
+ * "CAD figures at USD/CAD 1.40623, as of 2026-10-21 04:46 UTC.": the rate
400
+ * and its date for each currency in `currencies` the report converted into
401
+ * the base currency, as markets quote the pair (USD/JPY, not JPY/USD), so a
402
+ * converted figure is never without what it was converted at. A rate already
403
+ * stated is not repeated, and a cost basis at its own rate is excepted.
404
+ */
405
+ function conversionNotes(toBase: ReportBaseConverter, currencies: ReadonlySet<string>, rates: ReportRateNotes): string[] {
406
+ return (toBase.conversions?.() ?? [])
407
+ .filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency && !rates.noted.has(conversion.currency))
408
+ .map(({ currency, baseCurrency, rate, asOf }) => {
409
+ rates.noted.add(currency);
410
+ const quoted = quotedFxPair(currency, baseCurrency, rate);
411
+ const except = rates.boughtAt.has(currency) ? ", except a cost basis at the rate it was bought at" : "";
412
+ return `${baseCurrency} figures at ${quoted.pair} ${formatFxRate(quoted.rate)}${asOf != null ? `, as of ${formatTimestamp(asOf)}` : ""}${except}.`;
413
+ });
414
+ }
415
+
357
416
  /** A source's enterprise value is in the units of the capitalization it reports beside it,
358
417
  * which without a declared unit is the listing's quote currency (as the overview labels it). */
359
418
  function enterpriseValueCurrency(
@@ -378,9 +437,9 @@ interface FundamentalsMetric {
378
437
 
379
438
  type MetricFigure = Omit<FundamentalsMetric, "label">;
380
439
 
381
- /** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. */
382
- function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false): MetricFigure {
383
- return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
440
+ /** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. `codes`: per share too (6.54 USD). */
441
+ function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false, codes = false): MetricFigure {
442
+ return { text: formatReportedMoney(value, currency, perShare, codes), value, unit: currency?.trim() || "ccy?" };
384
443
  }
385
444
 
386
445
  /** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
@@ -570,6 +629,7 @@ function fundamentalsMetrics(
570
629
  enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
571
630
  distributions: DividendDistributions | null = null,
572
631
  bond: FundBondFacts | null = null,
632
+ codes = false,
573
633
  ): FundamentalsMetric[] {
574
634
  const signed = (value: number) => colorBySign(formatPercent(value), value);
575
635
  const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
@@ -581,7 +641,7 @@ function fundamentalsMetrics(
581
641
  { label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
582
642
  { label: "PEG", ...multipleFigure(fundamentals?.pegRatio) },
583
643
  // The flows and EPS are one trailing-twelve-month block, the same twelve months for each line.
584
- { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
644
+ { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true, codes) },
585
645
  {
586
646
  label: dividendYieldLabel(fundamentals?.dividendYieldBasis, bond),
587
647
  ...forwardYieldFigure(fundamentals, distributions),
@@ -684,6 +744,8 @@ type FundamentalsReportData = TickerFinancials & {
684
744
  bank?: BankFiguresPayload | null;
685
745
  /** In place of `bank`, for a bank whose figures could not be read. */
686
746
  bankNote?: string | null;
747
+ /** A base currency other than USD: every amount reads with its code (`writesCurrencyCodes`). */
748
+ currencyCodes?: boolean;
687
749
  };
688
750
 
689
751
  /** "Latest quarter 2026-06-30, 10-Q filed 2026-08-06." and the newer quarter's results release, for the lines under a report's figures. */
@@ -723,7 +785,10 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
723
785
  const quote = financials.quote;
724
786
  const session = getRegularSessionDisplay(quote);
725
787
  if (!quote || !session) return [];
726
- const options = withCurrencyMinorDigits(quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType), quote.currency);
788
+ const options = withCurrencyMinorDigits({
789
+ ...quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType),
790
+ ...financials.currencyCodes ? { currencyCode: true } : {},
791
+ }, quote.currency);
727
792
  const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
728
793
  const extended = getExtendedSessionDisplay(quote);
729
794
  const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
@@ -732,7 +797,8 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
732
797
  session.changePercent != null && Number.isFinite(session.changePercent)
733
798
  ? {
734
799
  label: "Change",
735
- text: colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, options, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change),
800
+ // With what the move is measured from, as `ticker` and `quote` say it: "vs Fri 16 Oct close".
801
+ text: quoteChangeText(quote, session, options),
736
802
  value: session.changePercent,
737
803
  unit: "%",
738
804
  }
@@ -767,7 +833,7 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
767
833
  ? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
768
834
  : { text: "—" };
769
835
  const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
770
- undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null);
836
+ undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null, snapshot.currencyCodes === true);
771
837
  if (view === "fundamentals") {
772
838
  return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
773
839
  .filter((metric) => metric.text !== "—");
@@ -912,8 +978,11 @@ export async function buildTickerReport({
912
978
  distributions = null,
913
979
  statementFilings = null,
914
980
  bondFund = null,
981
+ memberships,
915
982
  }: {
916
983
  symbol: string;
984
+ /** The lists holding the company, by any of its listings (loadCompanyMemberships); the saved listing's own lists when absent. */
985
+ memberships?: CompanyMemberships;
917
986
  /** The filings behind the latest statements, and a newer quarter's results release. */
918
987
  statementFilings?: StatementFilingsReport | null;
919
988
  /** A bond fund's SEC yield, yield to worst and duration (loadReportBondFacts). */
@@ -927,7 +996,7 @@ export async function buildTickerReport({
927
996
  tickerFile: TickerRecord | null;
928
997
  financials: TickerFinancials;
929
998
  config: AppConfig;
930
- toBase: (value: number, fromCurrency: string) => Promise<number>;
999
+ toBase: ReportBaseConverter;
931
1000
  notes?: string;
932
1001
  /** Why there is no quote, when a source said; "Quote unavailable." otherwise. */
933
1002
  quoteNote?: string;
@@ -944,8 +1013,14 @@ export async function buildTickerReport({
944
1013
  const profile = financials.profile;
945
1014
  const name = quote?.name || tickerFile?.metadata.name || symbol;
946
1015
  const baseQuoteOptions = quoteFormatOptions(quote, tickerFile?.metadata.assetCategory, financials.quoteMetadata?.instrumentType);
1016
+ const baseCurrency = config.baseCurrency.trim().toUpperCase();
1017
+ const codes = writesCurrencyCodes(config);
1018
+ const rates: ReportRateNotes = {
1019
+ noted: new Set(),
1020
+ boughtAt: boughtRateCurrencies(tickerFile, quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "", config.baseCurrency),
1021
+ };
947
1022
  // Pad money prices to the currency's minor unit so a range reads £35.10 - £35.485, never past it (JPY has none).
948
- const quoteOptions = withCurrencyMinorDigits(baseQuoteOptions, quote?.currency);
1023
+ const quoteOptions = withCurrencyMinorDigits(codes ? { ...baseQuoteOptions, currencyCode: true } : baseQuoteOptions, quote?.currency);
949
1024
  const lines: string[] = [];
950
1025
 
951
1026
  lines.push(`${cliStyles.accent(quote?.symbol ?? symbol)} ${cliStyles.bold(name)}`);
@@ -956,9 +1031,10 @@ export async function buildTickerReport({
956
1031
  const indexLevel = isIndexLevel(quote?.instrumentType?.trim() || financials.quoteMetadata?.instrumentType?.trim() || tickerFile?.metadata.assetCategory);
957
1032
  const summaryParts = [
958
1033
  listingVenueLabel(listingExchange, quote, financials, tickerFile) || undefined,
959
- indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${quoteCurrency}` : undefined,
1034
+ indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${currencyCodeLabel(quoteCurrency)}` : undefined,
960
1035
  quoteMarketStateLabel(quote) ?? undefined,
961
- quote?.dataSource ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
1036
+ // A listing that no longer trades has no delay to speak of: its quote is its last session's close.
1037
+ quote?.dataSource && !quote.delisting ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
962
1038
  ].filter((part): part is string => !!part);
963
1039
  if (summaryParts.length > 0) {
964
1040
  lines.push(cliStyles.muted(summaryParts.join(METADATA_SEPARATOR)));
@@ -977,14 +1053,15 @@ export async function buildTickerReport({
977
1053
  lines.push(cliStyles.muted(metadataParts.join(METADATA_SEPARATOR)));
978
1054
  }
979
1055
 
980
- const portfolioNames = tickerFile ? formatPortfolioNames(config, tickerFile.metadata.portfolios) : [];
981
- const watchlistNames = tickerFile ? formatWatchlistNames(config, tickerFile.metadata.watchlists) : [];
1056
+ // A list that holds the company as another of its listings says which: "Watchlists R (as RCI.B:TSX)".
1057
+ const lists = memberships ?? companyMemberships(config, tickerFile, [], null);
1058
+ const listNames = (held: readonly ListMembership[]) => held.map((list) => list.via ? `${list.name} (as ${list.via})` : list.name).join(", ");
982
1059
  const membershipParts = [
983
- portfolioNames.length > 0
984
- ? `Portfolios ${portfolioNames.join(", ")}`
1060
+ lists.portfolios.length > 0
1061
+ ? `Portfolios ${listNames(lists.portfolios)}`
985
1062
  : undefined,
986
- watchlistNames.length > 0
987
- ? `Watchlists ${watchlistNames.join(", ")}`
1063
+ lists.watchlists.length > 0
1064
+ ? `Watchlists ${listNames(lists.watchlists)}`
988
1065
  : undefined,
989
1066
  ].filter((part): part is string => !!part);
990
1067
  if (membershipParts.length > 0) {
@@ -994,21 +1071,27 @@ export async function buildTickerReport({
994
1071
  // Market cap and the multiples are measured at the regular session price the Quote section heads with.
995
1072
  const priced = atRegularSessionPrice(financials);
996
1073
  const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
997
- const convertedMarketCap = capitalization ? await toBase(capitalization.value, capitalization.currency) : Number.NaN;
1074
+ // Market cap and EV read in the currency they are reported in, as the revenue and income beside them do,
1075
+ // with the base currency's figure after them when it differs: 627.2B USD (878.15B CAD).
1076
+ const convertedCurrencies = new Set<string>();
1077
+ const inBase = async (value: number, currency: string): Promise<string> => {
1078
+ // A minor unit converts as the hundredth of its currency it is: 2.75T GBp is 27.5B GBP, never 2.75T GBP.
1079
+ const unit = heldCurrencyUnit(currency);
1080
+ if (!unit.currency || (unit.currency === baseCurrency && unit.divisor === 1)) return "";
1081
+ const major = value / unit.divisor;
1082
+ const converted = unit.currency === baseCurrency ? major : await toBase(major, unit.currency);
1083
+ if (!Number.isFinite(converted)) return "";
1084
+ if (unit.currency !== baseCurrency) convertedCurrencies.add(unit.currency);
1085
+ return ` (${formatCompact(converted)} ${baseCurrency})`;
1086
+ };
998
1087
  const marketCapText = capitalization
999
- ? Number.isFinite(convertedMarketCap)
1000
- ? `${formatCompact(convertedMarketCap)} ${config.baseCurrency}`
1001
- : `${formatCompact(capitalization.value)} ${capitalization.currency}`
1088
+ ? `${formatCompact(capitalization.value)} ${capitalization.currency}${await inBase(capitalization.value, capitalization.currency)}`
1002
1089
  : "—";
1003
1090
  // Shown in the same currency as the market cap, so the two can be compared.
1004
1091
  const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
1005
1092
  const evCurrency = enterpriseValueCurrency(quote, fundamentals);
1006
- // A minor unit such as GBp stays as reported: the converter would read it as the major currency.
1007
- const convertedEnterpriseValue = enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
1008
- ? await toBase(enterpriseValue, evCurrency) : Number.NaN;
1009
- const enterpriseValueText = Number.isFinite(convertedEnterpriseValue)
1010
- ? `${formatCompact(convertedEnterpriseValue)} ${config.baseCurrency}`
1011
- : formatReportedMoney(enterpriseValue, evCurrency);
1093
+ const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency
1094
+ ? await inBase(enterpriseValue, evCurrency) : ""}`;
1012
1095
 
1013
1096
  if (quote) {
1014
1097
  // As the Overview reads it: the regular session, then the extended-hours print against its close.
@@ -1023,13 +1106,16 @@ export async function buildTickerReport({
1023
1106
  const volume = reportedQuoteVolume(quote);
1024
1107
  // A futures price is its latest trade; the exchange settlement is its own row, dated by its session.
1025
1108
  appendMetricSection(lines, "Quote", [
1026
- ["Last", colorBySign(formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions), session.change)],
1109
+ // A price in cents says what it is per: 314.95¢/lb.
1110
+ ["Last", colorBySign(`${formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions)}${minorUnitPricePer(futuresContract, quote.currency)}`, session.change)],
1027
1111
  ...futuresContract
1028
1112
  ? [["Settle", futuresSettleText(futuresContract, quote)] as [string, string]]
1029
1113
  : [],
1030
- ["Change", colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, quoteOptions, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change)],
1114
+ // The move says what it is measured from: "vs Fri 16 Oct close", a future's "vs Fri 16 Oct settle".
1115
+ ["Change", quoteChangeText(quote, session, quoteOptions, futuresContract)],
1031
1116
  ["Open", quote.open != null ? formatMarketPriceWithCurrency(quote.open, quote.currency, quoteOptions) : "—"],
1032
- ["Day Range", quote.low != null || quote.high != null
1117
+ // A listing that no longer trades: its last session's range, never today's.
1118
+ [quote.delisting ? "Last Session" : "Day Range", quote.low != null || quote.high != null
1033
1119
  ? formatPriceRange(quote.low, quote.high, quote.currency, quoteOptions)
1034
1120
  : "—"],
1035
1121
  ["52W Range", quote.low52w != null || quote.high52w != null
@@ -1050,10 +1136,11 @@ export async function buildTickerReport({
1050
1136
  }
1051
1137
 
1052
1138
  const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
1053
- { text: enterpriseValueText }, distributions, bondFund);
1139
+ { text: enterpriseValueText }, distributions, bondFund, codes);
1054
1140
  const beforeFundamentals = lines.length;
1055
1141
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
1056
1142
  for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
1143
+ if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies, rates)) lines.push(cliStyles.muted(note));
1057
1144
  // Where a fund's facts would go: under its fundamentals, or in their place.
1058
1145
  const fund = fundFactsNote(quote?.symbol ?? symbol,
1059
1146
  quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
@@ -1071,13 +1158,13 @@ export async function buildTickerReport({
1071
1158
  const latestAnnual = latestFinancialPeriod(financials.annualStatements, row => row.date);
1072
1159
  if (latestAnnual) {
1073
1160
  appendMetricSection(lines, `Latest Annual (${statementPeriodText(latestAnnual.date, statementFilings?.latestAnnual ?? null)})`,
1074
- buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual)));
1161
+ buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual), codes));
1075
1162
  }
1076
1163
 
1077
1164
  const latestQuarter = latestFinancialPeriod(financials.quarterlyStatements, row => row.date);
1078
1165
  if (latestQuarter) {
1079
1166
  appendMetricSection(lines, `Latest Quarter (${statementPeriodText(latestQuarter.date, statementFilings?.latestQuarter ?? null)})`,
1080
- buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter)));
1167
+ buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter), codes));
1081
1168
  }
1082
1169
  // The statements stop a quarter short of the results the company has released.
1083
1170
  if (statementFilings?.resultsRelease && (latestAnnual || latestQuarter)) lines.push(cliStyles.muted(resultsReleaseText(statementFilings.resultsRelease)));
@@ -1113,7 +1200,7 @@ export async function buildTickerReport({
1113
1200
  link: filing.filingUrl,
1114
1201
  })), hiddenFilingsNote(hiddenSecFilings));
1115
1202
 
1116
- await appendTickerPositions(lines, tickerFile, quote, config, toBase);
1203
+ await appendTickerPositions(lines, tickerFile, quote, config, toBase, rates);
1117
1204
 
1118
1205
  return lines.join("\n");
1119
1206
  }
@@ -1132,9 +1219,11 @@ function buildTickerStructuredData({
1132
1219
  distributions,
1133
1220
  statementFilings,
1134
1221
  bondFund = null,
1222
+ memberships,
1135
1223
  }: {
1136
1224
  symbol: string;
1137
1225
  listing: CliListing;
1226
+ memberships: CompanyMemberships;
1138
1227
  statementFilings: StatementFilingsReport | null;
1139
1228
  bondFund?: FundBondFacts | null;
1140
1229
  futuresContract: FuturesContractTermsPayload | null;
@@ -1161,6 +1250,10 @@ function buildTickerStructuredData({
1161
1250
  name: quote.name,
1162
1251
  // Last and Change as the text report reads them, then the extended print from that close.
1163
1252
  ...getQuoteSessionFields(quote),
1253
+ // What Change is measured from ("vs Fri 16 Oct close"), and the quote's previous close with its exchange-local date.
1254
+ changeBasis: changeBasisJson(quoteChangeBasis(quote, { terms: futuresContract })),
1255
+ previousClose: quote.previousClose ?? null,
1256
+ previousCloseDate: quotePreviousCloseDate(quote),
1164
1257
  priceBasis: quote.priceBasis ?? null,
1165
1258
  currency: quote.currency,
1166
1259
  marketCap: priced.quote?.marketCap ?? null,
@@ -1186,6 +1279,8 @@ function buildTickerStructuredData({
1186
1279
  watchlists: formatWatchlistNames(config, tickerFile.metadata.watchlists),
1187
1280
  positions: tickerFile.metadata.positions,
1188
1281
  } : null,
1282
+ // The lists holding the company by any of its listings; `via` names the other listing a list holds it as.
1283
+ memberships,
1189
1284
  fundamentals: financials.fundamentals || priceReturns.return1Y != null || priceReturns.return3Y != null ? exportedDividendFundamentals(exportedFundamentals({
1190
1285
  ...priced.fundamentals,
1191
1286
  ...priceReturns,
@@ -1318,6 +1413,15 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1318
1413
  quarter: latestFinancialPeriod(resolvedFinancials.quarterlyStatements, (row) => row.date),
1319
1414
  annual: latestFinancialPeriod(resolvedFinancials.annualStatements, (row) => row.date),
1320
1415
  });
1416
+ // A list holding the company as another of its listings counts (a list of RCI.B:TSX holds RCI); read beside the rest. It never fails.
1417
+ const membershipsLoad = loadCompanyMemberships({
1418
+ config,
1419
+ own: tickerFile,
1420
+ symbol: listing.symbol,
1421
+ exchange: canonicalExchange(listing.exchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange),
1422
+ loadTickers: () => store.loadAllTickers(),
1423
+ dataProvider,
1424
+ });
1321
1425
  const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
1322
1426
  notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
1323
1427
  dataProvider.getNews({
@@ -1351,6 +1455,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1351
1455
  const coinNote = listing.exchange ? null
1352
1456
  : notTheCoinNote(listing.symbol, quote ?? (tickerFile ? { name: tickerFile.metadata.name, instrumentType: tickerFile.metadata.assetCategory } : null));
1353
1457
  const listingNote = [choiceNote, coinNote].filter((note): note is string => !!note).join("\n") || undefined;
1458
+ const memberships = await membershipsLoad;
1354
1459
  if (dependencies.printResult) {
1355
1460
  const warnings = [choiceNote, coinNote, quoteNote].filter((note): note is string => !!note);
1356
1461
  dependencies.printResult({
@@ -1370,6 +1475,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1370
1475
  distributions: await distributions,
1371
1476
  statementFilings: await statementFilings,
1372
1477
  bondFund: await bondFund,
1478
+ memberships,
1373
1479
  }),
1374
1480
  });
1375
1481
  return;
@@ -1392,6 +1498,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1392
1498
  distributions: await distributions,
1393
1499
  statementFilings: await statementFilings,
1394
1500
  bondFund: await bondFund,
1501
+ memberships,
1395
1502
  }));
1396
1503
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
1397
1504
  });
@@ -0,0 +1,27 @@
1
+ import { isVenueAbroad } from "../tickers/listing-fate";
2
+ import type { Quote } from "../types/financials";
3
+ import { isCurrencyCode } from "../utils/currency-pair";
4
+ import { canonicalExchange } from "../utils/exchanges";
5
+ import { UNLISTED_TYPES, type CliListing } from "./listing-arg";
6
+
7
+ // Quoted as dollars per unit (EUR/USD); every other currency as units per dollar (USD/MXN).
8
+ const DOLLARS_PER_UNIT = new Set(["EUR", "GBP", "AUD", "NZD"]);
9
+
10
+ /**
11
+ * "MXN is also a currency: fx MXN, quote USD/MXN" for a bare currency code
12
+ * that read as a listing abroad (MXN is mixi Inc on Stuttgart's exchange): the
13
+ * quote is that company's, and a currency was likely what was typed. Null for
14
+ * a code that is no currency, for the dollar (a US fund trades as USD, and
15
+ * there is no USD/USD), for a listing that named or saved its venue, and for
16
+ * one in the US.
17
+ */
18
+ export function currencyCodeNote(
19
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved">,
20
+ quote: Pick<Quote, "listingExchangeName" | "exchangeName" | "instrumentType"> | null | undefined,
21
+ ): string | null {
22
+ const code = listing.symbol;
23
+ if (!quote || listing.exchange || listing.saved || code === "USD" || !/^[A-Z]{3}$/.test(code) || !isCurrencyCode(code)) return null;
24
+ if (UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return null;
25
+ if (!isVenueAbroad(canonicalExchange(quote.listingExchangeName || quote.exchangeName))) return null;
26
+ return `${code} is also a currency: fx ${code}, quote ${DOLLARS_PER_UNIT.has(code) ? `${code}/USD` : `USD/${code}`}`;
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { currencyMinorDigits } from "../market-data/market/format";
2
2
  import { parseTypedNumber, readCurrencyAmount } from "../utils/decimal-comma";
3
+ import { currenciesNamed } from "../utils/currency-names";
3
4
 
4
5
  /**
5
6
  * What `gloomberb fx` is asked for. A bare code is one unit of it in the base
@@ -12,6 +13,8 @@ export interface FxRequest {
12
13
  currency: string;
13
14
  /** The currency the price is in: the base currency for a bare code, the quote of a pair. */
14
15
  baseCurrency: string;
16
+ /** Each currency typed as its name (`yen`) and the code it was read as, said beside the answer. */
17
+ readAs?: string[];
15
18
  }
16
19
 
17
20
  const CURRENCY_CODE = /^[A-Z]{3}$/;
@@ -30,19 +33,45 @@ function isKnownCurrency(code: string): boolean {
30
33
  return knownCurrencies.has(code);
31
34
  }
32
35
 
36
+ /**
37
+ * A currency typed as its name: the code a name that is one currency's reads
38
+ * as, or the sentence listing the currencies a name several share (`peso`),
39
+ * with `retry` the argument that asks for the first of them. Null for text
40
+ * that names none.
41
+ */
42
+ function readCurrencyName(typed: string, retry: (code: string) => string): { code: string } | { error: string } | null {
43
+ const named = currenciesNamed(typed);
44
+ if (named.length === 0) return null;
45
+ if (named.length === 1) return { code: named[0]!.code };
46
+ const choices = named.map(({ code, qualifier }) => (qualifier ? `${code} (${qualifier})` : code)).join(", ");
47
+ return { error: `${typed} names several currencies: ${choices}. Write one as its code, such as fx ${retry(named[0]!.code)}.` };
48
+ }
49
+
33
50
  /** The request, or the sentence saying what is wrong with the argument. */
34
51
  export function parseFxRequest(raw: string, baseCurrency: string): FxRequest | { error: string } {
52
+ const typed = raw.trim().split("/").map((part) => part.trim());
35
53
  const text = raw.trim().toUpperCase();
36
- const parts = text.split("/").map((part) => part.trim());
54
+ const parts = typed.map((part) => part.toUpperCase());
37
55
  if (parts.length > 2 || parts.some((part) => !part)) return { error: `"${raw}" is not a currency or a pair such as USD/NGN.` };
38
56
  // EURUSD is the pair typed without its slash: say how to write it.
39
57
  const joined = parts.length === 1 && /^[A-Z]{6}$/.test(text) && isKnownCurrency(text.slice(0, 3)) && isKnownCurrency(text.slice(3));
40
58
  if (joined) return { error: `${text} is not a currency code; write the pair as ${text.slice(0, 3)}/${text.slice(3)}.` };
59
+ // A name (peso, yen, Swiss franc) is a currency's code. Three letters that are a code stay one: only a name that is no code reads as one.
60
+ const readAs: string[] = [];
61
+ for (const [index, part] of parts.entries()) {
62
+ if (isKnownCurrency(part)) continue;
63
+ const named = readCurrencyName(typed[index]!, (code) => parts.map((other, at) => at === index ? code : other).join("/"));
64
+ if (!named) continue;
65
+ if ("error" in named) return named;
66
+ readAs.push(`Read ${typed[index]} as ${named.code}.`);
67
+ parts[index] = named.code;
68
+ }
41
69
  const bad = parts.filter((part) => !CURRENCY_CODE.test(part));
42
70
  if (bad.length > 0) return { error: `${bad.join(" and ")} ${bad.length > 1 ? "are not currency codes" : "is not a currency code"}; use three letters such as NGN.` };
43
- return parts.length === 2
71
+ const request = parts.length === 2
44
72
  ? { currency: parts[0]!, baseCurrency: parts[1]! }
45
73
  : { currency: parts[0]!, baseCurrency: baseCurrency.trim().toUpperCase() };
74
+ return readAs.length > 0 ? { ...request, readAs } : request;
46
75
  }
47
76
 
48
77
  /** What `gloomberb fx` is asked for, with the amount to convert when one comes first. */
@@ -55,7 +84,14 @@ export interface FxArgs {
55
84
  }
56
85
 
57
86
  /** Every form `fx` takes, for its usage line. */
58
- export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to>";
87
+ export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to> [--date <day>]";
88
+
89
+ /** A currency's name of several words (`hong kong dollar`, `100 swiss franc`) as the one argument it is. */
90
+ function joinNameWords(args: readonly string[]): readonly string[] {
91
+ const amount = args.length > 2 && parseTypedNumber(args[0]).kind !== "invalid" ? 1 : 0;
92
+ const name = args.slice(amount).join(" ");
93
+ return args.length - amount > 1 && currenciesNamed(name).length > 0 ? [...args.slice(0, amount), name] : args;
94
+ }
59
95
 
60
96
  /**
61
97
  * `fx` arguments: a currency or a pair (`EUR`, `USD/NGN`), or an amount and
@@ -66,7 +102,8 @@ export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/
66
102
  * refused naming both readings. An amount and one currency converts to USD
67
103
  * (`toUsdByDefault`). The error sentence otherwise.
68
104
  */
69
- export function parseFxArgs(args: readonly string[], baseCurrency: string): FxArgs | { error: string } {
105
+ export function parseFxArgs(words: readonly string[], baseCurrency: string): FxArgs | { error: string } {
106
+ const args = joinNameWords(words);
70
107
  if (args.length === 1) {
71
108
  const request = parseFxRequest(args[0]!, baseCurrency);
72
109
  if (!("error" in request)) return { request };