@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -0,0 +1,214 @@
1
+ import { logFailure } from "../utils/log-failure";
2
+ import type { StallEvent } from "../utils/main-thread-stall";
3
+ import { VERSION } from "../version";
4
+ import { reportCrash } from "./crash-reports";
5
+
6
+ /**
7
+ * Freezes, made visible outside the app's own Debug Log.
8
+ *
9
+ * The stall monitor (utils/main-thread-stall.ts) notices when a process could
10
+ * not answer the user for a while and names the last measured section, and
11
+ * hands the stall to its sink. The terminal app and the desktop app's Bun
12
+ * process record their own stalls here; the desktop window sends the stalls
13
+ * it had in front of the user (stall-reports-dom.ts) to the Bun process,
14
+ * which records them too. Where a host is installed, each
15
+ * stall:
16
+ *
17
+ * - goes to a plain-text stall log in the profile folder, one line each, so a
18
+ * user or support can read what froze and when;
19
+ * - from a second up, counts toward a report sent through the crash reporter
20
+ * (and so behind the same off switch), the way update failures are: the
21
+ * duration bucket, the section's fixed name, the process, the longest
22
+ * stall and how many there were. No free text: a section name keeps only
23
+ * its fixed leading part, so a ticker in a queue key never leaves.
24
+ *
25
+ * One report per process, bucket, section and app version a day; stalls of
26
+ * the same kind after that are counted and carried in the next report.
27
+ */
28
+
29
+ /** Which process stalled. */
30
+ export type StallProcess = "window" | "app" | "terminal";
31
+
32
+ /** What error tracking gets: fixed values only. */
33
+ export interface StallReport {
34
+ process: StallProcess;
35
+ bucket: StallBucket;
36
+ section: string;
37
+ appVersion: string;
38
+ platform: "macos" | "windows" | "linux" | "other";
39
+ arch: "arm64" | "x64" | "other";
40
+ /** Stalls of this kind since the last report, this one included. */
41
+ count: number;
42
+ /** The longest of them, rounded to 100 ms. */
43
+ longestMs: number;
44
+ }
45
+
46
+ type StallBucket = "1-2s" | "2-5s" | "5-15s" | "15s+";
47
+
48
+ /** What the host keeps on disk between launches. */
49
+ export interface StallReportState {
50
+ /** When each kind was last reported, in ms. */
51
+ sent: Record<string, number>;
52
+ /** Stalls counted since each kind was last reported. */
53
+ pending: Record<string, { count: number; longestMs: number }>;
54
+ }
55
+
56
+ export interface StallReporterHost {
57
+ /** The running version; defaults to this build's. */
58
+ appVersion?: string;
59
+ platform: string;
60
+ arch: string;
61
+ /** Appends one line to the stall log. */
62
+ appendLog(line: string): void;
63
+ load(): StallReportState;
64
+ save(state: StallReportState): void;
65
+ now?(): number;
66
+ /** Defaults to the crash reporter. */
67
+ send?(report: StallReport): void;
68
+ }
69
+
70
+ const DAY_MS = 24 * 60 * 60 * 1000;
71
+ /** Shorter freezes go to the log only. */
72
+ const STALL_REPORT_MIN_MS = 1_000;
73
+ const SECTION_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*/;
74
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,60})?$/;
75
+ /** Kinds with counts waiting for their next report; the oldest go first past this. */
76
+ const MAX_KINDS_KEPT = 200;
77
+
78
+ let host: StallReporterHost | null = null;
79
+
80
+ /** The fixed part of a section name: lower-case dotted words, cut where anything else starts. */
81
+ function stallSectionName(name: string | undefined): string {
82
+ if (!name) return "unmeasured";
83
+ const match = SECTION_PATTERN.exec(name);
84
+ const section = match?.[0].replace(/\.$/, "");
85
+ return section && section.length <= 80 ? section : "unmeasured";
86
+ }
87
+
88
+ function stallBucket(blockedMs: number): StallBucket {
89
+ if (blockedMs < 2_000) return "1-2s";
90
+ if (blockedMs < 5_000) return "2-5s";
91
+ if (blockedMs < 15_000) return "5-15s";
92
+ return "15s+";
93
+ }
94
+
95
+ /** A stall another process sent over, kept only when it has the right shape. */
96
+ export function parseStallEvent(value: unknown): StallEvent | null {
97
+ if (!value || typeof value !== "object") return null;
98
+ const { blockedMs, lastMeasured, lastMeasuredEndedMsAgo } = value as Record<string, unknown>;
99
+ if (typeof blockedMs !== "number" || !Number.isFinite(blockedMs) || blockedMs <= 0) return null;
100
+ return {
101
+ blockedMs,
102
+ ...(typeof lastMeasured === "string" && lastMeasured.length <= 200 ? { lastMeasured } : {}),
103
+ ...(typeof lastMeasuredEndedMsAgo === "number" && Number.isFinite(lastMeasuredEndedMsAgo) ? { lastMeasuredEndedMsAgo } : {}),
104
+ };
105
+ }
106
+
107
+ function formatSeconds(ms: number): string {
108
+ return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`;
109
+ }
110
+
111
+ /** One line of the stall log: when, which process, how long, and what ran last. */
112
+ function formatStallLogLine(process: StallProcess, event: StallEvent, at: number): string {
113
+ const time = new Date(at).toISOString().replace(/\.\d{3}Z$/, "Z");
114
+ const last = event.lastMeasured
115
+ ? `last measured: ${event.lastMeasured}${event.lastMeasuredEndedMsAgo === undefined ? "" : `, ended ${Math.round(event.lastMeasuredEndedMsAgo)} ms before`}`
116
+ : "nothing measured before it";
117
+ return `${time} ${process.padEnd(8)} froze ${formatSeconds(event.blockedMs).padStart(6)} ${last}`;
118
+ }
119
+
120
+ function reportPlatform(platform: string): StallReport["platform"] {
121
+ if (platform === "darwin") return "macos";
122
+ if (platform === "win32") return "windows";
123
+ if (platform === "linux") return "linux";
124
+ return "other";
125
+ }
126
+
127
+ function reportArch(arch: string): StallReport["arch"] {
128
+ return arch === "arm64" || arch === "x64" ? arch : "other";
129
+ }
130
+
131
+ function sendThroughCrashReporter(report: StallReport): void {
132
+ const details = [
133
+ `process=${report.process}`,
134
+ `count=${report.count}`,
135
+ `longest=${report.longestMs}ms`,
136
+ `platform=${report.platform}-${report.arch}`,
137
+ ].join(" ");
138
+ // A plain object, not an Error: it has no stack, so no file path goes along.
139
+ // The message is the bucket and section, so error tracking keeps one issue per kind.
140
+ reportCrash(
141
+ { name: "MainThreadStall", message: `${report.bucket} after ${report.section}` },
142
+ { kind: "plugin", plugin: "stall-monitor", pane: details },
143
+ );
144
+ }
145
+
146
+ function loadState(current: StallReporterHost): StallReportState {
147
+ try {
148
+ const state = current.load();
149
+ return { sent: { ...state.sent }, pending: { ...state.pending } };
150
+ } catch (error) {
151
+ logFailure("telemetry-stalls", "Could not read stall report state")(error);
152
+ return { sent: {}, pending: {} };
153
+ }
154
+ }
155
+
156
+ function prune(state: StallReportState, now: number): void {
157
+ for (const [key, at] of Object.entries(state.sent)) {
158
+ if (!(now - at < DAY_MS) || at > now) delete state.sent[key];
159
+ }
160
+ const kinds = Object.keys(state.pending);
161
+ for (const key of kinds.slice(0, Math.max(0, kinds.length - MAX_KINDS_KEPT))) delete state.pending[key];
162
+ }
163
+
164
+ /** Records one stall of `process`. Does nothing where no host is installed; never throws. */
165
+ export function recordStall(process: StallProcess, event: StallEvent): void {
166
+ const current = host;
167
+ if (!current || !(event.blockedMs > 0)) return;
168
+ try {
169
+ const now = current.now?.() ?? Date.now();
170
+ current.appendLog(formatStallLogLine(process, event, now));
171
+ if (event.blockedMs < STALL_REPORT_MIN_MS) return;
172
+ const appVersion = (current.appVersion ?? VERSION).trim().replace(/^v/, "");
173
+ const section = stallSectionName(event.lastMeasured);
174
+ const bucket = stallBucket(event.blockedMs);
175
+ const version = VERSION_PATTERN.test(appVersion) ? appVersion : "unknown";
176
+ const key = `${process}|${bucket}|${section}|${version}`;
177
+ const state = loadState(current);
178
+ prune(state, now);
179
+ const pending = state.pending[key] ?? { count: 0, longestMs: 0 };
180
+ pending.count += 1;
181
+ pending.longestMs = Math.max(pending.longestMs, Math.round(event.blockedMs / 100) * 100);
182
+ if (state.sent[key] !== undefined) {
183
+ state.pending[key] = pending;
184
+ } else {
185
+ delete state.pending[key];
186
+ state.sent[key] = now;
187
+ (current.send ?? sendThroughCrashReporter)({
188
+ process,
189
+ bucket,
190
+ section,
191
+ appVersion: version,
192
+ platform: reportPlatform(current.platform),
193
+ arch: reportArch(current.arch),
194
+ count: pending.count,
195
+ longestMs: pending.longestMs,
196
+ });
197
+ }
198
+ current.save(state);
199
+ } catch (error) {
200
+ logFailure("telemetry-stalls", "Could not record a stall")(error);
201
+ }
202
+ }
203
+
204
+ export function installStallReporter(next: StallReporterHost): () => void {
205
+ host = next;
206
+ return () => {
207
+ if (host === next) host = null;
208
+ };
209
+ }
210
+
211
+ /** Test seam. */
212
+ export function resetStallReporterForTests(): void {
213
+ host = null;
214
+ }
@@ -0,0 +1,14 @@
1
+ import { beforeEach } from "bun:test";
2
+ import { resetCrashReporterForTests } from "../telemetry/crash-reports";
3
+
4
+ /**
5
+ * `bun test` runs every file in one process, so module state outlives the file
6
+ * that set it. A crash report queued while no reporter is installed, such as a
7
+ * plugin that fails to register in an app runtime test, would wait in the
8
+ * queue and go out with the next reporter another file installs, and its key
9
+ * would keep a later identical report from being queued at all. Every test
10
+ * starts with no reporter, an empty queue and nothing seen.
11
+ */
12
+ beforeEach(() => {
13
+ resetCrashReporterForTests();
14
+ });
@@ -745,6 +745,16 @@ const THEME_TRAITS: Readonly<Record<string, readonly ThemeTrait[]>> = {
745
745
  "high-contrast": ["high-contrast", "colorblind-safe"],
746
746
  };
747
747
 
748
+ /**
749
+ * Whether a theme's gains and losses are told apart by something other than
750
+ * the red and green everyone reads: a color-blind safe theme's blue and
751
+ * vermillion, which a chart keys (`▲▼` beside a candle series) so a reader
752
+ * knows which is which.
753
+ */
754
+ export function themeKeysDirection(id: string): boolean {
755
+ return (THEME_TRAITS[id] ?? []).includes("colorblind-safe");
756
+ }
757
+
748
758
  export interface ThemeSummary {
749
759
  id: string;
750
760
  name: string;
@@ -1,5 +1,6 @@
1
1
  import { logFailure } from "../utils/log-failure";
2
2
  import type { DataProvider } from "../types/data-provider";
3
+ import type { Quote } from "../types/financials";
3
4
  import type { InstrumentSearchResult } from "../types/instrument";
4
5
  import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../utils/exchanges";
5
6
  import { companyNameMatch, sameCompany, shortCompanyName } from "./company-name";
@@ -12,7 +13,7 @@ import { hasNonLatinLetters } from "../utils/typed-text";
12
13
 
13
14
  export interface CompanyNameListing {
14
15
  symbol: string;
15
- /** Canonical code of its exchange. */
16
+ /** Canonical code of its exchange; empty for a US symbol read bare (a line its company `moved` to). */
16
17
  exchange: string;
17
18
  /** The listing's name as search gives it. */
18
19
  name: string;
@@ -22,8 +23,24 @@ export interface CompanyNameListing {
22
23
  * run together (JPM, JPMorgan Chase & Co.), which the reader is asked about.
23
24
  */
24
25
  meant?: Omit<CompanyNameListing, "meant">;
26
+ /**
27
+ * When the company's US line, which search still lists, moved to another
28
+ * symbol and this is the line it trades as now (GETY became GETYQ): the
29
+ * change in one line, "GETY became GETYQ on 2026-10-22".
30
+ */
31
+ moved?: string;
25
32
  }
26
33
 
34
+ /**
35
+ * Where a listing's company trades now when it moved off the symbol (GETY
36
+ * became GETYQ on 2026-10-22), from the line's quote (null when it has none)
37
+ * and the name search gives it: the symbol it took, and the change in one
38
+ * line. Null when it did not move, or that cannot be told.
39
+ */
40
+ export type MovedLineLookup = (
41
+ listing: { symbol: string; exchange: string; quote: Quote | null; name: string | null },
42
+ ) => Promise<{ symbol: string; note: string } | null>;
43
+
27
44
  /** Share lines that are not the company's common stock, though search may type them as such. */
28
45
  const NOT_COMMON = /preferred|warrant|right|unit|note|bond|debenture/i;
29
46
  const RECEIPT = /depositary|^(?:ADR|ADS|GDR|CDR|BDR)$/i;
@@ -70,7 +87,7 @@ export function isCompanyNameInput(input: string): boolean {
70
87
  export async function findListingOfCompanyName(
71
88
  dataProvider: Pick<DataProvider, "search"> & Partial<Pick<DataProvider, "getQuote">>,
72
89
  typed: string,
73
- options: { meant?: boolean; oneWord?: boolean; quoted?: boolean } = {},
90
+ options: { meant?: boolean; oneWord?: boolean; quoted?: boolean; movedLine?: MovedLineLookup } = {},
74
91
  ): Promise<CompanyNameListing | null> {
75
92
  const name = typed.trim();
76
93
  if (!name) return null;
@@ -110,8 +127,12 @@ export async function findListingOfCompanyName(
110
127
  const lines = ranked.filter((listing) => listing === first || (!listing.home && !otherHomes.has(plainName(listing.result.name))
111
128
  && sameCompany(first.result.name, listing.result.name)
112
129
  && listing.rank.slice(0, NAME_RANKS).every((value, index) => value === first.rank[index])));
113
- const best = options.quoted ? await firstQuoted(dataProvider, lines) : first;
114
- const found = { symbol: best.result.symbol.toUpperCase(), exchange: best.exchange, name: best.result.name };
130
+ const quoted = options.quoted ? await firstQuoted(dataProvider, lines, options.movedLine) : { line: first, moved: null };
131
+ const best = quoted.line;
132
+ const found: CompanyNameListing = quoted.moved
133
+ // The line it moved to is a US symbol, read bare as `quote` reads one (GETYQ, which trades over the counter).
134
+ ? { symbol: quoted.moved.symbol, exchange: "", name: best.result.name, moved: quoted.moved.note }
135
+ : { symbol: best.result.symbol.toUpperCase(), exchange: best.exchange, name: best.result.name };
115
136
  const meant = best.stock && options.meant ? await stockNamedRunTogether(dataProvider, name, results) : null;
116
137
  return meant ? { ...found, meant } : found;
117
138
  }
@@ -127,22 +148,41 @@ interface RankedListing {
127
148
  exchange: string;
128
149
  }
129
150
 
151
+ /** A US exchange, or the US over-the-counter market a line moves to when it leaves one. */
152
+ function isUsVenue(exchange: string): boolean {
153
+ return isUsListingExchange(exchange) || exchange.startsWith("OTC") || exchange === "PNK";
154
+ }
155
+
130
156
  /**
131
157
  * The first of a company's lines, in their order, that has a quote: the first
132
158
  * itself, or when it has none, the first of the next few that does, asked
133
- * together. The first stands when none has a quote, when the source is too
134
- * busy to say, and when nothing here can ask for quotes.
159
+ * together. A first US line whose company moved to another symbol (GETY
160
+ * became GETYQ) is followed there before any other line, so a name never lands
161
+ * on a listing abroad that still quotes the old shares. The first stands when none has a quote,
162
+ * when the source is too busy to say, and when nothing here can ask for quotes.
135
163
  */
136
- async function firstQuoted<T extends RankedListing>(dataProvider: Partial<Pick<DataProvider, "getQuote">>, lines: readonly T[]): Promise<T> {
164
+ async function firstQuoted<T extends RankedListing>(
165
+ dataProvider: Partial<Pick<DataProvider, "getQuote">>,
166
+ lines: readonly T[],
167
+ movedLine?: MovedLineLookup,
168
+ ): Promise<{ line: T; moved: { symbol: string; note: string } | null }> {
137
169
  const first = lines[0]!;
138
- if (!dataProvider.getQuote) return first;
139
- const quoted = ({ result, exchange }: RankedListing) => dataProvider.getQuote!(result.symbol.toUpperCase(), exchange).then(hasPrice);
170
+ const only = (line: T) => ({ line, moved: null });
171
+ if (!dataProvider.getQuote) return only(first);
172
+ const quoteOf = ({ result, exchange }: RankedListing) => dataProvider.getQuote!(result.symbol.toUpperCase(), exchange);
173
+ let quote: Quote | null = null;
140
174
  try {
141
- if (await quoted(first)) return first;
175
+ quote = await quoteOf(first) ?? null;
142
176
  } catch (error) {
143
177
  // A busy source says nothing of the listing; any other failure is a listing with no quote.
144
- if (isProviderBusy(error)) return first;
178
+ if (isProviderBusy(error)) return only(first);
179
+ }
180
+ if (movedLine && isUsVenue(first.exchange)) {
181
+ const symbol = first.result.symbol.toUpperCase();
182
+ const moved = await movedLine({ symbol, exchange: first.exchange, quote, name: first.result.name?.trim() || null });
183
+ if (moved && moved.symbol.toUpperCase() !== symbol) return { line: first, moved: { ...moved, symbol: moved.symbol.toUpperCase() } };
145
184
  }
185
+ if (hasPrice(quote)) return only(first);
146
186
  const keys = new Set([listingKey(first)]);
147
187
  const next: T[] = [];
148
188
  for (const line of lines) {
@@ -151,11 +191,11 @@ async function firstQuoted<T extends RankedListing>(dataProvider: Partial<Pick<D
151
191
  keys.add(listingKey(line));
152
192
  next.push(line);
153
193
  }
154
- const priced = await Promise.all(next.map((line) => quoted(line).catch(() => {
194
+ const priced = await Promise.all(next.map((line) => quoteOf(line).then(hasPrice).catch(() => {
155
195
  // Intentional ignore: a line whose quote fails is a line with no quote.
156
196
  return false;
157
197
  })));
158
- return next.find((_, index) => priced[index]) ?? first;
198
+ return only(next.find((_, index) => priced[index]) ?? first);
159
199
  }
160
200
 
161
201
  /** A name as two venues may spell it alike: "SIEMENS LTD" for Siemens Ltd. */
@@ -99,12 +99,53 @@ export function companyNameMatch(typed: string, name: string | null | undefined)
99
99
  return { extra: words.size - matched.size, abbreviated };
100
100
  }
101
101
 
102
- /** Items grouped by the company their names are, in the order each company first appears. Unnamed items are left out. */
103
- export function groupByCompany<T>(items: readonly T[], nameOf: (item: T) => string | null | undefined): T[][] {
102
+ // Words one fund's venues name it with or without: "iShares MSCI Japan Index Fund" in New York is "iShares MSCI Japan ETF" in Mexico.
103
+ const FUND_NOISE = new Set(["etf", "etfs", "fund", "funds", "index", "trust", "ucits"]);
104
+
105
+ function instrumentWords(name: string | null | undefined): Set<string> {
106
+ return new Set([...companyWords(name)].filter((word) => !FUND_NOISE.has(word)));
107
+ }
108
+
109
+ /**
110
+ * Whether two venues' names can be one instrument's: one company's
111
+ * (`sameCompany`), or one fund's however a venue words it, with or without
112
+ * "ETF", "Index Fund" or "Trust" ("Technology Select Sector SPDR Fund" in
113
+ * Buenos Aires is State Street's ETF in New York). Two names that are not are
114
+ * another company's or another fund's ("W.A.G Payment Solutions" is not the
115
+ * iShares fund, nor "UBS MSCI India" iShares' India fund). An empty name
116
+ * matches none.
117
+ */
118
+ export function sameListedName(left: string | null | undefined, right: string | null | undefined): boolean {
119
+ if (sameCompany(left, right)) return true;
120
+ const a = instrumentWords(left);
121
+ const b = instrumentWords(right);
122
+ if (a.size === 0 || b.size === 0) return false;
123
+ const [fewer, more] = a.size <= b.size ? [a, b] : [b, a];
124
+ return [...fewer].every((word) => more.has(word));
125
+ }
126
+
127
+ /**
128
+ * Whether a venue names its line a depositary receipt ("Toyota Motor
129
+ * Corporation CEDEAR", "SS SPDR S&P 500 ETF Trust - CEDEAR"): a receipt of a
130
+ * line elsewhere, whose short form of that line's name says little of whose it is.
131
+ */
132
+ export function namesDepositaryReceipt(name: string | null | undefined): boolean {
133
+ return /\b(?:cedears?|adrs?|ads|cdrs?|cdis?|depositary)\b/i.test(name ?? "");
134
+ }
135
+
136
+ /**
137
+ * Items grouped by the company their names are (`same`, `sameCompany` unless
138
+ * given), in the order each company first appears. Unnamed items are left out.
139
+ */
140
+ export function groupByCompany<T>(
141
+ items: readonly T[],
142
+ nameOf: (item: T) => string | null | undefined,
143
+ same: (left: string | null | undefined, right: string | null | undefined) => boolean = sameCompany,
144
+ ): T[][] {
104
145
  const groups: T[][] = [];
105
146
  for (const item of items) {
106
147
  if (companyWords(nameOf(item)).size === 0) continue;
107
- const group = groups.find((members) => members.some((member) => sameCompany(nameOf(member), nameOf(item))));
148
+ const group = groups.find((members) => members.some((member) => same(nameOf(member), nameOf(item))));
108
149
  if (group) group.push(item);
109
150
  else groups.push([item]);
110
151
  }
@@ -174,10 +215,15 @@ function nameWord(word: string, opening: boolean, domain: boolean): string {
174
215
  * the way its source means it and comes back as it is.
175
216
  */
176
217
  export function companyNameCase(name: string): string {
177
- if (!/\p{Lu}/u.test(name) || name !== name.toUpperCase()) return name;
218
+ // A brand that opens in lower case before capitals (aTYR, iROBOT) is still a name in capitals: aTyr, iRobot.
219
+ const brand = /^\p{Ll}(?=\p{Lu}{2})/u;
220
+ const capitals = name.split(/\s+/).map((word) => word.replace(brand, "")).join(" ");
221
+ if (!/\p{Lu}/u.test(name) || capitals !== capitals.toUpperCase()) return name;
178
222
  let words = 0;
179
223
  return name.replace(/\S+/g, (token) => {
180
224
  const opening = words++ === 0;
225
+ const lead = brand.exec(token)?.[0];
226
+ if (lead) return `${lead}${token.charAt(1)}${token.slice(2).toLowerCase()}`;
181
227
  // A separator inside a word (Coca-Cola, AT&T, Amazon.com, McDonald's) cuts it into words of its own.
182
228
  return token.split(/([-/.&,'\u2019]+)/).map((part, index, parts) => {
183
229
  if (index % 2 === 1) return part;
@@ -45,3 +45,10 @@ export function indexRootSuggestion(symbol: string): string | null {
45
45
  const root = indexRootFor(symbol);
46
46
  return root ? `Did you mean ${root.symbol} (${root.name})?` : null;
47
47
  }
48
+
49
+ /** Cboe's Treasury yield indices: 13-week bill, 5-, 10- and 30-year notes. Each level is a yield in percent. */
50
+ const YIELD_INDEX_SYMBOLS: ReadonlySet<string> = new Set(["^IRX", "^FVX", "^TNX", "^TYX"]);
51
+
52
+ export function isYieldIndexSymbol(symbol: string): boolean {
53
+ return YIELD_INDEX_SYMBOLS.has(symbol.trim().toUpperCase());
54
+ }
@@ -182,7 +182,7 @@ export function describeListingFate(
182
182
  * ended long ago. Only such a line can be a company that moved away, so only
183
183
  * its change lookup is waited for.
184
184
  */
185
- function mayHaveMoved(quote: Quote | null, now = Date.now()): boolean {
185
+ export function lineMayHaveMoved(quote: Quote | null, now = Date.now()): boolean {
186
186
  return !!quote && (isQuoteBeforeLatestSession(quote, now) || endedTradingDate(quote, now) != null);
187
187
  }
188
188
 
@@ -206,7 +206,7 @@ export async function loadListingFate(
206
206
  // A lookup started beside the command's request gets a short grace once that is ready, so a slow
207
207
  // one never holds a symbol that trades. A line that stopped is waited for: only its company can
208
208
  // have moved, and then the answer decides whether the command serves anything at all.
209
- const changes = await lookup.settle(!options.changes || mayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : undefined);
209
+ const changes = await lookup.settle(!options.changes || lineMayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : undefined);
210
210
  return describeListingFate(listing, quote, changes);
211
211
  } catch (error) {
212
212
  logFailure("tickers-listing-fate", "Could not resolve listing fate")(error);
@@ -28,6 +28,17 @@ export interface ListingVenue {
28
28
  /** Canonical code. */
29
29
  exchange: string;
30
30
  name: string;
31
+ /**
32
+ * The listing's own symbol there, when search lists it under another
33
+ * spelling than the one asked for: BP for `BP.` (London's code for it).
34
+ */
35
+ symbol?: string;
36
+ }
37
+
38
+ /** The venue's own symbol, when it is not `asked` (ignoring case). */
39
+ function ownSymbol(listed: string | undefined, asked: string): { symbol: string } | Record<string, never> {
40
+ const symbol = listed?.trim().toUpperCase();
41
+ return symbol && symbol !== asked.trim().toUpperCase() ? { symbol } : {};
31
42
  }
32
43
 
33
44
  /**
@@ -50,14 +61,15 @@ export async function listingVenues(symbol: string, { store, dataProvider }: Lis
50
61
  : {
51
62
  exchange: resolved.result.exchange === "SMART" ? resolved.result.primaryExchange ?? "" : resolved.result.exchange || resolved.result.primaryExchange || "",
52
63
  name: resolved.result.name,
64
+ ...ownSymbol(parsePublicTickerKey(resolved.result.symbol).symbol, symbol),
53
65
  };
54
- venues = only?.exchange ? [{ exchange: canonicalExchange(only.exchange), name: only.name }] : [];
66
+ venues = only?.exchange ? [{ ...only, exchange: canonicalExchange(only.exchange) }] : [];
55
67
  } catch (error) {
56
68
  // One listing settles the picker; several are its choices.
57
69
  if (!(error instanceof AmbiguousTickerError) || error instanceof AmbiguousContractError) return [];
58
70
  venues = error.listings.flatMap((listing) => {
59
- const exchange = parsePublicTickerKey(listing).exchange;
60
- return exchange ? [{ exchange, name: error.listingNames[listing] ?? "" }] : [];
71
+ const key = parsePublicTickerKey(listing);
72
+ return key.exchange ? [{ exchange: key.exchange, name: error.listingNames[listing] ?? "", ...ownSymbol(key.symbol, symbol) }] : [];
61
73
  });
62
74
  }
63
75
  return venues.filter((venue, index) => venues.findIndex((other) => other.exchange === venue.exchange) === index);
@@ -7,6 +7,7 @@ import { getListingSymbol, listingSuffixConflictsWithExchange, tickerHasListingS
7
7
  import { parseOptionSymbol } from "../../utils/options";
8
8
  import { resolveCurrencyUnit } from "../../utils/currency-units";
9
9
  import { currencyPairSpelling } from "../../utils/currency-pair";
10
+ import { typedCoinPairListing } from "../../utils/crypto-pair";
10
11
  import { assetClassMarketSymbol, parseAssetClassQuery } from "./asset-classes";
11
12
  import { bareFuturesCode, futuresContractByCode } from "../../market-data/futures-board";
12
13
  import { searchContractKey, searchInstrumentKey } from "./identity";
@@ -387,9 +388,12 @@ export function resolveTickerSearch(options: ResolveTickerSearchOptions): Promis
387
388
  // JPY=X are EURUSD=X and USDJPY=X. USD-NGN and USDNGN, which a security could also be written as
388
389
  // (MNT-USD is a coin), are the pair only when nothing is listed under those letters.
389
390
  const pair = pickListing || future ? null : currencyPairSpelling(typed);
390
- const symbol = future?.symbol ?? (pair && !pair.ambiguous ? pair.symbol : typed);
391
+ // A coin pair written as crypto exchanges write it (BTC/GBP, or BTC/GBP:KRAKEN) is the crypto venue's BTC-GBP,
392
+ // and what was typed when the venue has no such coin.
393
+ const coin = pickListing || future || pair || options.tickers.has(typed) ? null : typedCoinPairListing(typed);
394
+ const symbol = future?.symbol ?? coin?.symbol ?? (pair && !pair.ambiguous ? pair.symbol : typed);
391
395
  const literal = resolveTickerSymbol(symbol, options, pickListing ? requested : null);
392
- const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : []];
396
+ const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : [], ...coin ? [typed] : []];
393
397
  // A listing suffix search does not carry (CEZ.PR) is read from its own quote, as `quote` reads it.
394
398
  const quoted = !pickListing && tickerHasListingSuffix(symbol) && !parsePublicTickerKey(symbol).exchange;
395
399
  // Most input has no venue code to retry: hand back the literal lookup
@@ -4,6 +4,7 @@ import type {
4
4
  } from "./types";
5
5
  import { getListingSymbol, tickerHasListingSuffix } from "../../sources/listing-symbols";
6
6
  import { currencyPairCode } from "../../utils/currency-pair";
7
+ import { typedCoinPairListing } from "../../utils/crypto-pair";
7
8
  import { canonicalExchange, exchangeCodeVenues, isUsListingExchange, parsePublicTickerKey, typedExchangeCode } from "../../utils/exchanges";
8
9
  import { assetClassKeeps, assetClassMarketSymbol, instrumentClassCode, parseAssetClassQuery } from "./asset-classes";
9
10
  import { futuresContractByCode } from "../../market-data/futures-board";
@@ -322,9 +323,12 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
322
323
  .map(({ item }) => getTickerSearchListingKey(item)),
323
324
  );
324
325
 
326
+ // BTC/GBP reads as a currency pair's spelling, and is the crypto venue's BTC-GBP.
327
+ const coinListing = typedCoinPairListing(query)?.symbol;
325
328
  const filtered = ranked.filter(({ item, textScore }) => {
326
329
  if (textScore <= 0) return false;
327
- if (isExplicitMarketSymbol(query) && !isExplicitMarketSymbol(item.symbol || item.label)) return false;
330
+ if (isExplicitMarketSymbol(query) && !isExplicitMarketSymbol(item.symbol || item.label)
331
+ && parsePublicTickerKey(item.symbol || item.label).symbol !== coinListing) return false;
328
332
  if (item.kind !== "search") return true;
329
333
  return !matchedLocalListings.has(getTickerSearchListingKey(item));
330
334
  });
@@ -11,6 +11,7 @@ import {
11
11
  import { canonicalExchange, parsePublicTickerKey, publicTickerKey } from "../../utils/exchanges";
12
12
  import { scopedBrokerContractIdentityKey } from "../../utils/instrument-identity";
13
13
  import { listingSuffixVenue } from "../../sources/listing-symbols";
14
+ import { coinPairQuoteCurrency, exchangeLineCoinListing, isCoinRowType } from "../../utils/crypto-pair";
14
15
 
15
16
  /**
16
17
  * The key a result is saved under when its symbol is already saved for
@@ -36,6 +37,18 @@ export async function upsertTickerFromSearchResult(
36
37
  result: InstrumentSearchResult,
37
38
  options: { tickerSymbol?: string } = {},
38
39
  ): Promise<{ ticker: TickerRecord; created: boolean }> {
40
+ // A crypto exchange's line of a coin pair (BTC/GBP on COINBASE PRO), which no source quotes, is saved as the
41
+ // crypto venue's listing of the pair (BTC-GBP on CCC), in the currency the pair is quoted in.
42
+ const coin = result.brokerContract ? null
43
+ : exchangeLineCoinListing(getSearchResultSymbol(result), listingVenue(result), { coinRow: isCoinRowType(result.type) });
44
+ if (coin) {
45
+ result = { ...result, symbol: coin.symbol, exchange: coin.exchange, primaryExchange: undefined, currency: coin.currency };
46
+ if (options.tickerSymbol) options = { ...options, tickerSymbol: publicTickerKey(coin.symbol, coin.exchange) };
47
+ } else if (!result.currency && !result.brokerContract && canonicalExchange(listingVenue(result)) === "CCC") {
48
+ // The venue's own row of a pair (BTC-GBP) often comes without its currency, which the pair names.
49
+ const currency = coinPairQuoteCurrency(getSearchResultSymbol(result));
50
+ if (currency) result = { ...result, currency };
51
+ }
39
52
  let symbol = options.tickerSymbol ?? getSearchResultSymbol(result);
40
53
  // A vendor's suffixed symbol (GARAN.IS) is saved as the listing it is (GARAN on BIST), under the key
41
54
  // GARAN:BIST is saved under, so one listing is one row. A key the caller names is kept as given.
@@ -412,6 +412,25 @@ export interface NearMissListing {
412
412
  symbol: string;
413
413
  }
414
414
 
415
+ /**
416
+ * `BP.`, `RR.`: London's code for a company with a two-letter symbol, which
417
+ * the exchange writes with a dot to make three. No other venue writes a
418
+ * trailing dot, though one can trail a US ticker by slip (`GE.`).
419
+ */
420
+ const LONDON_DOTTED = /^([A-Z]{2})\.$/;
421
+
422
+ /**
423
+ * A two-letter symbol written with London's trailing dot (`BP.`): what was
424
+ * typed and the symbol (BP). Whether it is London's line (BP:LSE) or a US
425
+ * ticker with a stray dot (`GE.`) is for where London lists it to say
426
+ * (`resolveCliListing`). Null for anything else.
427
+ */
428
+ export function readLondonDottedSymbol(raw: string): NearMissListing | null {
429
+ const typed = raw.trim();
430
+ const london = LONDON_DOTTED.exec(typed.toUpperCase());
431
+ return london ? { typed, symbol: london[1]! } : null;
432
+ }
433
+
415
434
  /** `BRK/B`: a US share class after a slash, as a broker writes it. One letter before the slash is a ratio or an abbreviation (P/E, N/A). */
416
435
  const SLASH_CLASS = /^([A-Z]{2,5})\/([A-Z])$/;
417
436
  /** `700.HK`: a Hong Kong code without its leading zeros. */