gloomberb 0.18.0 → 0.18.2

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 (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -2,6 +2,9 @@ import { apiClient } from "../../api-client";
2
2
  import type { NewsArticle } from "../../news/types";
3
3
  import { mapCloudNewsArticle } from "../../sources/gloomberb-cloud/news";
4
4
  import { cliStyles } from "../../utils/cli-output";
5
+ import { logFailure } from "../../utils/log-failure";
6
+ import { commodityNamedIn, commoditySearchName, isCommodityName } from "../../market-data/commodity-names";
7
+ import type { FuturesContract } from "../../market-data/futures-board";
5
8
 
6
9
  /** What the service accepts of a search: two characters to two hundred, with a letter or a number in them. */
7
10
  const MIN_TEXT_LENGTH = 2;
@@ -60,8 +63,8 @@ export function newsSearchTextProblem(text: string): string | null {
60
63
 
61
64
  /**
62
65
  * The word a symbol lookup found no stories for, as text to search stories for.
63
- * Null under three characters, where the fuzzy match finds names that merely
64
- * contain it (ZN, Telomir-Zn) and the lookup's own answer is the better one.
66
+ * Null under three characters, where the word is mostly a piece of other
67
+ * names (ZN in Telomir-Zn) and the lookup's own answer is the better one.
65
68
  */
66
69
  export function newsFallbackText(symbol: string): string | null {
67
70
  const text = symbol.trim().replace(/^\$/, "");
@@ -69,15 +72,39 @@ export function newsFallbackText(symbol: string): string | null {
69
72
  }
70
73
 
71
74
  /**
72
- * The stories matching the text, newest first. The service answers best match
73
- * first and holds stories back from an account without real-time access, as
74
- * every other news request.
75
+ * The stories matching the text, in the service's order: stories with every
76
+ * word in the headline first, then the ones with some only in the summary,
77
+ * newest first within each. The service holds stories back from an account
78
+ * without real-time access, as every other news request.
75
79
  */
76
80
  export async function searchNewsStories(text: string, limit: number): Promise<NewsArticle[]> {
77
81
  const response = await apiClient.searchCloudNews(text, limit);
78
- return response.items
79
- .map((item) => mapCloudNewsArticle(item))
80
- .sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
82
+ return response.items.map((item) => mapCloudNewsArticle(item));
83
+ }
84
+
85
+ /** The futures news route a search offers when no story matches it: `gloomberb news KC=F` for "arabica". */
86
+ export interface CommodityNewsRoute {
87
+ contract: FuturesContract;
88
+ command: string;
89
+ }
90
+
91
+ /**
92
+ * The future of the commodity a search names, offered when no story matched:
93
+ * `news KC=F` searches the stories of coffee, so it is offered only when
94
+ * those are not what just found nothing and they have a story to show.
95
+ */
96
+ export async function commodityNewsRoute(text: string): Promise<CommodityNewsRoute | null> {
97
+ const contract = commodityNamedIn(text);
98
+ const name = contract ? commoditySearchName(contract) : null;
99
+ if (!contract || !name || isCommodityName(text, name)) return null;
100
+ const stories = await searchNewsStories(name, 1).catch(logFailure("cli.news-search", "Search a commodity's stories failed", []));
101
+ return stories.length > 0 ? { contract, command: `gloomberb news ${contract.symbol}` } : null;
102
+ }
103
+
104
+ /** What a search that matched no story says: never a near miss, and the commodity's futures news where it has some. */
105
+ export function noStoriesMessage(text: string, route: CommodityNewsRoute | null): string {
106
+ const none = `No stories match "${text}".`;
107
+ return route ? `${none} For the ${route.contract.name} future, try: ${route.command}` : none;
81
108
  }
82
109
 
83
110
  /** The line above the stories: "Stories matching "european gas"", with why a word typed as a symbol is searched. */
@@ -22,6 +22,7 @@ import {
22
22
  } from "../../plugins/builtin/market-movers/model";
23
23
  import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
24
24
  import { fetchEconCalendarFrom } from "../../plugins/builtin/econ/calendar-source";
25
+ import { seriesNotOnFred } from "../../sources/gloomberb-cloud/fred-series";
25
26
  import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
26
27
  import {
27
28
  COUNTRY_CYCLE,
@@ -311,9 +312,27 @@ function parseFilter<T extends string>(value: string | undefined, values: readon
311
312
  return match;
312
313
  }
313
314
 
315
+ /** One of the pane's regions, or any country by its two-letter code (TR, DE), upper-cased. */
316
+ function parseCountry(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): CountryFilter {
317
+ if (value == null) return COUNTRY_CYCLE[0]!;
318
+ const text = value.trim();
319
+ const region = COUNTRY_CYCLE.find((candidate) => candidate.toLowerCase() === text.toLowerCase());
320
+ if (region) return region;
321
+ if (/^[a-z]{2}$/i.test(text)) return text.toUpperCase();
322
+ return ctx.fail(`Unknown country "${value}".`, `Use ${COUNTRY_CYCLE.join(", ")}, or a two-letter country code such as TR.`);
323
+ }
324
+
325
+ /** A country the calendar lists nothing for says which ones it does list, so a typo (UK for GB) is not read as a quiet week. */
326
+ function unlistedCountryMessage(country: CountryFilter, events: ReadonlyArray<{ country: string }>): string | null {
327
+ if ((COUNTRY_CYCLE as readonly string[]).includes(country)) return null;
328
+ const listed = [...new Set(events.map((event) => event.country).filter((code) => /^[A-Z]{2}$/.test(code)))].sort();
329
+ if (listed.length === 0 || listed.includes(country)) return null;
330
+ return `The calendar lists no ${country} events; it lists ${listed.join(", ")}.`;
331
+ }
332
+
314
333
  async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
315
334
  const rawArgs = [...args];
316
- const country: CountryFilter = parseFilter(takeOption(rawArgs, "--country"), COUNTRY_CYCLE, "country", ctx);
335
+ const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
317
336
  const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
318
337
  const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
319
338
  rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --impact.");
@@ -363,7 +382,7 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
363
382
  }, {
364
383
  dateKey: "date",
365
384
  defaultLimit: 50,
366
- empty: econEmptyMessage(from, listedThrough, listedAfter != null),
385
+ empty: unlistedCountryMessage(country, events) ?? econEmptyMessage(from, listedThrough, listedAfter != null),
367
386
  ...(footnote ? { footnote } : {}),
368
387
  columns: [
369
388
  // Text shows both halves of the event timestamp in UTC, never the host's zone; exports keep the source values.
@@ -485,7 +504,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
485
504
  const periodicity = periodicityOf(data.info?.frequency);
486
505
  const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
487
506
  // A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
488
- const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate));
507
+ const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate), seriesId);
489
508
  // FRED names the series' publisher when Gloom Cloud passes it on, and when it last updated the series.
490
509
  const publisher = ahead?.publisher ?? data.info?.source?.trim();
491
510
  const publishedAt = ahead ? ahead.publishedAt : fredPublicationTime(data.info, newestDate);
@@ -498,7 +517,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
498
517
  ...(latest ? { latestObservation: exportRow(latest) } : {}),
499
518
  },
500
519
  freshness: rowsFreshness(rows, {
501
- source: ahead?.source ?? "FRED", status: "not-a-feed", basis: "published statistics", observedKey: "date", oldest: null,
520
+ source: ahead?.source ?? notOnFredSource(seriesId) ?? "FRED", status: "not-a-feed", basis: "published statistics", observedKey: "date", oldest: null,
502
521
  // A monthly or quarterly observation is dated the first of its period, not a day it was seen.
503
522
  ...(periodicity ? { periodicity } : {}),
504
523
  ...(publisher ? { publisher } : {}),
@@ -522,6 +541,12 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
522
541
  });
523
542
  }
524
543
 
544
+ /** Who a series FRED does not carry comes from, for a source line whose rows do not name it: `TurkStat via the CBRT`. */
545
+ function notOnFredSource(seriesId: string): string | null {
546
+ const elsewhere = seriesNotOnFred(seriesId);
547
+ return elsewhere ? `${elsewhere.publisher} via ${elsewhere.via}` : null;
548
+ }
549
+
525
550
  const YIELD_TENORS: Record<string, string> = { DGS3MO: "3M", DGS2: "2Y", DGS10: "10Y", DGS30: "30Y" };
526
551
 
527
552
  async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
@@ -672,11 +697,11 @@ export const overviewCliCommands: CliCommandDef[] = [
672
697
  group: CLI_COMMAND_GROUPS.markets,
673
698
  usage: [ECON_USAGE],
674
699
  options: [
675
- { flags: "--country <region>", description: "US, G7, EU, or all (default all)" },
700
+ { flags: "--country <region>", description: "US, G7, EU, all (default), or one country by its two-letter code, such as TR" },
676
701
  { flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
677
702
  { flags: "--from <date>", description: `First day listed, as ${DATE_OPTION_EXAMPLES} (default today, UTC); an earlier day lists past events from the stored calendar and says where it starts` },
678
703
  ],
679
- examples: ["econ", "econ --country US --impact high", "econ --from 2026-10-12 --country US", "econ --tail 20"],
704
+ examples: ["econ", "econ --country US --impact high", "econ --country TR", "econ --from 2026-10-12 --country US", "econ --tail 20"],
680
705
  },
681
706
  execute: runEcon,
682
707
  },
@@ -5,6 +5,7 @@ import { saveConfig } from "../../data/config/store";
5
5
  import { builtinPluginGroupMembers } from "../../plugins/ownership";
6
6
  import type { AppConfig, TelemetryConfig } from "../../types/config";
7
7
  import { canonicalTimeZone } from "../../utils/utc-time";
8
+ import { readBaseCurrency } from "../../utils/base-currency";
8
9
  import { NIGHTLY_CHANNEL_SUMMARY, parseUpdateChannel, withUpdateChannel } from "../../updater/channel";
9
10
  import type { CliCommandDef } from "../../types/plugin";
10
11
  import { resolveCliDataDir, withCliServices, withConfigData } from "../context";
@@ -65,6 +66,32 @@ function parseTimezoneSetting(value: string): string | null {
65
66
  );
66
67
  }
67
68
 
69
+ /**
70
+ * The code `config set baseCurrency` stores (`cad` is CAD); fails on a name
71
+ * or sign, naming the code it stands for, and on anything that is no code.
72
+ */
73
+ function parseBaseCurrencySetting(value: string): string {
74
+ const reading = readBaseCurrency(value);
75
+ if ("code" in reading) return reading.code;
76
+ const [first] = reading.suggestions;
77
+ return fail(reading.error, first
78
+ ? `baseCurrency takes an ISO 4217 code: gloomberb config set baseCurrency ${first}`
79
+ : "baseCurrency takes an ISO 4217 code such as USD, EUR or CAD.");
80
+ }
81
+
82
+ /** A saved base currency that is no code (an older version stored names), with what to set instead; null for a code. */
83
+ function baseCurrencyProblem(value: unknown): string | null {
84
+ const reading = readBaseCurrency(typeof value === "string" ? value : "");
85
+ if ("code" in reading) return null;
86
+ const [first] = reading.suggestions;
87
+ return `${reading.error} Set one: gloomberb config set baseCurrency ${first ?? "<code>"}`;
88
+ }
89
+
90
+ function describeBaseCurrency(value: unknown): string {
91
+ const problem = baseCurrencyProblem(value);
92
+ return problem ? `${String(value)} ${cliStyles.warning(`(${problem})`)}` : String(value);
93
+ }
94
+
68
95
  function describeTimezone(value: unknown): string {
69
96
  return typeof value === "string" && value ? value : "not set, times print in UTC alone";
70
97
  }
@@ -189,6 +216,9 @@ export function createSystemCliCommands(): CliCommandDef[] {
189
216
  checks.push({ check: "database", status: "ok", detail: join(services.dataDir, ".gloomberb-cache.db") });
190
217
  checks.push({ check: "plugins", status: "ok", detail: String(services.services.pluginRegistry.allPlugins.size) });
191
218
  checks.push({ check: "capabilities", status: "ok", detail: String(services.services.pluginRegistry.capabilities.manifests().length) });
219
+ // Only a saved value no conversion can price is worth a line.
220
+ const baseCurrencyIssue = baseCurrencyProblem(services.config.baseCurrency);
221
+ if (baseCurrencyIssue) checks.push({ check: "baseCurrency", status: "warn", detail: baseCurrencyIssue });
192
222
  const theme = services.config.theme;
193
223
  checks.push({
194
224
  check: "theme",
@@ -311,6 +341,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
311
341
  ...(key === KEYBINDINGS_CONFIG_KEY ? { format: summarizeKeybindings } : {}),
312
342
  ...(key === "theme" ? { format: (value: unknown) => describeThemeId(String(value)) } : {}),
313
343
  ...(key === TIMEZONE_CONFIG_KEY ? { format: describeTimezone } : {}),
344
+ ...(key === "baseCurrency" ? { format: describeBaseCurrency } : {}),
314
345
  })),
315
346
  });
316
347
  return;
@@ -329,7 +360,8 @@ export function createSystemCliCommands(): CliCommandDef[] {
329
360
  }
330
361
  ctx.printResult({ data: { key, value: safeConfig[key] } }, {
331
362
  text: (data) => key === KEYBINDINGS_CONFIG_KEY ? renderKeybindings(data.value)
332
- : key === TIMEZONE_CONFIG_KEY ? describeTimezone(data.value) : describeConfigValue(data.value),
363
+ : key === TIMEZONE_CONFIG_KEY ? describeTimezone(data.value)
364
+ : key === "baseCurrency" ? describeBaseCurrency(data.value) : describeConfigValue(data.value),
333
365
  });
334
366
  return;
335
367
  }
@@ -367,6 +399,8 @@ export function createSystemCliCommands(): CliCommandDef[] {
367
399
  // A display name may come unquoted: config set theme White Phosphor.
368
400
  const parsedValue = key === "theme"
369
401
  ? requireThemeId(args.slice(2).join(" "))
402
+ : key === "baseCurrency"
403
+ ? parseBaseCurrencySetting(args.slice(2).join(" "))
370
404
  : key === TIMEZONE_CONFIG_KEY
371
405
  ? parseTimezoneSetting(value)
372
406
  : key === UPDATE_CHANNEL_CONFIG_KEY
@@ -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,16 @@ 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 } from "../../utils/currency-units";
36
40
  import type { AppConfig } from "../../types/config";
37
41
  import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
38
- import { quoteTradingDay } from "../../market-data/quotes/day-range";
39
42
  import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
40
43
  import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
41
44
  import { instrumentTypeLabel, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
@@ -44,9 +47,10 @@ import type { SecFilingItem } from "../../types/data-provider";
44
47
  import type { NewsArticle } from "../../news/types";
45
48
  import type { TickerRecord } from "../../types/ticker";
46
49
  import type { CliCommandContext } from "../../types/plugin";
47
- import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
50
+ import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
48
51
  import { isFuturesTicker } from "../../portfolio/futures-positions";
49
- import { createBaseConverter } from "../base-converter";
52
+ import { createBaseConverter, type BaseConversion } from "../base-converter";
53
+ import { formatFxRate } from "../fx-pair";
50
54
  import { initMarketData, withMarketData } from "../context";
51
55
  import { fail } from "../errors";
52
56
  import type { MarketContext } from "../types";
@@ -58,6 +62,7 @@ import {
58
62
  formatSignedCurrency,
59
63
  formatTimestamp,
60
64
  formatWatchlistNames,
65
+ quoteChangeText,
61
66
  } from "../helpers";
62
67
  import { NotesFiles } from "../../plugins/builtin/notes/files";
63
68
  import { isKnownNonUsListing, secFilingItemCodes } from "../../utils/sec";
@@ -106,10 +111,6 @@ async function loadRecentSecFilings(
106
111
  // Prose such as a company description stays readable on wide terminals.
107
112
  const MAX_PROSE_WIDTH = 100;
108
113
  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
114
 
114
115
  interface TickerCommandDependencies {
115
116
  /** `--exchange`: the listing, for a symbol that trades in several places. */
@@ -149,25 +150,22 @@ function appendMetricSection(lines: string[], title: string, metrics: Array<[str
149
150
  }
150
151
 
151
152
  /**
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.
153
+ * Out of the regular session the bid and ask say whose book they are: the pre-market or after-hours book beside
154
+ * Last and the extended print, or a closed market's last quote with its day (ITA's $202.00 against $208.74
155
+ * around a $206.65 close is no open book).
154
156
  */
155
157
  function bidAskText(quote: Quote, options: MarketFormatOptions): string {
156
158
  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})`)}`;
159
+ const label = text === "—" ? null : quoteBookSessionLabel(quote);
160
+ return label ? `${text} ${cliStyles.muted(`(${label})`)}` : text;
163
161
  }
164
162
 
165
163
  function wrapProse(text: string): string {
166
164
  return wrapText(text, Math.min(cliTerminalWidth() ?? MAX_PROSE_WIDTH, MAX_PROSE_WIDTH)).join("\n");
167
165
  }
168
166
 
169
- function buildStatementMetrics(statement: FinancialStatement, currency?: string): Array<[string, string]> {
170
- const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare);
167
+ function buildStatementMetrics(statement: FinancialStatement, currency?: string, codes = false): Array<[string, string]> {
168
+ const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare, codes);
171
169
  return [
172
170
  ["Revenue", money(statement.totalRevenue)],
173
171
  ["Gross Profit", money(statement.grossProfit)],
@@ -303,8 +301,11 @@ function shouldFetchSecFilings(
303
301
  }
304
302
 
305
303
  async function appendTickerPositions(lines: string[], tickerFile: TickerRecord | null, quote: TickerFinancials["quote"],
306
- config: AppConfig, toBase: (value: number, currency: string) => Promise<number>): Promise<void> {
304
+ config: AppConfig, toBase: ReportBaseConverter): Promise<void> {
307
305
  const quoteCurrency = quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "";
306
+ const codes = writesCurrencyCodes(config);
307
+ const money = codes ? formatCurrencyCode : formatCurrency;
308
+ const converted = new Set<string>();
308
309
  if (tickerFile && tickerFile.metadata.positions.length > 0) {
309
310
  lines.push("");
310
311
  lines.push(renderSection("Positions"));
@@ -318,10 +319,15 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
318
319
  const positionCurrency = metrics.positionCurrency;
319
320
  const currentPrice = getPortfolioQuoteDisplay(metrics, quote)?.price ?? null;
320
321
  const currentPriceBase = currentPrice != null && quoteCurrency ? await toBase(currentPrice, quoteCurrency) : null;
321
- const costBasisBase = positionCurrency ? await toBase(metrics.signedCost, positionCurrency) : Number.NaN;
322
322
  const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
323
+ if (positionCurrency) converted.add(positionCurrency.toUpperCase());
324
+ if (quoteCurrency && currentPrice != null) converted.add(quoteCurrency.toUpperCase());
323
325
  const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
324
326
  { currency: config.baseCurrency, convert: value => value * positionRate }, quote);
327
+ // A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
328
+ const costBasisBase = !positionCurrency ? Number.NaN
329
+ : lotCostFxRate(position, positionCurrency, config.baseCurrency) !== null ? baseMetrics.signedCost
330
+ : await toBase(metrics.signedCost, positionCurrency);
325
331
  const marketValueBase = resolvePortfolioMarketValue(baseMetrics, currentPriceBase)?.net ?? Number.NaN;
326
332
  const selectedPnl = resolvePortfolioPositionPnl(baseMetrics, currentPriceBase);
327
333
  const pnl = selectedPnl.value;
@@ -331,26 +337,51 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
331
337
  const stats: CliStatEntry[] = [
332
338
  [
333
339
  "Position",
334
- `${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 }) : "—"}`,
340
+ `${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 }) : "—"}`,
335
341
  ],
336
- ["Cost Basis", formatCurrency(costBasisBase, config.baseCurrency)],
337
- ["Market Value", formatCurrency(marketValueBase, config.baseCurrency)],
342
+ ["Cost Basis", money(costBasisBase, config.baseCurrency)],
343
+ ["Market Value", money(marketValueBase, config.baseCurrency)],
338
344
  [
339
345
  selectedPnl.basis === "broker-snapshot" ? "Broker P&L" : "P&L",
340
- pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency), pnl),
346
+ pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency, codes), pnl),
341
347
  ],
342
348
  ];
343
349
  if (position.markPrice != null) {
344
- stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"]);
350
+ stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"]);
345
351
  }
346
352
  lines.push(renderStats(stats));
347
353
  if (index < positions.length - 1) {
348
354
  lines.push("");
349
355
  }
350
356
  }
357
+ for (const note of conversionNotes(toBase, converted)) lines.push(cliStyles.muted(note));
351
358
  }
352
359
  }
353
360
 
361
+ /** The ticker report's converter: what `createBaseConverter` makes, whose rates a report states; a plain function in tests. */
362
+ type ReportBaseConverter = ((value: number, fromCurrency: string) => Promise<number>) & { conversions?(): BaseConversion[] };
363
+
364
+ /**
365
+ * A base currency other than USD reads every amount with its code (259.36
366
+ * USD, 878.15B CAD): $ alone is also the Canadian, Australian and other
367
+ * dollars. A USD base keeps the signs it always had.
368
+ */
369
+ export function writesCurrencyCodes(config: Pick<AppConfig, "baseCurrency"> | undefined): boolean {
370
+ const base = config?.baseCurrency?.trim().toUpperCase();
371
+ return !!base && base !== "USD";
372
+ }
373
+
374
+ /**
375
+ * "CAD at USD/CAD 1.40623, as of 2026-10-21 04:46 UTC.": the rate and its
376
+ * date for each currency in `currencies` the report converted into the base
377
+ * currency, so a converted figure is never without what it was converted at.
378
+ */
379
+ function conversionNotes(toBase: ReportBaseConverter, currencies: ReadonlySet<string>): string[] {
380
+ return (toBase.conversions?.() ?? [])
381
+ .filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency)
382
+ .map(({ currency, baseCurrency, rate, asOf }) => `${baseCurrency} figures at ${currency}/${baseCurrency} ${formatFxRate(rate)}${asOf != null ? `, as of ${formatTimestamp(asOf)}` : ""}.`);
383
+ }
384
+
354
385
  /** A source's enterprise value is in the units of the capitalization it reports beside it,
355
386
  * which without a declared unit is the listing's quote currency (as the overview labels it). */
356
387
  function enterpriseValueCurrency(
@@ -375,9 +406,9 @@ interface FundamentalsMetric {
375
406
 
376
407
  type MetricFigure = Omit<FundamentalsMetric, "label">;
377
408
 
378
- /** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. */
379
- function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false): MetricFigure {
380
- return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
409
+ /** 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). */
410
+ function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false, codes = false): MetricFigure {
411
+ return { text: formatReportedMoney(value, currency, perShare, codes), value, unit: currency?.trim() || "ccy?" };
381
412
  }
382
413
 
383
414
  /** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
@@ -567,6 +598,7 @@ function fundamentalsMetrics(
567
598
  enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
568
599
  distributions: DividendDistributions | null = null,
569
600
  bond: FundBondFacts | null = null,
601
+ codes = false,
570
602
  ): FundamentalsMetric[] {
571
603
  const signed = (value: number) => colorBySign(formatPercent(value), value);
572
604
  const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
@@ -578,7 +610,7 @@ function fundamentalsMetrics(
578
610
  { label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
579
611
  { label: "PEG", ...multipleFigure(fundamentals?.pegRatio) },
580
612
  // The flows and EPS are one trailing-twelve-month block, the same twelve months for each line.
581
- { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
613
+ { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true, codes) },
582
614
  {
583
615
  label: dividendYieldLabel(fundamentals?.dividendYieldBasis, bond),
584
616
  ...forwardYieldFigure(fundamentals, distributions),
@@ -681,6 +713,8 @@ type FundamentalsReportData = TickerFinancials & {
681
713
  bank?: BankFiguresPayload | null;
682
714
  /** In place of `bank`, for a bank whose figures could not be read. */
683
715
  bankNote?: string | null;
716
+ /** A base currency other than USD: every amount reads with its code (`writesCurrencyCodes`). */
717
+ currencyCodes?: boolean;
684
718
  };
685
719
 
686
720
  /** "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. */
@@ -720,7 +754,10 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
720
754
  const quote = financials.quote;
721
755
  const session = getRegularSessionDisplay(quote);
722
756
  if (!quote || !session) return [];
723
- const options = withCurrencyMinorDigits(quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType), quote.currency);
757
+ const options = withCurrencyMinorDigits({
758
+ ...quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType),
759
+ ...financials.currencyCodes ? { currencyCode: true } : {},
760
+ }, quote.currency);
724
761
  const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
725
762
  const extended = getExtendedSessionDisplay(quote);
726
763
  const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
@@ -729,7 +766,8 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
729
766
  session.changePercent != null && Number.isFinite(session.changePercent)
730
767
  ? {
731
768
  label: "Change",
732
- text: colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, options, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change),
769
+ // With what the move is measured from, as `ticker` and `quote` say it: "vs Fri 16 Oct close".
770
+ text: quoteChangeText(quote, session, options),
733
771
  value: session.changePercent,
734
772
  unit: "%",
735
773
  }
@@ -764,7 +802,7 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
764
802
  ? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
765
803
  : { text: "—" };
766
804
  const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
767
- undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null);
805
+ undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null, snapshot.currencyCodes === true);
768
806
  if (view === "fundamentals") {
769
807
  return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
770
808
  .filter((metric) => metric.text !== "—");
@@ -924,7 +962,7 @@ export async function buildTickerReport({
924
962
  tickerFile: TickerRecord | null;
925
963
  financials: TickerFinancials;
926
964
  config: AppConfig;
927
- toBase: (value: number, fromCurrency: string) => Promise<number>;
965
+ toBase: ReportBaseConverter;
928
966
  notes?: string;
929
967
  /** Why there is no quote, when a source said; "Quote unavailable." otherwise. */
930
968
  quoteNote?: string;
@@ -941,8 +979,10 @@ export async function buildTickerReport({
941
979
  const profile = financials.profile;
942
980
  const name = quote?.name || tickerFile?.metadata.name || symbol;
943
981
  const baseQuoteOptions = quoteFormatOptions(quote, tickerFile?.metadata.assetCategory, financials.quoteMetadata?.instrumentType);
982
+ const baseCurrency = config.baseCurrency.trim().toUpperCase();
983
+ const codes = writesCurrencyCodes(config);
944
984
  // Pad money prices to the currency's minor unit so a range reads £35.10 - £35.485, never past it (JPY has none).
945
- const quoteOptions = withCurrencyMinorDigits(baseQuoteOptions, quote?.currency);
985
+ const quoteOptions = withCurrencyMinorDigits(codes ? { ...baseQuoteOptions, currencyCode: true } : baseQuoteOptions, quote?.currency);
946
986
  const lines: string[] = [];
947
987
 
948
988
  lines.push(`${cliStyles.accent(quote?.symbol ?? symbol)} ${cliStyles.bold(name)}`);
@@ -953,9 +993,10 @@ export async function buildTickerReport({
953
993
  const indexLevel = isIndexLevel(quote?.instrumentType?.trim() || financials.quoteMetadata?.instrumentType?.trim() || tickerFile?.metadata.assetCategory);
954
994
  const summaryParts = [
955
995
  listingVenueLabel(listingExchange, quote, financials, tickerFile) || undefined,
956
- indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${quoteCurrency}` : undefined,
996
+ indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${currencyCodeLabel(quoteCurrency)}` : undefined,
957
997
  quoteMarketStateLabel(quote) ?? undefined,
958
- quote?.dataSource ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
998
+ // A listing that no longer trades has no delay to speak of: its quote is its last session's close.
999
+ quote?.dataSource && !quote.delisting ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
959
1000
  ].filter((part): part is string => !!part);
960
1001
  if (summaryParts.length > 0) {
961
1002
  lines.push(cliStyles.muted(summaryParts.join(METADATA_SEPARATOR)));
@@ -991,21 +1032,25 @@ export async function buildTickerReport({
991
1032
  // Market cap and the multiples are measured at the regular session price the Quote section heads with.
992
1033
  const priced = atRegularSessionPrice(financials);
993
1034
  const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
994
- const convertedMarketCap = capitalization ? await toBase(capitalization.value, capitalization.currency) : Number.NaN;
1035
+ // Market cap and EV read in the currency they are reported in, as the revenue and income beside them do,
1036
+ // with the base currency's figure after them when it differs: 627.2B USD (878.15B CAD).
1037
+ const convertedCurrencies = new Set<string>();
1038
+ const inBase = async (value: number, currency: string): Promise<string> => {
1039
+ if (currency.trim().toUpperCase() === baseCurrency) return "";
1040
+ const converted = await toBase(value, currency);
1041
+ if (!Number.isFinite(converted)) return "";
1042
+ convertedCurrencies.add(currency.trim().toUpperCase());
1043
+ return ` (${formatCompact(converted)} ${baseCurrency})`;
1044
+ };
995
1045
  const marketCapText = capitalization
996
- ? Number.isFinite(convertedMarketCap)
997
- ? `${formatCompact(convertedMarketCap)} ${config.baseCurrency}`
998
- : `${formatCompact(capitalization.value)} ${capitalization.currency}`
1046
+ ? `${formatCompact(capitalization.value)} ${capitalization.currency}${await inBase(capitalization.value, capitalization.currency)}`
999
1047
  : "—";
1000
1048
  // Shown in the same currency as the market cap, so the two can be compared.
1001
1049
  const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
1002
1050
  const evCurrency = enterpriseValueCurrency(quote, fundamentals);
1003
1051
  // A minor unit such as GBp stays as reported: the converter would read it as the major currency.
1004
- const convertedEnterpriseValue = enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
1005
- ? await toBase(enterpriseValue, evCurrency) : Number.NaN;
1006
- const enterpriseValueText = Number.isFinite(convertedEnterpriseValue)
1007
- ? `${formatCompact(convertedEnterpriseValue)} ${config.baseCurrency}`
1008
- : formatReportedMoney(enterpriseValue, evCurrency);
1052
+ const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
1053
+ ? await inBase(enterpriseValue, evCurrency) : ""}`;
1009
1054
 
1010
1055
  if (quote) {
1011
1056
  // As the Overview reads it: the regular session, then the extended-hours print against its close.
@@ -1020,13 +1065,16 @@ export async function buildTickerReport({
1020
1065
  const volume = reportedQuoteVolume(quote);
1021
1066
  // A futures price is its latest trade; the exchange settlement is its own row, dated by its session.
1022
1067
  appendMetricSection(lines, "Quote", [
1023
- ["Last", colorBySign(formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions), session.change)],
1068
+ // A price in cents says what it is per: 314.95¢/lb.
1069
+ ["Last", colorBySign(`${formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions)}${minorUnitPricePer(futuresContract, quote.currency)}`, session.change)],
1024
1070
  ...futuresContract
1025
1071
  ? [["Settle", futuresSettleText(futuresContract, quote)] as [string, string]]
1026
1072
  : [],
1027
- ["Change", colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, quoteOptions, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change)],
1073
+ // The move says what it is measured from: "vs Fri 16 Oct close", a future's "vs Fri 16 Oct settle".
1074
+ ["Change", quoteChangeText(quote, session, quoteOptions, futuresContract)],
1028
1075
  ["Open", quote.open != null ? formatMarketPriceWithCurrency(quote.open, quote.currency, quoteOptions) : "—"],
1029
- ["Day Range", quote.low != null || quote.high != null
1076
+ // A listing that no longer trades: its last session's range, never today's.
1077
+ [quote.delisting ? "Last Session" : "Day Range", quote.low != null || quote.high != null
1030
1078
  ? formatPriceRange(quote.low, quote.high, quote.currency, quoteOptions)
1031
1079
  : "—"],
1032
1080
  ["52W Range", quote.low52w != null || quote.high52w != null
@@ -1047,10 +1095,11 @@ export async function buildTickerReport({
1047
1095
  }
1048
1096
 
1049
1097
  const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
1050
- { text: enterpriseValueText }, distributions, bondFund);
1098
+ { text: enterpriseValueText }, distributions, bondFund, codes);
1051
1099
  const beforeFundamentals = lines.length;
1052
1100
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
1053
1101
  for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
1102
+ if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies)) lines.push(cliStyles.muted(note));
1054
1103
  // Where a fund's facts would go: under its fundamentals, or in their place.
1055
1104
  const fund = fundFactsNote(quote?.symbol ?? symbol,
1056
1105
  quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
@@ -1068,13 +1117,13 @@ export async function buildTickerReport({
1068
1117
  const latestAnnual = latestFinancialPeriod(financials.annualStatements, row => row.date);
1069
1118
  if (latestAnnual) {
1070
1119
  appendMetricSection(lines, `Latest Annual (${statementPeriodText(latestAnnual.date, statementFilings?.latestAnnual ?? null)})`,
1071
- buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual)));
1120
+ buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual), codes));
1072
1121
  }
1073
1122
 
1074
1123
  const latestQuarter = latestFinancialPeriod(financials.quarterlyStatements, row => row.date);
1075
1124
  if (latestQuarter) {
1076
1125
  appendMetricSection(lines, `Latest Quarter (${statementPeriodText(latestQuarter.date, statementFilings?.latestQuarter ?? null)})`,
1077
- buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter)));
1126
+ buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter), codes));
1078
1127
  }
1079
1128
  // The statements stop a quarter short of the results the company has released.
1080
1129
  if (statementFilings?.resultsRelease && (latestAnnual || latestQuarter)) lines.push(cliStyles.muted(resultsReleaseText(statementFilings.resultsRelease)));
@@ -1158,6 +1207,10 @@ function buildTickerStructuredData({
1158
1207
  name: quote.name,
1159
1208
  // Last and Change as the text report reads them, then the extended print from that close.
1160
1209
  ...getQuoteSessionFields(quote),
1210
+ // What Change is measured from ("vs Fri 16 Oct close"), and the quote's previous close with its exchange-local date.
1211
+ changeBasis: changeBasisJson(quoteChangeBasis(quote, { terms: futuresContract })),
1212
+ previousClose: quote.previousClose ?? null,
1213
+ previousCloseDate: quotePreviousCloseDate(quote),
1161
1214
  priceBasis: quote.priceBasis ?? null,
1162
1215
  currency: quote.currency,
1163
1216
  marketCap: priced.quote?.marketCap ?? null,