gloomberb 0.17.6 → 0.18.0

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 (768) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/package.json +6 -1
  4. package/patches/electrobun@1.18.1.patch +74 -0
  5. package/src/account/auth-model.ts +3 -2
  6. package/src/account/device-signin-dialog.tsx +38 -13
  7. package/src/account/device-signin.ts +2 -1
  8. package/src/account/upgrade-dialog.tsx +6 -2
  9. package/src/api-client/askg.ts +5 -3
  10. package/src/api-client/bank-figures.ts +73 -0
  11. package/src/api-client/credit-boards.ts +21 -1
  12. package/src/api-client/crypto-markets.ts +88 -1
  13. package/src/api-client/data.ts +104 -4
  14. package/src/api-client/debt-maturities.ts +18 -0
  15. package/src/api-client/equity-screener.ts +3 -0
  16. package/src/api-client/errors.ts +13 -0
  17. package/src/api-client/fund-facts.ts +72 -0
  18. package/src/api-client/futures-curve.ts +26 -3
  19. package/src/api-client/geo.ts +11 -1
  20. package/src/api-client/index.ts +25 -2
  21. package/src/api-client/insider-signals.ts +95 -11
  22. package/src/api-client/normalizers.ts +1 -0
  23. package/src/api-client/paths.ts +20 -1
  24. package/src/api-client/request.ts +11 -3
  25. package/src/api-client/research-activity.ts +15 -8
  26. package/src/api-client/socket.ts +6 -3
  27. package/src/api-client/supply-chain-graph.ts +2 -2
  28. package/src/api-client/supply-chain.ts +8 -0
  29. package/src/api-client/tape.ts +12 -0
  30. package/src/api-client/types.ts +66 -2
  31. package/src/api-client/upgrade-experiment.ts +4 -3
  32. package/src/api-client/wall-experiments.ts +5 -3
  33. package/src/api-client/wall-summary.ts +3 -1
  34. package/src/api-client/web-experiments.ts +5 -2
  35. package/src/api-client/yield-curves.ts +17 -0
  36. package/src/app/command-bar-intro.ts +38 -0
  37. package/src/app/keybindings/actions.ts +1 -1
  38. package/src/app/pane-runtime/pane-template-runtime.ts +35 -7
  39. package/src/app/pane-runtime/plugin-bindings.ts +4 -0
  40. package/src/app/pane-runtime/ticker-open-runtime.ts +4 -14
  41. package/src/app/pane-runtime/ticker-open-target.ts +31 -0
  42. package/src/app/runtime/desktop-deeplink.ts +3 -4
  43. package/src/app/runtime/plugin-bindings.ts +3 -2
  44. package/src/app/runtime/startup.ts +3 -4
  45. package/src/app/runtime/ticker-refresh.ts +13 -12
  46. package/src/app/runtime/update.ts +3 -2
  47. package/src/architecture/core-builtin-allowlist.json +0 -3
  48. package/src/architecture/silent-catch-allowlist.json +3 -0
  49. package/src/architecture/ui-literals-allowlist.json +186 -0
  50. package/src/architecture/ui-tokens-allowlist.json +40 -0
  51. package/src/ask-gloom/pane-context.ts +3 -1
  52. package/src/brokers/signed-in/catalog.ts +2 -1
  53. package/src/brokers/signed-in/client.ts +7 -2
  54. package/src/brokers/signed-in/sign-in-dialog.tsx +120 -26
  55. package/src/brokers/signed-in/sign-in.ts +22 -20
  56. package/src/brokers/sync-broker-instance.ts +2 -1
  57. package/src/capabilities/chart-series.ts +2 -0
  58. package/src/capabilities/registry.ts +1 -3
  59. package/src/cli/bank-section.ts +147 -0
  60. package/src/cli/base-converter.ts +5 -2
  61. package/src/cli/board-coins.ts +70 -0
  62. package/src/cli/commands/fred-series.ts +90 -0
  63. package/src/cli/commands/market.ts +340 -69
  64. package/src/cli/commands/news-search.ts +35 -2
  65. package/src/cli/commands/options-chain.ts +7 -5
  66. package/src/cli/commands/options-leaps.ts +3 -3
  67. package/src/cli/commands/overview.ts +78 -29
  68. package/src/cli/commands/plugins.ts +1 -0
  69. package/src/cli/commands/search.ts +87 -24
  70. package/src/cli/commands/system.ts +5 -2
  71. package/src/cli/commands/ticker.ts +337 -60
  72. package/src/cli/context.ts +2 -1
  73. package/src/cli/desktop-pane-shot.ts +11 -3
  74. package/src/cli/earnings-calendar.ts +2 -1
  75. package/src/cli/errors.ts +10 -0
  76. package/src/cli/export-source.ts +18 -3
  77. package/src/cli/glossary.ts +196 -14
  78. package/src/cli/history-rows.ts +60 -9
  79. package/src/cli/index-roots.ts +26 -0
  80. package/src/cli/index.ts +34 -9
  81. package/src/cli/known-gaps.ts +156 -22
  82. package/src/cli/listing-arg.ts +156 -12
  83. package/src/cli/listing-suggestion.ts +4 -2
  84. package/src/cli/name-entries.ts +76 -0
  85. package/src/cli/not-a-ticker.ts +15 -6
  86. package/src/cli/pane-functions/access-gate.ts +22 -0
  87. package/src/cli/pane-functions/capabilities.ts +62 -4
  88. package/src/cli/pane-functions/catalog.ts +94 -39
  89. package/src/cli/pane-functions/data.ts +6 -5
  90. package/src/cli/pane-functions/discovery.ts +1 -1
  91. package/src/cli/pane-functions/dom.ts +3 -3
  92. package/src/cli/pane-functions/freshness.ts +49 -8
  93. package/src/cli/pane-functions/function-help.ts +13 -10
  94. package/src/cli/pane-functions/headless.ts +177 -42
  95. package/src/cli/pane-functions/index.ts +64 -8
  96. package/src/cli/pane-functions/options.ts +17 -3
  97. package/src/cli/pane-functions/report-notices.ts +17 -3
  98. package/src/cli/pane-functions/resolver.ts +48 -13
  99. package/src/cli/pane-functions/screenshot.ts +124 -23
  100. package/src/cli/pane-functions/shot-options.ts +68 -0
  101. package/src/cli/perpetual-contract.ts +3 -1
  102. package/src/cli/report-tables.ts +10 -2
  103. package/src/cli/staged-tickers.ts +5 -0
  104. package/src/cli/statement-filings.ts +261 -0
  105. package/src/cli/symbol-args.ts +47 -8
  106. package/src/cli/symbol-list.ts +9 -2
  107. package/src/cli/symbol-rows.ts +269 -29
  108. package/src/cli/ticker-resolution.ts +148 -34
  109. package/src/cli/treasury-yields.ts +86 -0
  110. package/src/cli/types.ts +3 -0
  111. package/src/cli/uncovered-instrument.ts +20 -0
  112. package/src/components/chart/composite/axis-overlays.tsx +16 -9
  113. package/src/components/chart/composite/composite-chart.tsx +85 -6
  114. package/src/components/chart/composite/downsample.ts +19 -3
  115. package/src/components/chart/composite/format.ts +3 -0
  116. package/src/components/chart/composite/legend.tsx +155 -76
  117. package/src/components/chart/composite/panel-bitmap.ts +8 -2
  118. package/src/components/chart/composite/panel-surface.tsx +3 -3
  119. package/src/components/chart/composite/toolbar.tsx +12 -7
  120. package/src/components/chart/composite/types.ts +6 -0
  121. package/src/components/chart/composite/use-composite-chart-keyboard.ts +28 -1
  122. package/src/components/chart/curve/curve-surface.tsx +1 -1
  123. package/src/components/chart/static/series.ts +39 -0
  124. package/src/components/chart/static/trail-chart-surface.tsx +2 -1
  125. package/src/components/command-bar/commands/collection-list.ts +45 -15
  126. package/src/components/command-bar/commands/collection.ts +10 -3
  127. package/src/components/command-bar/commands/registry.ts +2 -2
  128. package/src/components/command-bar/list/badge.ts +4 -3
  129. package/src/components/command-bar/list/model.ts +5 -0
  130. package/src/components/command-bar/list/view.tsx +1 -1
  131. package/src/components/command-bar/panel/index.tsx +19 -8
  132. package/src/components/command-bar/panel/native-surface.ts +6 -4
  133. package/src/components/command-bar/panel/prompt-example.ts +22 -0
  134. package/src/components/command-bar/routes/root/provider-search.ts +3 -9
  135. package/src/components/command-bar/routes/root/results.ts +13 -0
  136. package/src/components/command-bar/routes/root/start-here.ts +90 -0
  137. package/src/components/command-bar/routes/ticker-search/results.ts +42 -3
  138. package/src/components/command-bar/routes/ticker-search/route.ts +3 -10
  139. package/src/components/command-bar/theme-picker.tsx +3 -1
  140. package/src/components/command-bar/workflow/builtin.ts +5 -1
  141. package/src/components/command-bar/workflow/collection-actions.ts +21 -12
  142. package/src/components/command-bar/workflow/ops.ts +21 -1
  143. package/src/{plugins/builtin/portfolio-list/command-bar.ts → components/command-bar/workflow/portfolio-position.ts} +42 -9
  144. package/src/components/command-bar/workflow/tickers.ts +6 -4
  145. package/src/components/feedback-dialog.tsx +1 -1
  146. package/src/components/form-modal/broker-step.tsx +7 -63
  147. package/src/components/form-modal/content.tsx +7 -2
  148. package/src/components/form-modal/field-row.tsx +6 -8
  149. package/src/components/form-modal/host.tsx +3 -3
  150. package/src/components/form-modal/model.ts +3 -0
  151. package/src/components/form-modal/routes.ts +17 -2
  152. package/src/components/index.ts +6 -6
  153. package/src/components/input-search-bar.tsx +43 -6
  154. package/src/components/layout/detached-pane-shell.tsx +0 -1
  155. package/src/components/layout/floating-pane.tsx +0 -1
  156. package/src/components/layout/header.tsx +8 -7
  157. package/src/components/layout/pane/footer/index.tsx +7 -4
  158. package/src/components/layout/pane/footer/model.ts +28 -2
  159. package/src/components/layout/pane/header.tsx +9 -19
  160. package/src/components/layout/pane/index.tsx +0 -1
  161. package/src/components/layout/pane/instrument-coverage-gate.tsx +57 -0
  162. package/src/components/layout/pane/pane-tabs.tsx +4 -3
  163. package/src/components/layout/pane/sizing.ts +9 -0
  164. package/src/components/layout/pane/ticker-fate-gate.tsx +8 -2
  165. package/src/components/layout/shell/pane/capture.ts +44 -20
  166. package/src/components/layout/status-bar.tsx +3 -6
  167. package/src/components/markdown-text.tsx +17 -2
  168. package/src/components/market-board/index.tsx +19 -8
  169. package/src/components/metric-treemap/heat-surface.tsx +2 -7
  170. package/src/components/onboarding/account-step/qr-panel.tsx +5 -2
  171. package/src/components/onboarding/onboarding-frame.tsx +24 -21
  172. package/src/components/onboarding/portfolio-step/positions-panel.tsx +20 -10
  173. package/src/components/onboarding/wizard-positions.ts +40 -11
  174. package/src/components/pane-settings-dialog/desktop.tsx +12 -50
  175. package/src/components/pane-settings-dialog/field-dialogs.tsx +1 -1
  176. package/src/components/price-performance.tsx +18 -2
  177. package/src/components/reader/article-reader.tsx +11 -10
  178. package/src/components/reader/detail-reader.tsx +9 -5
  179. package/src/components/reader/reader-text.tsx +51 -24
  180. package/src/components/sign-in-code-panel.tsx +129 -75
  181. package/src/components/ticker/badge/index.tsx +2 -1
  182. package/src/components/ui/action-row.tsx +5 -2
  183. package/src/components/ui/audio-player.tsx +70 -0
  184. package/src/components/ui/card.tsx +75 -0
  185. package/src/components/ui/data-table/index.tsx +4 -2
  186. package/src/components/ui/data-table/types.ts +11 -0
  187. package/src/components/ui/detail-scroll-body.tsx +1 -1
  188. package/src/components/ui/dialog-actions.tsx +1 -21
  189. package/src/components/ui/display.tsx +13 -5
  190. package/src/components/ui/external-link.tsx +4 -2
  191. package/src/components/ui/fields.tsx +12 -0
  192. package/src/components/ui/figure.tsx +5 -3
  193. package/src/components/ui/frame.tsx +40 -6
  194. package/src/components/ui/icon.tsx +4 -0
  195. package/src/components/ui/index.ts +4 -1
  196. package/src/components/ui/list-view.tsx +8 -0
  197. package/src/components/ui/prose.tsx +97 -25
  198. package/src/components/ui/query-bar-keys.tsx +3 -1
  199. package/src/components/ui/query-bar.tsx +6 -0
  200. package/src/components/ui/quote.ts +42 -0
  201. package/src/components/ui/range-track.tsx +15 -2
  202. package/src/components/ui/reveal-in-scroll-box.ts +21 -3
  203. package/src/components/ui/select-button.tsx +38 -3
  204. package/src/components/ui/status.tsx +73 -4
  205. package/src/components/ui/table-layout.ts +20 -0
  206. package/src/components/use-pinned-moment.ts +19 -0
  207. package/src/core/app-runtime.ts +2 -2
  208. package/src/core/state/app/instrument.ts +18 -1
  209. package/src/core/state/app/layout.ts +6 -0
  210. package/src/core/state/app/state.ts +14 -2
  211. package/src/data/cached-query.ts +1 -1
  212. package/src/data/config/chart-settings.ts +3 -1
  213. package/src/data/config/home.ts +4 -1
  214. package/src/data/config/store/node.ts +9 -4
  215. package/src/data/config/store/normalize.ts +9 -0
  216. package/src/data/json-storage.ts +6 -3
  217. package/src/data/json-ticker-repository.ts +3 -1
  218. package/src/data/plugin-cache.ts +5 -1
  219. package/src/data/sqlite/json.ts +3 -1
  220. package/src/data/ticker-repository.ts +5 -2
  221. package/src/feedback/diagnostics.ts +2 -6
  222. package/src/i18n/es.ts +86 -6
  223. package/src/i18n/index.ts +3 -9
  224. package/src/i18n/ja.ts +86 -6
  225. package/src/i18n/ko.ts +86 -6
  226. package/src/i18n/zh-cn.ts +86 -6
  227. package/src/i18n/zh-tw.ts +86 -6
  228. package/src/layout-marketplace/gallery-desktop.tsx +8 -4
  229. package/src/market-data/bars-per-year.ts +43 -0
  230. package/src/market-data/bond-funds.ts +85 -0
  231. package/src/market-data/coordinator/auxiliary.ts +5 -2
  232. package/src/market-data/coordinator/index.ts +6 -5
  233. package/src/market-data/futures-contract.ts +102 -5
  234. package/src/market-data/hooks.tsx +20 -11
  235. package/src/market-data/market/bar-period.ts +36 -0
  236. package/src/market-data/market/format.ts +5 -13
  237. package/src/market-data/market/freshness.ts +1 -3
  238. package/src/market-data/market/index-level.ts +51 -0
  239. package/src/market-data/model-dividend-yield.ts +57 -16
  240. package/src/market-data/quotes/live-valuation.ts +48 -2
  241. package/src/market-data/realized-volatility.ts +16 -7
  242. package/src/market-data/tax-equivalent.ts +344 -0
  243. package/src/news/aggregator.ts +4 -2
  244. package/src/notifications/notification-log.ts +5 -3
  245. package/src/plugins/auto-update-node.ts +1 -0
  246. package/src/plugins/builtin/account-management/footer.ts +1 -1
  247. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  248. package/src/plugins/builtin/account-management/model.ts +1 -0
  249. package/src/plugins/builtin/account-management/pane.tsx +71 -19
  250. package/src/plugins/builtin/account-management/trial-cancel.ts +40 -0
  251. package/src/plugins/builtin/alerts/cli.ts +7 -18
  252. package/src/plugins/builtin/alerts/constants.ts +8 -0
  253. package/src/plugins/builtin/alerts/event-rows.ts +16 -0
  254. package/src/plugins/builtin/alerts/events-pane.tsx +2 -14
  255. package/src/plugins/builtin/alerts/headless.ts +182 -0
  256. package/src/plugins/builtin/alerts/index.tsx +3 -0
  257. package/src/plugins/builtin/alerts/pane.tsx +1 -7
  258. package/src/plugins/builtin/alerts/storage.ts +17 -1
  259. package/src/plugins/builtin/analytics/index.tsx +6 -1
  260. package/src/plugins/builtin/analytics/metrics.ts +47 -9
  261. package/src/plugins/builtin/analytics/risk-headless.ts +14 -2
  262. package/src/plugins/builtin/analytics/risk-math.ts +83 -0
  263. package/src/plugins/builtin/analytics/risk-model.ts +97 -4
  264. package/src/plugins/builtin/analytics/risk-pane.tsx +11 -6
  265. package/src/plugins/builtin/analytics/sector-model.ts +14 -3
  266. package/src/plugins/builtin/awards/headless.ts +6 -5
  267. package/src/plugins/builtin/awards/index.tsx +1 -1
  268. package/src/plugins/builtin/backtest/client.ts +38 -8
  269. package/src/plugins/builtin/backtest/engine.ts +25 -13
  270. package/src/plugins/builtin/backtest/export.ts +27 -0
  271. package/src/plugins/builtin/backtest/format.ts +19 -3
  272. package/src/plugins/builtin/backtest/headless.ts +42 -28
  273. package/src/plugins/builtin/backtest/index.tsx +3 -0
  274. package/src/plugins/builtin/backtest/pane.tsx +39 -10
  275. package/src/plugins/builtin/backtest/report.ts +160 -0
  276. package/src/plugins/builtin/backtest/rules.ts +2 -0
  277. package/src/plugins/builtin/backtest/window.ts +112 -0
  278. package/src/plugins/builtin/bond-calculator/headless.ts +3 -2
  279. package/src/plugins/builtin/broker-manager/add-flow.tsx +4 -4
  280. package/src/plugins/builtin/catalysts/headless.ts +24 -13
  281. package/src/plugins/builtin/catalysts/index.tsx +4 -2
  282. package/src/plugins/builtin/catalysts/model.ts +18 -1
  283. package/src/plugins/builtin/catalysts/pane.tsx +2 -2
  284. package/src/plugins/builtin/cds/client.ts +28 -5
  285. package/src/plugins/builtin/cds/headless.ts +183 -31
  286. package/src/plugins/builtin/cds/index.tsx +1 -1
  287. package/src/plugins/builtin/cds/model.ts +229 -53
  288. package/src/plugins/builtin/cds/pane.tsx +82 -26
  289. package/src/plugins/builtin/central-bank-rates/headless.ts +1 -1
  290. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +39 -1
  291. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +77 -0
  292. package/src/plugins/builtin/chart-composer/chart-spec.ts +15 -0
  293. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +22 -2
  294. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -2
  295. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -2
  296. package/src/plugins/builtin/chart-composer/headless.ts +99 -21
  297. package/src/plugins/builtin/chart-composer/index.tsx +70 -13
  298. package/src/plugins/builtin/chart-composer/pane.tsx +65 -15
  299. package/src/plugins/builtin/chart-composer/presets.ts +79 -11
  300. package/src/plugins/builtin/chart-composer/series-catalog.ts +50 -14
  301. package/src/plugins/builtin/chart-composer/series-expression.ts +9 -7
  302. package/src/plugins/builtin/chart-composer/series-listings.ts +178 -0
  303. package/src/plugins/builtin/chart-composer/session-range.ts +87 -0
  304. package/src/plugins/builtin/chat/attachments/desktop.tsx +4 -1
  305. package/src/plugins/builtin/chat/content/composer-runtime.ts +49 -14
  306. package/src/plugins/builtin/chat/content/composer.tsx +22 -1
  307. package/src/plugins/builtin/chat/content/index.tsx +35 -5
  308. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -0
  309. package/src/plugins/builtin/chat/content/shortcuts.ts +32 -13
  310. package/src/plugins/builtin/chat/content/snapshot.ts +29 -6
  311. package/src/plugins/builtin/chat/ghost-user.ts +11 -3
  312. package/src/plugins/builtin/chat/layout.ts +2 -0
  313. package/src/plugins/builtin/chat/message/desktop.tsx +13 -5
  314. package/src/plugins/builtin/chat/message/header.tsx +26 -4
  315. package/src/plugins/builtin/chat/message/pane-chip.tsx +131 -35
  316. package/src/plugins/builtin/chat/message/render-state.ts +28 -1
  317. package/src/plugins/builtin/chat/message/terminal.tsx +9 -3
  318. package/src/plugins/builtin/chat/pane-links/composer.ts +87 -0
  319. package/src/plugins/builtin/chat/pane-links/model.ts +1 -1
  320. package/src/plugins/builtin/chat/share/flow.tsx +9 -73
  321. package/src/plugins/builtin/cloud/askg/controller.ts +5 -3
  322. package/src/plugins/builtin/cloud/askg/executor.ts +3 -1
  323. package/src/plugins/builtin/cloud/askg/lock-note.ts +16 -0
  324. package/src/plugins/builtin/cloud/askg/pane.tsx +6 -2
  325. package/src/plugins/builtin/cloud/auth-actions.tsx +4 -2
  326. package/src/plugins/builtin/cloud/company-picker.tsx +21 -26
  327. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +5 -3
  328. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  329. package/src/plugins/builtin/cloud/team/module.tsx +5 -1
  330. package/src/plugins/builtin/cloud/team/pane.tsx +11 -4
  331. package/src/plugins/builtin/cloud/team/views.ts +11 -4
  332. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +9 -4
  333. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +3 -2
  334. package/src/plugins/builtin/cloud/thesis/detail.tsx +2 -1
  335. package/src/plugins/builtin/cloud/thesis/exposure.ts +3 -0
  336. package/src/plugins/builtin/cloud/thesis/prompts.tsx +3 -2
  337. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +2 -1
  338. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +2 -1
  339. package/src/plugins/builtin/cloud/verification-status-widget.tsx +2 -1
  340. package/src/plugins/builtin/commodities-energy/index.tsx +8 -5
  341. package/src/plugins/builtin/commodities-energy/view-state.ts +11 -0
  342. package/src/plugins/builtin/company-attention/headless.ts +15 -7
  343. package/src/plugins/builtin/company-attention/index.tsx +1 -1
  344. package/src/plugins/builtin/company-attention/model.ts +49 -4
  345. package/src/plugins/builtin/company-attention/pane.tsx +11 -9
  346. package/src/plugins/builtin/company-kpis/headless.ts +4 -3
  347. package/src/plugins/builtin/company-kpis/index.tsx +2 -2
  348. package/src/plugins/builtin/company-kpis/pane.tsx +9 -3
  349. package/src/plugins/builtin/congress-trades/headless.ts +23 -2
  350. package/src/plugins/builtin/correlation/relationship/pane.tsx +18 -46
  351. package/src/plugins/builtin/cot/headless.ts +5 -2
  352. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +41 -13
  353. package/src/plugins/builtin/credit-boards/client.ts +9 -2
  354. package/src/plugins/builtin/credit-boards/headless.ts +93 -27
  355. package/src/plugins/builtin/credit-boards/index.tsx +15 -4
  356. package/src/plugins/builtin/credit-boards/model.ts +105 -6
  357. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +14 -3
  358. package/src/plugins/builtin/credit-documents/detail.tsx +2 -1
  359. package/src/plugins/builtin/credit-documents/index.tsx +3 -0
  360. package/src/plugins/builtin/credit-documents/model.ts +3 -10
  361. package/src/plugins/builtin/crypto-board/client.ts +152 -2
  362. package/src/plugins/builtin/crypto-board/headless.ts +138 -19
  363. package/src/plugins/builtin/crypto-board/index.tsx +5 -3
  364. package/src/plugins/builtin/crypto-board/model.ts +210 -12
  365. package/src/plugins/builtin/crypto-board/pane.tsx +148 -9
  366. package/src/plugins/builtin/crypto-board/series.ts +52 -1
  367. package/src/plugins/builtin/crypto-board/supply.ts +123 -0
  368. package/src/plugins/builtin/debt-maturities/client.ts +54 -1
  369. package/src/plugins/builtin/debt-maturities/headless.ts +27 -3
  370. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  371. package/src/plugins/builtin/debt-maturities/model.ts +58 -13
  372. package/src/plugins/builtin/debt-maturities/pane.tsx +47 -7
  373. package/src/plugins/builtin/dividend-yield/client.ts +42 -36
  374. package/src/plugins/builtin/dividend-yield/headless.ts +113 -18
  375. package/src/plugins/builtin/dividend-yield/index.tsx +5 -2
  376. package/src/plugins/builtin/dividend-yield/pane.tsx +37 -13
  377. package/src/plugins/builtin/dividend-yield/provider-client.ts +7 -3
  378. package/src/plugins/builtin/dividend-yield/ranking.ts +187 -0
  379. package/src/plugins/builtin/dividend-yield/reference-price.ts +28 -5
  380. package/src/plugins/builtin/dividend-yield/types.ts +6 -1
  381. package/src/plugins/builtin/dividend-yield/view.ts +1 -1
  382. package/src/plugins/builtin/docs/blocks.tsx +54 -8
  383. package/src/plugins/builtin/docs/pane.tsx +8 -13
  384. package/src/plugins/builtin/earnings/headless.ts +3 -2
  385. package/src/plugins/builtin/earnings-calls/data.ts +51 -0
  386. package/src/plugins/builtin/earnings-calls/headless.ts +7 -0
  387. package/src/plugins/builtin/earnings-calls/index.tsx +1 -0
  388. package/src/plugins/builtin/earnings-calls/model.ts +97 -0
  389. package/src/plugins/builtin/earnings-calls/pane.tsx +107 -7
  390. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +140 -22
  391. package/src/plugins/builtin/earnings-calls/use-call-playback.ts +194 -0
  392. package/src/plugins/builtin/econ/calendar-model.ts +22 -0
  393. package/src/plugins/builtin/econ/calendar-source.ts +35 -0
  394. package/src/plugins/builtin/econ/index.tsx +40 -8
  395. package/src/plugins/builtin/econ-statistics/defs.ts +7 -0
  396. package/src/plugins/builtin/econ-statistics/detail.tsx +8 -1
  397. package/src/plugins/builtin/econ-statistics/headless.ts +26 -5
  398. package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
  399. package/src/plugins/builtin/econ-statistics/stats.ts +10 -0
  400. package/src/plugins/builtin/econ-statistics/view.ts +19 -0
  401. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  402. package/src/plugins/builtin/equity-screener/model.ts +46 -4
  403. package/src/plugins/builtin/equity-screener/pane.tsx +3 -0
  404. package/src/plugins/builtin/estimate-revisions/client.ts +32 -22
  405. package/src/plugins/builtin/estimate-revisions/headless.ts +1 -0
  406. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -0
  407. package/src/plugins/builtin/estimate-revisions/pane.tsx +5 -3
  408. package/src/plugins/builtin/executives/index.tsx +1 -0
  409. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  410. package/src/plugins/builtin/fear-greed/index.tsx +9 -0
  411. package/src/plugins/builtin/filing-events/index.tsx +1 -0
  412. package/src/plugins/builtin/futures/index.tsx +2 -2
  413. package/src/plugins/builtin/futures/model.ts +34 -21
  414. package/src/plugins/builtin/futures/table.tsx +1 -1
  415. package/src/plugins/builtin/futures/use-board-terms.ts +2 -2
  416. package/src/plugins/builtin/futures-curve/headless.ts +4 -2
  417. package/src/plugins/builtin/futures-curve/model.ts +5 -7
  418. package/src/plugins/builtin/help/index.tsx +55 -8
  419. package/src/plugins/builtin/help/shortcut-table.tsx +15 -2
  420. package/src/plugins/builtin/holders/headless.ts +12 -0
  421. package/src/plugins/builtin/insider/headless.ts +311 -41
  422. package/src/plugins/builtin/insider/index.tsx +280 -106
  423. package/src/plugins/builtin/insider/insiders.tsx +224 -0
  424. package/src/plugins/builtin/insider/signals-cells.tsx +52 -0
  425. package/src/plugins/builtin/insider/signals-client.ts +135 -11
  426. package/src/plugins/builtin/insider/signals-model.ts +628 -56
  427. package/src/plugins/builtin/insider/signals.tsx +283 -83
  428. package/src/plugins/builtin/ipo-calendar/headless.ts +17 -7
  429. package/src/plugins/builtin/ipo-calendar/index.tsx +23 -3
  430. package/src/plugins/builtin/ipo-calendar/model.ts +84 -36
  431. package/src/plugins/builtin/ipo-calendar/pane.tsx +20 -8
  432. package/src/plugins/builtin/iv-history/headless.ts +2 -0
  433. package/src/plugins/builtin/jobs/headless.ts +1 -0
  434. package/src/plugins/builtin/jobs/index.tsx +1 -0
  435. package/src/plugins/builtin/jodi/headless.ts +2 -0
  436. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  437. package/src/plugins/builtin/kelly-sizer/portfolio.ts +1 -1
  438. package/src/plugins/builtin/market-heatmap/headless.ts +1 -0
  439. package/src/plugins/builtin/market-heatmap/portfolio.ts +3 -1
  440. package/src/plugins/builtin/market-valuation/pane.tsx +1 -1
  441. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +31 -57
  442. package/src/plugins/builtin/mna/distress/headless.ts +2 -1
  443. package/src/plugins/builtin/mna/headless.ts +14 -2
  444. package/src/plugins/builtin/mna/index.tsx +1 -0
  445. package/src/plugins/builtin/news/headless.ts +4 -0
  446. package/src/plugins/builtin/news/wire/news/footer.ts +5 -3
  447. package/src/plugins/builtin/news/wire/news/table.tsx +57 -11
  448. package/src/plugins/builtin/notes/cli.ts +8 -4
  449. package/src/plugins/builtin/notes/files.ts +66 -20
  450. package/src/plugins/builtin/notes/migration.ts +18 -6
  451. package/src/plugins/builtin/notes/owner.tsx +3 -2
  452. package/src/plugins/builtin/notes/quick-notes-pane.tsx +3 -2
  453. package/src/plugins/builtin/options-scenario/client.ts +7 -4
  454. package/src/plugins/builtin/options-scenario/headless.ts +5 -3
  455. package/src/plugins/builtin/pe-band/index.tsx +1 -1
  456. package/src/plugins/builtin/pe-band/model.ts +1 -15
  457. package/src/plugins/builtin/perps/headless.ts +30 -13
  458. package/src/plugins/builtin/perps/model.ts +45 -3
  459. package/src/plugins/builtin/perps/pane.tsx +7 -7
  460. package/src/plugins/builtin/portfolio-list/cell-version.ts +10 -4
  461. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +128 -14
  462. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +108 -46
  463. package/src/plugins/builtin/portfolio-list/cli/render.ts +368 -45
  464. package/src/plugins/builtin/portfolio-list/cli/shared.ts +8 -2
  465. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +24 -34
  466. package/src/plugins/builtin/portfolio-list/grid.tsx +3 -1
  467. package/src/plugins/builtin/portfolio-list/headless.ts +151 -29
  468. package/src/plugins/builtin/portfolio-list/income.ts +296 -0
  469. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  470. package/src/plugins/builtin/portfolio-list/pane/futures-terms.ts +58 -0
  471. package/src/plugins/builtin/portfolio-list/pane/index.tsx +27 -10
  472. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +47 -28
  473. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +3 -2
  474. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +10 -3
  475. package/src/plugins/builtin/portfolio-list/summary/index.tsx +26 -4
  476. package/src/plugins/builtin/portfolio-list/summary/totals.ts +31 -3
  477. package/src/plugins/builtin/portfolio-list/table.tsx +25 -5
  478. package/src/plugins/builtin/power/prices-headless.ts +2 -1
  479. package/src/plugins/builtin/realized-vol/chart-model.ts +29 -5
  480. package/src/plugins/builtin/realized-vol/charts.tsx +14 -8
  481. package/src/plugins/builtin/realized-vol/evidence.ts +2 -0
  482. package/src/plugins/builtin/realized-vol/headless.ts +6 -4
  483. package/src/plugins/builtin/realized-vol/model.ts +5 -1
  484. package/src/plugins/builtin/realized-vol/pane.tsx +31 -11
  485. package/src/plugins/builtin/realized-vol/settings.ts +1 -1
  486. package/src/plugins/builtin/research/analyst-headless.ts +4 -0
  487. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -0
  488. package/src/plugins/builtin/research/event-model.ts +22 -2
  489. package/src/plugins/builtin/research/headless-schema.ts +1 -0
  490. package/src/plugins/builtin/research/headless.ts +3 -0
  491. package/src/plugins/builtin/research/index.tsx +3 -0
  492. package/src/plugins/builtin/research/relative-valuation-columns.ts +27 -5
  493. package/src/plugins/builtin/research/relative-valuation-headless.ts +2 -0
  494. package/src/plugins/builtin/revenue-breakdown/index.tsx +1 -0
  495. package/src/plugins/builtin/reverse-dcf/headless.ts +9 -6
  496. package/src/plugins/builtin/reverse-dcf/index.tsx +2 -2
  497. package/src/plugins/builtin/reverse-dcf/model.ts +51 -4
  498. package/src/plugins/builtin/reverse-dcf/pane.tsx +23 -10
  499. package/src/plugins/builtin/risk-factors/headless.ts +7 -0
  500. package/src/plugins/builtin/risk-factors/index.tsx +1 -0
  501. package/src/plugins/builtin/scanner/feed.ts +5 -3
  502. package/src/plugins/builtin/scanner/headless.ts +9 -57
  503. package/src/plugins/builtin/scanner/index.ts +1 -2
  504. package/src/plugins/builtin/sec/forms.ts +2 -6
  505. package/src/plugins/builtin/sec/fund-facts-view.tsx +53 -0
  506. package/src/plugins/builtin/sec/fund-facts.ts +328 -0
  507. package/src/plugins/builtin/sec/headless.ts +140 -44
  508. package/src/plugins/builtin/sec/index.tsx +95 -30
  509. package/src/plugins/builtin/sec/model.ts +2 -44
  510. package/src/plugins/builtin/shared/headless-options.ts +10 -0
  511. package/src/plugins/builtin/shared/locked-rows.tsx +3 -1
  512. package/src/plugins/builtin/shared/move-rank.ts +108 -0
  513. package/src/plugins/builtin/shared/report-freshness.ts +25 -9
  514. package/src/plugins/builtin/shared/scanner-capture.ts +95 -0
  515. package/src/plugins/builtin/shared/treasury-cutoffs.ts +15 -0
  516. package/src/plugins/builtin/shared/use-quote-board.ts +1 -0
  517. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -0
  518. package/src/plugins/builtin/shared/volatility-indices.ts +32 -0
  519. package/src/plugins/builtin/short-interest/headless.ts +3 -0
  520. package/src/plugins/builtin/short-interest/watch-model.ts +2 -2
  521. package/src/plugins/builtin/social-mentions/client.ts +8 -1
  522. package/src/plugins/builtin/social-mentions/pane.tsx +8 -6
  523. package/src/plugins/builtin/supply-chain/client.ts +12 -3
  524. package/src/plugins/builtin/supply-chain/disclosure-quote.tsx +9 -5
  525. package/src/plugins/builtin/supply-chain/evidence.ts +3 -1
  526. package/src/plugins/builtin/supply-chain/flow-layout.ts +2 -1
  527. package/src/plugins/builtin/supply-chain/flow.tsx +23 -16
  528. package/src/plugins/builtin/supply-chain/graph-client.ts +8 -5
  529. package/src/plugins/builtin/supply-chain/graph-evidence.ts +1 -1
  530. package/src/plugins/builtin/supply-chain/graph-model.ts +10 -2
  531. package/src/plugins/builtin/supply-chain/graph-pane.tsx +9 -7
  532. package/src/plugins/builtin/supply-chain/graph.tsx +8 -6
  533. package/src/plugins/builtin/supply-chain/headless.ts +69 -27
  534. package/src/plugins/builtin/supply-chain/index.tsx +1 -1
  535. package/src/plugins/builtin/supply-chain/model.ts +76 -15
  536. package/src/plugins/builtin/supply-chain/pane.tsx +29 -15
  537. package/src/plugins/builtin/supply-chain/row-evidence.tsx +5 -4
  538. package/src/plugins/builtin/themes/model.ts +2 -5
  539. package/src/plugins/builtin/thirteenf/api.ts +50 -2
  540. package/src/plugins/builtin/thirteenf/data.ts +250 -11
  541. package/src/plugins/builtin/thirteenf/headless.ts +311 -28
  542. package/src/plugins/builtin/thirteenf/index.tsx +4 -1
  543. package/src/plugins/builtin/thirteenf/model.ts +244 -19
  544. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +133 -33
  545. package/src/plugins/builtin/thirteenf/overlap.ts +1 -1
  546. package/src/plugins/builtin/thirteenf/pane.tsx +24 -5
  547. package/src/plugins/builtin/thirteenf/report-notes.ts +78 -0
  548. package/src/plugins/builtin/thirteenf/types.ts +49 -0
  549. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -8
  550. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +17 -1
  551. package/src/plugins/builtin/ticker-detail/financials/model.ts +2 -42
  552. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  553. package/src/plugins/builtin/ticker-detail/headless.ts +23 -10
  554. package/src/plugins/builtin/ticker-detail/overview/components.tsx +27 -3
  555. package/src/plugins/builtin/ticker-detail/overview/dividend.ts +60 -0
  556. package/src/plugins/builtin/ticker-detail/overview/model.ts +46 -9
  557. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +3 -0
  558. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +36 -3
  559. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +18 -19
  560. package/src/plugins/builtin/time-sales/client.ts +7 -4
  561. package/src/plugins/builtin/time-sales/headless.ts +71 -22
  562. package/src/plugins/builtin/time-sales/model.ts +12 -0
  563. package/src/plugins/builtin/time-sales/pane.tsx +31 -7
  564. package/src/plugins/builtin/time-sales/window.ts +128 -0
  565. package/src/plugins/builtin/trace-bonds/client.ts +351 -33
  566. package/src/plugins/builtin/trace-bonds/headless.ts +60 -22
  567. package/src/plugins/builtin/trace-bonds/index.tsx +4 -2
  568. package/src/plugins/builtin/trace-bonds/model.ts +27 -25
  569. package/src/plugins/builtin/trace-bonds/pane.tsx +63 -45
  570. package/src/plugins/builtin/treasury-auctions/cache.ts +61 -15
  571. package/src/plugins/builtin/treasury-auctions/client.ts +45 -10
  572. package/src/plugins/builtin/treasury-auctions/headless.ts +35 -13
  573. package/src/plugins/builtin/treasury-auctions/index.tsx +5 -2
  574. package/src/plugins/builtin/treasury-auctions/model.ts +20 -16
  575. package/src/plugins/builtin/treasury-auctions/pane.tsx +29 -12
  576. package/src/plugins/builtin/vol-surface/charts.tsx +2 -1
  577. package/src/plugins/builtin/vol-surface/headless.ts +3 -2
  578. package/src/plugins/builtin/vol-surface/pane.tsx +2 -1
  579. package/src/plugins/builtin/vol-surface/stored.ts +4 -7
  580. package/src/plugins/builtin/volatility/chart-model.ts +16 -15
  581. package/src/plugins/builtin/volatility/charts.tsx +5 -5
  582. package/src/plugins/builtin/volatility/client.ts +73 -2
  583. package/src/plugins/builtin/volatility/evidence.ts +13 -14
  584. package/src/plugins/builtin/volatility/headless.ts +13 -7
  585. package/src/plugins/builtin/volatility/index.tsx +1 -1
  586. package/src/plugins/builtin/volatility/model.ts +104 -38
  587. package/src/plugins/builtin/volatility/pane.tsx +42 -24
  588. package/src/plugins/builtin/volatility/series.ts +22 -13
  589. package/src/plugins/builtin/world-venue-map/geo-series.ts +9 -0
  590. package/src/plugins/builtin/world-venue-map/headless.ts +8 -5
  591. package/src/plugins/builtin/world-venue-map/index.ts +16 -2
  592. package/src/plugins/builtin/world-venue-map/map.tsx +13 -17
  593. package/src/plugins/builtin/yield-curve/curves.ts +29 -5
  594. package/src/plugins/builtin/yield-curve/headless.ts +41 -8
  595. package/src/plugins/builtin/yield-curve/history.ts +5 -5
  596. package/src/plugins/builtin/yield-curve/index.tsx +30 -13
  597. package/src/plugins/compat.ts +3 -1
  598. package/src/plugins/dependencies.ts +8 -2
  599. package/src/plugins/host-link.ts +12 -5
  600. package/src/plugins/installer.ts +16 -7
  601. package/src/plugins/loader.ts +9 -2
  602. package/src/plugins/marketplace/activation.ts +10 -4
  603. package/src/plugins/registry/host-actions.ts +3 -0
  604. package/src/plugins/registry/index.ts +3 -1
  605. package/src/{plugins/builtin/portfolio-list → portfolio}/allocation.ts +74 -12
  606. package/src/{plugins/builtin/portfolio-list → portfolio}/column-values.ts +140 -53
  607. package/src/portfolio/futures-positions.ts +213 -0
  608. package/src/portfolio/futures-terms.ts +75 -0
  609. package/src/portfolio/manual-lots.ts +78 -0
  610. package/src/portfolio/mutations.ts +54 -10
  611. package/src/portfolio/position-metrics.ts +44 -10
  612. package/src/portfolio/valuation-basis.ts +31 -4
  613. package/src/public/__snapshots__/public-api.test.ts.snap +4 -0
  614. package/src/public/time-series.ts +2 -1
  615. package/src/remote/confirm-change.tsx +2 -1
  616. package/src/remote/server.ts +1 -1
  617. package/src/remote/watchlist.ts +9 -1
  618. package/src/renderers/dom/data-table/row.tsx +1 -1
  619. package/src/renderers/dom/desktop/audio-player.tsx +312 -0
  620. package/src/renderers/dom/desktop/control-styles.ts +17 -82
  621. package/src/renderers/dom/desktop/controls.tsx +39 -63
  622. package/src/renderers/dom/desktop/dialog-actions.tsx +13 -53
  623. package/src/renderers/dom/desktop/icons.tsx +8 -0
  624. package/src/renderers/dom/desktop/list-view.tsx +6 -6
  625. package/src/renderers/dom/desktop/popover.tsx +0 -3
  626. package/src/renderers/dom/desktop/query-bar.tsx +6 -4
  627. package/src/renderers/dom/desktop/select-field.tsx +9 -8
  628. package/src/renderers/dom/desktop/stack-header.ts +3 -0
  629. package/src/renderers/dom/dom-ui-host.tsx +3 -2
  630. package/src/renderers/dom/host/chart-surface.tsx +3 -3
  631. package/src/renderers/dom/host/size-observer.ts +33 -4
  632. package/src/renderers/dom/host/surface-3d.tsx +3 -2
  633. package/src/renderers/dom/host/surface-fit.ts +4 -1
  634. package/src/renderers/dom/host/tabs.tsx +22 -132
  635. package/src/renderers/dom/styles.css +358 -160
  636. package/src/renderers/electrobun/bun/core-capabilities.ts +1 -1
  637. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -1
  638. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  639. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +43 -1
  640. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +5 -1
  641. package/src/renderers/electrobun/shared/rpc-codec.ts +7 -0
  642. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +21 -17
  643. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  644. package/src/renderers/electrobun/view/desktop/window/bridge.ts +6 -8
  645. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +18 -0
  646. package/src/series/capability.ts +1 -0
  647. package/src/series/files.ts +9 -3
  648. package/src/series/registry.ts +40 -14
  649. package/src/series/resolve.ts +121 -18
  650. package/src/series/standalone.ts +3 -2
  651. package/src/series/types.ts +55 -1
  652. package/src/series/validate.ts +39 -0
  653. package/src/shares/api.ts +26 -7
  654. package/src/shares/bug-report-dialog.tsx +2 -2
  655. package/src/shares/bug-report-flow.tsx +4 -7
  656. package/src/shares/capture-flow.ts +5 -4
  657. package/src/shares/capture-preview.tsx +2 -2
  658. package/src/shares/chat.ts +7 -27
  659. package/src/shares/flow.tsx +86 -40
  660. package/src/shares/location.ts +1 -3
  661. package/src/shares/pane-image.ts +11 -9
  662. package/src/shares/pane.ts +5 -2
  663. package/src/shares/payload.ts +1 -1
  664. package/src/shares/personal-pane.ts +3 -3
  665. package/src/shares/portable-layout.ts +9 -2
  666. package/src/shares/ready.ts +42 -0
  667. package/src/shares/test-harness.ts +62 -0
  668. package/src/sources/gloomberb-cloud/batched-lookups.ts +73 -0
  669. package/src/sources/gloomberb-cloud/index.ts +64 -12
  670. package/src/sources/gloomberb-cloud/normalizers.ts +31 -2
  671. package/src/sources/gloomberb-cloud/perpetuals.ts +20 -1
  672. package/src/sources/history-omitted.ts +34 -0
  673. package/src/sources/history-result.ts +3 -0
  674. package/src/sources/history-retention.ts +1 -1
  675. package/src/sources/provider-errors.ts +43 -1
  676. package/src/sources/provider-router/batches.ts +3 -2
  677. package/src/sources/provider-router/cache.ts +2 -1
  678. package/src/sources/provider-router/cached-routes.ts +9 -3
  679. package/src/sources/provider-router/history.ts +9 -8
  680. package/src/sources/provider-router/index.ts +18 -3
  681. package/src/sources/provider-router/primary.ts +5 -4
  682. package/src/sources/provider-router/quote-route.ts +3 -1
  683. package/src/sources/provider-router/routing.ts +2 -1
  684. package/src/sources/provider-router/search.ts +47 -13
  685. package/src/sources/provider-router/streaming.ts +7 -4
  686. package/src/sources/sec-edgar.ts +1 -3
  687. package/src/state/app/context/index.tsx +9 -13
  688. package/src/state/app/tab-cursor.ts +149 -12
  689. package/src/state/hooks/quote-streaming.ts +3 -2
  690. package/src/state/hooks/session-snapshot.ts +3 -2
  691. package/src/state/persist-scheduler.ts +12 -4
  692. package/src/sync/controller.ts +22 -1
  693. package/src/sync/core-contributors.ts +58 -15
  694. package/src/team/collections-sync.ts +14 -4
  695. package/src/team/collections.ts +40 -11
  696. package/src/team/store.ts +5 -4
  697. package/src/telemetry/attention-counts.ts +8 -2
  698. package/src/telemetry/crash-reports-dom.ts +3 -0
  699. package/src/telemetry/crash-reports-node.ts +7 -4
  700. package/src/telemetry/crash-reports.ts +11 -11
  701. package/src/telemetry/usage-counts.ts +16 -12
  702. package/src/theme/colors.ts +2 -0
  703. package/src/theme/font-scale.ts +2 -0
  704. package/src/theme/ui-tokens.ts +71 -0
  705. package/src/tickers/batched-lookups.ts +186 -0
  706. package/src/tickers/company-name-listing.ts +97 -0
  707. package/src/tickers/company-name.ts +13 -0
  708. package/src/tickers/instrument-coverage.ts +112 -0
  709. package/src/tickers/listing-fate.ts +49 -9
  710. package/src/tickers/listing-venues.ts +24 -5
  711. package/src/tickers/open-target.ts +13 -5
  712. package/src/tickers/search/index.ts +112 -35
  713. package/src/tickers/search/ranking.ts +49 -1
  714. package/src/tickers/terminal-codes.ts +79 -0
  715. package/src/tickers/ticker-changes.ts +74 -3
  716. package/src/tickers/us-listing.ts +2 -1
  717. package/src/time-series/chart-data.ts +79 -8
  718. package/src/time-series/fundamentals.ts +55 -7
  719. package/src/time-series/ice-credit.ts +17 -2
  720. package/src/time-series/live-bars.ts +21 -8
  721. package/src/time-series/live-quotes.ts +2 -3
  722. package/src/time-series/price-comparison.ts +6 -1
  723. package/src/time-series/resolve.ts +65 -10
  724. package/src/time-series/spec.ts +2 -1
  725. package/src/time-series/studies.ts +3 -3
  726. package/src/time-series/types.ts +22 -0
  727. package/src/time-series/use-chart-resolution.ts +2 -1
  728. package/src/types/config.ts +7 -0
  729. package/src/types/data-provider.ts +20 -0
  730. package/src/types/headless.ts +142 -1
  731. package/src/types/instrument.ts +6 -0
  732. package/src/types/plugin.ts +32 -9
  733. package/src/types/price-history.ts +5 -0
  734. package/src/types/ticker-changes.ts +19 -0
  735. package/src/types/ticker.ts +10 -2
  736. package/src/ui/context-menu.tsx +2 -0
  737. package/src/ui/host.tsx +40 -4
  738. package/src/ui/measured-cells.ts +13 -7
  739. package/src/ui/qr.ts +7 -5
  740. package/src/updater/index.ts +5 -5
  741. package/src/utils/calendar-date.ts +86 -27
  742. package/src/utils/currency-units.ts +17 -0
  743. package/src/utils/debug-log.ts +1 -1
  744. package/src/utils/dividend-cash.ts +48 -0
  745. package/src/utils/dom-screenshot-rasters.ts +3 -1
  746. package/src/utils/dom-screenshot.ts +14 -10
  747. package/src/utils/exchange-rate-snapshot.ts +9 -4
  748. package/src/utils/external-url.ts +1 -0
  749. package/src/utils/fiscal-period.ts +47 -0
  750. package/src/utils/format.ts +5 -2
  751. package/src/utils/forward-dividend.ts +160 -11
  752. package/src/utils/futures-generic.ts +34 -8
  753. package/src/utils/http-proxy-response.ts +2 -1
  754. package/src/utils/local-utc-offset.ts +2 -0
  755. package/src/utils/log-failure.ts +31 -0
  756. package/src/utils/option-expiry.ts +12 -12
  757. package/src/utils/price-history-integrity.ts +56 -0
  758. package/src/utils/price-history.ts +13 -1
  759. package/src/utils/sec-filings.ts +245 -0
  760. package/src/utils/selection-clipboard.ts +5 -2
  761. package/src/utils/throttled-fetch.ts +1 -0
  762. package/src/{plugins/builtin/dividend-yield/trailing-cash.ts → utils/trailing-dividend.ts} +55 -8
  763. package/src/utils/url.ts +1 -0
  764. package/src/utils/utc-time.ts +11 -0
  765. package/src/plugins/builtin/chat/share/targets.ts +0 -27
  766. package/src/renderers/dom/host/top-surface.ts +0 -119
  767. package/src/shares/dialog.tsx +0 -221
  768. package/src/shares/live.ts +0 -19
@@ -16,10 +16,19 @@ import { getPortfolioPositionMetrics, resolvePortfolioMarketValue, resolvePortfo
16
16
  import {
17
17
  getPortfolioValuationDisplay,
18
18
  getPortfolioValuationTime,
19
- VALUATION_BASIS_LABELS,
19
+ valuationBasisLabel,
20
20
  valuationTimeIso,
21
+ valuesAtSettlement,
21
22
  type PortfolioValuationBasis,
22
23
  } from "../../../../portfolio/valuation-basis";
24
+ import {
25
+ futuresQuantityText,
26
+ futuresSettlementDisplay,
27
+ isFuturesTicker,
28
+ withFuturesContractSize,
29
+ type PortfolioFuturesTerms,
30
+ } from "../../../../portfolio/futures-positions";
31
+ import { dailyFuturesHistory, loadPortfolioFuturesTerms, type PortfolioFuturesSources } from "../../../../portfolio/futures-terms";
23
32
  import { formatUtcDayTime, parseReportTime } from "../../../../utils/utc-time";
24
33
  import { quoteFreshnessFields } from "../../shared/report-freshness";
25
34
  import { resolvePortfolioTotalsCurrency } from "../summary/totals";
@@ -33,7 +42,7 @@ import {
33
42
  type ExtendedSession,
34
43
  type ExtendedSessionDisplay,
35
44
  } from "../../../../market-data/market/status";
36
- import { formatPerpetualRate, headlineQuoteDisplay } from "../column-values";
45
+ import { formatPerpetualRate, headlineQuoteDisplay } from "../../../../portfolio/column-values";
37
46
  import type { AppConfig } from "../../../../types/config";
38
47
  import type { DataProvider } from "../../../../types/data-provider";
39
48
  import type { CliCommandContext } from "../../../../types/plugin";
@@ -63,13 +72,18 @@ import {
63
72
  formatAllocationWeight,
64
73
  formatTradeUnits,
65
74
  hasTargetWeights,
75
+ lotWeight,
66
76
  resolvePortfolioCash,
77
+ weightBasisNote,
67
78
  type AllocationHolding,
68
79
  type PortfolioAllocation,
69
- } from "../allocation";
80
+ } from "../../../../portfolio/allocation";
70
81
  import { findCachedPortfolioAccount } from "../cached-account";
71
82
  import { importedFromText } from "../imported-from";
72
83
  import { findNamedCollection } from "../../../../portfolio/collection-name";
84
+ import { isoDateToday } from "../../../../utils/calendar-date";
85
+ import { lastTradeDate } from "../../../../tickers/listing-fate";
86
+ import { DIVIDEND_READ_DEADLINE_MS, incomeGapNote, loadHoldingDividends, portfolioIncome, projectDrip, type DripYear, type HoldingDividend } from "../income";
73
87
 
74
88
  /** Money prices pad to the currency's minor unit ($337.90, not $337.9), as the ticker view does. */
75
89
  function priceFormatOptions(quote: Parameters<typeof quoteFormatOptions>[0] & { currency?: string }, assetCategory?: string) {
@@ -180,6 +194,12 @@ export interface ShowCollectionOptions {
180
194
  explain?: boolean;
181
195
  /** What a portfolio's value is priced at; the latest print when absent. */
182
196
  basis?: PortfolioValuationBasis;
197
+ /** `--drip`: project reinvested dividends this many years, with this percent withheld from each. */
198
+ drip?: { years: number; withholding: number };
199
+ /** How long every holding's dividend read may take together; a test shortens it. */
200
+ dividendDeadlineMs?: number;
201
+ /** Where futures' terms and settlements are read; Gloom Cloud's terms and the data provider's daily bars by default. */
202
+ futuresSources?: PortfolioFuturesSources;
183
203
  }
184
204
 
185
205
  export async function showCollection(name: string, ctx: CliCommandContext, options: ShowCollectionOptions = {}) {
@@ -260,6 +280,8 @@ interface PortfolioPositionValue {
260
280
  dayMove: { change: number; previousValue: number } | null;
261
281
  /** The `portfolio show --json` row. */
262
282
  row: Record<string, unknown>;
283
+ /** A future's contract size and settlement from its terms; null for any other holding. */
284
+ futures: PortfolioFuturesTerms | null;
263
285
  }
264
286
 
265
287
  /** What a row's value is priced at: exported after every other field, so existing columns keep their places. */
@@ -269,6 +291,8 @@ interface PortfolioValuationFields {
269
291
  valuationPrice: number | null;
270
292
  /** An ISO instant in UTC, or an ISO date for a close whose hour the venue's calendar does not give. */
271
293
  valuationAsOf: string | null;
294
+ /** Only on a future valued at its exchange settlement, which `valuationAsOf` dates by its session. */
295
+ valuationPriceKind?: "settle";
272
296
  }
273
297
 
274
298
  export interface PortfolioValuation {
@@ -281,13 +305,40 @@ export interface PortfolioValuation {
281
305
  unavailableCost: Set<string>;
282
306
  unavailableMarketValue: Set<string>;
283
307
  brokerPnlSymbols: Set<string>;
308
+ /** Futures held without a contract size: no value, cost or P&L, said once instead of as each. */
309
+ unknownContractSize: Set<string>;
310
+ /** Futures valued at the close without a confirmed settlement to value them at. */
311
+ unsettled: Set<string>;
312
+ /** Long and short market value of the priced lots, and their net and gross, in the portfolio's currency. */
313
+ exposure: PortfolioExposure;
314
+ }
315
+
316
+ /** A book's exposure: what its longs and its shorts are worth (short negative), net and gross. */
317
+ export interface PortfolioExposure {
318
+ long: number;
319
+ short: number;
320
+ net: number;
321
+ gross: number;
322
+ }
323
+
324
+ /** The exposure of lots at their signed market values; an unpriced lot (null) is left out, as the total leaves it. */
325
+ function portfolioExposure(values: Iterable<number | null | undefined>): PortfolioExposure {
326
+ let long = 0;
327
+ let short = 0;
328
+ for (const value of values) {
329
+ if (value == null || !Number.isFinite(value)) continue;
330
+ if (value >= 0) long += value;
331
+ else short += value;
332
+ }
333
+ return { long, short, net: long + short, gross: long - short };
284
334
  }
285
335
 
286
336
  /**
287
337
  * Values every position of a portfolio in the portfolio's currency, as
288
338
  * `portfolio show` prints it: cost basis, market value and unrealized P&L
289
339
  * from the current quote at `basis` (the latest print unless given), or the
290
- * broker's snapshot where that is the basis.
340
+ * broker's snapshot where that is the basis. A future counts contracts of the
341
+ * size `futures` gives for it, and at the close is valued at its settlement.
291
342
  */
292
343
  export async function valuePortfolioPositions({
293
344
  tickers,
@@ -297,6 +348,7 @@ export async function valuePortfolioPositions({
297
348
  baseCurrency,
298
349
  toBase,
299
350
  basis = "latest-print",
351
+ futures,
300
352
  }: {
301
353
  tickers: readonly TickerRecord[];
302
354
  quotes: ReadonlyMap<string, CollectionQuote>;
@@ -305,6 +357,8 @@ export async function valuePortfolioPositions({
305
357
  baseCurrency: string;
306
358
  toBase: (value: number, fromCurrency: string) => Promise<number>;
307
359
  basis?: PortfolioValuationBasis;
360
+ /** Each future's contract size and settlement (loadPortfolioFuturesTerms), by symbol. */
361
+ futures?: ReadonlyMap<string, PortfolioFuturesTerms>;
308
362
  }): Promise<PortfolioValuation> {
309
363
  const id = portfolioId;
310
364
  let totalPnl = 0;
@@ -312,16 +366,23 @@ export async function valuePortfolioPositions({
312
366
  const unavailableCost = new Set<string>();
313
367
  const unavailableMarketValue = new Set<string>();
314
368
  const brokerPnlSymbols = new Set<string>();
369
+ const unknownContractSize = new Set<string>();
370
+ const unsettled = new Set<string>();
315
371
  const values: PortfolioPositionValue[] = [];
316
372
  const holdings: AllocationHolding[] = [];
317
373
  const known = (value: number | null | undefined): number | null => value != null && Number.isFinite(value) ? value : null;
318
374
 
319
375
  for (const ticker of tickers) {
320
376
  const quote = quotes.get(ticker.metadata.ticker);
321
- const positions = ticker.metadata.positions.filter((position) => position.portfolio === id && position.shares !== 0);
377
+ const futuresTerms = isFuturesTicker(ticker) ? futures?.get(ticker.metadata.ticker) ?? { size: null, settlement: null } : null;
378
+ // A lot entered without its contract size takes the one the terms state; the entries keep the saved record.
379
+ const sized = withFuturesContractSize(ticker, futuresTerms?.size);
380
+ const positions = sized.metadata.positions.filter((position) => position.portfolio === id && position.shares !== 0);
322
381
  const displayedQuote = getActiveQuoteDisplay(quote);
323
382
  const activeQuote = displayedQuote && Number.isFinite(displayedQuote.price) ? displayedQuote : null;
324
- const { headline, extended } = collectionQuoteHeadline(quote);
383
+ // At the close a future's Last is the settlement it is valued at, and its move the one from the settlement before.
384
+ const settledHeadline = basis === "regular-close" && futuresTerms?.settlement ? futuresSettlementDisplay(futuresTerms.settlement) : null;
385
+ const { headline, extended } = settledHeadline ? { headline: settledHeadline, extended: null } : collectionQuoteHeadline(quote);
325
386
 
326
387
  if (positions.length === 0) {
327
388
  const quoteCurrency = quote?.currency || ticker.metadata.currency || baseCurrency;
@@ -329,8 +390,10 @@ export async function valuePortfolioPositions({
329
390
  const priced = basis === "regular-close" ? headline : activeQuote;
330
391
  const unitPrice = priced ? known(await toBase(priced.price, quoteCurrency)) : null;
331
392
  holdings.push({ symbol: ticker.metadata.ticker, held: false, marketValue: 0, units: 0, unitPrice });
332
- values.push({ ticker, quote, activeQuote, headline, extended, position: null, metrics: null, pnl: null, dayMove: null,
333
- valuation: { valuationBasis: basis, valuationPrice: known(priced?.price), valuationAsOf: valuationTimeIso(getPortfolioValuationTime(quote, basis)) },
393
+ values.push({ ticker, quote, activeQuote, headline, extended, position: null, metrics: null, pnl: null, dayMove: null, futures: futuresTerms,
394
+ valuation: { valuationBasis: basis, valuationPrice: known(priced?.price),
395
+ valuationAsOf: settledHeadline ? futuresTerms!.settlement!.date : valuationTimeIso(getPortfolioValuationTime(quote, basis)),
396
+ ...(settledHeadline ? { valuationPriceKind: "settle" as const } : {}) },
334
397
  row: { symbol: ticker.metadata.ticker, ...listingColumns(ticker, quote), exchange: ticker.metadata.exchange, shares: null, avgCost: null,
335
398
  positionCurrency: null, quotePrice: known(headline?.price), ...quoteMoveColumns(headline), ...extendedQuoteFields(extended), quoteCurrency: quote?.currency ?? null,
336
399
  volume: known(quote?.volume), costBasis: null, marketValue: null, unrealizedPnl: null, baseCurrency: currency } });
@@ -342,7 +405,9 @@ export async function valuePortfolioPositions({
342
405
  for (const position of positions) {
343
406
  const quoteCurrency = quote?.currency || ticker.metadata.currency || baseCurrency;
344
407
  const metrics = getPortfolioPositionMetrics({ ...ticker, metadata: { ...ticker.metadata, positions: [position] } }, id, quoteCurrency, undefined, quote);
345
- const valuationQuote = getPortfolioValuationDisplay(metrics, quote, basis);
408
+ const settlement = futuresTerms?.settlement ?? null;
409
+ const valuationQuote = getPortfolioValuationDisplay(metrics, quote, basis, settlement);
410
+ const atSettlement = valuesAtSettlement(metrics, basis);
346
411
  const positionCurrency = metrics.positionCurrency;
347
412
  const costBasisBase = positionCurrency ? await toBase(metrics.totalCost, positionCurrency) : Number.NaN;
348
413
 
@@ -354,6 +419,8 @@ export async function valuePortfolioPositions({
354
419
  const selectedPnl = resolvePortfolioPositionPnl(baseMetrics,
355
420
  valuationQuote ? await toBase(valuationQuote.price, quoteCurrency) : null);
356
421
  const pnl = selectedPnl.value;
422
+ if (metrics.unknownContractSize) unknownContractSize.add(ticker.metadata.ticker);
423
+ else if (atSettlement && !settlement) unsettled.add(ticker.metadata.ticker);
357
424
  if (!metrics.hasCostBasis) unavailableCost.add(ticker.metadata.ticker);
358
425
  if (known(currentValueBase) === null) unavailableMarketValue.add(ticker.metadata.ticker);
359
426
  if (selectedPnl.basis === "broker-snapshot" || selectedPnl.basis === "mixed") brokerPnlSymbols.add(ticker.metadata.ticker);
@@ -369,10 +436,11 @@ export async function valuePortfolioPositions({
369
436
  ? { change: await toBase(metrics.totalPriceUnits * change, quoteCurrency),
370
437
  previousValue: await toBase(metrics.grossPriceUnits * (valuationQuote.price - change), quoteCurrency) }
371
438
  : null;
372
- values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl,
439
+ values.push({ ticker, quote, activeQuote, headline, extended, position, metrics, pnl, futures: futuresTerms,
373
440
  dayMove: dayMove && Number.isFinite(dayMove.change) && Number.isFinite(dayMove.previousValue) ? dayMove : null,
374
441
  valuation: { valuationBasis: basis, valuationPrice: known(valuationQuote?.price),
375
- valuationAsOf: valuationTimeIso(getPortfolioValuationTime(quote, basis, metrics)) },
442
+ valuationAsOf: valuationTimeIso(getPortfolioValuationTime(quote, basis, metrics, settlement)),
443
+ ...(atSettlement && settlement ? { valuationPriceKind: "settle" as const } : {}) },
376
444
  row: { symbol: ticker.metadata.ticker, ...listingColumns(ticker, quote), exchange: ticker.metadata.exchange,
377
445
  shares: metrics.totalShares, avgCost: known(position.avgCost), positionCurrency,
378
446
  priceBasis: position.priceBasis ?? null, quantityUnit: metrics.priceBasis === "percent-of-par" ? "face" : null, quotePriceBasis: quote?.priceBasis ?? null,
@@ -385,7 +453,9 @@ export async function valuePortfolioPositions({
385
453
  }
386
454
  }
387
455
 
388
- return { basis, positions: values, holdings, totalPnl, unavailablePnl, unavailableCost, unavailableMarketValue, brokerPnlSymbols };
456
+ const exposure = portfolioExposure(values.map((entry) => entry.position ? entry.row.marketValue as number | null : null));
457
+ return { basis, positions: values, holdings, totalPnl, unavailablePnl, unavailableCost, unavailableMarketValue, brokerPnlSymbols,
458
+ unknownContractSize, unsettled, exposure };
389
459
  }
390
460
 
391
461
  /**
@@ -416,6 +486,25 @@ export function dayMoveLine(
416
486
  return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}`;
417
487
  }
418
488
 
489
+ /**
490
+ * `Exposure: long $600,000, short -$419,800,000, net -$419,200,000, gross $420,400,000`:
491
+ * what the priced holdings are worth on each side, in whole units, a short negative as every value is.
492
+ */
493
+ export function exposureLine(exposure: PortfolioExposure, currency: string): string {
494
+ const money = (value: number) => formatAllocationMoney(value, currency, { whole: true });
495
+ return `Exposure: long ${money(exposure.long)}, short ${money(exposure.short)}, net ${money(exposure.net)}, gross ${money(exposure.gross)}`;
496
+ }
497
+
498
+ /** What a hedged book's weights divide, in whole units as the exposure line states it; null for a book weighed on its total. */
499
+ export function weightBasisLine(allocation: Pick<PortfolioAllocation, "weightBasis" | "weightBase">, currency: string): string | null {
500
+ return weightBasisNote(allocation, (value) => formatAllocationMoney(value, currency, { whole: true }));
501
+ }
502
+
503
+ /** What the TOTAL row weighs: all of a total; nothing on a gross base, where the weights net to the book's net over its gross. */
504
+ function totalWeight(allocation: Pick<PortfolioAllocation, "weightBasis" | "total">): number | null {
505
+ return allocation.weightBasis === "total" && allocation.total != null && allocation.total > 0 ? 100 : null;
506
+ }
507
+
419
508
  /** A portfolio's cash line, as `portfolio show` and `fn PF` report it. */
420
509
  export interface PortfolioCashReport {
421
510
  /** In the cash's own currency. */
@@ -512,30 +601,49 @@ function portfolioValuationFreshness(
512
601
  source,
513
602
  observedKey: "valuationAsOf",
514
603
  citeOldest: true,
515
- ...(note ? { note: `priced at ${VALUATION_BASIS_LABELS[valuation.basis]}` } : {}),
604
+ ...(note ? { note: `priced at ${valuationBasisLabel(valuation.basis, { futures: holdsFutures(valuation) })}` } : {}),
516
605
  }, undefined, now);
517
606
  }
518
607
 
608
+ /** Whether a valued book holds a future, whose price at the close is its settlement. */
609
+ export function holdsFutures(valuation: Pick<PortfolioValuation, "positions">): boolean {
610
+ return valuation.positions.some((entry) => entry.position && entry.futures);
611
+ }
612
+
519
613
  /** `Priced at the latest print, extended hours included`: the line under a portfolio's title. */
520
- function valuationBasisLine(basis: PortfolioValuationBasis): string {
521
- return `Priced at ${VALUATION_BASIS_LABELS[basis]}`;
614
+ function valuationBasisLine(basis: PortfolioValuationBasis, futures: boolean): string {
615
+ return `Priced at ${valuationBasisLabel(basis, { futures })}`;
522
616
  }
523
617
 
524
618
  /**
525
619
  * An As of cell: when the price a row's value uses is from (`valuationAsOf`),
526
- * in UTC as its header says; a close dated only by its day says so.
620
+ * in UTC as its header says; a close dated only by its day says so, and a
621
+ * future's settlement names its session.
527
622
  */
528
- export function valuationTimeText(value: unknown, now = Date.now()): string {
623
+ export function valuationTimeText(value: unknown, now = Date.now(), kind?: "settle"): string {
529
624
  const time = parseReportTime(value);
530
625
  if (!time) return "—";
531
- return time.dateOnly ? `${formatUtcDayTime(time, now)} close` : formatUtcDayTime(time, now);
626
+ return time.dateOnly ? `${formatUtcDayTime(time, now)} ${kind ?? "close"}` : formatUtcDayTime(time, now);
627
+ }
628
+
629
+ /** A yield in percent with two decimals, as DVD shows one: 2.31%. */
630
+ function formatIncomeYield(value: number | null | undefined): string {
631
+ return value != null && Number.isFinite(value) ? `${value.toFixed(2)}%` : "—";
632
+ }
633
+
634
+ /** One holding's projected years, for the DRIP table and its export. */
635
+ interface DripProjection {
636
+ ticker: TickerRecord;
637
+ dividend: HoldingDividend & { perShare: number; currency: string; price: number };
638
+ shares: number;
639
+ years: DripYear[];
532
640
  }
533
641
 
534
642
  async function showCollectionWithMarketData(
535
643
  name: string,
536
644
  ctx: CliCommandContext,
537
645
  { config, store, dataProvider, persistence }: MarketContext,
538
- { explain = false, basis = "latest-print" }: ShowCollectionOptions,
646
+ { explain = false, basis = "latest-print", drip, dividendDeadlineMs = DIVIDEND_READ_DEADLINE_MS, futuresSources }: ShowCollectionOptions,
539
647
  ) {
540
648
  const tickers = (await store.loadAllTickers()).sort((left, right) =>
541
649
  left.metadata.ticker.localeCompare(right.metadata.ticker)
@@ -553,6 +661,9 @@ async function showCollectionWithMarketData(
553
661
  if (!matchedPortfolio && basis === "regular-close") {
554
662
  ctx.fail(`--close values a portfolio; "${collection.name}" is a watchlist.`, "Usage: gloomberb portfolio show <portfolio> --close");
555
663
  }
664
+ if (!matchedPortfolio && drip) {
665
+ ctx.fail(`--drip projects a portfolio's dividends; "${collection.name}" is a watchlist.`, "Usage: gloomberb portfolio show <portfolio> --drip");
666
+ }
556
667
  const isPortfolio = !!matchedPortfolio;
557
668
  const id = collection.id;
558
669
  const displayName = collection.name;
@@ -575,24 +686,34 @@ async function showCollectionWithMarketData(
575
686
  // The terms the report shows, which --explain follows it with.
576
687
  let explained: GlossaryEntry[] = [];
577
688
 
578
- const quotes = await loadCollectionQuotes(filtered, collection, dataProvider);
689
+ const futuresHeld = filtered.filter((ticker) => isFuturesTicker(ticker)
690
+ && ticker.metadata.positions.some((position) => position.portfolio === id && position.shares !== 0));
691
+ const [quotes, futures] = await Promise.all([
692
+ loadCollectionQuotes(filtered, collection, dataProvider),
693
+ isPortfolio ? loadPortfolioFuturesTerms(futuresHeld, {
694
+ settlement: basis === "regular-close",
695
+ sources: { history: dailyFuturesHistory(dataProvider), ...futuresSources },
696
+ }) : undefined,
697
+ ]);
579
698
  // Worst of across the holdings' quotes, as the PORT and watchlist reports state it.
580
699
  const broker = !!(matchedPortfolio?.brokerId || matchedPortfolio?.brokerInstanceId);
581
700
  const source = !isPortfolio ? "Local watchlist and Gloom Cloud"
582
701
  : broker ? "Your broker account and Gloom Cloud" : "Local portfolio and Gloom Cloud";
583
702
  let freshness = isPortfolio ? undefined : quotesFreshness(quotes.values(), { source });
703
+ // A watchlist's stale rows, named under the closing line that counts them.
704
+ let staleNote: string | null = null;
584
705
 
585
706
  if (!structured) {
586
707
  console.log(cliStyles.bold(displayName + (isPortfolio ? ` (${currency})` : "")));
587
708
  console.log(cliStyles.muted(`${filtered.length} ticker${filtered.length === 1 ? "" : "s"}`));
588
709
  const imported = matchedPortfolio ? importedFromText(matchedPortfolio) : null;
589
710
  if (imported) console.log(cliStyles.muted(imported));
590
- if (isPortfolio) console.log(cliStyles.muted(valuationBasisLine(basis)));
711
+ if (isPortfolio) console.log(cliStyles.muted(valuationBasisLine(basis, futuresHeld.length > 0)));
591
712
  console.log("");
592
713
  }
593
714
 
594
715
  if (matchedPortfolio) {
595
- const valuation = await valuePortfolioPositions({ tickers: filtered, quotes, portfolioId: id, currency, baseCurrency: config.baseCurrency, toBase, basis });
716
+ const valuation = await valuePortfolioPositions({ tickers: filtered, quotes, portfolioId: id, currency, baseCurrency: config.baseCurrency, toBase, basis, futures });
596
717
  // Exports carry no header line, so their source line names the basis.
597
718
  freshness = portfolioValuationFreshness(valuation, source, { note: structured });
598
719
  const {
@@ -602,22 +723,45 @@ async function showCollectionWithMarketData(
602
723
  unavailableCost,
603
724
  unavailableMarketValue,
604
725
  brokerPnlSymbols,
726
+ unknownContractSize,
727
+ unsettled,
728
+ exposure,
605
729
  } = valuation;
606
730
  const { allocation, cash } = await valuePortfolioAllocation({ valuation, portfolio: matchedPortfolio, account, toBase });
607
731
  const allocationBySymbol = new Map(allocation.rows.map((row) => [row.symbol, row]));
732
+ // What each holding pays a year: trailing dividends per share times its shares, in the portfolio's currency.
733
+ const held = filtered.filter((ticker) => valued.some((entry) => entry.ticker === ticker && entry.position));
734
+ const { dividends } = await loadHoldingDividends(held, quotes, dataProvider, { deadlineMs: dividendDeadlineMs });
735
+ // Lots without a position or shares are no lot of income; a row of their own keeps its place with none.
736
+ const income = await portfolioIncome(valued.map((entry) => ({
737
+ symbol: entry.position && entry.metrics ? entry.ticker.metadata.ticker : "",
738
+ shares: entry.metrics?.totalShares ?? 0,
739
+ value: typeof entry.row.marketValue === "number" ? entry.row.marketValue : null,
740
+ })), dividends, toBase);
741
+ const lotIncome = income.lots;
742
+ const totalIncome = income.totalIncome;
743
+ const portfolioYield = totalIncome != null && allocation.total != null && allocation.total > 0 ? (totalIncome / allocation.total) * 100 : null;
744
+ const incomeUnavailable = [...income.gaps].map(([symbol, gap]) => ({ symbol, ...gap }));
608
745
  const showTargets = allocation.targetSum != null;
609
746
  // Never dropped on a narrow terminal, so --explain reads the terms off them.
610
747
  const allocationHeaders = showTargets ? ["Weight", "Target", "Drift", "Trade", "Trade Value"] : ["Weight"];
611
748
  explained = reportGlossary("PF", allocationHeaders.join(" "));
612
- // A ticker held in several lots carries its allocation on its first row.
749
+ // A ticker held in several lots carries its allocation, the whole position's, on its first row.
613
750
  const firstRows = new Set<string>();
614
- const positionsExport = valued.map((entry): Record<string, unknown> & {
751
+ const lotCounts = new Map<string, number>();
752
+ for (const entry of valued) {
753
+ if (entry.position) lotCounts.set(entry.ticker.metadata.ticker, (lotCounts.get(entry.ticker.metadata.ticker) ?? 0) + 1);
754
+ }
755
+ const positionsExport = valued.map((entry, index): Record<string, unknown> & {
615
756
  weight: number | null; targetWeight: number | null; drift: number | null; tradeShares: number | null; tradeValue: number | null;
757
+ annualIncome: number | null; incomeYield: number | null;
616
758
  } => {
617
759
  const symbol = entry.ticker.metadata.ticker;
618
760
  const first = !firstRows.has(symbol);
619
761
  firstRows.add(symbol);
620
762
  const figures = first ? allocationBySymbol.get(symbol) : undefined;
763
+ const dividend = entry.position ? dividends.get(symbol) : undefined;
764
+ const lots = lotCounts.get(symbol) ?? 0;
621
765
  return {
622
766
  ...entry.row,
623
767
  weight: figures?.weight ?? null,
@@ -626,6 +770,18 @@ async function showCollectionWithMarketData(
626
770
  tradeShares: figures?.tradeUnits ?? null,
627
771
  tradeValue: figures?.tradeValue ?? null,
628
772
  ...entry.valuation,
773
+ // After the valuation fields, so every column an export had keeps its place.
774
+ dividendPerShare: dividend?.perShare ?? null,
775
+ dividendCurrency: dividend?.currency ?? null,
776
+ annualIncome: lotIncome[index]?.income ?? null,
777
+ incomeYield: lotIncome[index]?.yield ?? null,
778
+ // Only the rows of a ticker held in several lots: `lots` says the allocation on its first row is the whole position's, `lotWeight` is this lot's own share.
779
+ ...(lots > 1 ? { lots, lotWeight: lotWeight(typeof entry.row.marketValue === "number" ? entry.row.marketValue : null, allocation.weightBase) } : {}),
780
+ // Only on a future: `shares` counts contracts, each `contractMultiplier` units of the price, as the exchange sizes it.
781
+ ...(entry.futures && entry.position ? {
782
+ contractMultiplier: entry.position.multiplier ?? null,
783
+ contractSize: entry.futures.size?.size ?? null,
784
+ } : {}),
629
785
  };
630
786
  });
631
787
  const totalPnlValue = unavailablePnl.size > 0 ? null : totalPnl;
@@ -643,28 +799,91 @@ async function showCollectionWithMarketData(
643
799
  drift: allocation.cash.drift, tradeValue: allocation.cash.tradeValue,
644
800
  } : null;
645
801
 
802
+ // `--drip`: each holding with a known income and a price, its lots netted: a long reinvests, a short owes.
803
+ const dripNotProjected = new Set<string>();
804
+ const dripProjections: DripProjection[] = [];
805
+ if (drip) {
806
+ for (const ticker of held) {
807
+ const symbol = ticker.metadata.ticker;
808
+ const dividend = dividends.get(symbol);
809
+ if (!dividend) continue;
810
+ const shares = valued.filter((entry) => entry.ticker === ticker && entry.position && entry.metrics)
811
+ .reduce((sum, entry) => sum + entry.metrics!.totalShares, 0);
812
+ if (shares === 0) continue;
813
+ // Income the table could not show is not projected either; the note under it says why.
814
+ if (income.gaps.has(symbol) || dividend.perShare == null || !dividend.currency) continue;
815
+ if (dividend.perShare === 0) continue;
816
+ if (dividend.price == null || !(dividend.price > 0)) {
817
+ dripNotProjected.add(symbol);
818
+ continue;
819
+ }
820
+ const rate = await toBase(1, dividend.currency);
821
+ if (!Number.isFinite(rate) || rate <= 0) {
822
+ dripNotProjected.add(symbol);
823
+ continue;
824
+ }
825
+ dripProjections.push({
826
+ ticker, shares,
827
+ dividend: { ...dividend, perShare: dividend.perShare, currency: dividend.currency, price: dividend.price },
828
+ years: projectDrip({ symbol, shares, perShare: dividend.perShare, price: dividend.price, rate }, drip),
829
+ });
830
+ }
831
+ }
832
+ const dripAsOf = isoDateToday();
833
+ const dripMethod = drip
834
+ ? `Trailing twelve-month dividends per share, share prices and exchange rates as of ${dripAsOf} held flat; ${drip.withholding}% withheld from each dividend; what is left buys shares once a year, at the year's end. A short owes each dividend in full, every year, and reinvests nothing.`
835
+ : null;
836
+
646
837
  const accountingBasis = "Unrealized P&L on current positions; excludes realized trades, distributions and cash flows. This is not account investment return.";
647
838
  const manualAccounting = !matchedPortfolio.brokerId && !matchedPortfolio.brokerInstanceId
648
839
  ? "Manual holdings are snapshots. Reconcile shares and per-share cost after corporate actions using portfolio position set; cash distributions are not credited."
649
840
  : null;
841
+ if (structured && drip) {
842
+ const rows = dripProjections.flatMap(({ ticker, dividend, years }) => years.map((year) => ({
843
+ symbol: ticker.metadata.ticker, listing: listingLabel(ticker), year: year.year,
844
+ shares: year.shares, addedShares: year.addedShares,
845
+ dividendPerShare: dividend.perShare, dividendCurrency: dividend.currency, price: dividend.price,
846
+ grossIncome: year.grossIncome, withheld: year.withheld, netIncome: year.netIncome,
847
+ netIncomeBase: year.netIncomeBase, valueBase: year.valueBase, baseCurrency: currency,
848
+ })));
849
+ ctx.printResult({ data: rows, metadata: {
850
+ portfolioId: id, portfolioName: displayName, baseCurrency: currency,
851
+ years: drip.years, withholdingPercent: drip.withholding, asOf: dripAsOf, method: dripMethod,
852
+ annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
853
+ incomeComplete: income.gaps.size === 0, incomeUnavailable,
854
+ notProjectedSymbols: [...dripNotProjected, ...income.gaps.keys()],
855
+ complete: income.gaps.size === 0 && dripNotProjected.size === 0,
856
+ }, freshness });
857
+ return;
858
+ }
650
859
  if (structured) {
651
860
  // CSV and NDJSON read the cash and the total as rows; JSON keeps them in the metadata.
652
861
  const extraRows: Record<string, unknown>[] = [
653
862
  ...(cashExport ? [{ symbol: CASH_SYMBOL, marketValue: cashExport.value, baseCurrency: currency, weight: cashExport.weight,
654
863
  targetWeight: cashExport.targetWeight, drift: cashExport.drift, tradeShares: null, tradeValue: cashExport.tradeValue }] : []),
655
864
  { symbol: "TOTAL", marketValue: allocation.total, unrealizedPnl: totalPnlValue, baseCurrency: currency,
656
- weight: allocation.total != null && allocation.total > 0 ? 100 : null, targetWeight: allocation.targetSum, valuationBasis: basis },
865
+ weight: totalWeight(allocation), targetWeight: allocation.targetSum, valuationBasis: basis,
866
+ annualIncome: totalIncome, incomeYield: portfolioYield },
657
867
  ];
658
868
  ctx.printResult({ data: positionsExport, metadata: {
659
869
  portfolioId: id, portfolioName: displayName, baseCurrency: currency, currency,
660
870
  valuationBasis: basis,
661
871
  total: allocation.total, cash: cashExport, targetSum: allocation.targetSum,
872
+ // Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
873
+ ...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
662
874
  ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
663
875
  unpricedCount: allocation.unpriced.length, unpricedSymbols: allocation.unpriced,
664
876
  totalUnrealizedPnl: totalPnlValue,
665
- complete: unavailablePnl.size === 0 && unavailableCost.size === 0 && unavailableMarketValue.size === 0,
877
+ annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
878
+ incomeComplete: income.gaps.size === 0, incomeUnavailable,
879
+ incomeBasis: "Trailing twelve-month dividends per share times shares held, in the portfolio's currency; a short owes the dividend, so its income is negative. Portfolio yield is the total over the total value; annualIncome is null when any holding's income is unknown, and knownAnnualIncome sums the holdings that have one.",
880
+ complete: unavailablePnl.size === 0 && unavailableCost.size === 0 && unavailableMarketValue.size === 0 && income.gaps.size === 0,
666
881
  unavailableSymbols: [...new Set([...unavailablePnl, ...unavailableCost, ...unavailableMarketValue])],
667
882
  unavailableCostSymbols: [...unavailableCost], brokerPnlSymbols: [...brokerPnlSymbols],
883
+ // The priced holdings' market value: longs, shorts (negative), net and gross. Cash and unpriced holdings are not in it.
884
+ exposure,
885
+ ...(unknownContractSize.size > 0 ? { unknownContractSizeSymbols: [...unknownContractSize] } : {}),
886
+ ...(unsettled.size > 0 ? { unsettledSymbols: [...unsettled] } : {}),
668
887
  accountingBasis, ...(manualAccounting ? { manualAccounting } : {}),
669
888
  ...(matchedPortfolio.importedFrom ? { importedFrom: matchedPortfolio.importedFrom } : {}),
670
889
  ...(explain ? { glossary: glossaryExport(explained) } : {}),
@@ -692,54 +911,85 @@ async function showCollectionWithMarketData(
692
911
  return [weight, target, drift, trade, tradeValue];
693
912
  };
694
913
 
914
+ // Income and its yield, once any share or fund has one; with none known, the note under the table says why.
915
+ const showIncome = income.knownHoldings > 0;
916
+ const incomeCells = (income: number | null | undefined, yieldPercent: number | null | undefined): string[] =>
917
+ showIncome ? [money(income), formatIncomeYield(yieldPercent)] : [];
695
918
  const extendedSessions = extendedSessionsOf(valued.map(({ extended }) => extended?.session));
696
919
  const now = Date.now();
697
- const rows: string[][] = valued.map(({ ticker, quote, headline, extended, position, metrics, pnl, row, valuation: priced }, index) => {
698
- const asOf = valuationTimeText(priced.valuationAsOf, now);
920
+ // Where each ticker's lots end, to put its position line there.
921
+ const lastLot = new Map<string, number>();
922
+ valued.forEach((entry, index) => { if (entry.position) lastLot.set(entry.ticker.metadata.ticker, index); });
923
+ const rows: string[][] = valued.flatMap(({ ticker, quote, headline, extended, position, metrics, pnl, row, valuation: priced, futures: contract }, index): string[][] => {
924
+ const asOf = valuationTimeText(priced.valuationAsOf, now, priced.valuationPriceKind);
699
925
  const priceText = quote && headline
700
926
  ? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
701
927
  : "—";
702
928
  const changeText = headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
703
929
  const quoteCells = [priceText, changeText, ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory)];
704
930
  const exported = positionsExport[index]!;
705
- const figures = exported.weight != null || exported.targetWeight != null || exported.tradeValue != null
706
- ? { weight: exported.weight, targetWeight: exported.targetWeight, drift: exported.drift, tradeValue: exported.tradeValue, tradeUnits: exported.tradeShares }
707
- : undefined;
708
- const symbolUnits = allocationBySymbol.get(ticker.metadata.ticker)?.units ?? 0;
931
+ const symbol = ticker.metadata.ticker;
932
+ const lots = lotCounts.get(symbol) ?? 0;
933
+ // A lot of several weighs by its own value; its target, drift and trade belong to the position, on the line under its lots.
934
+ const figures = lots > 1
935
+ ? { weight: exported.lotWeight as number | null, targetWeight: null, drift: null, tradeValue: null, tradeUnits: null }
936
+ : exported.weight != null || exported.targetWeight != null || exported.tradeValue != null
937
+ ? { weight: exported.weight, targetWeight: exported.targetWeight, drift: exported.drift, tradeValue: exported.tradeValue, tradeUnits: exported.tradeShares }
938
+ : undefined;
939
+ const symbolUnits = allocationBySymbol.get(symbol)?.units ?? 0;
709
940
  if (!position || !metrics) {
710
- return [listingLabel(ticker), ...quoteCells, "—", "—", "—",
711
- ...allocationCells(figures, { unpriced: false, units: symbolUnits, assetCategory: ticker.metadata.assetCategory }), "—", asOf];
941
+ return [[listingLabel(ticker), ...quoteCells, "—", "—", "—",
942
+ ...allocationCells(figures, { unpriced: false, units: symbolUnits, assetCategory: ticker.metadata.assetCategory }),
943
+ ...incomeCells(null, null), "—", asOf]];
712
944
  }
713
945
  const positionCurrency = metrics.positionCurrency;
714
946
  const marketValue = row.marketValue as number | null;
715
947
  const unpriced = marketValue == null;
716
- return [
948
+ const quantityOptions = { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency };
949
+ // A future counts contracts, each the size its terms give: 1,000 × 100 oz.
950
+ const quantity = (units: number) => contract
951
+ ? futuresQuantityText(formatMarketQuantity(units, quantityOptions), contract.size
952
+ ?? (position.multiplier ? { size: formatMarketQuantity(position.multiplier) } : null))
953
+ : formatMarketQuantity(units, quantityOptions);
954
+ const lotRow = [
717
955
  listingLabel(ticker),
718
956
  ...quoteCells,
719
- formatMarketQuantity(metrics.totalShares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency }),
957
+ quantity(metrics.totalShares),
720
958
  formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }),
721
959
  unpriced ? NOT_AVAILABLE : wholeMoney(marketValue),
722
960
  ...allocationCells(figures, { unpriced, units: symbolUnits, assetCategory: ticker.metadata.assetCategory }),
961
+ ...incomeCells(exported.annualIncome, exported.incomeYield),
723
962
  pnl == null || !Number.isFinite(pnl) ? "—" : colorBySign(money(pnl, { signed: true }), pnl),
724
963
  asOf,
725
964
  ];
965
+ if (lots < 2 || lastLot.get(symbol) !== index) return [lotRow];
966
+ // The whole position under its lots: its shares and value, and the weight, target, drift and trade they add up to.
967
+ const whole = allocationBySymbol.get(symbol);
968
+ const wholeValue = whole?.marketValue ?? null;
969
+ return [lotRow, [`${listingLabel(ticker)} (${lots} lots)`, "", "", ...extendedSessions.map(() => ""),
970
+ quantity(symbolUnits), "", wholeValue == null ? NOT_AVAILABLE : wholeMoney(wholeValue),
971
+ ...allocationCells(whole, { unpriced: wholeValue == null, units: symbolUnits, assetCategory: ticker.metadata.assetCategory }),
972
+ ...(showIncome ? ["", ""] : []), "", ""]];
726
973
  });
727
974
  if (cashExport) {
728
975
  const label = cashExport.currency && cashExport.currency !== currency ? `${CASH_SYMBOL} ${cashExport.currency}` : CASH_SYMBOL;
729
976
  rows.push([label, "", "", ...extendedSessions.map(() => ""), "", "", cashExport.value == null ? NOT_AVAILABLE : wholeMoney(cashExport.value),
730
- ...allocationCells(cashExport, { unpriced: cashExport.value == null }), "", ""]);
977
+ ...allocationCells(cashExport, { unpriced: cashExport.value == null }), ...(showIncome ? ["", ""] : []), "", ""]);
731
978
  }
732
979
  const total = allocation.total;
733
980
  rows.push([
734
981
  cliStyles.bold("TOTAL"), "", "", ...extendedSessions.map(() => ""), "", "",
735
982
  cliStyles.bold(wholeMoney(total)),
736
- total != null && total > 0 ? formatAllocationWeight(100) : "—",
983
+ formatAllocationWeight(totalWeight(allocation)),
737
984
  ...(showTargets ? [formatAllocationWeight(allocation.targetSum), "", "", ""] : []),
985
+ ...(showIncome ? [cliStyles.bold(money(totalIncome)), formatIncomeYield(portfolioYield)] : []),
738
986
  totalPnlValue == null ? "—" : colorBySign(money(totalPnlValue, { signed: true }), totalPnlValue),
739
987
  "",
740
988
  ]);
741
989
 
742
- const quantityHeader = filtered.some(ticker => ticker.metadata.assetCategory?.toUpperCase() === "BOND") || positionsExport.some(row => row.quantityUnit === "face") ? "Qty" : "Shares";
990
+ // Face amounts and contracts are not shares.
991
+ const quantityHeader = filtered.some(ticker => ticker.metadata.assetCategory?.toUpperCase() === "BOND") || positionsExport.some(row => row.quantityUnit === "face")
992
+ || holdsFutures(valuation) ? "Qty" : "Shares";
743
993
  // A narrow terminal drops the cost, then the quote, then P&L, before any of the allocation.
744
994
  console.log(renderTable(
745
995
  [
@@ -752,6 +1002,11 @@ async function showCollectionWithMarketData(
752
1002
  { header: "Avg Cost", align: "right", optional: true, dropPriority: 3 },
753
1003
  { header: "Value", align: "right" },
754
1004
  ...allocationHeaders.map((header) => ({ header, align: "right" as const })),
1005
+ // A year of trailing dividends on the shares held, dropped after the quote and before P&L.
1006
+ ...(showIncome ? [
1007
+ { header: "Income", align: "right" as const, optional: true, dropPriority: 0.5 },
1008
+ { header: "Yield", align: "right" as const, optional: true, dropPriority: 0.5 },
1009
+ ] : []),
755
1010
  { header: "P&L", align: "right", optional: true },
756
1011
  // When each row's price is from, so a Friday close never hides behind a newer row.
757
1012
  { header: "As of (UTC)" },
@@ -760,10 +1015,15 @@ async function showCollectionWithMarketData(
760
1015
  ));
761
1016
  console.log("");
762
1017
  if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign));
1018
+ if (valued.some((entry) => entry.position)) console.log(exposureLine(exposure, currency));
763
1019
  const notes: string[] = [];
1020
+ const weightNote = weightBasisLine(allocation, currency);
1021
+ if (weightNote) notes.push(cliStyles.muted(weightNote));
764
1022
  // The notes name the rows as the table does.
765
1023
  const labels = new Map(filtered.map((ticker) => [ticker.metadata.ticker, listingLabel(ticker)]));
766
1024
  const named = (symbols: Iterable<string>) => [...symbols].map((symbol) => labels.get(symbol) ?? symbol).join(", ");
1025
+ // A future without its contract size or settlement is said once, not as a missing cost, value and P&L.
1026
+ const unsized = (symbols: ReadonlySet<string>) => [...symbols].filter((symbol) => !unknownContractSize.has(symbol) && !unsettled.has(symbol));
767
1027
  if (allocation.unpriced.length > 0) {
768
1028
  const count = allocation.unpriced.length;
769
1029
  notes.push(cliStyles.warning(`Total and weights exclude ${count} unpriced holding${count === 1 ? "" : "s"}: ${named(allocation.unpriced)}.`));
@@ -775,12 +1035,58 @@ async function showCollectionWithMarketData(
775
1035
  ? `No ${cashExport.currency}/${currency} rate for the cash, so the total and weights are unavailable.`
776
1036
  : "The cash's currency is unknown, so the total and weights are unavailable."));
777
1037
  }
778
- const targetNote = describeTargetSum(allocation.targetSum);
1038
+ const targetNote = describeTargetSum(allocation.targetSum, allocation.weightBasis);
779
1039
  if (targetNote) notes.push(cliStyles.muted(targetNote));
780
1040
  if (brokerPnlSymbols.size > 0) notes.push(cliStyles.muted(`P&L for ${named(brokerPnlSymbols)} is the broker's last snapshot.`));
781
- if (unavailableCost.size > 0) notes.push(cliStyles.muted(`Cost unavailable for ${named(unavailableCost)}.`));
782
- if (unavailablePnl.size > 0) notes.push(cliStyles.muted(`P&L unavailable for ${named(unavailablePnl)}.`));
1041
+ if (unknownContractSize.size > 0) {
1042
+ 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
+ }
1044
+ if (unsettled.size > 0) {
1045
+ notes.push(cliStyles.warning(`No confirmed settlement for ${named(unsettled)}, so ${unsettled.size === 1 ? "it is" : "they are"} not valued at the close.`));
1046
+ }
1047
+ if (unsized(unavailableCost).length > 0) notes.push(cliStyles.muted(`Cost unavailable for ${named(unsized(unavailableCost))}.`));
1048
+ if (unsized(unavailablePnl).length > 0) notes.push(cliStyles.muted(`P&L unavailable for ${named(unsized(unavailablePnl))}.`));
1049
+ const incomeNote = incomeGapNote(income, { name: named, money: (value) => money(value), deadlineMs: dividendDeadlineMs });
1050
+ if (incomeNote) notes.push(cliStyles.muted(incomeNote));
783
1051
  for (const note of notes) console.log(note);
1052
+ if (drip) {
1053
+ console.log("");
1054
+ console.log(renderSection(`DRIP projection, ${drip.years} year${drip.years === 1 ? "" : "s"}`));
1055
+ if (dripProjections.length === 0) {
1056
+ console.log(cliStyles.muted(dividends.size === 0 ? "No shares or funds to project."
1057
+ : income.gaps.size > 0 ? "Nothing to project: the income above is unavailable." : "No holding paid a dividend in the past year."));
1058
+ } else {
1059
+ const yearNumbers = Array.from({ length: drip.years }, (_, index) => index + 1);
1060
+ const finalYear = `Shares Y${drip.years}`;
1061
+ const quantity = (value: number, ticker: TickerRecord) => formatMarketQuantity(value, { assetCategory: ticker.metadata.assetCategory });
1062
+ const dripRows = dripProjections.map(({ ticker, shares, years }) => [
1063
+ listingLabel(ticker),
1064
+ quantity(shares, ticker),
1065
+ quantity(years.at(-1)!.shares, ticker),
1066
+ ...years.map((year) => money(year.netIncomeBase)),
1067
+ ]);
1068
+ dripRows.push([
1069
+ cliStyles.bold("TOTAL"), "", "",
1070
+ ...yearNumbers.map((year) => cliStyles.bold(money(dripProjections.reduce((sum, entry) => sum + entry.years[year - 1]!.netIncomeBase, 0)))),
1071
+ ]);
1072
+ console.log(renderTable(
1073
+ [
1074
+ { header: "Ticker" },
1075
+ { header: "Shares", align: "right" },
1076
+ { header: finalYear, align: "right" },
1077
+ ...yearNumbers.map((year) => ({ header: `Y${year} income`, align: "right" as const })),
1078
+ ],
1079
+ dripRows,
1080
+ ));
1081
+ const shorts = dripProjections.some((entry) => entry.shares < 0) ? "; shorts owe each dividend in full" : "";
1082
+ console.log(cliStyles.muted(`As of ${dripAsOf}: trailing dividends, prices and FX held flat; ${drip.withholding}% withheld; reinvested once a year${shorts}.`));
1083
+ }
1084
+ const unprojected = [
1085
+ ...(income.gaps.size > 0 && dripProjections.length > 0 ? [`${named([...income.gaps.keys()])} (income unavailable above)`] : []),
1086
+ ...(dripNotProjected.size > 0 ? [`${named(dripNotProjected)} (no price to reinvest at)`] : []),
1087
+ ];
1088
+ if (unprojected.length > 0) console.log(cliStyles.muted(`Not projected: ${unprojected.join("; ")}.`));
1089
+ }
784
1090
  } else {
785
1091
  const watched = await valueWatchlistRows({ tickers: filtered, quotes, dataProvider, currency, toBase });
786
1092
  if (structured) {
@@ -794,7 +1100,9 @@ async function showCollectionWithMarketData(
794
1100
  const extendedSessions = extendedSessionsOf(watched.map(({ extended }) => extended?.session));
795
1101
  // Funding is the one column a list earns by holding perpetual markets; any other list's table is unchanged.
796
1102
  const showFunding = watched.some(({ funding }) => funding !== undefined);
797
- const rows = watched.map(({ ticker, quote, headline, extended, venue, marketCap, funding }) => {
1103
+ // So is Last Trade, by holding a stale quote: the rows the closing line counts stale, dated as `quote` dates them.
1104
+ const stale = watched.filter((entry) => entry.stale);
1105
+ const rows = watched.map(({ ticker, quote, headline, extended, venue, marketCap, funding, stale: rowStale, lastTrade }) => {
798
1106
  const priceText = quote && headline
799
1107
  ? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
800
1108
  : "—";
@@ -804,6 +1112,7 @@ async function showCollectionWithMarketData(
804
1112
  venue ?? "—",
805
1113
  priceText,
806
1114
  changeText,
1115
+ ...stale.length > 0 ? [rowStale ? cliStyles.warning(lastTrade ? `STALE ${lastTrade}` : "STALE") : ""] : [],
807
1116
  ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
808
1117
  ...showFunding ? [funding == null ? "—" : colorBySign(formatPerpetualRate(funding, 4), funding)] : [],
809
1118
  marketCap == null ? "—" : `${formatCompact(marketCap)} ${currency}`,
@@ -816,6 +1125,7 @@ async function showCollectionWithMarketData(
816
1125
  { header: "Exchange" },
817
1126
  { header: "Last", align: "right" },
818
1127
  { header: "Chg", align: "right" },
1128
+ ...stale.length > 0 ? [{ header: "Last Trade", shrink: false }] : [],
819
1129
  ...extendedSessions.map((session) => ({ header: EXTENDED_SESSION_LABELS[session], align: "right" as const, optional: true, dropPriority: 1 })),
820
1130
  // As the TUI's FUND 8H: per eight hours whatever interval the venue pays on.
821
1131
  ...showFunding ? [{ header: "Fund 8H", align: "right" as const, optional: true, dropPriority: 1 }] : [],
@@ -826,8 +1136,12 @@ async function showCollectionWithMarketData(
826
1136
  ));
827
1137
  const notes = marketCapNotes(watched);
828
1138
  if (notes.length > 0) console.log(["", ...notes.map((note) => cliStyles.muted(note))].join("\n"));
1139
+ // The closing line counts the stale rows; the line under it names them.
1140
+ if (stale.length > 0) {
1141
+ staleNote = `Stale: ${namedList(stale.map(({ ticker, lastTrade }) => `${listingLabel(ticker)}${lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
1142
+ }
829
1143
  }
830
- if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
1144
+ if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}${staleNote ? `\n${cliStyles.muted(staleNote)}` : ""}`);
831
1145
  if (explain) console.log(`\n${renderReportGlossary(EXPLAINED_REPORT, explained)}`);
832
1146
  }
833
1147
 
@@ -843,6 +1157,10 @@ interface WatchlistRowValue {
843
1157
  marketCap: number | null;
844
1158
  /** A perpetual market's funding per eight hours, as a fraction; null when its market cannot be read, undefined for any other row. */
845
1159
  funding: number | null | undefined;
1160
+ /** Whether the quote is stale, as the closing line counts it. */
1161
+ stale: boolean;
1162
+ /** The exchange-local date of the quote's last trade. */
1163
+ lastTrade: string | null;
846
1164
  row: Record<string, unknown>;
847
1165
  }
848
1166
 
@@ -933,8 +1251,10 @@ async function valueWatchlistRows({
933
1251
  const kind = resolveTickerInstrumentKind(ticker, { quote: quote ?? snapshot?.quote });
934
1252
  const venue = exchangeShortName(quote?.tradingExchangeName || quote?.exchangeName, quote?.fullExchangeName) || ticker.metadata.exchange || null;
935
1253
  const funding = perpetualFunding.get(symbol);
1254
+ const stale = quoteFreshnessFields(quote).stale === true;
1255
+ const lastTrade = quote ? lastTradeDate(quote) : null;
936
1256
  values.push({
937
- ticker, quote, headline, extended, kind, venue, marketCap, funding,
1257
+ ticker, quote, headline, extended, kind, venue, marketCap, funding, stale, lastTrade,
938
1258
  row: {
939
1259
  symbol,
940
1260
  ...listingColumns(ticker, quote),
@@ -951,6 +1271,9 @@ async function valueWatchlistRows({
951
1271
  marketCapCurrency: currency,
952
1272
  // When the price shown was observed, as `quote` dates it.
953
1273
  quoteAsOf: quote ? known(quoteHeadlineTime(quote)) : null,
1274
+ // A stale quote (as the source line counts it) and the exchange-local date it last traded.
1275
+ stale,
1276
+ lastTradeDate: lastTrade,
954
1277
  },
955
1278
  });
956
1279
  }