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
@@ -12,7 +12,8 @@ import {
12
12
  import { formatCompact, formatPercentRaw } from "../../../../utils/format";
13
13
  import { currencyPriceMinDigits, formatMarketCostWithCurrency, formatMarketPrice, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions } from "../../../../market-data/market/format";
14
14
  import { resolvePriceBasis } from "../../../../market-data/market/price-basis";
15
- import { getPortfolioPositionMetrics, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../../../portfolio/position-metrics";
15
+ import { combinedChangeBasisLabel, quoteChangeBasis, type QuoteChangeBasis } from "../../../../market-data/market/change-basis";
16
+ import { getPortfolioPositionMetrics, lotCostAtTodaysRate, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../../../portfolio/position-metrics";
16
17
  import {
17
18
  getPortfolioValuationDisplay,
18
19
  getPortfolioValuationTime,
@@ -56,6 +57,7 @@ import { loadPerpetualMarket } from "../../../../sources/gloomberb-cloud/perpetu
56
57
  import { listedMarketCapitalization, selectMarketCapitalization } from "../../../../utils/market-capitalization";
57
58
  import { chunked, mapWithConcurrency } from "../../../../utils/concurrency";
58
59
  import { liveMarketCapitalization } from "../../../../market-data/quotes/live-valuation";
60
+ import { delistingLabel } from "../../../../market-data/quotes/delisting";
59
61
  import { instrumentFromTicker } from "../../../../market-data/request-types";
60
62
  import { toMarketDataContext } from "../../../../market-data/selectors";
61
63
  import { cliFreshnessFooter } from "../../../../cli/result";
@@ -282,6 +284,12 @@ interface PortfolioPositionValue {
282
284
  row: Record<string, unknown>;
283
285
  /** A future's contract size and settlement from its terms; null for any other holding. */
284
286
  futures: PortfolioFuturesTerms | null;
287
+ /**
288
+ * Only on a lot held in another currency than the portfolio's: the rate its
289
+ * cost converted at (one unit of its currency in the portfolio's, when
290
+ * bought), or null when it converted at today's rate.
291
+ */
292
+ costFx?: { rate: number; currency: string } | null;
285
293
  }
286
294
 
287
295
  /** What a row's value is priced at: exported after every other field, so existing columns keep their places. */
@@ -309,6 +317,8 @@ export interface PortfolioValuation {
309
317
  unknownContractSize: Set<string>;
310
318
  /** Futures valued at the close without a confirmed settlement to value them at. */
311
319
  unsettled: Set<string>;
320
+ /** Holdings in another currency whose cost converts at today's rate: no purchase date or cost FX rate was entered. */
321
+ costAtTodaysRate: Set<string>;
312
322
  /** Long and short market value of the priced lots, and their net and gross, in the portfolio's currency. */
313
323
  exposure: PortfolioExposure;
314
324
  }
@@ -333,6 +343,15 @@ function portfolioExposure(values: Iterable<number | null | undefined>): Portfol
333
343
  return { long, short, net: long + short, gross: long - short };
334
344
  }
335
345
 
346
+ /**
347
+ * What a book says of holdings in another currency whose cost converts at
348
+ * today's rate, which leaves the currency's move since purchase out of their
349
+ * P&L; `named` lists them as the table names its rows.
350
+ */
351
+ export function costAtTodaysRateNote(named: string): string {
352
+ return `Cost converted at today's rate for ${named}; give portfolio position set a --date or --cost-fx to use the rate when bought.`;
353
+ }
354
+
336
355
  /**
337
356
  * Values every position of a portfolio in the portfolio's currency, as
338
357
  * `portfolio show` prints it: cost basis, market value and unrealized P&L
@@ -368,6 +387,7 @@ export async function valuePortfolioPositions({
368
387
  const brokerPnlSymbols = new Set<string>();
369
388
  const unknownContractSize = new Set<string>();
370
389
  const unsettled = new Set<string>();
390
+ const costAtTodaysRate = new Set<string>();
371
391
  const values: PortfolioPositionValue[] = [];
372
392
  const holdings: AllocationHolding[] = [];
373
393
  const known = (value: number | null | undefined): number | null => value != null && Number.isFinite(value) ? value : null;
@@ -409,11 +429,17 @@ export async function valuePortfolioPositions({
409
429
  const valuationQuote = getPortfolioValuationDisplay(metrics, quote, basis, settlement);
410
430
  const atSettlement = valuesAtSettlement(metrics, basis);
411
431
  const positionCurrency = metrics.positionCurrency;
412
- const costBasisBase = positionCurrency ? await toBase(metrics.totalCost, positionCurrency) : Number.NaN;
413
-
414
432
  const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
433
+ // The cost converts at the rate the lot was bought at when one was entered, else at today's, as its value does.
415
434
  const baseMetrics = getPortfolioPositionMetrics({ ...ticker, metadata: { ...ticker.metadata, positions: [position] } }, id, quoteCurrency,
416
435
  { currency, convert: value => value * positionRate }, quote);
436
+ const boughtRate = positionCurrency ? lotCostFxRate(position, positionCurrency, currency) : null;
437
+ const costBasisBase = !positionCurrency ? Number.NaN
438
+ : boughtRate !== null ? baseMetrics.totalCost : await toBase(metrics.totalCost, positionCurrency);
439
+ // Only a lot entered by hand can be given the rate it was bought at; a broker's lot reads as it always has.
440
+ const atTodaysRate = position.broker === "manual" && !!positionCurrency && lotCostAtTodaysRate(position, positionCurrency, currency);
441
+ if (atTodaysRate && metrics.hasCostBasis) costAtTodaysRate.add(ticker.metadata.ticker);
442
+ const costFx = boughtRate !== null ? { rate: boughtRate, currency } : atTodaysRate ? null : undefined;
417
443
  const currentValueBase = resolvePortfolioMarketValue(baseMetrics,
418
444
  valuationQuote ? await toBase(valuationQuote.price, quoteCurrency) : null)?.gross ?? null;
419
445
  const selectedPnl = resolvePortfolioPositionPnl(baseMetrics,
@@ -436,7 +462,7 @@ export async function valuePortfolioPositions({
436
462
  ? { change: await toBase(metrics.totalPriceUnits * change, quoteCurrency),
437
463
  previousValue: await toBase(metrics.grossPriceUnits * (valuationQuote.price - change), quoteCurrency) }
438
464
  : null;
439
- values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl, futures: futuresTerms,
465
+ values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl, futures: futuresTerms, ...(costFx !== undefined ? { costFx } : {}),
440
466
  dayMove: dayMove && Number.isFinite(dayMove.change) && Number.isFinite(dayMove.previousValue) ? dayMove : null,
441
467
  valuation: { valuationBasis: basis, valuationPrice: known(valuationQuote?.price),
442
468
  valuationAsOf: valuationTimeIso(getPortfolioValuationTime(quote, basis, metrics, settlement)),
@@ -455,35 +481,52 @@ export async function valuePortfolioPositions({
455
481
 
456
482
  const exposure = portfolioExposure(values.map((entry) => entry.position ? entry.row.marketValue as number | null : null));
457
483
  return { basis, positions: values, holdings, totalPnl, unavailablePnl, unavailableCost, unavailableMarketValue, brokerPnlSymbols,
458
- unknownContractSize, unsettled, exposure };
484
+ unknownContractSize, unsettled, costAtTodaysRate, exposure };
459
485
  }
460
486
 
461
487
  /**
462
488
  * The holdings' move over the day of the prices they are valued at, in the
463
489
  * portfolio's currency, and that move against their value at the close
464
- * before. Null when any priced holding's move is unknown, as the pane's Day is.
490
+ * before, with what it is measured from as the pane's Day says it ("vs Fri 16
491
+ * Oct close", "vs mixed closes"). Null when any priced holding's move is
492
+ * unknown, as the pane's Day is.
465
493
  */
466
- export function portfolioDayMove(valuation: Pick<PortfolioValuation, "positions">): { change: number; changePercent: number | null } | null {
494
+ export function portfolioDayMove(
495
+ valuation: Pick<PortfolioValuation, "positions">,
496
+ now = Date.now(),
497
+ ): { change: number; changePercent: number | null; basis: string | null } | null {
467
498
  let change = 0;
468
499
  let previousValue = 0;
500
+ const bases: Array<QuoteChangeBasis | null> = [];
469
501
  for (const entry of valuation.positions) {
470
502
  if (!entry.position) continue;
471
503
  if (!entry.dayMove) return null;
472
504
  change += entry.dayMove.change;
473
505
  previousValue += entry.dayMove.previousValue;
506
+ bases.push(quoteChangeBasis(entry.quote, { terms: entry.futures }));
474
507
  }
475
- return { change, changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null };
508
+ return {
509
+ change,
510
+ changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null,
511
+ basis: combinedChangeBasisLabel(bases, now),
512
+ };
476
513
  }
477
514
 
478
- /** `Day's move: +$1,165.49 (+0.51%)`, the line a book valued at the close carries; `color` tints the figures by sign. */
515
+ /**
516
+ * `Day's move: +$1,165.49 (+0.51%) vs Fri 16 Oct close`, the line a book
517
+ * valued at the close carries; `color` tints the figures by sign, `muted` the
518
+ * basis.
519
+ */
479
520
  export function dayMoveLine(
480
521
  move: ReturnType<typeof portfolioDayMove>,
481
522
  currency: string,
482
523
  color: (text: string, value: number) => string = (text) => text,
524
+ muted: (text: string) => string = (text) => text,
483
525
  ): string {
484
526
  if (!move) return "Day's move: unavailable";
485
527
  const percent = move.changePercent != null ? ` (${formatPercentRaw(move.changePercent)})` : "";
486
- return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}`;
528
+ const basis = move.basis ? ` ${muted(move.basis)}` : "";
529
+ return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}${basis}`;
487
530
  }
488
531
 
489
532
  /**
@@ -725,6 +768,7 @@ async function showCollectionWithMarketData(
725
768
  brokerPnlSymbols,
726
769
  unknownContractSize,
727
770
  unsettled,
771
+ costAtTodaysRate,
728
772
  exposure,
729
773
  } = valuation;
730
774
  const { allocation, cash } = await valuePortfolioAllocation({ valuation, portfolio: matchedPortfolio, account, toBase });
@@ -782,6 +826,8 @@ async function showCollectionWithMarketData(
782
826
  contractMultiplier: entry.position.multiplier ?? null,
783
827
  contractSize: entry.futures.size?.size ?? null,
784
828
  } : {}),
829
+ // Only on a lot held in another currency: the rate its cost converted at when bought, null when at today's.
830
+ ...(entry.costFx !== undefined ? { costFxRate: entry.costFx?.rate ?? null, costFxCurrency: entry.costFx?.currency ?? null } : {}),
785
831
  };
786
832
  });
787
833
  const totalPnlValue = unavailablePnl.size > 0 ? null : totalPnl;
@@ -871,7 +917,7 @@ async function showCollectionWithMarketData(
871
917
  total: allocation.total, cash: cashExport, targetSum: allocation.targetSum,
872
918
  // Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
873
919
  ...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
874
- ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
920
+ ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
875
921
  unpricedCount: allocation.unpriced.length, unpricedSymbols: allocation.unpriced,
876
922
  totalUnrealizedPnl: totalPnlValue,
877
923
  annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
@@ -884,6 +930,8 @@ async function showCollectionWithMarketData(
884
930
  exposure,
885
931
  ...(unknownContractSize.size > 0 ? { unknownContractSizeSymbols: [...unknownContractSize] } : {}),
886
932
  ...(unsettled.size > 0 ? { unsettledSymbols: [...unsettled] } : {}),
933
+ // Holdings in another currency with no purchase date or cost FX rate: their cost converts at today's rate.
934
+ ...(costAtTodaysRate.size > 0 ? { costAtTodaysRateSymbols: [...costAtTodaysRate] } : {}),
887
935
  accountingBasis, ...(manualAccounting ? { manualAccounting } : {}),
888
936
  ...(matchedPortfolio.importedFrom ? { importedFrom: matchedPortfolio.importedFrom } : {}),
889
937
  ...(explain ? { glossary: glossaryExport(explained) } : {}),
@@ -1014,7 +1062,7 @@ async function showCollectionWithMarketData(
1014
1062
  rows,
1015
1063
  ));
1016
1064
  console.log("");
1017
- if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign));
1065
+ if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign, cliStyles.muted));
1018
1066
  if (valued.some((entry) => entry.position)) console.log(exposureLine(exposure, currency));
1019
1067
  const notes: string[] = [];
1020
1068
  const weightNote = weightBasisLine(allocation, currency);
@@ -1038,6 +1086,7 @@ async function showCollectionWithMarketData(
1038
1086
  const targetNote = describeTargetSum(allocation.targetSum, allocation.weightBasis);
1039
1087
  if (targetNote) notes.push(cliStyles.muted(targetNote));
1040
1088
  if (brokerPnlSymbols.size > 0) notes.push(cliStyles.muted(`P&L for ${named(brokerPnlSymbols)} is the broker's last snapshot.`));
1089
+ if (costAtTodaysRate.size > 0) notes.push(cliStyles.muted(costAtTodaysRateNote(named(costAtTodaysRate))));
1041
1090
  if (unknownContractSize.size > 0) {
1042
1091
  notes.push(cliStyles.warning(`No contract size for ${named(unknownContractSize)}, so ${unknownContractSize.size === 1 ? "its" : "their"} value, cost and P&L are not shown.`));
1043
1092
  }
@@ -1106,13 +1155,15 @@ async function showCollectionWithMarketData(
1106
1155
  const priceText = quote && headline
1107
1156
  ? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
1108
1157
  : "—";
1109
- const changeText = headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
1158
+ // A listing that no longer trades has no day's move: its last session's is years old.
1159
+ const changeText = quote?.delisting ? "—" : headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
1160
+ const staleText = quote?.delisting ? delistingLabel(quote.delisting) : lastTrade ? `STALE ${lastTrade}` : "STALE";
1110
1161
  return [
1111
1162
  listingLabel(ticker),
1112
1163
  venue ?? "—",
1113
1164
  priceText,
1114
1165
  changeText,
1115
- ...stale.length > 0 ? [rowStale ? cliStyles.warning(lastTrade ? `STALE ${lastTrade}` : "STALE") : ""] : [],
1166
+ ...stale.length > 0 ? [rowStale ? cliStyles.warning(staleText) : ""] : [],
1116
1167
  ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
1117
1168
  ...showFunding ? [funding == null ? "—" : colorBySign(formatPerpetualRate(funding, 4), funding)] : [],
1118
1169
  marketCap == null ? "—" : `${formatCompact(marketCap)} ${currency}`,
@@ -1138,7 +1189,8 @@ async function showCollectionWithMarketData(
1138
1189
  if (notes.length > 0) console.log(["", ...notes.map((note) => cliStyles.muted(note))].join("\n"));
1139
1190
  // The closing line counts the stale rows; the line under it names them.
1140
1191
  if (stale.length > 0) {
1141
- staleNote = `Stale: ${namedList(stale.map(({ ticker, lastTrade }) => `${listingLabel(ticker)}${lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
1192
+ staleNote = `Stale: ${namedList(stale.map(({ ticker, quote, lastTrade }) => `${listingLabel(ticker)}${quote?.delisting ? ` (${delistingLabel(quote.delisting)})`
1193
+ : lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
1142
1194
  }
1143
1195
  }
1144
1196
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}${staleNote ? `\n${cliStyles.muted(staleNote)}` : ""}`);
@@ -1274,6 +1326,8 @@ async function valueWatchlistRows({
1274
1326
  // A stale quote (as the source line counts it) and the exchange-local date it last traded.
1275
1327
  stale,
1276
1328
  lastTradeDate: lastTrade,
1329
+ // A listing that no longer trades: its status and last session.
1330
+ ...quote?.delisting ? { delisting: quote.delisting } : {},
1277
1331
  },
1278
1332
  });
1279
1333
  }
@@ -125,7 +125,7 @@ async function addToWatchlistCommand(args: string[], file: string | undefined, e
125
125
  dryRun,
126
126
  add: (ticker) => addTickerToWatchlist(ticker, watchlist.id),
127
127
  });
128
- printSymbolRows(ctx, rows, { verb: "add", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, strays: list.strays, retry, dryRun });
128
+ printSymbolRows(ctx, rows, { verb: "add", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, notes: list.notes, retry, dryRun });
129
129
  });
130
130
  }
131
131
 
@@ -158,7 +158,7 @@ async function removeFromWatchlistCommand(args: string[], file: string | undefin
158
158
  store: context.store,
159
159
  remove: (ticker) => removeWatchlistMembership(ticker, watchlist.id),
160
160
  });
161
- printSymbolRows(ctx, rows, { verb: "remove", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, strays: list.strays });
161
+ printSymbolRows(ctx, rows, { verb: "remove", collection: { kind: "watchlist", name: watchlist.name }, repeated: list.repeated, notes: list.notes });
162
162
  });
163
163
  }
164
164
 
@@ -24,6 +24,7 @@ import {
24
24
  valuationTimeText,
25
25
  valuePortfolioAllocation,
26
26
  valuePortfolioPositions,
27
+ costAtTodaysRateNote,
27
28
  type CollectionMatch,
28
29
  type PortfolioValuation,
29
30
  } from "./cli/render";
@@ -401,6 +402,7 @@ async function portfolioHoldings(
401
402
  : []),
402
403
  ...(targetNote ? [targetNote] : []),
403
404
  ...(target.portfolio.brokerInstanceId && !resolved.account ? [BROKER_ACCOUNT_MISSING_NOTE] : []),
405
+ ...(valuation.costAtTodaysRate.size > 0 ? [costAtTodaysRateNote([...valuation.costAtTodaysRate].join(", "))] : []),
404
406
  ];
405
407
 
406
408
  const positionColumns = allocation.weightBasis === "gross"
@@ -442,7 +444,7 @@ async function portfolioHoldings(
442
444
  cash: allocation.cash ? finite(allocation.cash.value) : null,
443
445
  total: allocation.total,
444
446
  // At the regular close, the holdings' move from the close before.
445
- ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
447
+ ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
446
448
  },
447
449
  // Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
448
450
  ...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
@@ -1,4 +1,5 @@
1
- import { isStablecoinCurrency } from "../../../../utils/currency-units";
1
+ import { heldCurrencyUnit, isStablecoinCurrency } from "../../../../utils/currency-units";
2
+ import { isIndexLevel } from "../../../../market-data/market/index-level";
2
3
  import { createRowValueCache } from "../../../../components/ui/row-value-cache";
3
4
  import { PRICE_SPARKLINE_COLUMN_ID } from "../../../../components/price-sparkline/view";
4
5
  import type { AppConfig, ColumnConfig } from "../../../../types/config";
@@ -196,6 +197,27 @@ export function buildTrackedCurrencies(
196
197
  return [...currencies].filter((currency) => !isStablecoinCurrency(currency));
197
198
  }
198
199
 
200
+ /**
201
+ * Whether a row is priced in another currency than the collection's totals
202
+ * (RY in CAD beside JNJ in USD, or a US stock in a CAD portfolio), so its
203
+ * prices need their currency beside them. Pence and cents count as their
204
+ * currency, whose sign the price already carries; an index has none.
205
+ */
206
+ export function holdsOtherCurrencies(
207
+ tickers: readonly TickerRecord[],
208
+ financialsMap: ReadonlyMap<string, TickerFinancials>,
209
+ totalsCurrency: string,
210
+ ): boolean {
211
+ const totals = heldCurrencyUnit(totalsCurrency).currency;
212
+ return tickers.some((ticker) => {
213
+ const quote = financialsMap.get(ticker.metadata.ticker)?.quote;
214
+ if (isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return false;
215
+ const currency = heldCurrencyUnit(ticker.metadata.positions.find((position) => position.currency)?.currency
216
+ || quote?.currency || ticker.metadata.currency).currency;
217
+ return !!currency && currency !== totals;
218
+ });
219
+ }
220
+
199
221
  export function getCollectionTypeFromConfig(config: AppConfig, collectionId: string | null): "portfolio" | "watchlist" | null {
200
222
  if (!collectionId) return null;
201
223
  if (config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
@@ -1,4 +1,6 @@
1
- import { Box } from "../../../../ui";
1
+ import { Box, useUiCapabilities } from "../../../../ui";
2
+ import { brokerPortfolioLogo } from "../../../../brokers/logos";
3
+ import { LogoMark } from "../../../../components/ui/logo-mark";
2
4
  import { describeFundamentalMarketCap } from "../../../../utils/market-capitalization";
3
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
6
  import {
@@ -53,6 +55,7 @@ import {
53
55
  renderSummarySegments,
54
56
  } from "../summary";
55
57
  import { usePortfolioAccountState } from "../summary/live-accounts";
58
+ import { useOfflineBrokerFooter } from "../../shared/broker-reconnect";
56
59
  import {
57
60
  getCollectionEntries,
58
61
  getPortfolioPaneSettings,
@@ -68,7 +71,7 @@ import { PortfolioGrid } from "../grid";
68
71
  import { useThrottledCursorSymbol } from "../use-throttled-cursor-symbol";
69
72
  import { useCursorNeighborPrefetch } from "../use-cursor-neighbor-prefetch";
70
73
  import { getSharedRegistry } from "../../../registry";
71
- import { usePluginAppActions } from "../../../runtime";
74
+ import { usePluginAppActions, usePluginBrokerActions } from "../../../runtime";
72
75
  import { isManualPortfolio } from "../../../../portfolio/mutations";
73
76
  import {
74
77
  cursorAfterRemoval,
@@ -77,8 +80,10 @@ import {
77
80
  tickerRemovalSummary,
78
81
  } from "./remove-ticker";
79
82
  import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
83
+ import { delistingLabel } from "../../../../market-data/quotes/delisting";
80
84
  import {
81
85
  buildTrackedCurrencies,
86
+ holdsOtherCurrencies,
82
87
  getCollectionTickersFromConfig,
83
88
  getCollectionTypeFromConfig,
84
89
  resolveVisibleWarmupRequirements,
@@ -190,12 +195,21 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
190
195
  const flashSymbols = useQuoteFlashMap(financialsMap, valueFlashingEnabled);
191
196
 
192
197
  const accountStateInput = useMemo(() => ({ brokerAccounts, config }), [brokerAccounts, config]);
193
- const { accountState, accountsError, brokerError } = usePortfolioAccountState(currentPortfolio, accountStateInput);
198
+ const { accountState, accountsError, offline } = usePortfolioAccountState(currentPortfolio, accountStateInput);
194
199
  const brokerPortfolio = isPortfolioTab && !!currentPortfolio?.brokerInstanceId;
200
+ // The positions stay as last synced while the broker is away, dated, with one key to bring it back.
201
+ const offlineBrokerFooter = useOfflineBrokerFooter(brokerPortfolio ? offline : null);
195
202
  const portfolioTargets = currentPortfolio?.targetWeights;
203
+ // Totals, money columns and account values are in the portfolio's currency.
204
+ const totalsCurrency = useMemo(
205
+ () => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
206
+ [config.baseCurrency, currentPortfolio],
207
+ );
208
+ // A boolean, so a quote tick does not rebuild the columns.
209
+ const otherCurrencies = holdsOtherCurrencies(tickers, financialsMap, totalsCurrency);
196
210
  const columns = useMemo(
197
- () => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio) }),
198
- [currentPortfolio, isPortfolioTab, paneSettings.columnIds],
211
+ () => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio), otherCurrencies }),
212
+ [currentPortfolio, isPortfolioTab, otherCurrencies, paneSettings.columnIds],
199
213
  );
200
214
  const supplementalData = usePortfolioSupplementalData(tickers, columns, appActive, financialsMap);
201
215
  // One lookup for the whole list, only while a perpetual column is on screen.
@@ -207,11 +221,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
207
221
  [columns],
208
222
  );
209
223
 
210
- // Totals, money columns and account values are in the portfolio's currency.
211
- const totalsCurrency = useMemo(
212
- () => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
213
- [config.baseCurrency, currentPortfolio],
214
- );
215
224
  // The broker account's cash when it reports one, else the cash entered by hand.
216
225
  const cashLine = useMemo(
217
226
  () => isPortfolioTab ? resolvePortfolioCash(currentPortfolio, accountState?.account) : null,
@@ -417,10 +426,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
417
426
  cancelPendingCursorSymbol();
418
427
  setCurrentCollectionId(collectionId);
419
428
  }, [cancelPendingCursorSymbol, setCurrentCollectionId]);
420
- const collectionTabs = useMemo(
421
- () => visibleCollections.map((collection) => ({ label: collection.name, value: collection.id })),
422
- [visibleCollections],
423
- );
429
+ const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
430
+ const desktop = useUiCapabilities().nativePaneChrome === true;
431
+ const collectionTabs = useMemo(() => visibleCollections.map((collection) => {
432
+ const portfolio = desktop && collection.kind === "portfolio"
433
+ ? config.portfolios.find((entry) => entry.id === collection.id)
434
+ : undefined;
435
+ // A broker account's tab leads with its broker's logo on the desktop and the web.
436
+ const logo = portfolio ? brokerPortfolioLogo(portfolio, config.brokerInstances, getBrokerAdapter, getBrokerLogo) : null;
437
+ return {
438
+ label: collection.name,
439
+ value: collection.id,
440
+ ...(logo ? { leading: <LogoMark src={logo.src} name={logo.name} /> } : {}),
441
+ };
442
+ }), [config.brokerInstances, config.portfolios, desktop, getBrokerAdapter, getBrokerLogo, visibleCollections]);
424
443
  const { strip: tabStrip, rows: headerHeight } = usePaneTabs(showCollectionTabs
425
444
  ? {
426
445
  tabs: collectionTabs,
@@ -548,7 +567,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
548
567
  accountState: summaryAccountState,
549
568
  accountStatusText: accountsError
550
569
  ? `Accounts unavailable: ${accountsError}`
551
- : brokerPortfolio ? brokerError ?? (accountState ? undefined : "Acct missing") : undefined,
570
+ : brokerPortfolio && !accountState ? "Acct missing" : undefined,
571
+ brokerOffline: brokerPortfolio && offline ? { dated: offline.syncedAt != null } : undefined,
552
572
  financialsMap,
553
573
  isPortfolioTab,
554
574
  refreshingSize,
@@ -557,9 +577,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
557
577
  }), [financialsMap, sortedTickers], [
558
578
  accountState,
559
579
  accountsError,
560
- brokerError,
561
580
  brokerPortfolio,
562
581
  isPortfolioTab,
582
+ offline,
563
583
  portfolioSummaryTotals,
564
584
  refreshingSize,
565
585
  summaryAccountState,
@@ -637,11 +657,14 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
637
657
 
638
658
  usePaneFooter("portfolio-list", () => ({
639
659
  info: [
660
+ ...offlineBrokerFooter.asOf,
640
661
  ...summaryFooterInfo,
662
+ ...offlineBrokerFooter.status,
641
663
  ...(fxStatusText && !fxWarning ? [{ id: "fx", parts: [{ text: `FX ${fxStatusText}`, tone: "muted" as const }] }] : []),
642
664
  ...(showsPerpetuals && perpetuals.stale ? [{ id: "perpetuals", parts: [{ text: "perpetuals stale", tone: "warning" as const }] }] : []),
643
665
  ],
644
666
  hints: [
667
+ ...offlineBrokerFooter.hints,
645
668
  ...(removalCollection
646
669
  ? [{
647
670
  id: "delete",
@@ -676,6 +699,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
676
699
  fxStatusText,
677
700
  fxWarning,
678
701
  isPortfolioTab,
702
+ offlineBrokerFooter,
679
703
  perpetuals.stale,
680
704
  removalCollection,
681
705
  removeSelectedTicker,
@@ -708,9 +732,12 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
708
732
  ? `${cursorSymbol} market cap: ${describeFundamentalMarketCap(selectedCap.provenance)}.` : undefined;
709
733
  const multiplierNotice = isPortfolioTab && selectedTicker && hasUnknownOptionMultiplier(selectedTicker, activeCollectionId)
710
734
  ? `${cursorSymbol} has no contract multiplier; its value assumes 1x.` : undefined;
735
+ // The row's change cells say DELISTED; the notice gives its last session.
736
+ const selectedDelisting = selectedFinancials?.quote?.delisting;
737
+ const delistedNotice = selectedDelisting ? `${cursorSymbol}: ${delistingLabel(selectedDelisting)}.` : undefined;
711
738
  usePaneNoticeFooter({
712
739
  registrationId: "portfolio-list-notices",
713
- notices: [...summaryNotices, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
740
+ notices: [...summaryNotices, delistedNotice, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
714
741
  focused: focused && !quickAddFocused,
715
742
  });
716
743
  const contentHeight = Math.max(1, height - headerHeight - summaryHeight - drawerHeight - quickAddHeight);
@@ -195,6 +195,7 @@ function describeColumnOption(column: ColumnConfig): string {
195
195
  if (column.id === "perp_premium") return "Premium of the stock's perpetual to its last price.";
196
196
  if (column.id === "perp_funding") return "Funding of the stock's perpetual, per 8 hours.";
197
197
  if (column.id === "as_of") return "When each row's price is from: its last print, or the regular close.";
198
+ if (column.id === "currency") return "The currency each row is priced in; shown by default once a row is in another currency than the totals.";
198
199
  return PORTFOLIO_ONLY_COLUMN_IDS.has(column.id)
199
200
  ? "Visible only when this pane is showing a portfolio."
200
201
  : "Visible for watchlists and portfolios.";
@@ -275,12 +276,29 @@ export function resolvePortfolioPaneCollectionId(
275
276
  return resolveActiveCollectionId(currentCollectionId, visibleCollections);
276
277
  }
277
278
 
279
+ /**
280
+ * A collection holding listings in another currency than its totals shows
281
+ * CCY after LAST, so RY's 278.08 reads as Canadian dollars beside JNJ's US
282
+ * ones, unless the pane's columns already place CCY.
283
+ */
284
+ function withCurrencyColumn(columnIds: string[]): string[] {
285
+ if (columnIds.includes("currency")) return columnIds;
286
+ const price = columnIds.indexOf("price");
287
+ const at = price >= 0 ? price + 1 : Math.min(1, columnIds.length);
288
+ return [...columnIds.slice(0, at), "currency", ...columnIds.slice(at)];
289
+ }
290
+
278
291
  export function resolveVisibleColumns(
279
292
  columnIds: string[],
280
293
  isPortfolioTab: boolean,
281
- { targetWeights = false }: { targetWeights?: boolean } = {},
294
+ { targetWeights = false, otherCurrencies = false }: {
295
+ targetWeights?: boolean;
296
+ /** A row is in another currency than the totals (`holdsOtherCurrencies`): CCY shows. */
297
+ otherCurrencies?: boolean;
298
+ } = {},
282
299
  ): ColumnConfig[] {
283
- const resolved = (isPortfolioTab && targetWeights ? withTargetColumns(columnIds) : columnIds)
300
+ const withTargets = isPortfolioTab && targetWeights ? withTargetColumns(columnIds) : columnIds;
301
+ const resolved = (otherCurrencies ? withCurrencyColumn(withTargets) : withTargets)
284
302
  .map((columnId) => PORTFOLIO_COLUMNS_BY_ID.get(columnId))
285
303
  .filter((column): column is ColumnConfig => column != null)
286
304
  .filter((column) => isPortfolioTab || !PORTFOLIO_ONLY_COLUMN_IDS.has(column.id));
@@ -224,6 +224,15 @@ export function resolvePortfolioAccountState(
224
224
  };
225
225
  }
226
226
 
227
+ /**
228
+ * What the day's move is measured from, after it, as every change line says
229
+ * it ("vs Fri 16 Oct close", "vs mixed closes"), so a narrow row never shows
230
+ * the move without it.
231
+ */
232
+ function dayBasisPart(totals: Pick<PortfolioSummaryTotals, "dayChangeBasis">): PortfolioSummarySegment["parts"] {
233
+ return totals.dayChangeBasis ? [{ text: totals.dayChangeBasis, tone: "muted" }] : [];
234
+ }
235
+
227
236
  /** Headline numbers in priority order, so the narrowest header row keeps the most important ones. */
228
237
  export function buildPortfolioSummarySegments({
229
238
  totals,
@@ -258,6 +267,7 @@ export function buildPortfolioSummarySegments({
258
267
  ? [createSummarySegment("avg-day", [
259
268
  { text: "Avg Day", tone: "label" },
260
269
  { text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: priceColor(totals.avgWatchlistChange), bold: true },
270
+ ...dayBasisPart(totals),
261
271
  ])]
262
272
  : [];
263
273
  }
@@ -332,6 +342,8 @@ export function buildPortfolioSummarySegments({
332
342
  { text: "Day", tone: "label" },
333
343
  { text: money(accountMetrics.dailyPnl, true), tone: "value", color: priceColor(accountMetrics.dailyPnl), bold: true },
334
344
  { text: `(${formatPercentRaw(accountMetrics.dailyPnlPct)})`, tone: "muted", color: priceColor(accountMetrics.dailyPnlPct) },
345
+ // The broker's own day is from its account's close, not the quotes': it names none.
346
+ ...(accountMetrics.brokerDay || !Number.isFinite(accountMetrics.dailyPnl) ? [] : dayBasisPart(totals)),
335
347
  ]));
336
348
  candidates.push(createSummarySegment("pnl", [
337
349
  { text: !Number.isFinite(account?.unrealizedPnl)
@@ -406,6 +418,7 @@ export function layoutPortfolioSummaryHeader(
406
418
  export function buildPortfolioFooterSegments({
407
419
  accountState,
408
420
  accountStatusText,
421
+ brokerOffline,
409
422
  financialsMap,
410
423
  isPortfolioTab,
411
424
  refreshingSize,
@@ -414,17 +427,25 @@ export function buildPortfolioFooterSegments({
414
427
  }: {
415
428
  accountState: PortfolioSummaryAccountState | null;
416
429
  accountStatusText?: string;
430
+ /**
431
+ * The portfolio's broker is offline. The pane says why, which stands in for
432
+ * the missing account and the quote time, and when it knows, when the
433
+ * positions were synced, which also dates the account.
434
+ */
435
+ brokerOffline?: { dated: boolean };
417
436
  financialsMap: Map<string, TickerFinancials>;
418
437
  isPortfolioTab: boolean;
419
438
  refreshingSize: number;
420
439
  sortedTickers: TickerRecord[];
421
440
  totals: PortfolioSummaryTotals;
422
441
  }): PaneFooterSegment[] {
442
+ const source: PaneFooterSegment[] = accountState ? [{ id: "source", parts: [{ text: accountState.sourceLabel, tone: "muted" }] }] : [];
443
+ if (isPortfolioTab && brokerOffline) return brokerOffline.dated ? [] : source;
423
444
  const accountStatus: PaneFooterSegment[] = isPortfolioTab && accountStatusText
424
445
  ? [{ id: "account-status", parts: [{ text: accountStatusText, tone: "muted" }] }]
425
446
  : [];
426
447
  // Cached account numbers stay on screen when a live refresh fails, so the failure sits beside their date.
427
- if (accountState) return [{ id: "source", parts: [{ text: accountState.sourceLabel, tone: "muted" }] }, ...accountStatus];
448
+ if (accountState) return [...source, ...accountStatus];
428
449
  if (isPortfolioTab ? !totals.hasPositions && !accountStatusText : totals.watchlistCount === 0) return [];
429
450
 
430
451
  const lastRefreshTimestamp = getMostRecentQuoteUpdate(
@@ -6,6 +6,9 @@ import type { Portfolio } from "../../../../types/ticker";
6
6
  import type { BrokerAccount } from "../../../../types/trading";
7
7
  import { getBrokerInstance } from "../../../../utils/broker-instances";
8
8
  import { usePluginBrokerActions } from "../../../runtime";
9
+ import type { OfflineBroker } from "../../shared/broker-reconnect";
10
+ import { tf } from "../../../../i18n";
11
+ import { isSignedInBrokerProfile, signedInBrokerId } from "../../../../brokers/signed-in/profile";
9
12
  import { resolvePortfolioAccountState, type ResolvedPortfolioAccountState } from "./index";
10
13
 
11
14
  const EMPTY_ACCOUNTS = { instanceId: null, accounts: [] as BrokerAccount[], error: null };
@@ -85,11 +88,40 @@ export interface PortfolioAccountStateResult {
85
88
  /** Set when the broker refused to list accounts, so "no cash" is not mistaken for a clean empty. */
86
89
  accountsError: string | null;
87
90
  /**
88
- * The broker's last call failed, such as the sync at startup after its
89
- * sign-in lapsed. Accounts are listed only while it is connected, so this
90
- * is the only word of why the account is missing or stale.
91
+ * The portfolio's broker while it cannot be reached, such as after its
92
+ * sign-in lapsed: the positions on screen are its last sync. Accounts are
93
+ * listed only while it is connected, so this is the only word of why the
94
+ * account is missing or stale.
91
95
  */
92
- brokerError: string | null;
96
+ offline: OfflineBroker | null;
97
+ }
98
+
99
+ /**
100
+ * The broker behind a portfolio while Gloom cannot reach it: disconnected,
101
+ * failing, or without the plugin that connects it. Null while it is connected
102
+ * or connecting, before it reports a status, for a profile turned off, and for
103
+ * one that only syncs on demand (IBKR Flex), which is never connected.
104
+ */
105
+ export function resolveOfflineBroker(
106
+ portfolio: Portfolio | null,
107
+ brokerInstance: BrokerInstanceConfig | null | undefined,
108
+ broker: BrokerAdapter | null | undefined,
109
+ status: BrokerConnectionStatus | null,
110
+ ): OfflineBroker | null {
111
+ if (!portfolio || !brokerInstance || brokerInstance.enabled === false) return null;
112
+ const syncedAt = portfolio.lastSyncedAt ?? brokerInstance.lastSyncedAt ?? null;
113
+ if (!broker) {
114
+ // A profile signed in through Gloom names its broker, not the adapter it needs.
115
+ const brokerName = (isSignedInBrokerProfile(brokerInstance) ? signedInBrokerId(brokerInstance) : brokerInstance.brokerType).toUpperCase();
116
+ return { instance: brokerInstance, brokerName, syncedAt, reason: tf("{broker} unavailable", { broker: brokerName }), reconnectable: false };
117
+ }
118
+ if (!status || status.state === "connected" || status.state === "connecting") return null;
119
+ if (status.mode?.toLowerCase() === "flex") return null;
120
+ const brokerName = broker.describeInstance?.(brokerInstance).brokerName ?? broker.name;
121
+ const reason = status.state === "error"
122
+ ? status.message?.trim() || tf("{broker} unavailable", { broker: brokerName })
123
+ : tf("{broker} disconnected", { broker: brokerName });
124
+ return { instance: brokerInstance, brokerName, syncedAt, reason, reconnectable: true };
93
125
  }
94
126
 
95
127
  /**
@@ -117,9 +149,12 @@ export function usePortfolioAccountState(
117
149
  () => resolvePortfolioAccountState(portfolio, state, snapshot),
118
150
  [portfolio, snapshot, state.brokerAccounts, state.config],
119
151
  );
120
- const brokerError = live.status?.state === "error" ? live.status.message?.trim() || "Broker unavailable" : null;
152
+ const offline = useMemo(
153
+ () => resolveOfflineBroker(portfolio, brokerInstance, broker, live.status),
154
+ [broker, brokerInstance, live.status, portfolio],
155
+ );
121
156
  return useMemo(
122
- () => ({ accountState, accountsError: live.error, brokerError }),
123
- [accountState, brokerError, live.error],
157
+ () => ({ accountState, accountsError: live.error, offline }),
158
+ [accountState, live.error, offline],
124
159
  );
125
160
  }