gloomberb 0.16.6 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -1,64 +1,52 @@
1
+ import type { ResolvedPane } from "../layout/pane-manager";
2
+ import { getPaneSettings } from "../pane-settings";
1
3
  import type { PluginRegistry } from "../plugins/registry";
2
- import type { AppConfig, PaneInstanceConfig } from "../types/config";
3
- import type { TickerRecord } from "../types/ticker";
4
+ import { resolveCollectionForPane, resolveTickerForPane, type AppState } from "../state/app/context";
5
+ import type { LayoutConfig } from "../types/config";
6
+ import type { PanePersonalData, PanePersonalDataContext } from "../types/plugin";
7
+ import { resolvePanePersonalData } from "./pane-image";
4
8
 
5
- /** Keys a pane names a portfolio or a broker account by. */
6
- const ACCOUNT_KEY = /collection|portfolio|account|broker/i;
9
+ export interface PanePersonalDataSource {
10
+ registry: Pick<PluginRegistry, "brokers" | "getPanePluginId" | "getBrokerPluginId">;
11
+ state: AppState;
12
+ /** The layout the pane is in, for its `personalData` rule. */
13
+ layout: LayoutConfig;
14
+ pane: Pick<ResolvedPane, "instance" | "def">;
15
+ }
7
16
 
8
- interface PersonalPaneSource {
9
- registry: Pick<PluginRegistry, "panes" | "brokers" | "getPanePluginId" | "getBrokerPluginId">;
10
- instance: Pick<PaneInstanceConfig, "paneId" | "params" | "settings">;
11
- paneState?: Readonly<Record<string, unknown>>;
12
- /** The ticker the pane shows, when it shows one. */
13
- symbol?: string | null;
14
- config: Pick<AppConfig, "portfolios" | "brokerInstances">;
15
- tickers?: ReadonlyMap<string, TickerRecord>;
17
+ /** What a pane shows now, for its `personalData` rule. */
18
+ function personalDataContext({ state, layout, pane }: Omit<PanePersonalDataSource, "registry">): PanePersonalDataContext {
19
+ const paneId = pane.instance.instanceId;
20
+ return {
21
+ config: state.config,
22
+ layout,
23
+ paneId,
24
+ paneType: pane.instance.paneId,
25
+ pane: pane.instance,
26
+ settings: getPaneSettings(pane.instance),
27
+ paneState: state.paneState[paneId] ?? {},
28
+ activeTicker: resolveTickerForPane(state, paneId),
29
+ activeCollectionId: resolveCollectionForPane(state, paneId),
30
+ tickers: state.tickers,
31
+ };
16
32
  }
17
33
 
18
- function accountValues(record: Readonly<Record<string, unknown>> | undefined): string[] {
19
- if (!record) return [];
20
- return Object.entries(record).flatMap(([key, value]) => {
21
- if (!ACCOUNT_KEY.test(key)) return [];
22
- if (typeof value === "string") return [value];
23
- return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
24
- });
34
+ /** Whether the plugin that owns a pane also connects a broker. */
35
+ function ownerConnectsBroker(registry: PanePersonalDataSource["registry"], paneType: string): boolean {
36
+ const owner = registry.getPanePluginId(paneType);
37
+ return !!owner && [...registry.brokers.keys()].some((brokerType) => registry.getBrokerPluginId(brokerType) === owner);
25
38
  }
26
39
 
27
40
  /**
28
- * Whether a pane shows the user's holdings, balances or account data, so its
29
- * contents are asked about before they leave the device. Read from what every
30
- * pane already declares, with no list of panes to keep up to date:
31
- *
32
- * - it keeps fields to itself when shared (`portableShare.private`): the
33
- * portfolio, account, broker, chat, team and thesis panes, and Ask Gloom;
34
- * - its plugin brings a broker, as an external broker plugin's panes do;
35
- * - it shows a ticker the user holds (a research pane lists the position);
36
- * - it is set to one of the user's portfolios or broker connections.
41
+ * The personal data a pane shows now, from its `PaneDef.personalData` (and,
42
+ * for a pane that declares nothing, the fallback in `resolvePanePersonalData`);
43
+ * null for market data. The one answer to "does this pane show the person's
44
+ * own data, so ask before it leaves the device": sharing its picture to chat
45
+ * and attaching it to an Ask Gloom question both read it here, from the same
46
+ * context.
37
47
  */
38
- export function paneShowsPersonalData({
39
- registry,
40
- instance,
41
- paneState,
42
- symbol,
43
- config,
44
- tickers,
45
- }: PersonalPaneSource): boolean {
46
- const def = registry.panes.get(instance.paneId);
47
- if (def?.portableShare?.private) return true;
48
-
49
- const pluginId = registry.getPanePluginId(instance.paneId);
50
- if (pluginId && [...registry.brokers.keys()].some((brokerId) => registry.getBrokerPluginId(brokerId) === pluginId)) {
51
- return true;
52
- }
53
-
54
- const held = symbol ? tickers?.get(symbol)?.metadata.positions : undefined;
55
- if (held?.some((position) => position.shares !== 0)) return true;
56
-
57
- const accounts = new Set([
58
- ...config.portfolios.map((portfolio) => portfolio.id),
59
- ...config.brokerInstances.map((instance) => instance.id),
60
- ]);
61
- return [instance.params, instance.settings, paneState]
62
- .flatMap(accountValues)
63
- .some((value) => accounts.has(value));
48
+ export function panePersonalData({ registry, ...source }: PanePersonalDataSource): PanePersonalData | null {
49
+ return resolvePanePersonalData(source.pane.def, personalDataContext(source), {
50
+ ownerConnectsBroker: ownerConnectsBroker(registry, source.pane.def.id),
51
+ });
64
52
  }
@@ -2,7 +2,7 @@ import { exchangeRateMetadata } from "../../utils/exchange-rate-snapshot";
2
2
  import type { ExchangeRateSnapshot } from "../../types/exchange-rate";
3
3
  import type { TimeRange } from "../../time-series/range";
4
4
  import { subtractTimeRange } from "../../time-series/date-window";
5
- import { HistoryCoverageError, parseHistoryCoverageStart } from "../history-coverage";
5
+ import { HistoryCoverageError, parseHistoryCoverageNote, parseHistoryCoverageStart } from "../history-coverage";
6
6
  import {
7
7
  normalizeChartResolutionSupport,
8
8
  type ChartResolutionSupport,
@@ -16,6 +16,7 @@ import type {
16
16
  SecFilingDocument,
17
17
  SecFilingItem,
18
18
  MarketDataRequestContext,
19
+ QuoteBatchOptions,
19
20
  QuoteBatchResult,
20
21
  QuoteSubscriptionTarget,
21
22
  SearchRequestContext,
@@ -177,6 +178,7 @@ function mapCloudPriceHistory(
177
178
  extendedHours = false,
178
179
  ): PriceHistoryResult {
179
180
  const coverageStart = parseHistoryCoverageStart(response.coverage);
181
+ const coverageNote = parseHistoryCoverageNote(response.coverage);
180
182
  if (response.status === "empty" && coverageStart
181
183
  && requestedStart.getTime() < Date.parse(coverageStart)) throw new HistoryCoverageError(coverageStart);
182
184
  // The server marks a cached copy it could not refresh in time. Bars that
@@ -237,6 +239,7 @@ function mapCloudPriceHistory(
237
239
  resolution,
238
240
  ...(session && matchingSource && matchingIdentity ? { session } : {}),
239
241
  ...(coverageStart ? { coverageStart } : {}),
242
+ ...(coverageNote ? { coverageNote } : {}),
240
243
  ...(extendedHours ? { extendedHours: true } : {}),
241
244
  };
242
245
  }
@@ -492,8 +495,10 @@ export class GloomberbCloudProvider implements AssetDataProvider {
492
495
 
493
496
  async getQuotesBatch(
494
497
  targets: QuoteSubscriptionTarget[],
495
- options: { forceRefresh?: boolean } = {},
498
+ options: QuoteBatchOptions = {},
496
499
  ): Promise<QuoteBatchResult[]> {
500
+ // This provider keeps no quote cache of its own.
501
+ if (options.cacheOnly) return targets.map((target) => ({ target, quote: null }));
497
502
  const { perpetuals, others } = splitPerpetualTargets(targets);
498
503
  if (perpetuals.length) {
499
504
  const [listed, answered] = await Promise.all([
@@ -120,6 +120,11 @@ export function mapQuote(
120
120
  divisor,
121
121
  ),
122
122
  extendedSessionPrint: typeof quote.extendedSessionPrint === "boolean" ? quote.extendedSessionPrint : undefined,
123
+ overnightPrice: normalizePriceValueByDivisor(quote.overnightPrice, divisor),
124
+ overnightChange: normalizePriceValueByDivisor(quote.overnightChange, divisor),
125
+ overnightPriceKind: quote.overnightPriceKind === "trade" || quote.overnightPriceKind === "mark"
126
+ ? quote.overnightPriceKind
127
+ : undefined,
123
128
  listingExchangeName,
124
129
  listingExchangeFullName,
125
130
  exchangeName: listingExchangeName,
@@ -19,14 +19,32 @@ export function parseHistoryCoverageStart(coverage: unknown): string | undefined
19
19
  return dates.length ? dates.reduce((latest, date) => date > latest ? date : latest) : undefined;
20
20
  }
21
21
 
22
- function historyCoverageMessage(coverageStart: string): string {
23
- return `Verified price history starts ${coverageStart}; earlier prices are excluded.`;
22
+ /**
23
+ * Why a source serves nothing before its coverage, when it says, such as
24
+ * "old-shekel quotes before 1992 are not served": one line of plain text.
25
+ */
26
+ export function parseHistoryCoverageNote(coverage: unknown): string | undefined {
27
+ if (!coverage || typeof coverage !== "object" || !parseHistoryCoverageStart(coverage)) return undefined;
28
+ return historyCoverageNoteText((coverage as Record<string, unknown>).note);
29
+ }
30
+
31
+ /** A coverage note fit to print: a short line without control characters, its closing full stop dropped. */
32
+ export function historyCoverageNoteText(value: unknown): string | undefined {
33
+ if (typeof value !== "string") return undefined;
34
+ const note = value.trim().replace(/\.$/, "");
35
+ return note && note.length <= 200 && !/[\u0000-\u001f\u007f]/.test(note) ? note : undefined;
36
+ }
37
+
38
+ function historyCoverageMessage(coverageStart: string, note?: string): string {
39
+ return note
40
+ ? `Price history starts ${coverageStart}; ${note}.`
41
+ : `Verified price history starts ${coverageStart}; earlier prices are excluded.`;
24
42
  }
25
43
 
26
44
  /** Shown while the visible window starts before the source's coverage. */
27
- export function historyCoverageNotice(coverageStart: string | undefined, visibleStart: number | null): string | null {
45
+ export function historyCoverageNotice(coverageStart: string | undefined, visibleStart: number | null, note?: string): string | null {
28
46
  if (!coverageStart || (visibleStart !== null && visibleStart >= Date.parse(coverageStart))) return null;
29
- return historyCoverageMessage(coverageStart);
47
+ return historyCoverageMessage(coverageStart, note);
30
48
  }
31
49
 
32
50
  /** The whole requested window lies before the source's declared coverage. */
@@ -4,7 +4,7 @@ import type { DataProvider, MarketDataRequestContext } from "../types/data-provi
4
4
  import type { PriceHistoryResult } from "../types/price-history";
5
5
  import { parseHistorySession } from "../market-data/history-session";
6
6
  import { canonicalHistoryInterval } from "./history-retention";
7
- import { isHistoryCoverageDate } from "./history-coverage";
7
+ import { historyCoverageNoteText, isHistoryCoverageDate } from "./history-coverage";
8
8
 
9
9
  export type HistoryResultRequest =
10
10
  | { kind: "range"; range: TimeRange }
@@ -40,6 +40,8 @@ export function normalizeHistoryResult(
40
40
  ...(session ? { session } : {}),
41
41
  ...(typeof result.sourceKey === "string" && /^(provider|broker):[^\r\n\0]+$/.test(result.sourceKey) ? { sourceKey: result.sourceKey } : {}),
42
42
  ...(isHistoryCoverageDate(result.coverageStart) ? { coverageStart: result.coverageStart } : {}),
43
+ ...(isHistoryCoverageDate(result.coverageStart) && historyCoverageNoteText(result.coverageNote)
44
+ ? { coverageNote: historyCoverageNoteText(result.coverageNote) } : {}),
43
45
  ...(result.extendedHours === true ? { extendedHours: true } : {}),
44
46
  ...(result.savedCopyBehind === true ? { savedCopyBehind: true as const } : {}),
45
47
  };
@@ -71,7 +71,8 @@ export function noteProviderAnswer(note: ProviderMissNote | undefined): void {
71
71
  if (note) note.notFound = false;
72
72
  }
73
73
 
74
- const NOT_A_TICKER = /^Not a ticker: (.+)\.$/;
74
+ // A command may follow it with the listing search suggests: "Not a ticker: USD_NGN. Did you mean USDNGN=X (...)?"
75
+ const NOT_A_TICKER = /^Not a ticker: (.+?)\.(?= Did you mean |$)/;
75
76
 
76
77
  /** "Not a ticker: APPLE.", what a request for a symbol no listing carries ends in. */
77
78
  function notATickerMessage(symbol: string): string {
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CachedFinancialsTarget,
3
3
  MarketDataRequestContext,
4
+ QuoteBatchOptions,
4
5
  QuoteBatchResult,
5
6
  QuoteSubscriptionTarget,
6
7
  TickerFinancialsBatchResult,
@@ -48,8 +49,11 @@ export class ProviderRouterBatchRoutes {
48
49
 
49
50
  async getQuotesBatch(
50
51
  targets: QuoteSubscriptionTarget[],
51
- options: { forceRefresh?: boolean } = {},
52
+ options: QuoteBatchOptions = {},
52
53
  ): Promise<QuoteBatchResult[]> {
54
+ if (options.cacheOnly) return targets.map((target) => ({ target, quote: this.cachedQuote(target) }));
55
+ // The signal is the caller's; providers are asked as before.
56
+ const providerOptions = { forceRefresh: options.forceRefresh };
53
57
  const forceRefresh = options.forceRefresh === true;
54
58
  const results = new Array<QuoteBatchResult | null>(targets.length).fill(null);
55
59
  const misses: Array<{ index: number; target: QuoteSubscriptionTarget }> = [];
@@ -91,7 +95,7 @@ export class ProviderRouterBatchRoutes {
91
95
  providerIndexes.set(key, bucket);
92
96
  }
93
97
  const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
94
- const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
98
+ const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, providerOptions).catch(() => []);
95
99
  for (const item of batchResults) {
96
100
  if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol, { recentAnswer: true })) continue;
97
101
  const key = this.quoteBatchKey(item.target);
@@ -197,6 +201,18 @@ export class ProviderRouterBatchRoutes {
197
201
  return results.map((result, index) => result ?? { target: targets[index]!, financials: null });
198
202
  }
199
203
 
204
+ /** The newest cached quote for a target from any source, however old: what a board paints first. */
205
+ private cachedQuote(target: QuoteSubscriptionTarget): Quote | null {
206
+ const context = target.context;
207
+ const entityKey = this.deps.getEntityKey(target.symbol, context?.instrument);
208
+ const variantKeys = this.deps.getTickerVariantCandidates(target.exchange);
209
+ const sourceKeys = [
210
+ ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
211
+ ...this.deps.getProviderSourceKeys(),
212
+ ];
213
+ return selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, true)?.value ?? null;
214
+ }
215
+
200
216
  private quoteBatchKey(target: QuoteSubscriptionTarget): string {
201
217
  return `${target.symbol.trim().toUpperCase()}:${canonicalExchange(target.exchange ?? "")}`;
202
218
  }
@@ -11,6 +11,7 @@ import type {
11
11
  CachedFinancialsTarget,
12
12
  DataProvider,
13
13
  MarketDataRequestContext,
14
+ QuoteBatchOptions,
14
15
  QuoteBatchResult,
15
16
  QuoteSubscriptionTarget,
16
17
  SearchRequestContext,
@@ -194,7 +195,7 @@ export class AssetDataRouter implements DataProvider {
194
195
 
195
196
  async getQuotesBatch(
196
197
  targets: QuoteSubscriptionTarget[],
197
- options: { forceRefresh?: boolean } = {},
198
+ options: QuoteBatchOptions = {},
198
199
  ): Promise<QuoteBatchResult[]> {
199
200
  const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, target.context));
200
201
  const results = await this.batchRoutes.getQuotesBatch(mapped.valid, options);
@@ -398,13 +398,14 @@ function useUpdatePaneLayout(options: { saveDelayMs?: number } = {}) {
398
398
  }, [dispatch, saveDelayMs, stateRef]);
399
399
  }
400
400
 
401
- /** Keep a content-derived pane title in its saved layout, without exposing config writes. */
402
- export function usePaneTitle(title: string, paneId?: string): void {
401
+ /** Keep a content-derived pane title in its saved layout, without exposing config writes. Null leaves the title alone. */
402
+ export function usePaneTitle(title: string | null, paneId?: string): void {
403
403
  const id = useScopedPaneId(paneId);
404
404
  // A follower's title changes with every cursor move in its source; that is
405
405
  // not worth a config write per step.
406
406
  const updateLayout = useUpdatePaneLayout({ saveDelayMs: LOW_PRIORITY_CONFIG_SAVE_DEBOUNCE_MS });
407
407
  useEffect(() => {
408
+ if (title == null) return;
408
409
  updateLayout((layout) => {
409
410
  const pane = findPaneInstance(layout, id);
410
411
  return !pane || pane.title === title ? layout
@@ -0,0 +1,31 @@
1
+ import { createPaneDiscoveryContext } from "../cli/pane-functions/discovery";
2
+ import { getLoadablePlugins } from "../plugins/catalog";
3
+ import { createDefaultConfig } from "../types/config";
4
+ import type { GloomPlugin, PaneDef } from "../types/plugin";
5
+
6
+ /**
7
+ * Every built-in pane definition, the panes a plugin registers in `setup()`
8
+ * included, as `gloomberb catalog` finds them. `dispose` stops the plugins
9
+ * that were set up.
10
+ */
11
+ export async function loadBuiltinPanes(): Promise<{ panes: Map<string, PaneDef>; dispose: () => void }> {
12
+ const config = createDefaultConfig("/gloom");
13
+ const { panes, paneTemplates: _templates, ...discovery } = createPaneDiscoveryContext({
14
+ getConfig: () => config,
15
+ panes: new Map(getLoadablePlugins().flatMap((plugin) => plugin.panes ?? []).map((pane) => [pane.id, pane])),
16
+ });
17
+ const started: GloomPlugin[] = [];
18
+ const dispose = () => {
19
+ for (const plugin of started.splice(0).reverse()) plugin.dispose?.();
20
+ };
21
+ try {
22
+ for (const plugin of getLoadablePlugins()) {
23
+ started.push(plugin);
24
+ await plugin.setup?.(discovery);
25
+ }
26
+ } catch (error) {
27
+ dispose();
28
+ throw error;
29
+ }
30
+ return { panes, dispose };
31
+ }
@@ -10,6 +10,7 @@ import {
10
10
  parsePublicTickerKey,
11
11
  typedExchangeCode,
12
12
  } from "../utils/exchanges";
13
+ import { currencyPairSpelling } from "../utils/currency-pair";
13
14
 
14
15
  export interface ListingStore {
15
16
  loadTicker(symbol: string): Promise<TickerRecord | null>;
@@ -78,6 +79,27 @@ export async function quotedOnVenue(listing: { symbol: string; exchange: string
78
79
  }
79
80
  }
80
81
 
82
+ /**
83
+ * A currency pair typed the way a security could also be written, USD-NGN or
84
+ * USDNGN, read as the pair the service lists (USDNGN=X) when search, as the
85
+ * command bar reads the same text, finds no listing under those letters: MNT-USD
86
+ * stays the coin. Null for any other text, and for a listing.
87
+ */
88
+ export async function readAmbiguousCurrencyPair(typed: string, { store, dataProvider }: ListingDeps): Promise<string | null> {
89
+ const pair = currencyPairSpelling(typed);
90
+ if (!pair?.ambiguous) return null;
91
+ try {
92
+ const tickers = new Map((await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const));
93
+ const resolved = await resolveTickerSearch({ query: typed, activeTicker: null, tickers, dataProvider });
94
+ const exchange = !resolved ? undefined : resolved.kind === "local" ? resolved.ticker.metadata.exchange : resolved.result.exchange;
95
+ const listed = resolved ? currencyPairSpelling(resolved.symbol, exchange) : null;
96
+ return listed && !listed.ambiguous && listed.symbol === pair.symbol ? pair.symbol : null;
97
+ } catch {
98
+ // Several listings under those letters, or no search: the letters stay a symbol.
99
+ return null;
100
+ }
101
+ }
102
+
81
103
  /** What a typed code that names several venues (7203:TSE) was read as. */
82
104
  export type SharedCodeReading =
83
105
  /** The one of the code's venues the symbol trades on. */
@@ -5,6 +5,7 @@ import { canonicalExchange, isSharedExchangeCode, isUsListingExchange, parsePubl
5
5
  import { getListingSymbol, tickerHasListingSuffix } from "../../sources/listing-symbols";
6
6
  import { parseOptionSymbol } from "../../utils/options";
7
7
  import { resolveCurrencyUnit } from "../../utils/currency-units";
8
+ import { currencyPairSpelling } from "../../utils/currency-pair";
8
9
  import { assetClassMarketSymbol, parseAssetClassQuery } from "./asset-classes";
9
10
  import { bareFuturesCode, futuresContractByCode } from "../../market-data/futures-board";
10
11
  import { searchContractKey, searchInstrumentKey } from "./identity";
@@ -311,9 +312,13 @@ export function resolveTickerSearch(options: ResolveTickerSearchOptions): Promis
311
312
  const typed = symbolSearchQuery(requested);
312
313
  // A futures board code no security uses is its future (TTF is TTF=F), unless a saved ticker holds it.
313
314
  const future = pickListing || options.tickers.has(typed) ? null : bareFuturesCode(typed);
314
- const symbol = future?.symbol ?? typed;
315
+ // A currency pair is the one listing the service has for it, however it is written: EUR/USD and
316
+ // JPY=X are EURUSD=X and USDJPY=X. USD-NGN and USDNGN, which a security could also be written as
317
+ // (MNT-USD is a coin), are the pair only when nothing is listed under those letters.
318
+ const pair = pickListing || future ? null : currencyPairSpelling(typed);
319
+ const symbol = future?.symbol ?? (pair && !pair.ambiguous ? pair.symbol : typed);
315
320
  const literal = resolveTickerSymbol(symbol, options, pickListing ? requested : null);
316
- const alternatives = pickListing ? [] : dottedVenueQueries(symbol);
321
+ const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : []];
317
322
  // Most input has no venue code to retry: hand back the literal lookup
318
323
  // itself, so it settles no later than it did before the retry existed.
319
324
  if (alternatives.length === 0) return literal;
@@ -649,6 +649,23 @@ function scoreSearchAlias(intent: SearchQueryIntent, alias: string): number {
649
649
  return score;
650
650
  }
651
651
 
652
+ /**
653
+ * Whether the query is, letter for letter, one of the codes the server sent an
654
+ * index with: IBOV and "IBOV Index" for ^BVSP. A code is how that index is
655
+ * typed (the symbol itself carries a caret nobody types), so the index answers
656
+ * to it as to an exact symbol. Only an index does: a stock or a fund named by a
657
+ * code keeps the rank its own symbol gives it.
658
+ */
659
+ export function answersToIndexCode(
660
+ item: Pick<TickerSearchRankableItem, "searchAliases" | "instrumentType">,
661
+ query: string,
662
+ ): boolean {
663
+ const code = (text: string) => text.trim().replace(/\s+/g, " ").toUpperCase();
664
+ const typed = code(query);
665
+ return !!typed && normalizeSearchText(item.instrumentType ?? "") === "INDEX"
666
+ && !!item.searchAliases?.some((alias) => code(alias) === typed);
667
+ }
668
+
652
669
  function scoreSymbolMatchRank(
653
670
  intent: SearchQueryIntent,
654
671
  item: Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>,
@@ -656,6 +673,8 @@ function scoreSymbolMatchRank(
656
673
  if (normalizeTickerSymbol(item.symbol || item.label) === normalizeTickerSymbol(intent.rawQuery)) return 4;
657
674
  if (isQualifiedTickerQuery(intent.rawQuery)) return matchesQualifiedTicker(item, intent.rawQuery) ? 4 : 0;
658
675
  if (parsePublicTickerKey(item.symbol || item.label).symbol === normalizeTickerSymbol(intent.rawQuery)) return 4;
676
+ // Below a real symbol of that name (SPX Technologies), above every partial match (IBOV11).
677
+ if (answersToIndexCode(item, intent.rawQuery)) return 2;
659
678
  if (isCryptoInstrumentType(item.instrumentType)) return 0;
660
679
  if (isExplicitMarketSymbol(item.symbol || item.label)) return 0;
661
680
  if (!intent.normalizedQuery && !intent.compactQuery) return 0;
@@ -139,6 +139,8 @@ export function liveQuoteObservation(
139
139
  ): LiveQuoteObservation | null {
140
140
  if (!quote || isQuoteStaleForCurrentSession(quote, quoteObservationNow(quote, now))) return null;
141
141
  if (hasUnknownBondHistoryBasis(quote, assetCategory)) return null;
142
+ // No history covers the overnight session: its prices form or move no bar.
143
+ if (quote.overnightPrice != null) return null;
142
144
  const quoteTime = quote.lastUpdated;
143
145
  const quotePrice = getActiveQuotePrice(quote);
144
146
  if (
@@ -1,13 +1,20 @@
1
- import type { PricePoint } from "../types/financials";
1
+ import type { PricePoint, QuoteMetadata } from "../types/financials";
2
2
  import type { PriceHistoryResult } from "../types/price-history";
3
3
  import type { InstrumentRef } from "../market-data/request-types";
4
4
  import { instrumentIdentityKey } from "../utils/instrument-identity";
5
5
  import type { TimeRange } from "./range";
6
- import type { ManualChartResolution } from "./resolution";
6
+ import type { ChartResolutionSupport, ManualChartResolution } from "./resolution";
7
7
 
8
8
  const MAX_PARSED_HISTORY = 32;
9
9
  const parsedHistory = new Map<string, PriceHistoryResult>();
10
10
 
11
+ function listingKey(instrument: InstrumentRef): string {
12
+ return instrumentIdentityKey({ ...instrument,
13
+ brokerId: instrument.brokerId ?? instrument.instrument?.brokerId,
14
+ brokerInstanceId: instrument.brokerInstanceId ?? instrument.instrument?.brokerInstanceId,
15
+ });
16
+ }
17
+
11
18
  export function parsedPriceHistoryKey(
12
19
  instrument: InstrumentRef,
13
20
  range: TimeRange,
@@ -15,15 +22,38 @@ export function parsedPriceHistoryKey(
15
22
  ): string {
16
23
  // This is an in-memory seed cache. Never reuse earlier symbol-only keys:
17
24
  // their observations may have come from a different broker contract.
18
- return JSON.stringify([
19
- "parsed-history-v2",
20
- instrumentIdentityKey({ ...instrument,
21
- brokerId: instrument.brokerId ?? instrument.instrument?.brokerId,
22
- brokerInstanceId: instrument.brokerInstanceId ?? instrument.instrument?.brokerInstanceId,
23
- }),
24
- range,
25
- resolution ?? "",
26
- ]);
25
+ return JSON.stringify(["parsed-history-v2", listingKey(instrument), range, resolution ?? ""]);
26
+ }
27
+
28
+ /**
29
+ * What a settled chart learned about a listing that its cached bars do not
30
+ * carry: the quote's currency and instrument type, and the intervals its
31
+ * source serves. A seed drawn without them labels its axes without units and
32
+ * offers placeholder intervals, so the settled chart that replaces it a
33
+ * moment later moves the plot and redraws every label.
34
+ */
35
+ interface SeedListingFacts {
36
+ quoteMetadata?: QuoteMetadata;
37
+ resolutionSupport?: ChartResolutionSupport[];
38
+ }
39
+
40
+ const MAX_LISTING_FACTS = 64;
41
+ const listingFacts = new Map<string, SeedListingFacts>();
42
+
43
+ export function rememberSeedListingFacts(instrument: InstrumentRef, facts: SeedListingFacts): void {
44
+ const key = listingKey(instrument);
45
+ const merged = { ...listingFacts.get(key), ...facts };
46
+ listingFacts.delete(key);
47
+ listingFacts.set(key, merged);
48
+ while (listingFacts.size > MAX_LISTING_FACTS) {
49
+ const oldest = listingFacts.keys().next().value;
50
+ if (oldest === undefined) break;
51
+ listingFacts.delete(oldest);
52
+ }
53
+ }
54
+
55
+ export function readSeedListingFacts(instrument: InstrumentRef): SeedListingFacts | undefined {
56
+ return listingFacts.get(listingKey(instrument));
27
57
  }
28
58
 
29
59
  export function rememberParsedPriceHistory(