gloomberb 0.16.2 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -1,17 +1,45 @@
1
1
  const PROVIDER_MISS_BRAND = Symbol("provider-miss");
2
2
  const EXPECTED_PROVIDER_MISS = /No data found|symbol may be delisted|"code":"Not Found"|No history for /i;
3
3
 
4
+ const MAX_REASON_LENGTH = 200;
5
+ // Whole ANSI sequences go first, so their printable tail does not outlive the escape byte.
6
+ const ANSI_SEQUENCE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)?)/g;
7
+ const LINE_BREAKS = /[\r\n\t\v\f\u0085\u2028\u2029]+/g;
8
+ // Remaining controls, and the bidirectional overrides that can reorder text in a terminal.
9
+ const CONTROL_CHARACTERS = /[\p{Cc}\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
10
+
11
+ /**
12
+ * A reason a data service wrote for an empty answer, made safe to show: one
13
+ * line, no control characters, at most 200 characters. Anything that is not a
14
+ * non-empty string has no reason.
15
+ */
16
+ export function cleanProviderReason(value: unknown): string | undefined {
17
+ if (typeof value !== "string") return undefined;
18
+ const line = value.replace(ANSI_SEQUENCE, "").replace(LINE_BREAKS, " ").replace(CONTROL_CHARACTERS, "").replace(/\s+/g, " ").trim();
19
+ if (!line) return undefined;
20
+ const characters = Array.from(line);
21
+ return characters.length > MAX_REASON_LENGTH
22
+ ? `${characters.slice(0, MAX_REASON_LENGTH - 1).join("").trimEnd()}…`
23
+ : line;
24
+ }
25
+
4
26
  export class ProviderMissError extends Error {
5
27
  readonly [PROVIDER_MISS_BRAND] = true;
28
+ /** Why the service has nothing, in its own words, when it said. */
29
+ readonly reason?: string;
30
+ /** The service found no listing for the symbol: it is not a ticker, which differs from a ticker it has no data for. */
31
+ readonly notFound: boolean;
6
32
 
7
- constructor(message = "Provider could not satisfy request") {
33
+ constructor(message = "Provider could not satisfy request", reason?: unknown, options: { notFound?: boolean } = {}) {
8
34
  super(message);
9
35
  this.name = "ProviderMissError";
36
+ this.reason = cleanProviderReason(reason);
37
+ this.notFound = options.notFound === true;
10
38
  }
11
39
  }
12
40
 
13
- export function createProviderMiss(message?: string): ProviderMissError {
14
- return new ProviderMissError(message);
41
+ export function createProviderMiss(message?: string, reason?: unknown, options?: { notFound?: boolean }): ProviderMissError {
42
+ return new ProviderMissError(message, reason, options);
15
43
  }
16
44
 
17
45
  function isProviderMiss(error: unknown): error is ProviderMissError {
@@ -19,6 +47,63 @@ function isProviderMiss(error: unknown): error is ProviderMissError {
19
47
  || (!!error && typeof error === "object" && PROVIDER_MISS_BRAND in error);
20
48
  }
21
49
 
50
+ /** The reason a provider gave for a miss, or undefined when it gave none. */
51
+ export function providerMissReason(error: unknown): string | undefined {
52
+ return isProviderMiss(error) ? error.reason : undefined;
53
+ }
54
+
55
+ /** Collects the latest reason across the providers of one request. */
56
+ export interface ProviderMissNote {
57
+ reason?: string;
58
+ /** True while every provider that failed said it found no listing; one that failed another way (a timeout) clears it. */
59
+ notFound?: boolean;
60
+ }
61
+
62
+ export function noteProviderMiss(note: ProviderMissNote | undefined, error: unknown): void {
63
+ if (!note) return;
64
+ const reason = providerMissReason(error);
65
+ if (reason) note.reason = reason;
66
+ note.notFound = note.notFound !== false && isProviderMiss(error) && error.notFound === true;
67
+ }
68
+
69
+ /** A provider answered with data the router could not use: whatever else is missing, the symbol exists. */
70
+ export function noteProviderAnswer(note: ProviderMissNote | undefined): void {
71
+ if (note) note.notFound = false;
72
+ }
73
+
74
+ const NOT_A_TICKER = /^Not a ticker: (.+)\.$/;
75
+
76
+ /** "Not a ticker: APPLE.", what a request for a symbol no listing carries ends in. */
77
+ function notATickerMessage(symbol: string): string {
78
+ return `Not a ticker: ${symbol}.`;
79
+ }
80
+
81
+ /** The symbol of a "Not a ticker: APPLE." message, or null for any other text. */
82
+ export function notATickerSymbol(message: string): string | null {
83
+ return NOT_A_TICKER.exec(message.split("\n")[0] ?? "")?.[1] ?? null;
84
+ }
85
+
86
+ /**
87
+ * Whether a message is the router's answer that no source serves a symbol: "No history provider
88
+ * available for 2222", or "Not a ticker: 2222." when the service found no listing for it.
89
+ * A timeout or any other failure is not.
90
+ */
91
+ export function isNoProviderMessage(message: string): boolean {
92
+ return /\bprovider available for\b/i.test(message) || notATickerSymbol(message) !== null;
93
+ }
94
+
95
+ /**
96
+ * The router's final error when no provider answered. A reason a provider gave
97
+ * replaces the generic wording, and so does the service's answer that no
98
+ * listing exists for `symbol` ("Not a ticker"); a timeout or any other failure
99
+ * keeps the generic message.
100
+ */
101
+ export function noProviderError(message: string, note: ProviderMissNote | undefined, symbol?: string): Error {
102
+ if (note?.reason) return createProviderMiss(note.reason, note.reason);
103
+ if (note?.notFound === true && symbol) return createProviderMiss(notATickerMessage(symbol), undefined, { notFound: true });
104
+ return new Error(message);
105
+ }
106
+
22
107
  export function shouldLogProviderError(error: unknown): boolean {
23
108
  if (isProviderMiss(error)) return false;
24
109
  const message = error instanceof Error ? error.message : String(error);
@@ -66,7 +66,7 @@ export class ProviderRouterBatchRoutes {
66
66
  const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
67
67
  const cached = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
68
68
  ? !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, target.exchange))
69
- : isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol))
69
+ : isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol, { recentAnswer: !rawCached.stale }))
70
70
  ? rawCached
71
71
  : null;
72
72
  if (cached && !forceRefresh && !cached.stale) {
@@ -93,7 +93,7 @@ export class ProviderRouterBatchRoutes {
93
93
  const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
94
94
  const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
95
95
  for (const item of batchResults) {
96
- if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol)) continue;
96
+ if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol, { recentAnswer: true })) continue;
97
97
  const key = this.quoteBatchKey(item.target);
98
98
  const sourceKey = this.deps.providerSourceKey(batchProvider);
99
99
  for (const entry of providerIndexes.get(key) ?? []) {
@@ -162,7 +162,7 @@ export class ProviderRouterBatchRoutes {
162
162
  const key = this.cachedFinancialsBatchKey(item.target);
163
163
  let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
164
164
  if (value && !item.target.instrument && !providerFinancialsMatchTarget(value, item.target.symbol, item.target.exchange)) continue;
165
- if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
165
+ if (value) value = dropUnusableProviderQuote(value, item.target.exchange, { recentAnswer: true });
166
166
  if (!value) continue;
167
167
  const sourceKey = this.deps.providerSourceKey(batchProvider);
168
168
  for (const entry of providerIndexes.get(key) ?? []) {
@@ -4,7 +4,7 @@ import type { CachedAssetArgs, CachedAssetMethod, CachedAssetValue, DataProvider
4
4
  import type { AnalystResearchData, CorporateActionsData, HolderData } from "../../types/financials";
5
5
  import { canonicalExchange } from "../../utils/exchanges";
6
6
  import { nonUsSecListingVenue } from "../../utils/sec";
7
- import { shouldLogProviderError } from "../provider-errors";
7
+ import { noProviderError, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
8
8
  import { hasBrokerContext, withBrokerTimeout } from "./brokers";
9
9
  import { publicListingExchange } from "../listing-target";
10
10
  import { buildVariantKey, compactUrl, listCachedResources, resolveCachePolicy, type ProviderRouterCachePolicyKey } from "./cache";
@@ -18,6 +18,8 @@ interface Route {
18
18
  variants: string[];
19
19
  request: (provider: DataProvider, force: boolean) => Promise<unknown> | undefined;
20
20
  error: string;
21
+ /** The symbol the request is for, so an answer that no listing exists can name it. */
22
+ symbol?: string;
21
23
  rank?: (value: any, fetchedAt?: number) => number;
22
24
  acceptCached?: (value: any) => boolean;
23
25
  decode?: (value: any) => unknown;
@@ -111,6 +113,7 @@ export class ProviderRouterCachedRoutes {
111
113
  let best: CachedValue<unknown> | null = null;
112
114
  let bestRank = -1;
113
115
  let lastError: unknown;
116
+ const misses: ProviderMissNote = {};
114
117
  for (const provider of providers) {
115
118
  try {
116
119
  const request = route.request(provider, force);
@@ -123,6 +126,7 @@ export class ProviderRouterCachedRoutes {
123
126
  if (rank > bestRank) { best = result; bestRank = rank; }
124
127
  } catch (error) {
125
128
  lastError = error;
129
+ noteProviderMiss(misses, error);
126
130
  if (shouldLogProviderError(error)) this.deps.logProviderError(`${provider.id} failed: ${error}`);
127
131
  }
128
132
  }
@@ -133,7 +137,7 @@ export class ProviderRouterCachedRoutes {
133
137
  const policy = resolveCachePolicy(undefined, route.policy);
134
138
  return { value: route.empty(), fetchedAt, staleAt: fetchedAt + policy.staleMs, expiresAt: fetchedAt + policy.expireMs, source: "" };
135
139
  }
136
- throw new Error(route.error);
140
+ throw noProviderError(route.error, misses, route.symbol);
137
141
  },
138
142
  });
139
143
  this.queries.set(key, query);
@@ -175,8 +179,8 @@ export class ProviderRouterCachedRoutes {
175
179
  && !hasBrokerContext(instrumentContext) && !instrumentContext?.instrument) {
176
180
  exchange = publicListingExchange(ticker, exchange);
177
181
  }
178
- const tickerRoute = (kind: string, policy: ProviderRouterCachePolicyKey): Pick<Route, "kind" | "policy" | "entityKey" | "variants"> => ({
179
- kind, policy, entityKey: this.deps.getEntityKey(ticker, instrumentContext?.instrument),
182
+ const tickerRoute = (kind: string, policy: ProviderRouterCachePolicyKey): Pick<Route, "kind" | "policy" | "entityKey" | "variants" | "symbol"> => ({
183
+ kind, policy, symbol: ticker, entityKey: this.deps.getEntityKey(ticker, instrumentContext?.instrument),
180
184
  variants: this.deps.getTickerVariantCandidates(exchange),
181
185
  });
182
186
  switch (method) {
@@ -15,6 +15,7 @@ import {
15
15
  selectCachedResource,
16
16
  sortCachedRecords,
17
17
  } from "./cache";
18
+ import { noProviderError, type ProviderMissNote } from "../provider-errors";
18
19
  import { withBrokerTimeout } from "./brokers";
19
20
  import {
20
21
  hasMeaningfulProfile,
@@ -31,7 +32,7 @@ import {
31
32
  type CachedFinancialsReadOptions,
32
33
  type CachedFinancialsSelection,
33
34
  } from "./financials";
34
- import type { ProviderRouterPrimaryRoutes } from "./primary";
35
+ import type { ProviderQuoteMissNote, ProviderRouterPrimaryRoutes } from "./primary";
35
36
  import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
36
37
 
37
38
  export interface ProviderRouterFinancialRouteDeps extends Pick<
@@ -178,13 +179,14 @@ export class ProviderRouterFinancialRoutes {
178
179
  }
179
180
 
180
181
  const brokerResult = await withBrokerTimeout(this.deps.primaryRoutes.fetchBrokerFinancials(ticker, exchange, context));
181
- const fallback = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context);
182
+ const misses: ProviderMissNote = {};
183
+ const fallback = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context, misses);
182
184
  const merged = mergeFinancials(brokerResult?.value ?? null, fallback?.value ?? null);
183
185
  if (isOptionTicker && !merged?.quote) {
184
186
  return quoteOnlyFinancials(merged);
185
187
  }
186
188
  if (!merged) {
187
- throw new Error(`No provider available for ${ticker}`);
189
+ throw noProviderError(`No provider available for ${ticker}`, misses, ticker);
188
190
  }
189
191
  return merged;
190
192
  }
@@ -200,7 +202,7 @@ export class ProviderRouterFinancialRoutes {
200
202
  const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
201
203
  const cachedIsStale = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
202
204
  ? isQuoteContributionStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, exchange))
203
- : !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker));
205
+ : !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker, { recentAnswer: !rawCached.stale }));
204
206
  const cached = rawCached && !cachedIsStale
205
207
  ? rawCached
206
208
  : null;
@@ -221,12 +223,16 @@ export class ProviderRouterFinancialRoutes {
221
223
  );
222
224
  }
223
225
 
224
- const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
226
+ const misses: ProviderQuoteMissNote = {};
227
+ const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context, misses);
225
228
  if (providerQuote) {
226
229
  return providerQuote.value;
227
230
  }
228
231
  if (cached) return cached.value;
229
- throw new Error(`No quote provider available for ${ticker}`);
232
+ // No source has a current quote: the last price one answered with, flagged
233
+ // stale and dated by its own time, rather than none.
234
+ if (misses.lastKnownQuote) return misses.lastKnownQuote;
235
+ throw noProviderError(`No quote provider available for ${ticker}`, misses, ticker);
230
236
  }
231
237
 
232
238
  private async getProviderReferenceQuote(
@@ -259,7 +265,7 @@ export class ProviderRouterFinancialRoutes {
259
265
  if (
260
266
  record
261
267
  && (includeStale || !record.stale)
262
- && isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker)
268
+ && isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker, { recentAnswer: !record.stale })
263
269
  ) {
264
270
  return record.value;
265
271
  }
@@ -4,10 +4,19 @@ import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
4
4
  import { coalesceFinancialPeriodAliases, mergeFinancialStatementRows } from "../../utils/financial-statements";
5
5
  import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
6
6
  import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS } from "../../utils/fundamentals";
7
- import { isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
7
+ import { hasValidQuoteObservationTime, isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
8
8
  import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
9
- import { parsePublicTickerKey } from "../../utils/exchanges";
10
- import { activeUsMarketSession, delayedFeedLagMs, isMiddayBreakPrint, isUsPriorSessionPremarketQuote } from "../../market-data/market/freshness";
9
+ import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges";
10
+ import {
11
+ activeUsMarketSession,
12
+ delayedFeedLagMs,
13
+ isMiddayBreakPrint,
14
+ isRegularSessionTime,
15
+ isUsExtendedHoursSessionPrint,
16
+ isUsPriorSessionPremarketQuote,
17
+ isUsRegularCloseQuoteInPostSession,
18
+ latestRegularSessionOpen,
19
+ } from "../../market-data/market/freshness";
11
20
  import {
12
21
  mergeQuoteContributionMaps,
13
22
  isQuoteContributionStaleForCurrentSession,
@@ -122,7 +131,50 @@ function isQuoteInActiveSession(quote: Quote, now: number): boolean {
122
131
  return false;
123
132
  }
124
133
 
125
- function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
134
+ // An opening auction can print before the continuous open (Hong Kong's at 09:20).
135
+ const OPENING_AUCTION_MS = 15 * 60_000;
136
+
137
+ /**
138
+ * Whether Gloom Cloud vouches that a delayed quote is current, however long
139
+ * ago its last trade was. The service judges more than that trade's age: it
140
+ * knows when it asked for the quote and how often the listing trades, so a
141
+ * thinly traded listing's hour-old trade can be the current price while its
142
+ * venue trades. Its verdict stands in for the age bound when:
143
+ * - the service answered on this request, or a cache entry still inside its
144
+ * TTL holds that answer (`recentAnswer`); a copy past the TTL is not vouched for;
145
+ * - the answer says `stale: false`. One without the flag vouches for nothing,
146
+ * and one flagged stale never gets this far;
147
+ * - the quote is delayed. The service bounds a delayed quote's feed age itself
148
+ * but not a real-time one's, so real-time quotes keep the age bound;
149
+ * - it reports the regular session, the venue's session is in progress as
150
+ * the delayed feed shows it, and the trade belongs to that session; a
151
+ * currency pair's session is the service's to judge; or
152
+ * - it reports a US extended-hours session and the trade is no older than
153
+ * the regular session that session extends: after the close, today's
154
+ * regular session or its after-hours; before the open, the previous
155
+ * regular session or anything since. Whether the quote carries the
156
+ * session's price is the freshness rule's call, made before this one.
157
+ * The answer says nothing about how often a listing trades, so a liquid
158
+ * listing the service wrongly vouches for is accepted too. The service's own
159
+ * bound for liquid listings, about twenty minutes, is tighter than this one.
160
+ */
161
+ function isVouchedCurrentByService(quote: Quote, exchange: string | undefined, now: number): boolean {
162
+ if (quote.providerId !== "gloomberb-cloud" || quote.stale !== false || quote.dataSource !== "delayed") return false;
163
+ if (quote.marketState === "PRE" || quote.marketState === "POST") {
164
+ return isUsExtendedHoursSessionPrint(quote.lastUpdated, exchange, quote.marketState, now);
165
+ }
166
+ if (quote.marketState !== "REGULAR") return false;
167
+ // A currency pair trades around the clock through the week, and the service
168
+ // measures its silence against how often that pair prints: a pegged or
169
+ // exotic pair such as SAR=X goes hours without a print while its feed is fine.
170
+ if (canonicalExchange(exchange) === "CCY") return true;
171
+ const feedTime = now - delayedFeedLagMs(exchange);
172
+ if (isRegularSessionTime(exchange, feedTime) !== true) return false;
173
+ const open = latestRegularSessionOpen(exchange, feedTime);
174
+ return open != null && quote.lastUpdated >= open - OPENING_AUCTION_MS;
175
+ }
176
+
177
+ function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now(), recentAnswer = false): boolean {
126
178
  // The shared freshness predicate already validates and dates a daily NAV.
127
179
  if (quote.priceObservation === "nav") return false;
128
180
  if (!isQuoteInActiveSession(quote, now)) return false;
@@ -132,12 +184,17 @@ function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
132
184
  if (isUsPriorSessionPremarketQuote(quote.lastUpdated, exchange, quote.marketState, now)) {
133
185
  return false;
134
186
  }
187
+ // Likewise today's regular close before any after-hours trade.
188
+ if (quote.postMarketPrice == null && isUsRegularCloseQuoteInPostSession(quote.lastUpdated, exchange, quote.marketState, now)) {
189
+ return false;
190
+ }
135
191
  if (!Number.isFinite(quote.lastUpdated)) return false;
136
192
  const delayed = quote.dataSource === "delayed";
137
193
  const maxAge = delayed ? ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS : ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
138
194
  if (now - quote.lastUpdated <= maxAge) return false;
139
195
  // While the venue pauses at midday, the morning's last print is the current price.
140
- return !isMiddayBreakPrint(quote.lastUpdated, exchange, now, delayed ? delayedFeedLagMs(exchange) : 0);
196
+ if (isMiddayBreakPrint(quote.lastUpdated, exchange, now, delayed ? delayedFeedLagMs(exchange) : 0)) return false;
197
+ return !(recentAnswer && isVouchedCurrentByService(quote, exchange, now));
141
198
  }
142
199
 
143
200
  export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symbol?: string, exchange?: string): quote is Quote {
@@ -157,26 +214,55 @@ export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symb
157
214
  return true;
158
215
  }
159
216
 
160
- export function isProviderQuoteUsableForCurrentSession(quote: Quote | null | undefined, exchange?: string, symbol?: string): quote is Quote {
217
+ /**
218
+ * `recentAnswer`: the source answered with this quote on this request, or a
219
+ * cache entry still inside its TTL holds it. Only then can the source's own
220
+ * verdict on a delayed quote stand in for the age bound.
221
+ */
222
+ export function isProviderQuoteUsableForCurrentSession(
223
+ quote: Quote | null | undefined,
224
+ exchange?: string,
225
+ symbol?: string,
226
+ options: { recentAnswer?: boolean } = {},
227
+ ): quote is Quote {
161
228
  if (!providerQuoteMatchesTarget(quote, symbol, exchange)) return false;
162
229
  const normalized = quoteWithFreshnessExchange(quote, exchange);
163
230
  if (isQuoteStaleForCurrentSession(normalized)) return false;
164
- if (isActiveProviderQuoteTooOld(normalized)) return false;
231
+ if (isActiveProviderQuoteTooOld(normalized, Date.now(), options.recentAnswer === true)) return false;
232
+ return hasQuotedPrice(normalized);
233
+ }
234
+
235
+ function hasQuotedPrice(quote: Quote): boolean {
165
236
  // Futures may trade or settle at zero or negative prices. Require explicit
166
237
  // source type metadata; an alias alone cannot establish the price domain.
167
- const futures = ["FUT", "FUTURE", "FUTURES"].includes(normalized.instrumentType?.trim().toUpperCase() ?? "");
238
+ const futures = ["FUT", "FUTURE", "FUTURES"].includes(quote.instrumentType?.trim().toUpperCase() ?? "");
168
239
  return [
169
- normalized.price,
170
- normalized.preMarketPrice,
171
- normalized.postMarketPrice,
172
- normalized.bid,
173
- normalized.ask,
174
- normalized.mark,
240
+ quote.price,
241
+ quote.preMarketPrice,
242
+ quote.postMarketPrice,
243
+ quote.bid,
244
+ quote.ask,
245
+ quote.mark,
175
246
  ].some((value) => futures
176
247
  ? typeof value === "number" && Number.isFinite(value)
177
248
  : finitePositiveNumber(value));
178
249
  }
179
250
 
251
+ /**
252
+ * A source's answer for the target that is priced and dated but not current
253
+ * for its session, flagged stale: the last price there is, for when no source
254
+ * has a current one. It keeps its own observation time, which dates it
255
+ * wherever it is shown, and the flag keeps it out of anything that needs a
256
+ * current price. Null for an answer that names another listing, carries no
257
+ * price or no valid time.
258
+ */
259
+ export function lastKnownProviderQuote(quote: Quote | null | undefined, exchange?: string, symbol?: string): Quote | null {
260
+ if (!providerQuoteMatchesTarget(quote, symbol, exchange)) return null;
261
+ const normalized = quoteWithFreshnessExchange(quote, exchange);
262
+ if (!hasValidQuoteObservationTime(normalized) || !hasQuotedPrice(normalized)) return null;
263
+ return { ...quote, stale: true };
264
+ }
265
+
180
266
  export function providerFinancialsMatchTarget(value: TickerFinancials, symbol: string, exchange?: string): boolean {
181
267
  if (value.quote && !providerQuoteMatchesTarget(value.quote, symbol, exchange)) return false;
182
268
  if (Object.values(value.quoteContributions ?? {}).some((quote) => !providerQuoteMatchesTarget(quote, symbol, exchange))) return false;
@@ -188,8 +274,12 @@ export function providerFinancialsMatchTarget(value: TickerFinancials, symbol: s
188
274
  return quoteMetadataMatchesTarget(metadata, hasListing ? symbol : parsePublicTickerKey(symbol).symbol, hasListing ? exchange : undefined);
189
275
  }
190
276
 
191
- export function dropUnusableProviderQuote(value: TickerFinancials, exchange?: string): TickerFinancials {
192
- if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange)) {
277
+ export function dropUnusableProviderQuote(
278
+ value: TickerFinancials,
279
+ exchange?: string,
280
+ options: { recentAnswer?: boolean } = {},
281
+ ): TickerFinancials {
282
+ if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange, undefined, options)) {
193
283
  return value;
194
284
  }
195
285
 
@@ -23,7 +23,7 @@ import { isRoundTheClockCoin } from "../../utils/crypto-pair";
23
23
  import { zonedDateKey } from "../../utils/zoned-date-time";
24
24
  import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
25
25
  import { calendarHistoryFetchState, calendarHistoryLastBarDate, dropLeadingPlaceholderBars, getPricePointTimestamp, hasUsablePriceHistory, preservePriceHistoryGaps, isPriceHistoryStaleForCurrentWindow, normalizePriceHistory, priceHistoryIntervalMs, type CalendarHistoryFetchState } from "../../utils/price-history";
26
- import { shouldLogProviderError } from "../provider-errors";
26
+ import { noProviderError, noteProviderAnswer, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
27
27
  import { HistoryCoverageError } from "../history-coverage";
28
28
  import {
29
29
  buildVariantKey,
@@ -66,6 +66,8 @@ interface HistoryAttempts {
66
66
  outcomes: Map<string, HistorySourceOutcome>;
67
67
  candidates: Map<string, HistoryRecoveryCandidate>;
68
68
  pending: Set<string>;
69
+ /** Whether every provider that failed said it found no listing for the symbol. */
70
+ misses: ProviderMissNote;
69
71
  coverageError?: HistoryCoverageError;
70
72
  retention?: HistoryRetention;
71
73
  }
@@ -105,6 +107,7 @@ function candidateForRequest(request: HistoryRequestDescriptor, sourceKey: strin
105
107
 
106
108
  function recordHistoryError(attempts: HistoryAttempts | undefined, request: HistoryRequestDescriptor, sourceKey: string, error: unknown): void {
107
109
  if (!attempts) return;
110
+ noteProviderMiss(attempts.misses, error);
108
111
  if (error instanceof InvalidHistoryResultError) {
109
112
  recordHistoryOutcome(attempts, sourceKey, "malformed");
110
113
  } else if (isHistoryRetentionError(error)) {
@@ -286,8 +289,13 @@ function calendarRecheckMarkerKey(request: HistoryRequestDescriptor) {
286
289
  variantKey: request.identity.variantKey, sourceKey: "router" };
287
290
  }
288
291
 
292
+ interface HistoryRouteDeps extends ProviderRouterCoreDeps {
293
+ /** False in a short-lived process that must await refreshes of stale copies. */
294
+ revalidatesInBackground?(): boolean;
295
+ }
296
+
289
297
  export class ProviderRouterHistoryRoutes {
290
- constructor(private readonly deps: ProviderRouterCoreDeps) {}
298
+ constructor(private readonly deps: HistoryRouteDeps) {}
291
299
  private readonly historyRefreshInFlight = new Map<string, Promise<unknown>>();
292
300
  private readonly calendarRecheckAt = new Map<string, number>();
293
301
 
@@ -557,8 +565,9 @@ export class ProviderRouterHistoryRoutes {
557
565
  || hasRangeBarSize(record.value.points, request.requestedRange));
558
566
  // A background revalidation cannot correct bars from before a close in
559
567
  // time: a one-shot CLI exits first, and this caller keeps the old bars.
560
- // Broader variants answer the same way. A current copy under another key
561
- // (such as the refetch of this range) answers first. Next come copies
568
+ // That holds for daily bars and for intraday bars that stop short of the
569
+ // close. Broader variants answer the same way. A current copy under
570
+ // another key (such as the refetch of this range) answers first. Next come copies
562
571
  // fetched after the latest settled close that are behind, due a re-check
563
572
  // or not, the one reaching furthest first. A copy fetched before that close
564
573
  // answers only when no later copy exists: its latest bar may be the session
@@ -606,12 +615,16 @@ export class ProviderRouterHistoryRoutes {
606
615
  if (cachedBeforeClose) this.markCalendarChecked(request, now);
607
616
  const usableCached = hasUsablePriceHistory(cachedValue.points) && cached && !cached.expired && !cachedHistoryStale
608
617
  && !cachedBeforeClose;
609
- if (usableCached && !forceRefresh) {
618
+ // A copy behind or before the latest session is re-checked on the paced
619
+ // schedule above, not on its short TTL: the source is likely to answer
620
+ // the same. A short-lived process (the CLI) closes its store before a
621
+ // background refresh lands, so it refetches a copy past its TTL before
622
+ // answering and falls back to that copy.
623
+ const refreshDue = !!cached?.stale && servedState === "current";
624
+ const awaitRefresh = refreshDue && !(this.deps.revalidatesInBackground?.() ?? true);
625
+ if (usableCached && !forceRefresh && !awaitRefresh) {
610
626
  const exactHit = request.exactCacheVariantKeys.includes(cached.variantKey);
611
- // A copy behind or before the latest session is re-checked on the
612
- // paced schedule above, not on its short TTL: the source is likely to
613
- // answer the same.
614
- if (cached.stale && servedState === "current") {
627
+ if (refreshDue) {
615
628
  scheduleRouterRevalidation(this.historyRefreshInFlight, request.identity.revalidationKey, () => this.refreshHistory(request));
616
629
  }
617
630
  return exactHit || !request.requestedRange
@@ -623,7 +636,7 @@ export class ProviderRouterHistoryRoutes {
623
636
  const onUnavailable = (sourceKey: string, value: PriceHistoryResult) => {
624
637
  if (value.points.length && !hasUsablePriceHistory(value.points)) supersededCacheSources.add(sourceKey);
625
638
  };
626
- const attempts: HistoryAttempts = { outcomes: new Map(), candidates: new Map(), pending: new Set() };
639
+ const attempts: HistoryAttempts = { outcomes: new Map(), candidates: new Map(), pending: new Set(), misses: {} };
627
640
  const brokerResult = await withBrokerTimeout(this.fetchBrokerHistory(request, brokerCandidates, onUnavailable, attempts));
628
641
  for (const sourceKey of [...attempts.pending]) recordHistoryOutcome(attempts, sourceKey, "timeout");
629
642
  if (brokerResult && hasUsablePriceHistory(brokerResult.value.points)) return withReportedGaps(brokerResult.value);
@@ -662,7 +675,9 @@ export class ProviderRouterHistoryRoutes {
662
675
  // A source answered a coin's history but it stops short of the present: say so, not that nothing answered.
663
676
  const behind = isRoundTheClockCoin(target.symbol, target.exchange || request.target.exchange)
664
677
  && [...attempts.outcomes.values()].some((outcome) => outcome.outcome === "stale");
665
- throw new Error(behind ? `Latest history for ${target.symbol} is behind` : request.missingProviderError);
678
+ if (behind) throw new Error(`Latest history for ${target.symbol} is behind`);
679
+ // The service has no listing for the symbol: say it is not a ticker, not that no source answered.
680
+ throw noProviderError(request.missingProviderError, { notFound: attempts.misses.notFound }, request.target.symbol);
666
681
  }
667
682
  return withReportedGaps(providerResult?.value ?? { points: [], resolution: historyResolutionForInterval(request.interval) });
668
683
  }
@@ -739,6 +754,7 @@ export class ProviderRouterHistoryRoutes {
739
754
  attempts?.pending.add(sourceKey);
740
755
  try {
741
756
  const fetched = await request.fetchProvider(provider);
757
+ noteProviderAnswer(attempts?.misses);
742
758
  if (fetched === null) { recordHistoryOutcome(attempts, sourceKey, "empty"); return null; }
743
759
  if (!Array.isArray(fetched.points)) { recordHistoryOutcome(attempts, sourceKey, "malformed"); return null; }
744
760
  const value = normalizeRequestResult({ ...fetched, sourceKey }, request);
@@ -109,7 +109,7 @@ export class AssetDataRouter implements DataProvider {
109
109
  getQuote: (ticker, exchange, context) => this.financialRoutes.getQuote(ticker, exchange, context),
110
110
  getTickerFinancials: (ticker, exchange, context) => this.financialRoutes.getTickerFinancials(ticker, exchange, context),
111
111
  });
112
- this.historyRoutes = new ProviderRouterHistoryRoutes(routeDeps);
112
+ this.historyRoutes = new ProviderRouterHistoryRoutes({ ...routeDeps, revalidatesInBackground: () => this.revalidateInBackground });
113
113
  this.newsRoutes = new ProviderRouterNewsRoutes({
114
114
  newsSourcesInPriorityOrder: () => this.newsSourcesInPriorityOrder(),
115
115
  logProviderError: (message) => providerLog.error(message),
@@ -4,7 +4,7 @@ import type { MarketDataRequestContext } from "../../types/data-provider";
4
4
  import type { Quote, TickerFinancials } from "../../types/financials";
5
5
  import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
6
6
  import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
7
- import { shouldLogProviderError } from "../provider-errors";
7
+ import { noteProviderAnswer, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
8
8
  import { quoteMetadataFromQuote } from "../../market-data/quotes/metadata";
9
9
  import {
10
10
  dropUnusableProviderQuote,
@@ -14,12 +14,19 @@ import {
14
14
  needsFinancialProfile,
15
15
  profileForSameListing,
16
16
  isProviderQuoteUsableForCurrentSession,
17
+ lastKnownProviderQuote,
17
18
  providerFinancialsMatchTarget,
18
19
  mergeMissingStatementArrays,
19
20
  mergeFinancials,
20
21
  } from "./financials";
21
22
  import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
22
23
 
24
+ /** What the providers of one quote request said when none had a current quote. */
25
+ export interface ProviderQuoteMissNote extends ProviderMissNote {
26
+ /** The newest priced answer for the target that was not current for its session, flagged stale. */
27
+ lastKnownQuote?: Quote;
28
+ }
29
+
23
30
  export class ProviderRouterPrimaryRoutes {
24
31
  constructor(private readonly options: ProviderRouterCoreDeps) {}
25
32
 
@@ -61,6 +68,7 @@ export class ProviderRouterPrimaryRoutes {
61
68
  ticker: string,
62
69
  exchange?: string,
63
70
  context?: MarketDataRequestContext,
71
+ misses?: ProviderMissNote,
64
72
  ): Promise<SourceResult<TickerFinancials> | null> {
65
73
  const entityKey = this.options.getEntityKey(ticker, context?.instrument);
66
74
  const variantKey = financialHistoryVariants(this.options.getTickerVariantCandidates(exchange), context)[0] ?? "";
@@ -70,11 +78,11 @@ export class ProviderRouterPrimaryRoutes {
70
78
  for (const provider of this.options.providersInPriorityOrder()) {
71
79
  try {
72
80
  const rawValue = await provider.getTickerFinancials(ticker, exchange, context);
73
- if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) continue;
81
+ if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) { noteProviderAnswer(misses); continue; }
74
82
  const resolvedValue = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawValue));
75
- if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) continue;
76
- let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange) : null;
77
- if (!value) continue;
83
+ if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) { noteProviderAnswer(misses); continue; }
84
+ let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange, { recentAnswer: true }) : null;
85
+ if (!value) { noteProviderAnswer(misses); continue; }
78
86
  const sourceKey = this.options.providerSourceKey(provider);
79
87
  if (context?.statementHistory === "extended" && !hasReusableExtendedHistory(value)) {
80
88
  const previous = selectCachedResource<TickerFinancials>(this.options.resources, "financials", entityKey, [variantKey], [sourceKey], true);
@@ -131,6 +139,7 @@ export class ProviderRouterPrimaryRoutes {
131
139
  if (!needsFinancialProfile(primaryResult.value) && (context?.statementHistory === "extended" ? primaryResult.value.statementHistory?.status === "available" : hasDetailedStatementRows(primaryResult.value) && hasDeepStatementHistory(primaryResult.value))) return primaryResult;
132
140
  }
133
141
  } catch (error) {
142
+ noteProviderMiss(misses, error);
134
143
  if (shouldLogProviderError(error)) {
135
144
  this.options.logProviderError(`${provider.id} failed: ${error}`);
136
145
  }
@@ -181,13 +190,21 @@ export class ProviderRouterPrimaryRoutes {
181
190
  ticker: string,
182
191
  exchange?: string,
183
192
  context?: MarketDataRequestContext,
193
+ misses?: ProviderQuoteMissNote,
184
194
  ): Promise<SourceResult<Quote> | null> {
185
195
  const entityKey = this.options.getEntityKey(ticker, context?.instrument);
186
196
  const variantKey = this.options.getTickerVariantCandidates(exchange)[0] ?? "";
187
197
  for (const provider of this.options.providersInPriorityOrder()) {
188
198
  try {
189
199
  const quote = await provider.getQuote(ticker, exchange, context);
190
- if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker)) continue;
200
+ if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker, { recentAnswer: true })) {
201
+ noteProviderAnswer(misses);
202
+ const lastKnown = misses && lastKnownProviderQuote(quote, exchange, ticker);
203
+ if (lastKnown && (misses.lastKnownQuote?.lastUpdated ?? -Infinity) < lastKnown.lastUpdated) {
204
+ misses.lastKnownQuote = lastKnown;
205
+ }
206
+ continue;
207
+ }
191
208
  const sourceKey = this.options.providerSourceKey(provider);
192
209
  this.options.cacheResource(
193
210
  "quote",
@@ -199,6 +216,7 @@ export class ProviderRouterPrimaryRoutes {
199
216
  );
200
217
  return { sourceKey, value: quote };
201
218
  } catch (error) {
219
+ noteProviderMiss(misses, error);
202
220
  if (shouldLogProviderError(error)) {
203
221
  this.options.logProviderError(`${provider.id} failed: ${error}`);
204
222
  }