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
@@ -0,0 +1,160 @@
1
+ import type { DockLayoutNode, DockSplitNode, LayoutConfig, PaneInstanceConfig } from "../../types/config";
2
+ import { collectDockLeafRefs, getDockedInstanceIds, getNodeAtPath, replaceNodeAtPath } from "./dock-tree";
3
+
4
+ /**
5
+ * A hidden part of the saved dock tree: the largest subtree with no pane on
6
+ * screen, and where it sat beside the panes that are.
7
+ */
8
+ interface HiddenDockBranch {
9
+ node: DockLayoutNode;
10
+ axis: DockSplitNode["axis"];
11
+ ratio: number;
12
+ /** Which child of its split it was. */
13
+ side: 0 | 1;
14
+ /** The shown panes on the other side of that split. */
15
+ besideIds: ReadonlySet<string>;
16
+ depth: number;
17
+ }
18
+
19
+ function isAllHidden(node: DockLayoutNode, hidden: ReadonlySet<string>): boolean {
20
+ return getDockedInstanceIds(node).every((instanceId) => hidden.has(instanceId));
21
+ }
22
+
23
+ function collectHiddenBranches(
24
+ node: DockLayoutNode,
25
+ hidden: ReadonlySet<string>,
26
+ depth: number,
27
+ out: HiddenDockBranch[],
28
+ ): void {
29
+ if (node.kind !== "split") return;
30
+ const children = [node.first, node.second] as const;
31
+ children.forEach((child, index) => {
32
+ const side = index as 0 | 1;
33
+ if (!isAllHidden(child, hidden)) {
34
+ collectHiddenBranches(child, hidden, depth + 1, out);
35
+ return;
36
+ }
37
+ const sibling = children[1 - side]!;
38
+ out.push({
39
+ node: child,
40
+ axis: node.axis,
41
+ ratio: node.ratio,
42
+ side,
43
+ besideIds: new Set(getDockedInstanceIds(sibling).filter((instanceId) => !hidden.has(instanceId))),
44
+ depth,
45
+ });
46
+ });
47
+ }
48
+
49
+ function commonPathPrefix(paths: ReadonlyArray<ReadonlyArray<0 | 1>>): Array<0 | 1> {
50
+ const [first, ...rest] = paths;
51
+ if (!first) return [];
52
+ let length = first.length;
53
+ for (const path of rest) {
54
+ let index = 0;
55
+ while (index < length && index < path.length && path[index] === first[index]) index += 1;
56
+ length = index;
57
+ }
58
+ return first.slice(0, length);
59
+ }
60
+
61
+ /**
62
+ * Puts a hidden branch back beside the smallest subtree that holds the panes
63
+ * it sat beside, or beside everything when those are gone. Branches put back
64
+ * first sat deeper, so the target grows over them to keep the old nesting.
65
+ */
66
+ function reattachBranch(
67
+ root: DockLayoutNode | null,
68
+ branch: HiddenDockBranch,
69
+ hidden: ReadonlySet<string>,
70
+ ): DockLayoutNode {
71
+ if (!root) return branch.node;
72
+ const beside = collectDockLeafRefs(root).filter((leaf) => branch.besideIds.has(leaf.instanceId));
73
+ let path = beside.length > 0 ? commonPathPrefix(beside.map((leaf) => leaf.path)) : [];
74
+ while (path.length > 0) {
75
+ const parent = getNodeAtPath(root, path.slice(0, -1));
76
+ if (parent?.kind !== "split") break;
77
+ const sibling = path[path.length - 1] === 0 ? parent.second : parent.first;
78
+ if (!isAllHidden(sibling, hidden)) break;
79
+ path = path.slice(0, -1);
80
+ }
81
+ const target = getNodeAtPath(root, path) ?? root;
82
+ const split: DockSplitNode = {
83
+ kind: "split",
84
+ axis: branch.axis,
85
+ ratio: branch.ratio,
86
+ first: branch.side === 0 ? branch.node : target,
87
+ second: branch.side === 0 ? target : branch.node,
88
+ };
89
+ return replaceNodeAtPath(root, path, split);
90
+ }
91
+
92
+ function restoreHiddenDockTree(
93
+ saved: DockLayoutNode | null,
94
+ edited: DockLayoutNode | null,
95
+ hidden: ReadonlySet<string>,
96
+ ): DockLayoutNode | null {
97
+ if (!saved || !getDockedInstanceIds(saved).some((instanceId) => hidden.has(instanceId))) return edited;
98
+ if (isAllHidden(saved, hidden)) {
99
+ return edited ? { kind: "split", axis: "horizontal", ratio: 0.5, first: saved, second: edited } : saved;
100
+ }
101
+ const branches: HiddenDockBranch[] = [];
102
+ collectHiddenBranches(saved, hidden, 0, branches);
103
+ branches.sort((a, b) => b.depth - a.depth);
104
+ let root = edited;
105
+ for (const branch of branches) root = reattachBranch(root, branch, hidden);
106
+ return root;
107
+ }
108
+
109
+ /**
110
+ * The edited list with each hidden entry back after the entry it followed in
111
+ * the saved one, so the order of what is on screen stays the edit's.
112
+ */
113
+ function restoreHiddenEntries<T extends { instanceId: string }>(
114
+ saved: readonly T[],
115
+ edited: readonly T[],
116
+ hidden: ReadonlySet<string>,
117
+ ): T[] {
118
+ if (!saved.some((entry) => hidden.has(entry.instanceId))) return [...edited];
119
+ const result = [...edited];
120
+ let previousId: string | null = null;
121
+ for (const entry of saved) {
122
+ if (hidden.has(entry.instanceId)) {
123
+ const index = previousId === null ? -1 : result.findIndex((candidate) => candidate.instanceId === previousId);
124
+ result.splice(index + 1, 0, entry);
125
+ previousId = entry.instanceId;
126
+ } else if (result.some((candidate) => candidate.instanceId === entry.instanceId)) {
127
+ previousId = entry.instanceId;
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Panes whose plugin is off, or whose pane type nothing registered, stay in
135
+ * the saved layout while the shell leaves them off screen. The shell edits
136
+ * the layout it shows, so before an edit is saved this puts every hidden pane
137
+ * back from `saved`: its instance and state, its floating or popped-out frame,
138
+ * and its docked branch beside the panes it sat next to. A hidden pane the
139
+ * edit itself kept is left as the edit has it.
140
+ *
141
+ * With nothing edited, `restoreHiddenPanes(saved, shown)` is `saved` again.
142
+ */
143
+ export function restoreHiddenPanes(
144
+ saved: LayoutConfig,
145
+ edited: LayoutConfig,
146
+ isHidden: (instance: PaneInstanceConfig) => boolean,
147
+ ): LayoutConfig {
148
+ const editedIds = new Set(edited.instances.map((instance) => instance.instanceId));
149
+ const hidden = new Set(saved.instances
150
+ .filter((instance) => !editedIds.has(instance.instanceId) && isHidden(instance))
151
+ .map((instance) => instance.instanceId));
152
+ if (hidden.size === 0) return edited;
153
+ return {
154
+ ...edited,
155
+ dockRoot: restoreHiddenDockTree(saved.dockRoot, edited.dockRoot, hidden),
156
+ instances: restoreHiddenEntries(saved.instances, edited.instances, hidden),
157
+ floating: restoreHiddenEntries(saved.floating, edited.floating, hidden),
158
+ detached: restoreHiddenEntries(saved.detached ?? [], edited.detached ?? [], hidden),
159
+ };
160
+ }
@@ -49,6 +49,7 @@ export {
49
49
  swapPanes,
50
50
  } from "./docking";
51
51
 
52
+ export { restoreHiddenPanes } from "./hidden-panes";
52
53
  export {
53
54
  removeFloatingPanes,
54
55
  removePane,
@@ -140,7 +140,7 @@ export function readyQuoteEntry(
140
140
  quote: Quote,
141
141
  source: string,
142
142
  attempts: ProviderAttempt[],
143
- options: { keepNewerHeldQuote?: boolean } = {},
143
+ options: { keepNewerHeldQuote?: boolean; acceptLastKnown?: boolean } = {},
144
144
  ): QueryEntry<Quote> {
145
145
  const held = options.keepNewerHeldQuote ? heldQuoteSupersedes(current, quote) : null;
146
146
  if (held) {
@@ -148,6 +148,12 @@ export function readyQuoteEntry(
148
148
  }
149
149
  if (isQuoteStaleForCurrentSession(quote)) {
150
150
  const keepFreshQuote = hasFreshQuoteForCurrentSession([current.data, current.lastGoodData]);
151
+ // A request answers with a stale quote only when no source has a current
152
+ // one (`acceptLastKnown`). With nothing current held, that last known
153
+ // price, flagged stale, beats no price. Stream ticks never bring one in.
154
+ if (options.acceptLastKnown && !keepFreshQuote) {
155
+ return readyEntry(current, { ...quote, stale: true }, source, attempts, { keepLastGoodOnEmpty: true });
156
+ }
151
157
  return readyEntry(current, null, current.source ?? source, attempts, { keepLastGoodOnEmpty: keepFreshQuote });
152
158
  }
153
159
  return readyEntry(current, quote, source, attempts, { keepLastGoodOnEmpty: true });
@@ -111,7 +111,7 @@ export async function loadQuoteEntry({
111
111
  const resolvedQuote = resolveQuote?.(instrument, quote) ?? quote;
112
112
  const source = resolvedQuote.providerId ?? dataProvider.id;
113
113
  const attempts = [createAttempt(source, startedAt, "success")];
114
- return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts, { keepNewerHeldQuote: true }));
114
+ return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts, { keepNewerHeldQuote: true, acceptLastKnown: true }));
115
115
  } catch (error) {
116
116
  return storeQuoteFailure(quoteStore, key, dataProvider.id, startedAt, error);
117
117
  }
@@ -182,7 +182,7 @@ export async function loadQuoteBatchEntries({
182
182
  const quote = resolveQuote?.(instrument, item.quote) ?? item.quote;
183
183
  const source = quote.providerId ?? dataProvider.id;
184
184
  const attempts = [createAttempt(source, Date.now(), "success")];
185
- results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts, { keepNewerHeldQuote: true })));
185
+ results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts, { keepNewerHeldQuote: true, acceptLastKnown: true })));
186
186
  });
187
187
  }
188
188
 
@@ -9,6 +9,11 @@ import { quoteFutureToleranceMs } from "../quotes/clock";
9
9
  import { zonedDateKey, zonedDateTimeParts, zonedWallClockToUtcMs } from "../../utils/zoned-date-time";
10
10
 
11
11
  const ALWAYS_OPEN_EXCHANGES = new Set(["CCC"]);
12
+
13
+ /** Spot crypto venues have no equity open or close. */
14
+ export function isAlwaysOpenExchange(exchange: string | undefined): boolean {
15
+ return ALWAYS_OPEN_EXCHANGES.has(canonicalExchange(exchange));
16
+ }
12
17
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
13
18
  const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
14
19
  const ALWAYS_OPEN_MAX_AGE_MS = 2 * 60 * 60 * 1000;
@@ -61,6 +66,11 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
61
66
  B3: 10 * 60,
62
67
  BYMA: 11 * 60,
63
68
  JSE: 9 * 60,
69
+ // The opening auction ends at 10:00 and the feed stamps its first bar 09:59.
70
+ TASE: 9 * 60 + 59,
71
+ TADAWUL: 10 * 60,
72
+ QE: 9 * 60 + 30,
73
+ DFM: 10 * 60,
64
74
  };
65
75
  // Local regular close with the closing auction, rounded up. A close taken too
66
76
  // early would let a copy fetched during the auction pass as final.
@@ -74,7 +84,37 @@ const REGULAR_CLOSE_MINUTES: Record<string, number> = {
74
84
  JPX: 15 * 60 + 30, HKEX: 16 * 60 + 10, TWSE: 14 * 60 + 30, TPEX: 14 * 60 + 30,
75
85
  NSE: 16 * 60, BSE: 16 * 60, ASX: 16 * 60 + 15, SGX: 17 * 60 + 20, KRX: 16 * 60, KOSDAQ: 16 * 60,
76
86
  NZX: 17 * 60, SSE: 15 * 60 + 30, SZSE: 15 * 60 + 30,
77
- BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15, TASE: 17 * 60 + 40,
87
+ BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15,
88
+ // Monday to Thursday: trading at last ends 17:30, after the closing auction.
89
+ // Not later: a complete 5-minute copy ends with its 17:09 bar, which must
90
+ // reach the close less the half hour a history copy may lag.
91
+ TASE: 17 * 60 + 30,
92
+ // Continuous trading to 15:00, the closing auction to 15:10 and trading at
93
+ // last to 15:20. A complete 5-minute copy ends with its 14:55 bar.
94
+ TADAWUL: 15 * 60 + 20,
95
+ // Continuous trading to 13:00, the closing auction to 13:10 and trading at
96
+ // last to 13:15. A complete 5-minute copy ends with its 13:05 or 13:10 bar.
97
+ QE: 13 * 60 + 15,
98
+ // Continuous trading to 14:45, the closing auction to 14:55 and trading at
99
+ // the close to 15:00. A complete 5-minute copy ends with its 14:55 bar.
100
+ DFM: 15 * 60,
101
+ };
102
+ // Venues whose week is not Monday to Friday, as their days off (0 is Sunday).
103
+ // Tadawul and Qatar trade Sunday to Thursday. Dubai's DFM has traded Monday to
104
+ // Friday since 2022, like the venues not listed here.
105
+ const WEEKEND_DAYS: Record<string, readonly number[]> = {
106
+ TADAWUL: [5, 6],
107
+ QE: [5, 6],
108
+ };
109
+ const DEFAULT_WEEKEND_DAYS: readonly number[] = [0, 6];
110
+ // Venues whose regular session ends earlier on one weekday every week, as
111
+ // local minutes by weekday (0 is Sunday); the other days close as above. TASE
112
+ // has traded Monday to Friday since January 2026, and on Fridays trading at
113
+ // last ends 13:50 (closing auction 13:44 to 13:45). A complete 5-minute copy
114
+ // ends with its 13:29 bar. Holidays and the shortened days around Sukkot and
115
+ // Pesach are not weekly and are not listed.
116
+ const WEEKDAY_CLOSE_MINUTES: Record<string, Partial<Record<number, number>>> = {
117
+ TASE: { 5: 13 * 60 + 50 },
78
118
  };
79
119
  // Venues that pause at midday, as local minutes [start, end), with a zone for
80
120
  // those the session tables above do not cover. Jakarta pauses longer on Fridays.
@@ -189,12 +229,17 @@ function isPublishedClosure(exchange: string, date: string): boolean {
189
229
  return getPublishedUsEquityCalendarDay(exchange, date) === "closed";
190
230
  }
191
231
 
232
+ /** Whether `weekday` (0 is Sunday) is in the venue's trading week. */
233
+ function isTradingWeekday(exchange: string, weekday: number): boolean {
234
+ return !(WEEKEND_DAYS[exchange] ?? DEFAULT_WEEKEND_DAYS).includes(weekday);
235
+ }
236
+
192
237
  function isLocalTradingDay(exchange: string, date: string): boolean {
193
238
  const weekday = localWeekday(date);
194
- return weekday != null && weekday !== 0 && weekday !== 6 && !isPublishedClosure(exchange, date);
239
+ return weekday != null && isTradingWeekday(exchange, weekday) && !isPublishedClosure(exchange, date);
195
240
  }
196
241
 
197
- /** Weekdays in (earlier, later], less published closures for venues with a calendar. */
242
+ /** Days of the venue's trading week in (earlier, later], less published closures for venues with a calendar. */
198
243
  function localTradingDaysBetween(exchange: string, earlierDate: string, laterDate: string): number {
199
244
  const earlierDay = isoLocalDateToUtcDay(earlierDate);
200
245
  const laterDay = isoLocalDateToUtcDay(laterDate);
@@ -212,6 +257,12 @@ function localWeekday(date: string): number | null {
212
257
  return day == null ? null : new Date(day * MS_PER_DAY).getUTCDay();
213
258
  }
214
259
 
260
+ /** The local close of the venue's regular session on `date`, in minutes of the day. */
261
+ function regularCloseMinutes(exchange: string, date: string): number | undefined {
262
+ const weekday = localWeekday(date);
263
+ return (weekday == null ? undefined : WEEKDAY_CLOSE_MINUTES[exchange]?.[weekday]) ?? REGULAR_CLOSE_MINUTES[exchange];
264
+ }
265
+
215
266
  /** The zone a venue's session dates are read in, or null when unknown. */
216
267
  export function sessionCalendarTimeZone(exchange: string | undefined): string | null {
217
268
  const canonical = canonicalExchange(exchange);
@@ -221,9 +272,9 @@ export function sessionCalendarTimeZone(exchange: string | undefined): string |
221
272
 
222
273
  /**
223
274
  * The latest regular session that closed at or before `time`: the published
224
- * calendar for US venues, otherwise local weekdays less published closures. A
225
- * venue without a known close hour is taken to close at local midnight. Null
226
- * for round-the-clock and unknown venues.
275
+ * calendar for US venues, otherwise the days of the venue's trading week less
276
+ * published closures. A venue without a known close hour is taken to close at
277
+ * local midnight. Null for round-the-clock and unknown venues.
227
278
  */
228
279
  export function latestRegularSessionClose(
229
280
  exchange: string | undefined,
@@ -241,7 +292,7 @@ export function latestRegularSessionClose(
241
292
  if (published) {
242
293
  if (published.kind === "session") close = published.close;
243
294
  } else if (isLocalTradingDay(canonical, date)) {
244
- const minutes = REGULAR_CLOSE_MINUTES[canonical] ?? 24 * 60;
295
+ const minutes = regularCloseMinutes(canonical, date) ?? 24 * 60;
245
296
  close = zonedWallClockToUtcMs(timeZone, Number(date.slice(0, 4)), Number(date.slice(5, 7)),
246
297
  Number(date.slice(8, 10)), Math.floor(minutes / 60), minutes % 60, 0);
247
298
  }
@@ -267,8 +318,8 @@ export function regularSessionCloseUtcMinute(exchange: string | undefined, time:
267
318
  /**
268
319
  * The open of the regular session `time` falls in, or of the latest one
269
320
  * before it: the published calendar for US venues, otherwise the venue's
270
- * local open on a weekday that is not a published closure. Null for
271
- * round-the-clock venues and venues without a known open hour.
321
+ * local open on a day of its trading week that is not a published closure.
322
+ * Null for round-the-clock venues and venues without a known open hour.
272
323
  */
273
324
  export function latestRegularSessionOpen(exchange: string | undefined, time: number): number | null {
274
325
  const canonical = canonicalExchange(exchange);
@@ -294,6 +345,40 @@ export function latestRegularSessionOpen(exchange: string | undefined, time: num
294
345
  return null;
295
346
  }
296
347
 
348
+ /**
349
+ * The open of the first regular session after `time`, with its local date and
350
+ * the venue's zone: the published calendar for US venues, otherwise the
351
+ * venue's local open on the next day of its trading week that is not a
352
+ * published closure. Null for round-the-clock venues and venues without a
353
+ * known open hour.
354
+ */
355
+ export function nextRegularSessionOpen(
356
+ exchange: string | undefined,
357
+ time: number,
358
+ ): { date: string; open: number; timeZone: string } | null {
359
+ const canonical = canonicalExchange(exchange);
360
+ const timeZone = sessionCalendarTimeZone(canonical);
361
+ if (!timeZone || ALWAYS_OPEN_EXCHANGES.has(canonical) || !Number.isFinite(time)) return null;
362
+ const minutes = REGULAR_OPEN_MINUTES[canonical];
363
+ const { year, month, day } = zonedDateTimeParts(time, timeZone);
364
+ const today = Date.UTC(year, month - 1, day) / MS_PER_DAY;
365
+ for (let offset = 0; offset <= SESSION_LOOKBACK_DAYS; offset++) {
366
+ const date = new Date((today + offset) * MS_PER_DAY).toISOString().slice(0, 10);
367
+ const published = getPublishedUsEquitySession(canonical, date);
368
+ let open: number | null = null;
369
+ if (published) {
370
+ if (published.kind === "session") open = published.open;
371
+ } else if (minutes === undefined) {
372
+ return null;
373
+ } else if (isLocalTradingDay(canonical, date)) {
374
+ open = zonedWallClockToUtcMs(timeZone, Number(date.slice(0, 4)), Number(date.slice(5, 7)),
375
+ Number(date.slice(8, 10)), Math.floor(minutes / 60), minutes % 60, 0);
376
+ }
377
+ if (open != null && open > time) return { date, open, timeZone };
378
+ }
379
+ return null;
380
+ }
381
+
297
382
  /**
298
383
  * Whether `time` is inside a regular session: at or after its open and
299
384
  * before its close. Null for venues without known hours.
@@ -309,7 +394,7 @@ export function isRegularSessionTime(exchange: string | undefined, time: number)
309
394
  /**
310
395
  * True when the venue's full-day closures for the year of `date` are
311
396
  * published: US venues, JPX, NSE, BSE, SSE, SZSE, KRX, KOSDAQ, TWSE, TPEX,
312
- * HKEX, SGX and ASX. Elsewhere a local holiday reads as a weekday.
397
+ * HKEX, SGX and ASX. Elsewhere a local holiday reads as a trading day.
313
398
  */
314
399
  export function hasPublishedSessionCalendar(exchange: string | undefined, date: string): boolean {
315
400
  const canonical = canonicalExchange(exchange);
@@ -345,7 +430,7 @@ export function isMiddayBreakPrint(
345
430
  if (!pause || !timeZone || !Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now) return false;
346
431
  const { year, month, day } = zonedDateTimeParts(now, timeZone);
347
432
  const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
348
- if (weekday === 0 || weekday === 6) return false;
433
+ if (!isTradingWeekday(canonical, weekday)) return false;
349
434
  const [start, end] = weekday === 5 && pause.friday ? pause.friday : pause.weekdays;
350
435
  const at = (minutes: number) => zonedWallClockToUtcMs(timeZone, year, month, day, Math.floor(minutes / 60), minutes % 60, 0);
351
436
  const breakStart = at(start);
@@ -443,20 +528,35 @@ export function isUsRegularCloseQuoteInPostSession(
443
528
  }
444
529
 
445
530
  /**
446
- * Whether a US print belongs to the after-hours session in progress: today's,
447
- * from the regular close. An earlier day's after-hours print is not.
531
+ * Whether a US print belongs to the extended-hours session the source labels,
532
+ * or to the regular session it extends. POST, after today's regular close (a
533
+ * published early close included) and before 20:00: a print from today's
534
+ * regular session or its after-hours. PRE, before today's open: a print from
535
+ * the previous regular session or since, so its after-hours or this morning's
536
+ * pre-market. An earlier print is not, and neither is any print on a day
537
+ * without a session.
448
538
  */
449
- export function isUsPostSessionPrint(
539
+ export function isUsExtendedHoursSessionPrint(
450
540
  timestampMs: number,
451
541
  exchange: string | undefined,
542
+ marketState: MarketState | undefined,
452
543
  now = Date.now(),
453
544
  ): boolean {
454
545
  const canonical = canonicalExchange(exchange);
455
- if (!isUsListingExchange(canonical)) return false;
546
+ if ((marketState !== "PRE" && marketState !== "POST") || !isUsListingExchange(canonical)) return false;
456
547
  if (!Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now + quoteFutureToleranceMs()) return false;
457
- if (!Number.isFinite(new Date(now).getTime()) || usSessionState(now) !== "POST") return false;
458
- const currentDate = exchangeLocalDate(canonical, now);
459
- return !!currentDate && exchangeLocalDate(canonical, timestampMs) === currentDate && usSessionState(timestampMs) === "POST";
548
+ if (!Number.isFinite(new Date(now).getTime())) return false;
549
+ const today = exchangeLocalDate(canonical, now);
550
+ const close = latestRegularSessionClose(canonical, now);
551
+ if (!today || !close || !isLocalTradingDay(canonical, today)) return false;
552
+ const session = usSessionState(now);
553
+ const closedToday = close.date === today;
554
+ if (marketState === "POST" ? !closedToday || (session !== "REGULAR" && session !== "POST") : closedToday || session !== "PRE") {
555
+ return false;
556
+ }
557
+ // The open of the session that closed last: today's after the close, the previous one before the open.
558
+ const open = latestRegularSessionOpen(canonical, close.close);
559
+ return open != null && timestampMs >= open;
460
560
  }
461
561
 
462
562
  export function isTimestampStaleForExchangeSession(
@@ -24,6 +24,9 @@ export interface ActiveQuoteDisplay {
24
24
 
25
25
  export type ExtendedSession = "PRE" | "POST";
26
26
 
27
+ /** What reports call each extended session's line or column. */
28
+ export const EXTENDED_SESSION_LABELS: Record<ExtendedSession, string> = { PRE: "Pre-Market", POST: "After Hours" };
29
+
27
30
  export interface ExtendedSessionDisplay extends ActiveQuoteDisplay {
28
31
  session: ExtendedSession;
29
32
  }
@@ -278,11 +281,48 @@ export function getExtendedSessionDisplay(quote: Quote | null | undefined): Exte
278
281
  return { session: "POST", price, change, changePercent: (change / close) * 100 };
279
282
  }
280
283
 
284
+ /**
285
+ * A quote's headline and extended print as flat figures, for a report row or
286
+ * an export: price, change and changePercent are the regular session's
287
+ * (getRegularSessionDisplay), the extended fields the pre-market or
288
+ * after-hours print measured from its close (getExtendedSessionDisplay), all
289
+ * null when there is none.
290
+ */
291
+ export function getQuoteSessionFields(quote: Quote | null | undefined) {
292
+ const headline = getRegularSessionDisplay(quote);
293
+ const extended = getExtendedSessionDisplay(quote);
294
+ return {
295
+ price: headline?.price ?? null,
296
+ change: headline?.change ?? null,
297
+ changePercent: headline?.changePercent ?? null,
298
+ extendedSession: extended?.session ?? null,
299
+ extendedPrice: extended?.price ?? null,
300
+ extendedChange: extended?.change ?? null,
301
+ extendedChangePercent: extended?.changePercent ?? null,
302
+ };
303
+ }
304
+
281
305
  /** The extended display only while its session is open: the pre-market or the after-hours session. */
282
306
  function getOpenExtendedSessionDisplay(quote: Quote | null | undefined): ExtendedSessionDisplay | null {
283
307
  return quote?.marketState === "PRE" || quote?.marketState === "POST" ? getExtendedSessionDisplay(quote) : null;
284
308
  }
285
309
 
310
+ /**
311
+ * The regular session alone, for a board that never shows extended trading:
312
+ * the live quote while the session trades, then the completed session's close
313
+ * and its move (getCompletedRegularSessionDisplay). Null outside the session
314
+ * when the quote cannot say which close that is, rather than an extended
315
+ * print or a flat move standing in for one. A quote that does not say its
316
+ * session is read as the regular one, as getRegularSessionDisplay does.
317
+ */
318
+ export function getRegularSessionMoveDisplay(quote: Quote | null | undefined): ActiveQuoteDisplay | null {
319
+ if (!quote) return null;
320
+ if (quote.marketState == null || quote.marketState === "REGULAR") {
321
+ return { price: quote.price, change: quote.change, changePercent: quote.changePercent };
322
+ }
323
+ return getCompletedRegularSessionDisplay(quote);
324
+ }
325
+
286
326
  /**
287
327
  * The move a board of tiles colors by: the open pre-market or after-hours
288
328
  * session's, from the regular close, otherwise the day's regular session.
@@ -0,0 +1,69 @@
1
+ import { parseFuturesGeneric } from "../utils/futures-generic";
2
+
3
+ /** A US-listed fund whose options track an instrument that has none of its own. */
4
+ export interface ListedOptionsAlternative {
5
+ symbol: string;
6
+ name: string;
7
+ }
8
+
9
+ /**
10
+ * Futures roots with no option chain here, and the listed fund whose options
11
+ * are the nearest stand-in. One table for every surface that says so.
12
+ */
13
+ const FUTURES_ALTERNATIVES: Readonly<Record<string, ListedOptionsAlternative>> = {
14
+ GC: { symbol: "GLD", name: "SPDR Gold Shares" },
15
+ SI: { symbol: "SLV", name: "iShares Silver Trust" },
16
+ HG: { symbol: "CPER", name: "United States Copper Index Fund" },
17
+ PL: { symbol: "PPLT", name: "abrdn Physical Platinum Shares" },
18
+ PA: { symbol: "PALL", name: "abrdn Physical Palladium Shares" },
19
+ CL: { symbol: "USO", name: "United States Oil Fund" },
20
+ BZ: { symbol: "BNO", name: "United States Brent Oil Fund" },
21
+ NG: { symbol: "UNG", name: "United States Natural Gas Fund" },
22
+ LTH: { symbol: "LIT", name: "Global X Lithium & Battery Tech ETF" },
23
+ UX: { symbol: "URA", name: "Global X Uranium ETF" },
24
+ ZC: { symbol: "CORN", name: "Teucrium Corn Fund" },
25
+ ZW: { symbol: "WEAT", name: "Teucrium Wheat Fund" },
26
+ ZS: { symbol: "SOYB", name: "Teucrium Soybean Fund" },
27
+ ES: { symbol: "SPY", name: "SPDR S&P 500 ETF" },
28
+ NQ: { symbol: "QQQ", name: "Invesco QQQ Trust" },
29
+ YM: { symbol: "DIA", name: "SPDR Dow Jones Industrial Average ETF" },
30
+ RTY: { symbol: "IWM", name: "iShares Russell 2000 ETF" },
31
+ ZB: { symbol: "TLT", name: "iShares 20+ Year Treasury Bond ETF" },
32
+ ZN: { symbol: "IEF", name: "iShares 7-10 Year Treasury Bond ETF" },
33
+ "6E": { symbol: "FXE", name: "Invesco CurrencyShares Euro Trust" },
34
+ "6J": { symbol: "FXY", name: "Invesco CurrencyShares Japanese Yen Trust" },
35
+ BTC: { symbol: "IBIT", name: "iShares Bitcoin Trust" },
36
+ ETH: { symbol: "ETHA", name: "iShares Ethereum Trust" },
37
+ };
38
+
39
+ const MONTH_CODE_CONTRACT = /^([A-Z0-9]{1,3}?)[FGHJKMNQUVXZ]\d{1,4}$/;
40
+
41
+ /** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. */
42
+ function futuresRoot(symbol: string): string | null {
43
+ const ticker = symbol.trim().toUpperCase().replace(/[.:][A-Z]+$/, "");
44
+ if (ticker.endsWith("=F")) {
45
+ const body = ticker.slice(0, -2);
46
+ return FUTURES_ALTERNATIVES[body] ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
47
+ }
48
+ const generic = parseFuturesGeneric(ticker);
49
+ if (generic) return generic.root;
50
+ const contract = MONTH_CODE_CONTRACT.exec(ticker)?.[1];
51
+ return contract && FUTURES_ALTERNATIVES[contract] ? contract : null;
52
+ }
53
+
54
+ /** The listed fund whose options stand in for an instrument with no chain, if there is one. */
55
+ export function listedOptionsAlternative(symbol: string): ListedOptionsAlternative | null {
56
+ const root = futuresRoot(symbol);
57
+ return root ? FUTURES_ALTERNATIVES[root] ?? null : null;
58
+ }
59
+
60
+ /** Whether an options request failed because nothing lists options on the instrument. */
61
+ export function isOptionsUnavailableError(error: unknown): boolean {
62
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
63
+ return /\boptions provider available for\b/i.test(message);
64
+ }
65
+
66
+ /** "Options are not available for GC=F.": the dead end itself, before any follow-up. */
67
+ export function optionsUnavailableTitle(symbol: string): string {
68
+ return `Options are not available for ${symbol.trim().toUpperCase()}.`;
69
+ }
@@ -1,11 +1,14 @@
1
1
  import { resolvePriceBasis } from "../market/price-basis";
2
+ import { isAlwaysOpenExchange } from "../market/freshness";
2
3
  import type {
4
+ MarketState,
3
5
  Quote,
4
6
  QuoteContribution,
5
7
  QuoteContributionMap,
6
8
  SessionConfidence,
7
9
  TickerFinancials,
8
10
  } from "../../types/financials";
11
+ import { classifyInstrumentType } from "../../tickers/instrument-kind";
9
12
  import { quoteTradingDay, reconcileQuoteDayRange } from "./day-range";
10
13
 
11
14
  const RETAINED_DESCRIPTIVE_FIELDS = ["high52w", "low52w", "marketCap", "name", "instrumentType"] as const;
@@ -48,6 +51,17 @@ function inferSessionConfidence(
48
51
  return "unknown";
49
52
  }
50
53
 
54
+ /**
55
+ * A CCC coin trades through the equity close. The live tape often leaves the
56
+ * session off, and a copied equity close would paint the coin shut.
57
+ */
58
+ function alwaysOpenSession(quote: Pick<Quote, "listingExchangeName" | "exchangeName" | "instrumentType" | "marketState">): { marketState: MarketState; sessionConfidence: SessionConfidence } | null {
59
+ const open = isAlwaysOpenExchange(quote.listingExchangeName ?? quote.exchangeName)
60
+ || classifyInstrumentType(quote.instrumentType) === "crypto";
61
+ if (!open || quote.marketState === "REGULAR") return null;
62
+ return { marketState: "REGULAR", sessionConfidence: "derived" };
63
+ }
64
+
51
65
  function normalizeListingExchange(
52
66
  quote: Quote | QuoteContribution,
53
67
  providerId: string,
@@ -128,6 +142,8 @@ export function normalizeQuoteContribution(
128
142
  const { listingExchangeName, listingExchangeFullName } = normalizeListingExchange(quote, providerId);
129
143
  const routingExchangeName = quote.routingExchangeName;
130
144
  const routingExchangeFullName = quote.routingExchangeFullName ?? routingExchangeName;
145
+ const exchangeName = listingExchangeName ?? quote.exchangeName;
146
+ const session = alwaysOpenSession({ ...quote, listingExchangeName, exchangeName });
131
147
 
132
148
  return finalizeSessionFields(reconcileQuoteDayRange({
133
149
  ...quote,
@@ -136,9 +152,10 @@ export function normalizeQuoteContribution(
136
152
  listingExchangeFullName,
137
153
  routingExchangeName,
138
154
  routingExchangeFullName,
139
- exchangeName: listingExchangeName ?? quote.exchangeName,
155
+ exchangeName,
140
156
  fullExchangeName: listingExchangeFullName ?? quote.fullExchangeName,
141
- sessionConfidence: inferSessionConfidence(quote, providerId),
157
+ ...session,
158
+ sessionConfidence: session?.sessionConfidence ?? inferSessionConfidence(quote, providerId),
142
159
  }));
143
160
  }
144
161
 
@@ -38,6 +38,18 @@ function metadataExchange(value?: string): string {
38
38
  return exchange === "NY MERCANTILE" ? "NYMEX" : exchange;
39
39
  }
40
40
 
41
+ /**
42
+ * The pair an FX symbol names, base then quote: EURUSD for EUR/USD and
43
+ * EURUSD=X, USDJPY for USD/JPY, USDJPY=X and JPY=X, the dollar pair's short
44
+ * spelling.
45
+ */
46
+ function currencyPairCode(symbol: string): string | null {
47
+ const pair = /^([A-Z]{3})\/([A-Z]{3})$/.exec(symbol) ?? /^([A-Z]{3})([A-Z]{3})=X$/.exec(symbol);
48
+ if (pair) return `${pair[1]}${pair[2]}`;
49
+ const dollarPair = /^([A-Z]{3})=X$/.exec(symbol);
50
+ return dollarPair ? `USD${dollarPair[1]}` : null;
51
+ }
52
+
41
53
  /** Qualified listings must agree; provider suffix spellings use the existing exact-listing normalizer. */
42
54
  export function quoteMetadataMatchesTarget(metadata: QuoteMetadata, symbol: string, exchange?: string): boolean {
43
55
  const target = parsePublicTickerKey(symbol);
@@ -48,6 +60,10 @@ export function quoteMetadataMatchesTarget(metadata: QuoteMetadata, symbol: stri
48
60
  if (symbolExchange && actualExchange && symbolExchange !== actualExchange) return false;
49
61
  if (requestedExchange && actualExchange !== requestedExchange) return false;
50
62
  if (target.symbol === actual.symbol) return true;
63
+ // Gloom Cloud answers a currency pair in one spelling however it was asked
64
+ // for: EUR/USD comes back as EURUSD=X on its currency venue.
65
+ const pair = actualExchange === "CCY" ? currencyPairCode(actual.symbol) : null;
66
+ if (pair && pair === currencyPairCode(target.symbol)) return true;
51
67
  const listing = requestedExchange || actualExchange;
52
68
  if (!listing) return false;
53
69
  const candidates = getListingSymbolsToTry(target.symbol, listing, { exactExchange: true });