gloomberb 0.16.3 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -9,12 +9,16 @@ import type {
9
9
  import type { TickerRecord } from "../../../types/ticker";
10
10
  import { formatNumber, formatPercentRaw } from "../../../utils/format";
11
11
  import {
12
+ collectionQuoteHeadline,
13
+ extendedQuoteFields,
14
+ extendedSessionsOf,
12
15
  findCollection,
13
16
  loadCollectionQuotes,
14
17
  valuePortfolioAllocation,
15
18
  valuePortfolioPositions,
16
19
  type CollectionMatch,
17
20
  } from "./cli/render";
21
+ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
18
22
  import {
19
23
  BROKER_ACCOUNT_MISSING_NOTE,
20
24
  CASH_SYMBOL,
@@ -61,6 +65,7 @@ function textOf(value: unknown): string | undefined {
61
65
  const POSITION_COLUMNS: HeadlessPaneColumn[] = [
62
66
  { key: "symbol", header: "Ticker" },
63
67
  { key: "name", header: "Name" },
68
+ { key: "exchange", header: "Exchange" },
64
69
  { key: "shares", header: "Qty", align: "right", format: quantity },
65
70
  { key: "avgCost", header: "Avg Cost", align: "right", format: unitMoney("positionCurrency"), description: "Per unit, in the position's currency." },
66
71
  { key: "price", header: "Last", align: "right", format: unitMoney("priceCurrency"), description: "In the quote's currency." },
@@ -79,6 +84,26 @@ const TARGET_COLUMNS: HeadlessPaneColumn[] = [
79
84
  { key: "tradeValue", header: "Trade Value", align: "right", format: signedAmount, description: "The trade in the portfolio's currency." },
80
85
  ];
81
86
 
87
+ /** The pre-market or after-hours print and its move from the regular close, for the rows that have one. */
88
+ function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
89
+ return {
90
+ key: "extendedPrice",
91
+ header: EXTENDED_SESSION_LABELS[session],
92
+ align: "right",
93
+ format: (value, row) => row.extendedSession === session && typeof value === "number"
94
+ ? `${unitMoney("priceCurrency")(value, row)} ${percent(row.extendedChangePercent)}`
95
+ : formatNumber(undefined),
96
+ description: "Measured from the regular close in Last.",
97
+ };
98
+ }
99
+
100
+ /** `columns` with an extended column after Chg for each session a row has a print for. */
101
+ function withExtendedColumns(columns: HeadlessPaneColumn[], sessions: readonly ExtendedSession[]): HeadlessPaneColumn[] {
102
+ if (sessions.length === 0) return columns;
103
+ const at = columns.findIndex((column) => column.key === "changePercent") + 1;
104
+ return [...columns.slice(0, at), ...sessions.map(extendedColumn), ...columns.slice(at)];
105
+ }
106
+
82
107
  const WATCHLIST_COLUMNS: HeadlessPaneColumn[] = [
83
108
  { key: "symbol", header: "Ticker" },
84
109
  { key: "name", header: "Name" },
@@ -154,8 +179,9 @@ async function portfolioHoldings(
154
179
  const gross = held.reduce((sum, entry) => sum + Math.abs(finite(entry.row.marketValue) ?? 0), 0);
155
180
  const sum = (key: string) => held.reduce((total, entry) => total + (finite(entry.row[key]) ?? 0), 0);
156
181
  const seen = new Set<string>();
182
+ const extendedSessions = extendedSessionsOf(held.map(({ extended }) => extended?.session));
157
183
  const rows = held
158
- .map(({ ticker, activeQuote, row }) => {
184
+ .map(({ ticker, headline, extended, row }) => {
159
185
  const marketValue = finite(row.marketValue);
160
186
  // A ticker held in several lots carries its allocation on its first row.
161
187
  const figures = seen.has(ticker.metadata.ticker) ? undefined : allocationBySymbol.get(ticker.metadata.ticker);
@@ -170,9 +196,11 @@ async function portfolioHoldings(
170
196
  shares: finite(row.shares),
171
197
  avgCost: finite(row.avgCost),
172
198
  positionCurrency: row.positionCurrency ?? null,
199
+ // Last and Chg are the regular session; market value and P&L are at the live price.
173
200
  price: finite(row.quotePrice),
174
201
  priceCurrency: row.quoteCurrency ?? null,
175
- changePercent: finite(activeQuote?.changePercent),
202
+ changePercent: finite(headline?.changePercent),
203
+ ...extendedQuoteFields(extended),
176
204
  marketValue,
177
205
  unrealizedPnl: finite(row.unrealizedPnl),
178
206
  weight: figures?.weight ?? null,
@@ -216,7 +244,7 @@ async function portfolioHoldings(
216
244
 
217
245
  return {
218
246
  freshness: { source: broker ? "Your broker account and Gloom Cloud" : "Local portfolio and Gloom Cloud" },
219
- columns: showTargets ? [...POSITION_COLUMNS, ...TARGET_COLUMNS] : POSITION_COLUMNS,
247
+ columns: withExtendedColumns(showTargets ? [...POSITION_COLUMNS, ...TARGET_COLUMNS] : POSITION_COLUMNS, extendedSessions),
220
248
  rows: shown,
221
249
  complete: unavailable.length === 0,
222
250
  ...(unavailable.length
@@ -254,21 +282,24 @@ async function watchlistHoldings(
254
282
  ctx.signal.throwIfAborted();
255
283
  const rows = tickers.map((ticker) => {
256
284
  const quote = quotes.get(ticker.metadata.ticker);
285
+ // As the watchlist pane shows it: the regular session, then any extended print from its close.
286
+ const { headline, extended } = collectionQuoteHeadline(quote);
257
287
  return {
258
288
  ...quoteFreshnessFields(quote),
259
289
  updatedAt: quote?.lastUpdated ?? null,
260
290
  symbol: ticker.metadata.ticker,
261
291
  name: ticker.metadata.name ?? null,
262
292
  exchange: ticker.metadata.exchange || null,
263
- price: finite(quote?.price),
293
+ price: finite(headline?.price ?? quote?.price),
264
294
  priceCurrency: quote?.currency ?? null,
265
- changePercent: finite(quote?.changePercent),
295
+ changePercent: finite(headline ? headline.changePercent : quote?.changePercent),
296
+ ...extendedQuoteFields(extended),
266
297
  };
267
298
  });
268
299
  const shown = rows.slice(0, limit);
269
300
  return {
270
301
  freshness: { source: "Local watchlist and Gloom Cloud" },
271
- columns: WATCHLIST_COLUMNS,
302
+ columns: withExtendedColumns(WATCHLIST_COLUMNS, extendedSessionsOf(shown.map((row) => row.extendedSession))),
272
303
  rows: shown,
273
304
  metadata: {
274
305
  collection: { kind: "watchlist", id: target.id, name: target.name },
@@ -288,7 +319,7 @@ async function watchlistHoldings(
288
319
  export const collectionHoldingsHeadless: HeadlessPaneDefinition<"rows"> = {
289
320
  shape: "rows",
290
321
  description:
291
- "Positions held in a portfolio, broker or manual: symbol, quantity, average cost, last price, market value, unrealized P&L and weight of the total with cash, largest first, then the cash line and totals. With target weights set, each row adds its target, drift and the trade to reach it. For a watchlist, its tickers with quotes. Takes a portfolio or watchlist ID; the first portfolio when omitted.",
322
+ "Positions held in a portfolio, broker or manual: symbol, exchange, shares, average cost, last price, market value, unrealized P&L and weight of the total with cash, largest first, then the cash line and totals. With target weights set, each row adds its target, drift and the trade to reach it. For a watchlist, its tickers with quotes. Takes a portfolio or watchlist ID; the first portfolio when omitted.",
292
323
  discovery: {
293
324
  dataRequirements: ["Local portfolios, watchlists and synced broker positions; current quotes"],
294
325
  limitations: ["Unrealized P&L on current positions; excludes realized trades, distributions and cash flows"],
@@ -46,7 +46,10 @@ export function realizedVolHeadless(initialView: "graph" | "cone"): HeadlessPane
46
46
  estimator: ESTIMATOR_OPTIONS.find(({ value }) => value === args.options.estimator)?.value,
47
47
  windows, lookbackYears: Number(args.options.lookbackYears) === 2 ? 2 : 1 });
48
48
  const errors = [history.error, iv?.error].filter((value): value is string => !!value);
49
+ // The cone rows are as of the last daily bar, not of when the report ran.
50
+ const lastBar = model.asOf && Number.isFinite(model.asOf.getTime()) ? model.asOf.toISOString().slice(0, 10) : null;
49
51
  return {
52
+ freshness: { asOf: lastBar },
50
53
  sections: [
51
54
  { title: "Volatility cone", columns: [
52
55
  { key: "window", header: "Sessions" },
@@ -164,7 +164,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
164
164
  { id: "estimator", label: "Estimator", value: estimator, options: ESTIMATOR_OPTIONS, onChange: (value: string) => setEstimator(value as RealizedVolatilityEstimator) },
165
165
  { id: "lookback", label: "Lookback", value: String(lookback), options: [{ value: "1", label: "1Y" }, { value: "2", label: "2Y" }], onChange: setLookback },
166
166
  ]} meta={reference ? `ATM IV observed ${reference.date.toISOString().slice(0, 16).replace("T", " ")} UTC` : "Annualized %"} />
167
- {!symbol ? <EmptyState title="Choose a ticker." /> : <PaneStatusBody subject="realized volatility" loading={history.loading && !model}
167
+ {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="realized volatility" loading={history.loading && !model}
168
168
  error={!model ? history.error ?? identityError ?? null : null} empty={!!model && !model.history.length}>
169
169
  {model && view === "cone" ? <>
170
170
  <StatGrid items={coneStats} width={width} />
@@ -25,7 +25,7 @@ import { formatPercent } from "../../../utils/format";
25
25
  import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
26
26
  import { instrumentFromTicker } from "../../../market-data/request-types";
27
27
  import { usePaneTicker } from "../../../state/app/context";
28
- import { isUsEquityTicker } from "../../../utils/sec";
28
+ import { mayBeUsEquityTicker } from "../../../utils/sec";
29
29
  import { useAssetData, usePluginPaneState } from "../../runtime";
30
30
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
31
31
  import { SignInWall } from "../cloud/auth-actions";
@@ -496,7 +496,7 @@ export function CorporateActionsView({
496
496
  : null;
497
497
  const instrument = useMemo(() => instrumentFromTicker(ticker, symbol), [symbol, ticker]);
498
498
  const secFilingsEntry = useSecFilingsQuery(
499
- openRow?.status === "Earnings" && instrument && isUsEquityTicker(ticker)
499
+ openRow?.status === "Earnings" && instrument && mayBeUsEquityTicker(ticker)
500
500
  ? { instrument, count: SEC_EVENT_FILING_LIMIT }
501
501
  : null,
502
502
  );
@@ -558,7 +558,7 @@ export function EquityDiagnosticView({ focused, width, height }: {
558
558
  // Walls and empty states are drawn straight into the pane, like every other
559
559
  // pane's; only a report sits in the padded scroll area.
560
560
  if (!symbol) {
561
- return <PaneStatusBody empty emptyTitle="No ticker selected." emptyMessage="Move the cursor in a list pane to populate this view." />;
561
+ return <PaneStatusBody empty emptyTitle="Select a ticker." emptyMessage="Move the cursor in a list pane to populate this view." />;
562
562
  }
563
563
  if (signInRequired || verificationRequired) {
564
564
  return <SignInWall placement="diag-signin" width={width} height={height} symbol={symbol} exchange={exchange} action="run the Equity Diagnostic" needsVerification={verificationRequired} />;
@@ -4,7 +4,9 @@ import type {
4
4
  RevenueBreakdownPayload,
5
5
  RevenueBreakdownView,
6
6
  } from "../../../api-client/revenue-breakdown";
7
+ import type { IssuerListingParams } from "../../../api-client/paths";
7
8
  import { createPluginCache } from "../../../data/plugin-cache";
9
+ import { secListingKey } from "../../../utils/sec";
8
10
 
9
11
  /**
10
12
  * Revenue by product, segment or region comes from Gloom Cloud, which reads
@@ -59,10 +61,12 @@ export async function fetchRevenueBreakdown(
59
61
  symbol: string,
60
62
  view: RevenueBreakdownView,
61
63
  client: Pick<typeof apiClient, "getCloudRevenueBreakdown"> = apiClient,
64
+ listing?: IssuerListingParams,
62
65
  ): Promise<RevenueBreakdownPayload> {
63
66
  try {
64
- return validateRevenueBreakdown(await client.getCloudRevenueBreakdown(symbol, view));
67
+ return validateRevenueBreakdown(await client.getCloudRevenueBreakdown(symbol, view, listing));
65
68
  } catch (error) {
69
+ // 404 is also a listing abroad whose company the SEC does not know.
66
70
  if (error instanceof ApiRequestError && error.status === 404) throw new Error(NO_BREAKDOWN);
67
71
  throw error;
68
72
  }
@@ -74,8 +78,9 @@ export interface RevenueResource {
74
78
  refreshError: string | null;
75
79
  }
76
80
 
77
- const cacheKey = (symbol: string, view: RevenueBreakdownView, pro: boolean) =>
78
- `${symbol.toUpperCase()}:${view}:${pro ? "full" : "preview"}`;
81
+ // SAN in Paris is not SAN in New York: a listing abroad is cached under its venue.
82
+ const cacheKey = (symbol: string, view: RevenueBreakdownView, pro: boolean, listing?: IssuerListingParams) =>
83
+ `${secListingKey(symbol, listing?.exchange)}:${view}:${pro ? "full" : "preview"}`;
79
84
 
80
85
  // Seeds the first paint while the mount load runs; cache age is not staleness,
81
86
  // and a failed refresh reports its own.
@@ -83,8 +88,9 @@ export function cachedRevenueBreakdown(
83
88
  symbol: string,
84
89
  view: RevenueBreakdownView,
85
90
  pro: boolean,
91
+ listing?: IssuerListingParams,
86
92
  ): RevenueResource | null {
87
- const cached = revenueBreakdownCache.get(cacheKey(symbol, view, pro), { allowExpired: true });
93
+ const cached = revenueBreakdownCache.get(cacheKey(symbol, view, pro, listing), { allowExpired: true });
88
94
  if (!cached) return null;
89
95
  try {
90
96
  return { payload: validateRevenueBreakdown(cached.data), stale: false, refreshError: null };
@@ -98,10 +104,11 @@ export async function loadRevenueBreakdown(
98
104
  view: RevenueBreakdownView,
99
105
  pro: boolean,
100
106
  force = false,
107
+ listing?: IssuerListingParams,
101
108
  ): Promise<RevenueResource> {
102
109
  const result = await revenueBreakdownCache.load(
103
- cacheKey(symbol, view, pro),
104
- () => fetchRevenueBreakdown(symbol, view),
110
+ cacheKey(symbol, view, pro, listing),
111
+ () => fetchRevenueBreakdown(symbol, view, apiClient, listing),
105
112
  { force },
106
113
  );
107
114
  // A company that stopped reporting a breakdown must not keep showing an old one.
@@ -3,6 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchRevenueBreakdown } from "./client";
4
4
  import { quarterLabel, reportedSpan } from "./model";
5
5
  import { SEC_FILINGS } from "../shared/report-freshness";
6
+ import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
6
7
 
7
8
  export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
8
9
  shape: "bundle",
@@ -34,10 +35,12 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
34
35
  ],
35
36
  describe: (args) => `Revenue breakdown | ${args.symbols[0]}`,
36
37
  async load(args, ctx) {
38
+ const symbol = args.symbols[0]!;
37
39
  const data = reportedSpan(await fetchRevenueBreakdown(
38
- args.symbols[0]!,
40
+ symbol,
39
41
  (args.options.view as RevenueBreakdownView | undefined) ?? "product",
40
42
  ctx.apiClient,
43
+ await resolveHeadlessIssuerListing(ctx, symbol),
41
44
  ));
42
45
  return {
43
46
  complete: data.access === "full",
@@ -30,7 +30,7 @@ import { Blurred, LockedOverlay, UpgradeLabel } from "../shared/locked-rows";
30
30
  import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
31
31
  import { usePlanAccess } from "../../../api-client/plan-access";
32
32
  import { useResearchCloudSession } from "../shared/research-cloud-session";
33
- import { listingIdentity } from "../shared/ticker-request";
33
+ import { listingIdentity, useIssuerListing } from "../shared/ticker-request";
34
34
  import { QuarterBars, type BarHover } from "./bars";
35
35
  import {
36
36
  cachedRevenueBreakdown,
@@ -82,17 +82,20 @@ function RevenueBreakdownView({ width, height, focused }: { width: number; heigh
82
82
  const openUpgrade = useCloudUpgradeAction("seg");
83
83
  const identity = listingIdentity(boundSymbol, ticker?.metadata.exchange ?? "");
84
84
  const symbol = identity?.symbol ?? null;
85
+ const { name } = useIssuerListing(ticker);
86
+ const exchange = identity?.exchange ?? "";
87
+ const listing = useMemo(() => ({ exchange, name }), [exchange, name]);
85
88
  const [requestedView, setView] = usePaneSettingValue<RevenueBreakdownView>("view", "product");
86
89
  const [mode, setMode] = usePluginPaneState<RevenueMode>("revenue:mode", "trend");
87
90
  const [sort, setSort] = usePluginPaneState<RevenueSort>("revenue:sort", { column: "revenue", direction: "desc" });
88
91
  const view = REVENUE_VIEWS.includes(requestedView) ? requestedView : "product";
89
92
 
90
93
  const loader = useCallback(
91
- (force: boolean) => loadRevenueBreakdown(symbol!, view, pro, force),
92
- [symbol, view, pro, session.requestKey],
94
+ (force: boolean) => loadRevenueBreakdown(symbol!, view, pro, force, listing),
95
+ [symbol, listing, view, pro, session.requestKey],
93
96
  );
94
97
  const resource = useAsyncResource(symbol ? loader : null, {
95
- initialData: () => (symbol ? cachedRevenueBreakdown(symbol, view, pro) : null),
98
+ initialData: () => (symbol ? cachedRevenueBreakdown(symbol, view, pro, listing) : null),
96
99
  clearOnError: (error) => error instanceof Error && error.message === NO_BREAKDOWN,
97
100
  });
98
101
  useAutoRefresh(resource.updatedAt, resource.load);
@@ -36,7 +36,11 @@ export const reverseDcfHeadless: HeadlessPaneDefinition<"bundle"> = {
36
36
  ...Object.fromEntries(row.implied.map((value, index) => [`t${index}`, implied(value)])) })) },
37
37
  ],
38
38
  complete: !inputs.stale && !inputs.error && !model.error,
39
- ...(inputs.stale ? { freshness: { status: "stale" as const } } : {}),
39
+ // Dated by the price the enterprise value is marked at, not by when the report ran.
40
+ freshness: {
41
+ asOf: inputs.financials?.quote?.lastUpdated ?? null,
42
+ ...(inputs.stale ? { status: "stale" as const } : {}),
43
+ },
40
44
  unavailableSymbols: model.implied ? [] : [instrument.symbol],
41
45
  errors: [inputs.error, model.error].filter((value): value is string => !!value),
42
46
  metadata: { currency: model.currency, discountRate, terminalGrowth: TERMINAL_GROWTH, discountRates: DISCOUNT_RATES,
@@ -91,7 +91,7 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
91
91
 
92
92
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
93
93
  <QueryBar width={width} filters={[{ id: "discount", label: "Discount", value: String(discount), options: DISCOUNT_OPTIONS, onChange: setDiscount }]} />
94
- {!symbol ? <EmptyState title="Choose a ticker." /> : <PaneStatusBody subject="reverse DCF" loading={inputs.loading && !model}
94
+ {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="reverse DCF" loading={inputs.loading && !model}
95
95
  error={!model ? inputs.error ?? identityError ?? null : model.error} empty={false}>
96
96
  {model && !model.error ? <>
97
97
  <StatGrid items={stats} width={width} />
@@ -4,7 +4,9 @@ import {
4
4
  type CloudRiskReportPayload,
5
5
  } from "../../../api-client";
6
6
  import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
7
+ import { listingAbroad, type IssuerListingParams } from "../../../api-client/paths";
7
8
  import type { HeadlessPaneApiClient, PluginPersistence } from "../../../types/plugin";
9
+ import { secListingKey } from "../../../utils/sec";
8
10
 
9
11
  /** Annual reports are immutable; discovery of newly produced reports is not. */
10
12
  const LIST_KIND = "reports";
@@ -88,12 +90,20 @@ function loadCached<T extends object>(
88
90
  return request.promise;
89
91
  }
90
92
 
91
- export function loadRiskReportsWithClient(client: RiskApiClient, ticker: string, options?: { force?: boolean }): Promise<RiskReportsResult> {
92
- const key = ticker.trim().toUpperCase();
93
+ interface RiskLoadOptions {
94
+ force?: boolean;
95
+ /** The listing's venue and company, for a listing outside the US. */
96
+ listing?: IssuerListingParams;
97
+ }
98
+
99
+ export function loadRiskReportsWithClient(client: RiskApiClient, ticker: string, options?: RiskLoadOptions): Promise<RiskReportsResult> {
100
+ const symbol = ticker.trim().toUpperCase();
101
+ // SAN in Paris is not SAN in New York: a listing abroad is cached under its venue.
102
+ const key = secListingKey(symbol, options?.listing?.exchange);
93
103
  return loadCached(client, LIST_KIND, key, activeListFetches, LIST_CACHE_POLICY, options?.force ?? false, async () => {
94
104
  let payload: CloudRiskReportListPayload;
95
105
  try {
96
- payload = await client.getRiskReports(key);
106
+ payload = await client.getRiskReports(symbol, options?.listing);
97
107
  } catch (error) {
98
108
  // The list answers 404 when no 10-K risk report is on file (20-F filers, funds).
99
109
  if (error instanceof ApiRequestError && error.status === 404) return { company: null, reports: [] };
@@ -104,12 +114,16 @@ export function loadRiskReportsWithClient(client: RiskApiClient, ticker: string,
104
114
  });
105
115
  }
106
116
 
107
- export function loadRiskReportWithClient(client: RiskApiClient, ticker: string, year: number, options?: { force?: boolean }): Promise<RiskReportResult> {
117
+ export function loadRiskReportWithClient(client: RiskApiClient, ticker: string, year: number, options?: RiskLoadOptions): Promise<RiskReportResult> {
108
118
  const symbol = ticker.trim().toUpperCase();
109
- return loadCached(client, REPORT_KIND, `${symbol}:${year}`, activeReportFetches, REPORT_CACHE_POLICY, options?.force ?? false, async () => {
110
- const payload = await client.getRiskReport(symbol, year);
119
+ const abroad = listingAbroad(symbol, options?.listing?.exchange) !== null;
120
+ const key = `${secListingKey(symbol, options?.listing?.exchange)}:${year}`;
121
+ return loadCached(client, REPORT_KIND, key, activeReportFetches, REPORT_CACHE_POLICY, options?.force ?? false, async () => {
122
+ const payload = await client.getRiskReport(symbol, year, options?.listing);
111
123
  // SEC rows spell share classes BRK-B; a BRK.B request is the same company.
112
- const sameTicker = (value: string | undefined) => value?.toUpperCase().replace(/\./g, "-") === symbol.replace(/\./g, "-");
124
+ // A listing abroad reads its registrant's rows, filed under that
125
+ // registrant's US ticker (Sanofi's SNY for SAN in Paris).
126
+ const sameTicker = (value: string | undefined) => abroad || value?.toUpperCase().replace(/\./g, "-") === symbol.replace(/\./g, "-");
113
127
  if (!payload || !sameTicker(payload.ticker) || payload.reportYear !== year) {
114
128
  throw new Error(`Risk report response does not match ${symbol} ${year}.`);
115
129
  }
@@ -117,5 +131,5 @@ export function loadRiskReportWithClient(client: RiskApiClient, ticker: string,
117
131
  });
118
132
  }
119
133
 
120
- export const loadRiskReports = (ticker: string, options?: { force?: boolean }) => loadRiskReportsWithClient(apiClient, ticker, options);
121
- export const loadRiskReport = (ticker: string, year: number, options?: { force?: boolean }) => loadRiskReportWithClient(apiClient, ticker, year, options);
134
+ export const loadRiskReports = (ticker: string, options?: RiskLoadOptions) => loadRiskReportsWithClient(apiClient, ticker, options);
135
+ export const loadRiskReport = (ticker: string, year: number, options?: RiskLoadOptions) => loadRiskReportWithClient(apiClient, ticker, year, options);
@@ -1,6 +1,7 @@
1
1
  import type { HeadlessBundleSection, HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { loadRiskReportWithClient, loadRiskReportsWithClient, type RiskReportsResult } from "./data";
3
3
  import { SEC_FILINGS } from "../shared/report-freshness";
4
+ import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
4
5
 
5
6
  export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
6
7
  shape: "bundle",
@@ -16,9 +17,11 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
16
17
  const selection = String(args.options.year ?? "latest").trim().toLowerCase();
17
18
  if (selection !== "latest" && !/^\d{4}$/.test(selection)) throw new Error("Risk report year must be a four-digit filing year or latest.");
18
19
  const force = args.options.refresh === true;
20
+ // A listing abroad names its venue and company, so it never reads a US namesake's reports.
21
+ const listing = await resolveHeadlessIssuerListing(context, ticker);
19
22
  // A specific immutable filing does not depend on the latest discovery list.
20
23
  let list: RiskReportsResult | null = null;
21
- if (selection === "latest") list = await loadRiskReportsWithClient(context.apiClient, ticker, { force });
24
+ if (selection === "latest") list = await loadRiskReportsWithClient(context.apiClient, ticker, { force, listing });
22
25
  const year = list
23
26
  ? [...list.reports].sort((a, b) => b.reportYear - a.reportYear)[0]?.reportYear
24
27
  : Number(selection);
@@ -27,7 +30,7 @@ export const riskFactorsHeadless: HeadlessPaneDefinition<"bundle"> = {
27
30
  ? `No cached risk report available for ${ticker}; report discovery failed: ${list.refreshError}`
28
31
  : `No 10-K risk reports on file for ${ticker}.`);
29
32
  }
30
- const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force });
33
+ const report = await loadRiskReportWithClient(context.apiClient, ticker, year, { force, listing });
31
34
  const sections: HeadlessBundleSection[] = [
32
35
  { title: "Filing", entries: [
33
36
  { label: "Ticker", value: report.ticker },
@@ -21,7 +21,7 @@ import { isPermanentClientError } from "../../../api-client/errors";
21
21
  import { usePluginPaneState } from "../../runtime";
22
22
  import { useFilingYearReader } from "../shared/filing-year-reader";
23
23
  import { useProFeatureWall, type ProFeatureWallCopy, type ReadGuard } from "../shared/pro-feature-wall";
24
- import { useBoundTicker } from "../shared/ticker-request";
24
+ import { useBoundTicker, useIssuerListing } from "../shared/ticker-request";
25
25
  import { loadRiskReport, loadRiskReports } from "./data";
26
26
 
27
27
  export const RISK_FACTORS_PANE_ID = "risk-factors";
@@ -100,8 +100,9 @@ function RiskFactorsReader({
100
100
  nested = false,
101
101
  guard,
102
102
  }: RiskFactorsPaneProps & { guard: ReadGuard }) {
103
- const { symbol } = useBoundTicker();
103
+ const { symbol, ticker: boundTicker } = useBoundTicker();
104
104
  const ticker = symbol ? symbol.toUpperCase() : null;
105
+ const listing = useIssuerListing(boundTicker);
105
106
  const nativePaneChrome = useUiCapabilities().nativePaneChrome === true;
106
107
  const rendererHost = useRendererHost();
107
108
 
@@ -109,13 +110,13 @@ function RiskFactorsReader({
109
110
  // one the pane opened with, so a restored year applies to it.
110
111
  const [selectedYear, setSelectedYear] = usePluginPaneState<number | null>("filingYear", null);
111
112
  const [selectedTicker, setSelectedTicker] = useState<string | null>(ticker);
112
- const listLoader = useCallback((force: boolean) => guard(loadRiskReports(ticker!, { force })), [guard, ticker]);
113
+ const listLoader = useCallback((force: boolean) => guard(loadRiskReports(ticker!, { force, listing })), [guard, listing, ticker]);
113
114
  const list = useAsyncResource(ticker ? listLoader : null, { clearOnError: isPermanentClientError });
114
115
  const years = useMemo(() => (list.data?.reports ?? []).map((entry) => entry.reportYear).sort((a, b) => b - a), [list.data]);
115
116
  // Null follows the newest discovered filing; an explicit choice stays on that year.
116
117
  const year = selectedTicker === ticker && selectedYear !== null
117
118
  ? selectedYear : years[0] ?? null;
118
- const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force })), [guard, ticker, year]);
119
+ const reportLoader = useCallback((force: boolean) => guard(loadRiskReport(ticker!, year!, { force, listing })), [guard, listing, ticker, year]);
119
120
  const detail = useAsyncResource(ticker && year !== null ? reportLoader : null, { clearOnError: isPermanentClientError });
120
121
  const report = detail.data;
121
122
  const listError = list.error ?? list.data?.refreshError ?? null;
@@ -166,7 +167,7 @@ function RiskFactorsReader({
166
167
  const bodyWidth = Math.max(12, width - 2);
167
168
  const proseWidth = Math.min(bodyWidth, READING_WIDTH);
168
169
 
169
- if (!ticker) return <EmptyState title="Pick a ticker to see its risk factors." />;
170
+ if (!ticker) return <EmptyState title="Select a ticker." />;
170
171
  if (!list.data && list.loading) return <PaneStatusBody loading align="center" loadingLabel="Loading risk factors..." />;
171
172
  if (!list.data && listError && year === null) return <PaneStatusBody error={listError} errorTitle="Could not load risk reports." />;
172
173
  if (year === null) return <EmptyState title={`No 10-K risk factors on file for ${ticker}.`} />;
@@ -133,7 +133,7 @@ export function SeasonalityPane({ width, height, focused }: PaneProps) {
133
133
  return <Box width={width} height={height} flexDirection="column" overflow="hidden">
134
134
  {strip}
135
135
  <QueryBar width={width} filters={[{ id: "lookback", label: "Lookback", value: String(lookback), options: LOOKBACK_OPTIONS, onChange: setLookback }]} />
136
- {!symbol ? <EmptyState title="Choose a ticker." /> : <PaneStatusBody subject="seasonality" loading={history.loading && !model}
136
+ {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="seasonality" loading={history.loading && !model}
137
137
  error={!model ? history.error ?? identityError ?? null : null} empty={!!model && !model.years.length}>
138
138
  {model ? <>
139
139
  <StatGrid items={stats} width={width} />
@@ -10,7 +10,7 @@ import { useDebouncedPluginPaneState } from "../../runtime";
10
10
  import { usePaneSettingValue } from "../../../state/app/context";
11
11
  import type { ScrollBoxRenderable } from "../../../ui";
12
12
  import { EmptyState, FeedDataTableStackView, Spinner, StatGrid, usePaneNoticeFooter, useTableLoadMore, type FeedDataTableItem, type StatItem } from "../../../components";
13
- import { isUsEquityOrFundTicker, isUsEquityTicker, secFilingItemCodes } from "../../../utils/sec";
13
+ import { mayBeUsEquityOrFundTicker, mayBeUsEquityTicker, secFilingItemCodes } from "../../../utils/sec";
14
14
  import { parseForm4Xml, transactionTypeLabel } from "../insider/insider-data";
15
15
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
16
16
  import {
@@ -241,7 +241,7 @@ function SecView({ width, height, focused }: { width: number; height: number; fo
241
241
  // ticker. The fund filings view (ETF) lists other forms, so it leaves it.
242
242
  useSecFilingFocusRequest(forms ? null : ticker?.metadata.ticker, setOpenItemId);
243
243
  // A form list is the fund filings view (ETF), which US-listed funds open too.
244
- const eligibleTicker = forms ? isUsEquityOrFundTicker(ticker) : isUsEquityTicker(ticker);
244
+ const eligibleTicker = forms ? mayBeUsEquityOrFundTicker(ticker) : mayBeUsEquityTicker(ticker);
245
245
  const instrument = instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null);
246
246
  const filingsEntry = useSecFilingsQuery(
247
247
  instrument && eligibleTicker
@@ -449,7 +449,7 @@ export const secModule: PluginModule = {
449
449
  keywords: ["sec", "filings", "10-k", "10-q", "8-k"],
450
450
  shortcut: "SEC",
451
451
  shortcutAliases: ["CF"],
452
- canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
452
+ canCreate: (_context, options) => !options?.ticker || mayBeUsEquityTicker(options.ticker),
453
453
  }),
454
454
  headless: secHeadless,
455
455
  },
@@ -462,7 +462,7 @@ export const secModule: PluginModule = {
462
462
  keywords: ["etf", "fund", "n-1a", "485bpos", "497", "n-csr", "n-cen", "n-port", "prospectus"],
463
463
  shortcut: "ETF",
464
464
  viewKey: "etf",
465
- canCreate: (_context, options) => !options?.ticker || isUsEquityOrFundTicker(options.ticker),
465
+ canCreate: (_context, options) => !options?.ticker || mayBeUsEquityOrFundTicker(options.ticker),
466
466
  settings: () => ({ forms: ETF_FORMS_SETTING }),
467
467
  }),
468
468
  headless: createSecHeadless(undefined, {
@@ -481,7 +481,7 @@ export const secModule: PluginModule = {
481
481
  order: 45,
482
482
  component: SecView,
483
483
  instruments: ["equity"],
484
- isVisible: ({ ticker }) => isUsEquityTicker(ticker),
484
+ isVisible: ({ ticker }) => mayBeUsEquityTicker(ticker),
485
485
  });
486
486
  },
487
487
  dispose() {
@@ -1,10 +1,12 @@
1
1
  import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { PricePoint, Quote } from "../../../types/financials";
3
3
  import type { SectorDef } from "./sector-data";
4
+ import { getRegularSessionDisplay } from "../../../market-data/market/status";
4
5
  import {
5
6
  computeTrailingReturn,
6
7
  historySessionBefore,
7
8
  latestHistoryDate,
9
+ sectorExtendedFields,
8
10
  sectorReturnTargetDate,
9
11
  sectorQuoteSessionDate,
10
12
  sectorReturnStartDate,
@@ -41,6 +43,8 @@ export async function loadSectorRows(
41
43
  sector.etf,
42
44
  await provider.getPriceHistory(sector.etf, "", "1Y").catch(() => []),
43
45
  ] as const)));
46
+ // The quotes as sent carry any pre-market or after-hours print, for its own column.
47
+ const sentQuotes = new Map(quotes);
44
48
  for (const [symbol, quote] of quotes) {
45
49
  if (quote) quotes.set(symbol, completedSessionQuote(quote, histories.get(symbol) ?? []));
46
50
  }
@@ -52,7 +56,10 @@ export async function loadSectorRows(
52
56
  let history: PricePoint[] = histories.get(sector.etf) ?? [];
53
57
  const quote = quotes.get(sector.etf) ?? null;
54
58
  if (!quote && history.length === 0) return { etf: sector.etf, row: null };
55
- const lastReportedPrice = quote && Number.isFinite(quote.price) && quote.price > 0 ? quote.price : null;
59
+ // Price and 1D are the regular session, as `ticker` reads them: after the close
60
+ // its close and move, with an extended print in its own column.
61
+ const headline = getRegularSessionDisplay(quote);
62
+ const lastReportedPrice = headline && Number.isFinite(headline.price) && headline.price > 0 ? headline.price : null;
56
63
  const sessionDate = quoteDates.get(sector.etf) ?? null;
57
64
  const priceIssue = !quote || lastReportedPrice == null ? "quote unavailable"
58
65
  : !sessionDate ? "quote session unknown"
@@ -60,7 +67,7 @@ export async function loadSectorRows(
60
67
  : sessionDate !== asOfDate ? `quote from ${sessionDate}; shared session is ${asOfDate}`
61
68
  : null;
62
69
  const price = priceIssue ? null : lastReportedPrice;
63
- const changePercent = price != null && Number.isFinite(quote?.changePercent) ? quote!.changePercent : null;
70
+ const changePercent = price != null && Number.isFinite(headline?.changePercent) ? headline!.changePercent! : null;
64
71
  const quoteIssue = priceIssue ?? (changePercent == null ? "1D change unavailable" : null);
65
72
  // A strict trailing request may begin after the prior year's weekend or
66
73
  // holiday. Ask for a small boundary buffer when the provider supports it.
@@ -89,6 +96,7 @@ export async function loadSectorRows(
89
96
  quoteUpdatedAt: quote && Number.isFinite(quote.lastUpdated) ? quote.lastUpdated : null,
90
97
  quoteDataSource: quote?.dataSource,
91
98
  changePercent,
99
+ ...sectorExtendedFields(price == null ? null : sentQuotes.get(sector.etf)),
92
100
  return1M: month?.value ?? null,
93
101
  return1Y: year?.value ?? null,
94
102
  returnAsOfDate: asOfDate,
@@ -3,19 +3,39 @@ import type {
3
3
  HeadlessPaneDefinition,
4
4
  HeadlessPaneLoadArgs,
5
5
  } from "../../../types/plugin";
6
+ import type { HeadlessPaneColumn } from "../../../types/headless";
6
7
  import { formatCurrency, formatPercentRaw } from "../../../utils/format";
8
+ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
7
9
  import { loadSectorRows, type SectorRowOutcome } from "./client";
8
10
  import { getSectorCollection, type SectorCollectionId, type SectorDef } from "./sector-data";
9
- import { DEFAULT_SORT_PREFERENCE, sectorRowIssues, sortRows, type SectorRow } from "./sector-model";
11
+ import { DEFAULT_SORT_PREFERENCE, sectorExtendedSessions, sectorRowIssues, sortRows, type SectorRow } from "./sector-model";
10
12
 
11
- const COLUMNS = [
13
+ const price = (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency));
14
+ const percent = (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value));
15
+
16
+ const LEAD_COLUMNS: HeadlessPaneColumn[] = [
12
17
  { key: "name", header: "Sector" },
13
18
  { key: "etf", header: "ETF" },
14
- { key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency)) },
15
- { key: "changePercent", header: "1D", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
16
- { key: "return1M", header: "1M", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
17
- { key: "return1Y", header: "1Y", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
19
+ { key: "price", header: "Last", align: "right", format: price },
20
+ { key: "changePercent", header: "1D", align: "right", format: percent },
21
+ ];
22
+ const RETURN_COLUMNS: HeadlessPaneColumn[] = [
23
+ { key: "return1M", header: "1M", align: "right", format: percent },
24
+ { key: "return1Y", header: "1Y", align: "right", format: percent },
18
25
  ];
26
+ const COLUMNS = [...LEAD_COLUMNS, ...RETURN_COLUMNS];
27
+
28
+ /** The pre-market or after-hours print and its move from the regular close, for the funds that have one. */
29
+ function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
30
+ return {
31
+ key: "extendedPrice",
32
+ header: EXTENDED_SESSION_LABELS[session],
33
+ align: "right",
34
+ format: (value, row) => row.extendedSession === session && typeof value === "number"
35
+ ? `${price(value, row)} ${percent(row.extendedChangePercent)}`
36
+ : "-",
37
+ };
38
+ }
19
39
 
20
40
  export function projectSectorRows(
21
41
  definitions: readonly SectorDef[],
@@ -36,6 +56,10 @@ export function projectSectorRows(
36
56
  quoteDataSource: byEtf.get(definition.etf)?.quoteDataSource,
37
57
  quoteIssue: byEtf.get(definition.etf)?.quoteIssue ?? null,
38
58
  lastReportedPrice: byEtf.get(definition.etf)?.lastReportedPrice ?? null,
59
+ extendedSession: byEtf.get(definition.etf)?.extendedSession ?? null,
60
+ extendedPrice: byEtf.get(definition.etf)?.extendedPrice ?? null,
61
+ extendedChange: byEtf.get(definition.etf)?.extendedChange ?? null,
62
+ extendedChangePercent: byEtf.get(definition.etf)?.extendedChangePercent ?? null,
39
63
  returnIntegrity: byEtf.get(definition.etf)?.returnIntegrity ?? {},
40
64
  returnAsOfDate: byEtf.get(definition.etf)?.returnAsOfDate ?? null,
41
65
  return1MStartDate: byEtf.get(definition.etf)?.return1MStartDate ?? null,
@@ -82,7 +106,12 @@ export function createSectorsHeadless(
82
106
  const unavailableReturns = rows.filter((row) => row.return1M == null || row.return1Y == null).map((row) => row.etf);
83
107
  const unavailableDailyChanges = rows.filter((row) => row.changePercent == null).map((row) => row.etf);
84
108
  const unavailableSymbols = [...new Set([...unavailableQuotes, ...unavailableReturns, ...unavailableDailyChanges])];
109
+ // Last and 1D are the regular session; an extended print gets a column only when a fund has one.
110
+ const extendedSessions = sectorExtendedSessions(rows);
85
111
  return {
112
+ ...(extendedSessions.length > 0
113
+ ? { columns: [...LEAD_COLUMNS, ...extendedSessions.map(extendedColumn), ...RETURN_COLUMNS] }
114
+ : {}),
86
115
  unavailableSymbols: unavailableSymbols.length > 0 ? unavailableSymbols : undefined,
87
116
  errors: rows.flatMap((row) => sectorRowIssues(row).map((issue) => `${row.etf}: ${issue}.`)),
88
117
  // The quote behind each row says whether it is real-time or delayed, and how current it is.