gloomberb 0.16.2 → 0.16.4

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 (527) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -3,6 +3,8 @@ import type {
3
3
  GloomPlugin,
4
4
  GloomPluginContext,
5
5
  } from "../../types/plugin";
6
+ import { pluginContextInNamespace } from "../registry/plugin-state";
7
+ import { withPluginStateNamespace } from "../runtime/context";
6
8
 
7
9
  type PluginMetadataKey =
8
10
  | "id"
@@ -21,6 +23,22 @@ type PluginMetadata = Pick<GloomPlugin, PluginMetadataKey>;
21
23
  // Brokers and declared hosts belong to external plugins; no built-in module contributes them.
22
24
  export type PluginModule = Omit<GloomPlugin, PluginMetadataKey | "broker" | "hosts">;
23
25
 
26
+ /** A module with a say in how it is composed. A bare module takes the defaults. */
27
+ interface PluginModuleEntry {
28
+ module: PluginModule;
29
+ /**
30
+ * The state namespace the module keeps when it sits in a plugin whose own
31
+ * namespace is different: its panes, tabs and slots read pane, plugin and
32
+ * config state there, and its setup gets persistence and caches there. Set
33
+ * it when a module moves to another plugin so its users keep their state.
34
+ */
35
+ stateId?: string;
36
+ /** Left out of the plugin the desktop backend process runs. */
37
+ rendererOnly?: boolean;
38
+ }
39
+
40
+ type PluginModuleInput = PluginModule | PluginModuleEntry;
41
+
24
42
  const HANDLED_MODULE_KEYS = [
25
43
  "cliCommands",
26
44
  "setup",
@@ -36,7 +54,43 @@ const ALL_MODULE_KEYS_HANDLED: MissingModuleKey extends never ? true : never = t
36
54
  void ALL_MODULE_KEYS_HANDLED;
37
55
 
38
56
  interface CompositePluginOptions extends PluginMetadata {
39
- modules: readonly PluginModule[];
57
+ modules: readonly PluginModuleInput[];
58
+ }
59
+
60
+ function moduleEntry(input: PluginModuleInput): PluginModuleEntry {
61
+ return "module" in input ? input : { module: input };
62
+ }
63
+
64
+ /**
65
+ * The module as it runs under a namespace other than its plugin's: every
66
+ * surface it renders and the context its setup receives are moved there,
67
+ * while what it contributes still belongs to the plugin.
68
+ */
69
+ function moduleInNamespace(module: PluginModule, stateId: string): PluginModule {
70
+ const slots = module.slots && Object.fromEntries(Object.entries(module.slots).map(([slotName, renderer]) => [
71
+ slotName,
72
+ renderer && withPluginStateNamespace(stateId, renderer as (props: unknown) => ReactNode),
73
+ ])) as PluginModule["slots"];
74
+ return {
75
+ ...module,
76
+ ...(module.panes ? {
77
+ panes: module.panes.map((pane) => ({ ...pane, component: withPluginStateNamespace(stateId, pane.component) })),
78
+ } : {}),
79
+ ...(slots ? { slots } : {}),
80
+ ...(module.setup ? {
81
+ setup: (ctx: GloomPluginContext) => {
82
+ const scoped = pluginContextInNamespace(ctx, stateId);
83
+ return module.setup!({
84
+ ...scoped,
85
+ registerPane: (pane) => ctx.registerPane({ ...pane, component: withPluginStateNamespace(stateId, pane.component) }),
86
+ registerTickerResearchTab: (tab) => ctx.registerTickerResearchTab({
87
+ ...tab,
88
+ component: withPluginStateNamespace(stateId, tab.component),
89
+ }),
90
+ });
91
+ },
92
+ } : {}),
93
+ };
40
94
  }
41
95
 
42
96
  function composeSlots(modules: readonly PluginModule[]): GloomPlugin["slots"] {
@@ -65,14 +119,23 @@ function composeSlots(modules: readonly PluginModule[]): GloomPlugin["slots"] {
65
119
  return slots as GloomPlugin["slots"];
66
120
  }
67
121
 
122
+ const compositions = new WeakMap<GloomPlugin, CompositePluginOptions>();
123
+ const backendVariants = new WeakMap<GloomPlugin, GloomPlugin>();
124
+
68
125
  /**
69
126
  * Combines implementation modules under one real plugin identity.
70
127
  *
71
128
  * Modules deliberately have no identity or toggle metadata. Every contribution
72
- * is owned, persisted, enabled, and disposed through the returned parent plugin.
129
+ * is owned, enabled, and disposed through the returned parent plugin, and
130
+ * persisted under its namespace unless the module's entry keeps another.
73
131
  */
74
132
  export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlugin {
75
- const { modules, ...metadata } = options;
133
+ const { modules: inputs, ...metadata } = options;
134
+ const pluginStateId = metadata.stateId ?? metadata.id;
135
+ const modules = inputs.map((input) => {
136
+ const { module, stateId } = moduleEntry(input);
137
+ return stateId && stateId !== pluginStateId ? moduleInNamespace(module, stateId) : module;
138
+ });
76
139
  const cliCommands = modules.flatMap((module) => module.cliCommands ?? []);
77
140
  const panes = modules.flatMap((module) => module.panes ?? []);
78
141
  const paneTemplates = modules.flatMap((module) => module.paneTemplates ?? []);
@@ -80,7 +143,7 @@ export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlug
80
143
  const slots = composeSlots(modules);
81
144
  let startedModules: PluginModule[] = [];
82
145
 
83
- return {
146
+ const plugin: GloomPlugin = {
84
147
  ...metadata,
85
148
  ...(cliCommands.length > 0 ? { cliCommands } : {}),
86
149
  ...(panes.length > 0 ? { panes } : {}),
@@ -114,4 +177,24 @@ export function composeBuiltinPlugin(options: CompositePluginOptions): GloomPlug
114
177
  if (firstError) throw firstError;
115
178
  },
116
179
  };
180
+ compositions.set(plugin, options);
181
+ return plugin;
182
+ }
183
+
184
+ /**
185
+ * The plugin as the desktop backend process runs it: the same identity
186
+ * without its renderer-only modules. Anything else comes back unchanged.
187
+ */
188
+ export function withoutRendererOnlyModules(plugin: GloomPlugin): GloomPlugin {
189
+ const options = compositions.get(plugin);
190
+ if (!options?.modules.some((input) => moduleEntry(input).rendererOnly)) return plugin;
191
+ let variant = backendVariants.get(plugin);
192
+ if (!variant) {
193
+ variant = composeBuiltinPlugin({
194
+ ...options,
195
+ modules: options.modules.filter((input) => !moduleEntry(input).rendererOnly),
196
+ });
197
+ backendVariants.set(plugin, variant);
198
+ }
199
+ return variant;
117
200
  }
@@ -139,6 +139,13 @@ export function resolvePortfolioNetLiquidation(
139
139
  return convertAccountValue(account.netLiquidation) + brokerSnapshotDelta(totals, account, basis, convertAccountValue).net;
140
140
  }
141
141
 
142
+ /** Gross exposure per unit of equity; above 1 the account borrows or is short. */
143
+ export function resolvePortfolioLeverage(grossValue: number, netLiquidation: number | null | undefined): number | null {
144
+ return isFiniteNumber(netLiquidation) && netLiquidation > 0 && Number.isFinite(grossValue) && grossValue >= 0
145
+ ? grossValue / netLiquidation
146
+ : null;
147
+ }
148
+
142
149
  export function resolvePortfolioAccountMetrics(
143
150
  totals: PortfolioSummaryTotals,
144
151
  account?: BrokerAccount | null,
@@ -167,9 +174,17 @@ export function resolvePortfolioAccountMetrics(
167
174
  : isFiniteNumber(account?.netLiquidation)
168
175
  ? convertAccountValue(account.netLiquidation) - brokerDailyPnl
169
176
  : null;
177
+ // Without the broker's day, the quotes' day is still a move of the equity,
178
+ // which margin puts below the positions the totals divide by.
179
+ const quotedPreviousEquity = brokerDailyPnl == null && account && isFiniteNumber(account.netLiquidation)
180
+ && Number.isFinite(dailyPnl)
181
+ ? convertAccountValue(account.netLiquidation) + delta.net - dailyPnl
182
+ : null;
170
183
  const dailyPnlPct = previousNetLiquidation != null
171
184
  ? percentChange(dailyPnl, previousNetLiquidation)
172
- : totals.dailyPnlPct;
185
+ : quotedPreviousEquity != null && quotedPreviousEquity > 0
186
+ ? percentChange(dailyPnl, quotedPreviousEquity)
187
+ : totals.dailyPnlPct;
173
188
 
174
189
  const liveUnrealizedPnl = liveTotal(totals, totals.unrealizedPnl);
175
190
  const brokerUnrealizedPnl = liveUnrealizedPnl == null && isFiniteNumber(account?.unrealizedPnl)
@@ -17,6 +17,9 @@ import { formatNumber } from "../../../utils/format";
17
17
  /** The cash line's symbol in targets and reports. */
18
18
  export const CASH_SYMBOL = "CASH";
19
19
 
20
+ /** A broker portfolio without its account: margin can put its positions far above what it is worth. */
21
+ export const BROKER_ACCOUNT_MISSING_NOTE = "The broker account has not synced on this device, so its cash and margin are unknown: the total is the positions alone, not the account's value.";
22
+
20
23
  /** One ticker of a portfolio, valued in the portfolio's currency. */
21
24
  export interface AllocationHolding {
22
25
  symbol: string;
@@ -6,17 +6,16 @@ import type { BrokerAccount } from "../../../types/trading";
6
6
  import { resolvePortfolioAccountState } from "./summary";
7
7
 
8
8
  /**
9
- * The broker account a portfolio was last synced with, as saved on this
10
- * device, for reports that run without a broker connection. The pane reads
11
- * the same account from app state.
9
+ * Every profile's broker accounts as last synced on this device, by profile,
10
+ * for reports and screenshots that run without a broker connection. The pane
11
+ * reads the same accounts from app state.
12
12
  */
13
- export function findCachedPortfolioAccount(
14
- config: AppConfig,
15
- portfolio: Portfolio,
13
+ export function peekCachedBrokerAccounts(
14
+ config: Pick<AppConfig, "brokerInstances">,
16
15
  resources: Pick<AppResourceStorePort, "list"> | null | undefined,
17
- ): BrokerAccount | null {
18
- if (!resources || !portfolio.brokerInstanceId) return null;
16
+ ): Record<string, BrokerAccount[]> {
19
17
  const brokerAccounts: Record<string, BrokerAccount[]> = {};
18
+ if (!resources) return brokerAccounts;
20
19
  for (const instance of config.brokerInstances) {
21
20
  try {
22
21
  const accounts = peekPersistedBrokerAccounts(resources, instance);
@@ -25,9 +24,19 @@ export function findCachedPortfolioAccount(
25
24
  // An unreadable snapshot leaves the portfolio without account figures.
26
25
  }
27
26
  }
27
+ return brokerAccounts;
28
+ }
29
+
30
+ /** The broker account a portfolio was last synced with, as saved on this device. */
31
+ export function findCachedPortfolioAccount(
32
+ config: AppConfig,
33
+ portfolio: Portfolio,
34
+ resources: Pick<AppResourceStorePort, "list"> | null | undefined,
35
+ ): BrokerAccount | null {
36
+ if (!resources || !portfolio.brokerInstanceId) return null;
28
37
  return resolvePortfolioAccountState(
29
38
  portfolio,
30
- { config, brokerAccounts },
39
+ { config, brokerAccounts: peekCachedBrokerAccounts(config, resources) },
31
40
  { status: null, accounts: [] },
32
41
  )?.account ?? null;
33
42
  }
@@ -11,7 +11,7 @@ import type { AppConfig } from "../../../../types/config";
11
11
  import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin";
12
12
  import type { Portfolio, TickerRecord } from "../../../../types/ticker";
13
13
  import type { TickerRepository } from "../../../../data/ticker-repository";
14
- import { canonicalExchange, parsePublicTickerKey } from "../../../../utils/exchanges";
14
+ import { canonicalTickerKey, parsePublicTickerKey } from "../../../../utils/exchanges";
15
15
  import {
16
16
  addTickerToPortfolio,
17
17
  adoptFirstPositionCurrency,
@@ -38,7 +38,6 @@ import {
38
38
  } from "../allocation";
39
39
  import { findCachedPortfolioAccount } from "../cached-account";
40
40
  import {
41
- describeTicker,
42
41
  failPortfolioCommand,
43
42
  isCashArgument,
44
43
  listingFields,
@@ -204,12 +203,19 @@ async function setPositionCommand(
204
203
  await store.saveTicker(result.ticker);
205
204
  const adopted = firstPosition ? adoptFirstPositionCurrency(config, portfolio.id, currency) : null;
206
205
  if (adopted) await saveConfig(adopted);
207
- console.log(cliStyles.success(`Set position for ${describeTicker(result.ticker)} in "${portfolio.name}".`));
208
- console.log(renderStats([
209
- ["Shares", formatMarketQuantity(shares, { assetCategory: result.ticker.metadata.assetCategory })],
210
- ["Average Cost", formatMarketCostWithCurrency(avgCost, currency, { assetCategory: result.ticker.metadata.assetCategory })],
211
- ["Currency", currency],
212
- ]));
206
+ const saved = savedListingName(result.ticker);
207
+ ctx.printResult({
208
+ data: { changed: true, ...listingFields(saved), portfolio: portfolio.name, shares, avgCost, currency },
209
+ }, {
210
+ text: () => [
211
+ cliStyles.success(`Set position for ${saved.label} in "${portfolio.name}".`),
212
+ renderStats([
213
+ ["Shares", formatMarketQuantity(shares, { assetCategory: result.ticker.metadata.assetCategory })],
214
+ ["Average Cost", formatMarketCostWithCurrency(avgCost, currency, { assetCategory: result.ticker.metadata.assetCategory })],
215
+ ["Currency", currency],
216
+ ]),
217
+ ].join("\n"),
218
+ });
213
219
  } catch (error) {
214
220
  failPortfolioCommand(ctx, error, `Failed to set position for ${symbol} in "${portfolioName}".`);
215
221
  }
@@ -275,7 +281,8 @@ async function clearCashCommand(name: string, ctx: CliCommandContext) {
275
281
  });
276
282
  }
277
283
 
278
- function printTargetSumNote(config: AppConfig, portfolioId: string, tickers: readonly TickerRecord[]) {
284
+ /** The note under a target change when the targets do not add up to the whole portfolio, else null. */
285
+ function targetSumNote(config: AppConfig, portfolioId: string, tickers: readonly TickerRecord[]): string | null {
279
286
  const portfolio = config.portfolios.find((entry) => entry.id === portfolioId);
280
287
  const listed = new Set([CASH_SYMBOL, ...tickers.filter((ticker) => ticker.metadata.portfolios.includes(portfolioId)).map((ticker) => ticker.metadata.ticker)]);
281
288
  let sum: number | null = null;
@@ -283,24 +290,32 @@ function printTargetSumNote(config: AppConfig, portfolioId: string, tickers: rea
283
290
  if (listed.has(symbol)) sum = (sum ?? 0) + weight;
284
291
  }
285
292
  const note = describeTargetSum(sum);
286
- if (note) console.log(cliStyles.muted(note));
293
+ return note ? cliStyles.muted(note) : null;
287
294
  }
288
295
 
289
- /** The portfolio's own ticker for a symbol, without a search when it is already listed. */
296
+ /**
297
+ * The portfolio's own row for a listing, without a search when it is already
298
+ * listed. A named exchange (SAN:EPA, or SAN --exchange EPA) matches only a row
299
+ * on that venue, whatever key the row is saved under: a bare SAN row is one
300
+ * listing, Paris or New York, never both. A bare SAN takes the row saved under
301
+ * that key, else any row of the symbol.
302
+ */
290
303
  async function findListedTicker(store: TickerRepository, portfolio: Portfolio, symbol: string, exchange?: string): Promise<TickerRecord | null> {
291
304
  const listing = parseListingArg(symbol, exchange);
292
- const typed = symbol.trim().toUpperCase();
293
305
  const listed = (await store.loadAllTickers()).filter((ticker) => ticker.metadata.portfolios.includes(portfolio.id));
306
+ const savedAs = (ticker: TickerRecord, key: string) => ticker.metadata.ticker.toUpperCase() === key;
307
+ if (!listing.exchange) {
308
+ const sameSymbol = listed.filter((ticker) => parsePublicTickerKey(ticker.metadata.ticker).symbol === listing.symbol);
309
+ return sameSymbol.find((ticker) => savedAs(ticker, listing.symbol)) ?? sameSymbol[0] ?? null;
310
+ }
311
+ const onVenue = listed.filter((ticker) => canonicalTickerKey(ticker.metadata.ticker, ticker.metadata.exchange) === listing.key);
294
312
  // The key as saved (VTI:XMEX), else the same listing under another spelling of its venue (VTI:BMV).
295
- return listed.find((ticker) => [typed, listing.key].includes(ticker.metadata.ticker.toUpperCase()))
296
- ?? listed.find((ticker) => {
297
- const saved = parsePublicTickerKey(ticker.metadata.ticker);
298
- return saved.symbol === listing.symbol
299
- && (!listing.exchange || canonicalExchange(saved.exchange ?? ticker.metadata.exchange) === listing.exchange);
300
- })
301
- ?? null;
313
+ return onVenue.find((ticker) => savedAs(ticker, listing.key)) ?? onVenue[0] ?? null;
302
314
  }
303
315
 
316
+ /** What a target command reports for the cash line, in the fields a listing has. */
317
+ const CASH_LISTING = { key: CASH_SYMBOL, symbol: CASH_SYMBOL, exchange: "", name: null };
318
+
304
319
  async function setTargetCommand(rest: string[], exchange: string | undefined, ctx: CliCommandContext) {
305
320
  const rawWeight = rest.at(-1);
306
321
  const symbol = rest.at(-2);
@@ -313,8 +328,12 @@ async function setTargetCommand(rest: string[], exchange: string | undefined, ct
313
328
  if (isCashArgument(symbol, exchange)) {
314
329
  const next = setPortfolioTargetWeight(config, portfolio.id, CASH_SYMBOL, weight);
315
330
  await saveConfig(next);
316
- console.log(cliStyles.success(`Set the cash target in "${portfolio.name}" to ${formatAllocationWeight(weight)}.`));
317
- printTargetSumNote(next, portfolio.id, await store.loadAllTickers());
331
+ const note = targetSumNote(next, portfolio.id, await store.loadAllTickers());
332
+ ctx.printResult({
333
+ data: { changed: true, ...listingFields(CASH_LISTING), portfolio: portfolio.name, targetWeight: weight },
334
+ }, {
335
+ text: () => [cliStyles.success(`Set the cash target in "${portfolio.name}" to ${formatAllocationWeight(weight)}.`), note].filter(Boolean).join("\n"),
336
+ });
318
337
  return;
319
338
  }
320
339
  let ticker = await findListedTicker(store, portfolio, symbol, exchange);
@@ -324,7 +343,7 @@ async function setTargetCommand(rest: string[], exchange: string | undefined, ct
324
343
  if (!resolved.metadata.portfolios.includes(portfolio.id)) {
325
344
  if (!isManualPortfolio(portfolio)) {
326
345
  throw new PortfolioCliError(
327
- `${describeTicker(resolved)} is not in "${portfolio.name}".`,
346
+ `${savedListingName(resolved).label} is not in "${portfolio.name}".`,
328
347
  "A broker portfolio lists what the broker holds; set targets on its holdings.",
329
348
  );
330
349
  }
@@ -338,9 +357,17 @@ async function setTargetCommand(rest: string[], exchange: string | undefined, ct
338
357
  }
339
358
  const next = setPortfolioTargetWeight(config, portfolio.id, ticker.metadata.ticker, weight);
340
359
  await saveConfig(next);
341
- if (added) console.log(cliStyles.success(`Added ${describeTicker(ticker)} to "${portfolio.name}".`));
342
- console.log(cliStyles.success(`Set the target for ${describeTicker(ticker)} in "${portfolio.name}" to ${formatAllocationWeight(weight)}.`));
343
- printTargetSumNote(next, portfolio.id, await store.loadAllTickers());
360
+ const saved = savedListingName(ticker);
361
+ const note = targetSumNote(next, portfolio.id, await store.loadAllTickers());
362
+ ctx.printResult({
363
+ data: { changed: true, ...listingFields(saved), portfolio: portfolio.name, targetWeight: weight, added },
364
+ }, {
365
+ text: () => [
366
+ added ? cliStyles.success(`Added ${saved.label} to "${portfolio.name}".`) : null,
367
+ cliStyles.success(`Set the target for ${saved.label} in "${portfolio.name}" to ${formatAllocationWeight(weight)}.`),
368
+ note,
369
+ ].filter(Boolean).join("\n"),
370
+ });
344
371
  } catch (error) {
345
372
  failPortfolioCommand(ctx, error, `Failed to set the target for ${symbol} in "${name}".`);
346
373
  }
@@ -355,21 +382,28 @@ async function clearTargetCommand(rest: string[], exchange: string | undefined,
355
382
  const whole = config.portfolios.some((entry) => [entry.id, entry.name].some((value) => value.toLowerCase() === rest.join(" ").trim().toLowerCase()));
356
383
  const portfolio = requirePortfolio(config, whole ? rest.join(" ") : rest.slice(0, -1).join(" "));
357
384
  const rawSymbol = whole ? undefined : rest.at(-1);
358
- const symbol = rawSymbol == null ? undefined
359
- : isCashArgument(rawSymbol, exchange) ? CASH_SYMBOL
360
- : (await findListedTicker(store, portfolio, rawSymbol, exchange))?.metadata.ticker ?? parseListingArg(rawSymbol, exchange).key;
385
+ const cash = rawSymbol != null && isCashArgument(rawSymbol, exchange);
386
+ const listing = rawSymbol == null || cash ? null : parseListingArg(rawSymbol, exchange);
387
+ // The row the listing is: the portfolio's own, else any saved one; a target is kept under that row's key.
388
+ const ticker = listing ? await findListedTicker(store, portfolio, rawSymbol!, exchange) ?? await loadSavedListing(store, listing) : null;
389
+ const saved = ticker ? savedListingName(ticker) : null;
390
+ const symbol = cash ? CASH_SYMBOL : ticker?.metadata.ticker ?? listing?.key;
361
391
  const { config: next, removed } = clearPortfolioTargetWeights(config, portfolio.id, symbol);
392
+ const fields = cash ? listingFields(CASH_LISTING) : saved ? listingFields(saved) : listing ? listingFields({ ...listing, name: null }) : {};
393
+ const named = saved?.label ?? listing?.key ?? CASH_SYMBOL;
394
+ const data = { changed: removed.length > 0, ...fields, portfolio: portfolio.name, removedTargets: removed.length };
362
395
  if (removed.length === 0) {
363
- console.log(cliStyles.warning(symbol ? `"${portfolio.name}" has no target for ${symbol}.` : `"${portfolio.name}" has no targets.`));
396
+ ctx.printResult({ data }, { text: () => cliStyles.warning(symbol ? `"${portfolio.name}" has no target for ${named}.` : `"${portfolio.name}" has no targets.`) });
364
397
  return;
365
398
  }
366
399
  await saveConfig(next);
367
- if (symbol) {
368
- const ticker = symbol === CASH_SYMBOL ? null : await store.loadTicker(symbol);
369
- console.log(cliStyles.success(`Cleared the ${symbol === CASH_SYMBOL ? "cash target" : `target for ${ticker ? describeTicker(ticker) : symbol}`} in "${portfolio.name}".`));
370
- } else {
371
- console.log(cliStyles.success(`Cleared ${removed.length} target${removed.length === 1 ? "" : "s"} in "${portfolio.name}".`));
372
- }
400
+ ctx.printResult({ data }, {
401
+ text: () => cliStyles.success(
402
+ symbol
403
+ ? `Cleared the ${cash ? "cash target" : `target for ${named}`} in "${portfolio.name}".`
404
+ : `Cleared ${removed.length} target${removed.length === 1 ? "" : "s"} in "${portfolio.name}".`,
405
+ ),
406
+ });
373
407
  } catch (error) {
374
408
  failPortfolioCommand(ctx, error, `Failed to clear targets in "${rest.join(" ")}".`);
375
409
  }
@@ -407,7 +441,8 @@ async function showTargetsCommand(name: string, ctx: CliCommandContext) {
407
441
  [{ header: "Ticker" }, { header: "Name" }, { header: "Target", align: "right" }],
408
442
  rows.map((row) => [row.symbol, row.listed ? row.name ?? "" : cliStyles.muted("not in this portfolio"), formatAllocationWeight(row.targetWeight)]),
409
443
  ));
410
- printTargetSumNote(config, portfolio.id, tickers);
444
+ const note = targetSumNote(config, portfolio.id, tickers);
445
+ if (note) console.log(note);
411
446
  } catch (error) {
412
447
  failPortfolioCommand(ctx, error, `Failed to show targets for "${name}".`);
413
448
  }
@@ -12,7 +12,16 @@ import { currencyMinorDigits, formatMarketCostWithCurrency, formatMarketPriceWit
12
12
  import { resolvePriceBasis } from "../../../../market-data/market/price-basis";
13
13
  import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../position-metrics";
14
14
  import { resolvePortfolioTotalsCurrency } from "../summary/totals";
15
- import { exchangeShortName, getActiveQuoteDisplay } from "../../../../market-data/market/status";
15
+ import {
16
+ EXTENDED_SESSION_LABELS,
17
+ exchangeShortName,
18
+ getActiveQuoteDisplay,
19
+ getExtendedSessionDisplay,
20
+ type ActiveQuoteDisplay,
21
+ type ExtendedSession,
22
+ type ExtendedSessionDisplay,
23
+ } from "../../../../market-data/market/status";
24
+ import { headlineQuoteDisplay } from "../column-values";
16
25
  import type { AppConfig } from "../../../../types/config";
17
26
  import type { DataProvider } from "../../../../types/data-provider";
18
27
  import type { CliCommandContext } from "../../../../types/plugin";
@@ -24,6 +33,7 @@ import { cliFreshnessFooter } from "../../../../cli/result";
24
33
  import { quotesFreshness } from "../../../../cli/freshness";
25
34
  import type { BrokerAccount } from "../../../../types/trading";
26
35
  import {
36
+ BROKER_ACCOUNT_MISSING_NOTE,
27
37
  CASH_SYMBOL,
28
38
  buildPortfolioAllocation,
29
39
  describeTargetSum,
@@ -46,6 +56,38 @@ function priceFormatOptions(quote: Parameters<typeof quoteFormatOptions>[0] & {
46
56
  : options;
47
57
  }
48
58
 
59
+ /**
60
+ * What a collection's Last and Chg show, as the PF and watchlist panes do: the
61
+ * traded price until the regular close, then that close and its move, with a
62
+ * pre-market or after-hours print beside them, measured from that close.
63
+ * Market value and P&L keep the live price.
64
+ */
65
+ export function collectionQuoteHeadline(quote: CollectionQuote | undefined): {
66
+ headline: ActiveQuoteDisplay | null;
67
+ extended: ExtendedSessionDisplay | null;
68
+ } {
69
+ const traded = getActiveQuoteDisplay(quote);
70
+ const headline = traded && Number.isFinite(traded.price) ? headlineQuoteDisplay(traded, quote) : null;
71
+ const extended = headline ? getExtendedSessionDisplay(quote) : null;
72
+ return { headline, extended: extended && Number.isFinite(extended.price) ? extended : null };
73
+ }
74
+
75
+ /** The extended print as export fields: all null without one. */
76
+ export function extendedQuoteFields(extended: ExtendedSessionDisplay | null) {
77
+ return {
78
+ extendedSession: extended?.session ?? null,
79
+ extendedPrice: extended?.price ?? null,
80
+ extendedChange: extended?.change ?? null,
81
+ extendedChangePercent: extended?.changePercent ?? null,
82
+ };
83
+ }
84
+
85
+ /** The extended sessions among the rows' prints, in the order their columns go. */
86
+ export function extendedSessionsOf(sessions: Iterable<ExtendedSession | null | undefined>): ExtendedSession[] {
87
+ const present = new Set(sessions);
88
+ return (["PRE", "POST"] as const).filter((session) => present.has(session));
89
+ }
90
+
49
91
  export function renderCollectionOverview(config: AppConfig, tickers: TickerRecord[]): string {
50
92
  const blocks: string[] = [];
51
93
 
@@ -142,7 +184,12 @@ export async function loadCollectionQuotes(
142
184
  interface PortfolioPositionValue {
143
185
  ticker: TickerRecord;
144
186
  quote: CollectionQuote | undefined;
187
+ /** The live print, which values the position. */
145
188
  activeQuote: ReturnType<typeof getActiveQuoteDisplay> | null;
189
+ /** Last and Chg as shown: the regular session's (collectionQuoteHeadline). */
190
+ headline: ActiveQuoteDisplay | null;
191
+ /** A pre-market or after-hours print, from the close in `headline`. */
192
+ extended: ExtendedSessionDisplay | null;
146
193
  /** Null for a ticker kept in the portfolio without an open position. */
147
194
  position: TickerPosition | null;
148
195
  metrics: ReturnType<typeof getPortfolioPositionMetrics> | null;
@@ -198,14 +245,15 @@ export async function valuePortfolioPositions({
198
245
  const positions = ticker.metadata.positions.filter((position) => position.portfolio === id && position.shares !== 0);
199
246
  const displayedQuote = getActiveQuoteDisplay(quote);
200
247
  const activeQuote = displayedQuote && Number.isFinite(displayedQuote.price) ? displayedQuote : null;
248
+ const { headline, extended } = collectionQuoteHeadline(quote);
201
249
 
202
250
  if (positions.length === 0) {
203
251
  const quoteCurrency = quote?.currency || ticker.metadata.currency || baseCurrency;
204
252
  const unitPrice = activeQuote ? known(await toBase(activeQuote.price, quoteCurrency)) : null;
205
253
  holdings.push({ symbol: ticker.metadata.ticker, held: false, marketValue: 0, units: 0, unitPrice });
206
- values.push({ ticker, quote, activeQuote, position: null, metrics: null, pnl: null,
254
+ values.push({ ticker, quote, activeQuote, headline, extended, position: null, metrics: null, pnl: null,
207
255
  row: { symbol: ticker.metadata.ticker, exchange: ticker.metadata.exchange, shares: null, avgCost: null,
208
- positionCurrency: null, quotePrice: known(activeQuote?.price), quoteCurrency: quote?.currency ?? null,
256
+ positionCurrency: null, quotePrice: known(headline?.price), ...extendedQuoteFields(extended), quoteCurrency: quote?.currency ?? null,
209
257
  costBasis: null, marketValue: null, unrealizedPnl: null, baseCurrency: currency } });
210
258
  continue;
211
259
  }
@@ -237,11 +285,12 @@ export async function valuePortfolioPositions({
237
285
  holding.marketValue = holding.marketValue == null || marketValue == null ? null : holding.marketValue + marketValue;
238
286
  holding.units += metrics.totalShares;
239
287
  holding.unitPrice ??= valuationQuote ? known(await toBase(valuationQuote.price, quoteCurrency)) : null;
240
- values.push({ ticker, quote, activeQuote, position, metrics, pnl,
288
+ values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl,
241
289
  row: { symbol: ticker.metadata.ticker, exchange: ticker.metadata.exchange,
242
290
  shares: metrics.totalShares, avgCost: known(position.avgCost), positionCurrency,
243
291
  priceBasis: position.priceBasis ?? null, quantityUnit: metrics.priceBasis === "percent-of-par" ? "face" : null, quotePriceBasis: quote?.priceBasis ?? null,
244
- quotePrice: known(activeQuote?.price), quoteCurrency, quoteAsOf: quote?.lastUpdated ?? null,
292
+ // Last as shown, the regular close after it; market value and P&L are at the live price.
293
+ quotePrice: known(headline?.price), ...extendedQuoteFields(extended), quoteCurrency, quoteAsOf: quote?.lastUpdated ?? null,
245
294
  costBasis: known(direction * costBasisBase), marketValue,
246
295
  unrealizedPnl: known(pnl), baseCurrency: currency, dateAcquired: position.dateAcquired ?? null,
247
296
  pnlBasis: selectedPnl.basis, brokerUnrealizedPnl: known(position.unrealizedPnl), brokerPnlCurrency: positionCurrency, brokerPnlAsOf: null } });
@@ -295,6 +344,18 @@ function signed(text: string, value: number | null | undefined): string {
295
344
 
296
345
  const NOT_AVAILABLE = "n/a";
297
346
 
347
+ /** One cell per extended column: the print and its move from the close, as `quote` prints it, or blank. */
348
+ function extendedCells(
349
+ sessions: readonly ExtendedSession[],
350
+ extended: ExtendedSessionDisplay | null,
351
+ quote: CollectionQuote | undefined,
352
+ assetCategory: string | undefined,
353
+ ): string[] {
354
+ return sessions.map((session) => extended?.session === session && quote
355
+ ? `${formatMarketPriceWithCurrency(extended.price, quote.currency, priceFormatOptions(quote, assetCategory))} ${colorBySign(formatPercentRaw(extended.changePercent), extended.change)}`
356
+ : "");
357
+ }
358
+
298
359
  async function showCollectionWithMarketData(
299
360
  name: string,
300
361
  ctx: CliCommandContext,
@@ -435,18 +496,20 @@ async function showCollectionWithMarketData(
435
496
  return [weight, target, drift, trade, tradeValue];
436
497
  };
437
498
 
438
- const rows: string[][] = valued.map(({ ticker, quote, activeQuote, position, metrics, pnl, row }, index) => {
439
- const priceText = quote && activeQuote
440
- ? colorBySign(formatMarketPriceWithCurrency(activeQuote.price, quote.currency, priceFormatOptions(quote, ticker.metadata.assetCategory)), activeQuote.change)
499
+ const extendedSessions = extendedSessionsOf(valued.map(({ extended }) => extended?.session));
500
+ const rows: string[][] = valued.map(({ ticker, quote, headline, extended, position, metrics, pnl, row }, index) => {
501
+ const priceText = quote && headline
502
+ ? colorBySign(formatMarketPriceWithCurrency(headline.price, quote.currency, priceFormatOptions(quote, ticker.metadata.assetCategory)), headline.change)
441
503
  : "—";
442
- const changeText = activeQuote ? colorBySign(formatPercentRaw(activeQuote.changePercent), activeQuote.change) : "—";
504
+ const changeText = headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
505
+ const quoteCells = [priceText, changeText, ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory)];
443
506
  const exported = positionsExport[index]!;
444
507
  const figures = exported.weight != null || exported.targetWeight != null || exported.tradeValue != null
445
508
  ? { weight: exported.weight, targetWeight: exported.targetWeight, drift: exported.drift, tradeValue: exported.tradeValue, tradeUnits: exported.tradeShares }
446
509
  : undefined;
447
510
  const symbolUnits = allocationBySymbol.get(ticker.metadata.ticker)?.units ?? 0;
448
511
  if (!position || !metrics) {
449
- return [ticker.metadata.ticker, priceText, changeText, "—", "—", "—",
512
+ return [ticker.metadata.ticker, ...quoteCells, "—", "—", "—",
450
513
  ...allocationCells(figures, { unpriced: false, units: symbolUnits, assetCategory: ticker.metadata.assetCategory }), "—"];
451
514
  }
452
515
  const positionCurrency = metrics.positionCurrency;
@@ -454,8 +517,7 @@ async function showCollectionWithMarketData(
454
517
  const unpriced = marketValue == null;
455
518
  return [
456
519
  ticker.metadata.ticker,
457
- priceText,
458
- changeText,
520
+ ...quoteCells,
459
521
  formatMarketQuantity(metrics.totalShares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency }),
460
522
  formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }),
461
523
  unpriced ? NOT_AVAILABLE : wholeMoney(marketValue),
@@ -465,12 +527,12 @@ async function showCollectionWithMarketData(
465
527
  });
466
528
  if (cashExport) {
467
529
  const label = cashExport.currency && cashExport.currency !== currency ? `${CASH_SYMBOL} ${cashExport.currency}` : CASH_SYMBOL;
468
- rows.push([label, "", "", "", "", cashExport.value == null ? NOT_AVAILABLE : wholeMoney(cashExport.value),
530
+ rows.push([label, "", "", ...extendedSessions.map(() => ""), "", "", cashExport.value == null ? NOT_AVAILABLE : wholeMoney(cashExport.value),
469
531
  ...allocationCells(cashExport, { unpriced: cashExport.value == null }), ""]);
470
532
  }
471
533
  const total = allocation.total;
472
534
  rows.push([
473
- cliStyles.bold("TOTAL"), "", "", "", "",
535
+ cliStyles.bold("TOTAL"), "", "", ...extendedSessions.map(() => ""), "", "",
474
536
  cliStyles.bold(wholeMoney(total)),
475
537
  total != null && total > 0 ? formatAllocationWeight(100) : "—",
476
538
  ...(showTargets ? [formatAllocationWeight(allocation.targetSum), "", "", ""] : []),
@@ -484,6 +546,8 @@ async function showCollectionWithMarketData(
484
546
  { header: "Ticker" },
485
547
  { header: "Last", align: "right", optional: true, dropPriority: 1 },
486
548
  { header: "Chg", align: "right", optional: true, dropPriority: 2 },
549
+ // Before Chg, after Avg Cost, which is right of it at the same priority.
550
+ ...extendedSessions.map((session) => ({ header: EXTENDED_SESSION_LABELS[session], align: "right" as const, optional: true, dropPriority: 3 })),
487
551
  { header: quantityHeader, align: "right" },
488
552
  { header: "Avg Cost", align: "right", optional: true, dropPriority: 3 },
489
553
  { header: "Value", align: "right" },
@@ -507,6 +571,7 @@ async function showCollectionWithMarketData(
507
571
  notes.push(cliStyles.warning(`Total and weights exclude ${count} unpriced holding${count === 1 ? "" : "s"}: ${allocation.unpriced.join(", ")}.`));
508
572
  }
509
573
  if (cashExport?.source === "broker") notes.push(cliStyles.muted("Cash is the broker account's balance as of its last sync."));
574
+ if (matchedPortfolio.brokerInstanceId && !account) notes.push(cliStyles.warning(BROKER_ACCOUNT_MISSING_NOTE));
510
575
  if (cashExport && cashExport.value == null) {
511
576
  notes.push(cliStyles.warning(cashExport.currency
512
577
  ? `No ${cashExport.currency}/${currency} rate for the cash, so the total and weights are unavailable.`
@@ -520,12 +585,15 @@ async function showCollectionWithMarketData(
520
585
  for (const note of notes) console.log(note);
521
586
  } else {
522
587
  const rows: string[][] = [];
588
+ const shown = new Map(filtered.map((ticker) => [ticker, collectionQuoteHeadline(quotes.get(ticker.metadata.ticker))]));
589
+ const extendedSessions = extendedSessionsOf([...shown.values()].map(({ extended }) => extended?.session));
523
590
  for (const ticker of filtered) {
524
591
  const quote = quotes.get(ticker.metadata.ticker);
525
- const priceText = quote
526
- ? colorBySign(formatMarketPriceWithCurrency(quote.price, quote.currency, priceFormatOptions(quote, ticker.metadata.assetCategory)), quote.change)
592
+ const { headline, extended } = shown.get(ticker)!;
593
+ const priceText = quote && headline
594
+ ? colorBySign(formatMarketPriceWithCurrency(headline.price, quote.currency, priceFormatOptions(quote, ticker.metadata.assetCategory)), headline.change)
527
595
  : "—";
528
- const changeText = quote ? colorBySign(formatPercentRaw(quote.changePercent), quote.change) : "—";
596
+ const changeText = headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
529
597
  const marketCapText = quote?.marketCap != null
530
598
  ? `${formatCompact(await toBase(quote.marketCap, quote.currency || ticker.metadata.currency || currency))} ${currency}`
531
599
  : "—";
@@ -535,6 +603,7 @@ async function showCollectionWithMarketData(
535
603
  exchangeShortName(quote?.exchangeName, quote?.fullExchangeName) || ticker.metadata.exchange || "—",
536
604
  priceText,
537
605
  changeText,
606
+ ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
538
607
  marketCapText,
539
608
  ]);
540
609
  }
@@ -545,7 +614,9 @@ async function showCollectionWithMarketData(
545
614
  { header: "Exchange" },
546
615
  { header: "Last", align: "right" },
547
616
  { header: "Chg", align: "right" },
548
- { header: "Mkt Cap", align: "right" },
617
+ ...extendedSessions.map((session) => ({ header: EXTENDED_SESSION_LABELS[session], align: "right" as const, optional: true, dropPriority: 1 })),
618
+ // A narrow terminal drops these first.
619
+ { header: "Mkt Cap", align: "right", optional: true, dropPriority: 2 },
549
620
  ],
550
621
  rows,
551
622
  ));