gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -0,0 +1,307 @@
1
+ import type { UpdateFailureReport } from "../api-client";
2
+ import type { UpdateChannel } from "../types/config";
3
+ import type { UpdateFailure } from "../updater/failure";
4
+ import { logFailure } from "../utils/log-failure";
5
+ import { compareSemver } from "../utils/semver";
6
+ import { VERSION } from "../version";
7
+ import { reportCrash, type CrashReportContext } from "./crash-reports";
8
+
9
+ /**
10
+ * Failed update steps, sent to error tracking through the crash reporter (and
11
+ * so behind the same off switch). A report is the step, a fixed code, the
12
+ * versions, the channel, the platform and how the app was installed: never the
13
+ * error's text, which can hold a path, a user name or a signed URL.
14
+ *
15
+ * The process that runs the update reports it: the terminal app, and the
16
+ * desktop app's Bun process. The desktop view and the web install no host, so
17
+ * the same failure is never sent twice. One report per step, code and app
18
+ * version a day is enough to see a stuck updater; the hourly check that keeps
19
+ * failing offline adds nothing after the first.
20
+ *
21
+ * The desktop host also keeps what an install was about to do, so the next
22
+ * launch can tell an install that never took, or an update downloaded and
23
+ * then quit without, from one that worked (`reviewLastUpdateAttempt`).
24
+ */
25
+
26
+ /**
27
+ * Where the running app sits. macOS installs replace the app bundle in place,
28
+ * which only works from a folder the user can write: not from a disk image,
29
+ * and not from the read-only copy App Translocation runs a quarantined app from.
30
+ */
31
+ export type UpdateInstallKind =
32
+ | "desktop-applications"
33
+ | "desktop-translocated"
34
+ | "desktop-volume"
35
+ | "desktop-elsewhere"
36
+ /** Windows and Linux: the app folder the installer created. */
37
+ | "desktop-app-data"
38
+ | "terminal-binary"
39
+ | "terminal-managed"
40
+ | "terminal-package-manager"
41
+ | "terminal-source"
42
+ | "unknown";
43
+
44
+ /** One update the app downloaded or started installing, and when. */
45
+ export interface UpdateAttempt {
46
+ from: string;
47
+ to: string;
48
+ channel: UpdateChannel;
49
+ at: number;
50
+ }
51
+
52
+ /** What the host keeps on disk between launches. */
53
+ export interface UpdateReportState {
54
+ /** When each step, code and app version was last reported, in ms. */
55
+ sent: Record<string, number>;
56
+ /** A downloaded update waiting for the user's Restart. */
57
+ staged?: UpdateAttempt;
58
+ /** An install the user started; the next launch says whether it took. */
59
+ applying?: UpdateAttempt;
60
+ }
61
+
62
+ export interface UpdateFailureReporterHost {
63
+ installKind: UpdateInstallKind;
64
+ /** The running version; defaults to this build's. */
65
+ appVersion?: string;
66
+ /** `process.platform` and `process.arch`. */
67
+ platform: string;
68
+ arch: string;
69
+ load(): UpdateReportState;
70
+ save(state: UpdateReportState): void;
71
+ now?(): number;
72
+ /** Defaults to the crash reporter. */
73
+ send?(report: UpdateFailureReport): void;
74
+ }
75
+
76
+ export interface UpdateFailureContext {
77
+ /** The version the update was going to, when one was known. */
78
+ targetVersion?: string | null;
79
+ channel?: UpdateChannel | null;
80
+ }
81
+
82
+ const DAY_MS = 24 * 60 * 60 * 1000;
83
+ /** Older attempts say nothing about this launch: the user installed by hand, or reinstalled. */
84
+ const ATTEMPT_MAX_AGE_MS = 30 * DAY_MS;
85
+ /**
86
+ * The install quits the app and opens the new one as soon as it is gone. A new
87
+ * version first seen this long after is one the user opened again by hand.
88
+ */
89
+ const RELAUNCH_GRACE_MS = 10 * 60 * 1000;
90
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,60})?$/;
91
+
92
+ let host: UpdateFailureReporterHost | null = null;
93
+
94
+ function cleanVersion(value: string | null | undefined): string | undefined {
95
+ const version = value?.trim().replace(/^v/, "");
96
+ return version && VERSION_PATTERN.test(version) ? version : undefined;
97
+ }
98
+
99
+ function reportPlatform(platform: string): UpdateFailureReport["platform"] {
100
+ if (platform === "darwin") return "macos";
101
+ if (platform === "win32") return "windows";
102
+ if (platform === "linux") return "linux";
103
+ return "other";
104
+ }
105
+
106
+ function reportArch(arch: string): UpdateFailureReport["arch"] {
107
+ return arch === "arm64" || arch === "x64" ? arch : "other";
108
+ }
109
+
110
+ /**
111
+ * The report, from fixed values only: anything that is not a version, a
112
+ * known channel, platform or install kind is left out or named `other`.
113
+ */
114
+ function buildUpdateFailureReport(
115
+ failure: UpdateFailure,
116
+ fields: UpdateFailureContext & {
117
+ appVersion: string;
118
+ platform: string;
119
+ arch: string;
120
+ installKind: UpdateInstallKind;
121
+ },
122
+ ): UpdateFailureReport {
123
+ const targetVersion = cleanVersion(fields.targetVersion);
124
+ return {
125
+ phase: failure.phase,
126
+ code: failure.code,
127
+ appVersion: cleanVersion(fields.appVersion) ?? "unknown",
128
+ ...(targetVersion ? { targetVersion } : {}),
129
+ channel: fields.channel === "nightly" ? "nightly" : "stable",
130
+ platform: reportPlatform(fields.platform),
131
+ arch: reportArch(fields.arch),
132
+ installKind: fields.installKind,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * The report as the crash reporter sends it. Today's server keeps the error's
138
+ * type, message, plugin and pane: the message is the step and code, so error
139
+ * tracking groups one issue per failure, and the pane holds the rest as
140
+ * `key=value` pairs. The `update` field carries the same values for a server
141
+ * that reads it.
142
+ */
143
+ function updateFailureCrashReport(report: UpdateFailureReport): {
144
+ error: { name: string; message: string };
145
+ context: CrashReportContext;
146
+ } {
147
+ const details = [
148
+ `to=${report.targetVersion ?? "none"}`,
149
+ `channel=${report.channel}`,
150
+ `platform=${report.platform}-${report.arch}`,
151
+ `install=${report.installKind}`,
152
+ ].join(" ");
153
+ return {
154
+ // A plain object, not an Error: it has no stack, so no file path goes along.
155
+ error: { name: "UpdateFailed", message: `${report.phase}: ${report.code}` },
156
+ context: { kind: "plugin", plugin: "updater", pane: details, update: report },
157
+ };
158
+ }
159
+
160
+ function sendThroughCrashReporter(report: UpdateFailureReport): void {
161
+ const { error, context } = updateFailureCrashReport(report);
162
+ reportCrash(error, context);
163
+ }
164
+
165
+ /**
166
+ * Takes the day's slot for `key`, or says it is used: one report per step,
167
+ * code and version a day. Drops what is older than a day while at it.
168
+ */
169
+ function takeReportSlot(state: UpdateReportState, key: string, now: number): boolean {
170
+ for (const [entry, at] of Object.entries(state.sent)) {
171
+ if (!(now - at < DAY_MS) || at > now) delete state.sent[entry];
172
+ }
173
+ if (state.sent[key] !== undefined) return false;
174
+ state.sent[key] = now;
175
+ return true;
176
+ }
177
+
178
+ function loadState(current: UpdateFailureReporterHost): UpdateReportState {
179
+ try {
180
+ const state = current.load();
181
+ return { ...state, sent: { ...state.sent } };
182
+ } catch (error) {
183
+ logFailure("telemetry-update-failures", "Could not read update report state")(error);
184
+ return { sent: {} };
185
+ }
186
+ }
187
+
188
+ function saveState(current: UpdateFailureReporterHost, state: UpdateReportState): void {
189
+ try {
190
+ current.save(state);
191
+ } catch (error) {
192
+ logFailure("telemetry-update-failures", "Could not save update report state")(error);
193
+ }
194
+ }
195
+
196
+ function sendIfDue(
197
+ current: UpdateFailureReporterHost,
198
+ state: UpdateReportState,
199
+ failure: UpdateFailure,
200
+ context: UpdateFailureContext,
201
+ now: number,
202
+ ): void {
203
+ const report = buildUpdateFailureReport(failure, {
204
+ ...context,
205
+ appVersion: current.appVersion ?? VERSION,
206
+ platform: current.platform,
207
+ arch: current.arch,
208
+ installKind: current.installKind,
209
+ });
210
+ if (!takeReportSlot(state, `${report.phase}|${report.code}|${report.appVersion}`, now)) return;
211
+ (current.send ?? sendThroughCrashReporter)(report);
212
+ }
213
+
214
+ export function installUpdateFailureReporter(next: UpdateFailureReporterHost): () => void {
215
+ host = next;
216
+ return () => {
217
+ if (host === next) host = null;
218
+ };
219
+ }
220
+
221
+ /** Reports one failed step. Never throws; does nothing where no host is installed. */
222
+ export function reportUpdateFailure(failure: UpdateFailure, context: UpdateFailureContext = {}): void {
223
+ const current = host;
224
+ if (!current) return;
225
+ try {
226
+ const now = current.now?.() ?? Date.now();
227
+ const state = loadState(current);
228
+ // An install that failed in this run is reported here, not again at the next launch.
229
+ if (failure.phase === "apply" || failure.phase === "relaunch") delete state.applying;
230
+ sendIfDue(current, state, failure, context, now);
231
+ saveState(current, state);
232
+ } catch (error) {
233
+ logFailure("telemetry-update-failures", "Could not report update failure")(error);
234
+ }
235
+ }
236
+
237
+ function recordAttempt(field: "staged" | "applying", to: string, channel: UpdateChannel): void {
238
+ const current = host;
239
+ if (!current) return;
240
+ const state = loadState(current);
241
+ const attempt: UpdateAttempt = { from: current.appVersion ?? VERSION, to, channel, at: current.now?.() ?? Date.now() };
242
+ if (field === "applying") delete state.staged;
243
+ state[field] = attempt;
244
+ saveState(current, state);
245
+ }
246
+
247
+ /** A downloaded update is waiting for the user's Restart. */
248
+ export function noteUpdateStaged(to: string, channel: UpdateChannel): void {
249
+ recordAttempt("staged", to, channel);
250
+ }
251
+
252
+ /** The user pressed Restart: the next launch should be `to`. */
253
+ export function noteUpdateApplying(to: string, channel: UpdateChannel): void {
254
+ recordAttempt("applying", to, channel);
255
+ }
256
+
257
+ /** The waiting update was taken back (a channel switch); quitting without it is no longer news. */
258
+ export function forgetStagedUpdate(): void {
259
+ const current = host;
260
+ if (!current) return;
261
+ const state = loadState(current);
262
+ if (!state.staged) return;
263
+ delete state.staged;
264
+ saveState(current, state);
265
+ }
266
+
267
+ /**
268
+ * Run once at launch: what came of the last update this app started.
269
+ *
270
+ * - Restarted into the new version: nothing to report, unless the new version
271
+ * only came up long after, which is the app failing to reopen itself.
272
+ * - Still the old version after an install: it never took.
273
+ * - Still the old version with a download waiting: the app was quit without
274
+ * restarting into it, which the stuck installs may come down to.
275
+ */
276
+ export function reviewLastUpdateAttempt(): void {
277
+ const current = host;
278
+ if (!current) return;
279
+ const currentVersion = current.appVersion ?? VERSION;
280
+ try {
281
+ const state = loadState(current);
282
+ const { applying, staged } = state;
283
+ if (!applying && !staged) return;
284
+ delete state.applying;
285
+ delete state.staged;
286
+ const now = current.now?.() ?? Date.now();
287
+ const fresh = (attempt: UpdateAttempt | undefined) => !!attempt && now - attempt.at < ATTEMPT_MAX_AGE_MS && attempt.at <= now;
288
+ if (applying && fresh(applying)) {
289
+ const context = { targetVersion: applying.to, channel: applying.channel };
290
+ if (applying.from === currentVersion) {
291
+ sendIfDue(current, state, { phase: "apply", code: "not_applied" }, context, now);
292
+ } else if ((compareSemver(currentVersion, applying.to) ?? -1) >= 0 && now - applying.at > RELAUNCH_GRACE_MS) {
293
+ sendIfDue(current, state, { phase: "relaunch", code: "relaunch_missed" }, context, now);
294
+ }
295
+ } else if (staged && fresh(staged) && staged.from === currentVersion) {
296
+ sendIfDue(current, state, { phase: "apply", code: "not_restarted" }, { targetVersion: staged.to, channel: staged.channel }, now);
297
+ }
298
+ saveState(current, state);
299
+ } catch (error) {
300
+ logFailure("telemetry-update-failures", "Could not review the last update")(error);
301
+ }
302
+ }
303
+
304
+ /** Test seam. */
305
+ export function resetUpdateFailureReporterForTests(): void {
306
+ host = null;
307
+ }
@@ -2,7 +2,7 @@ import { logFailure } from "../utils/log-failure";
2
2
  import type { DataProvider } from "../types/data-provider";
3
3
  import type { InstrumentSearchResult } from "../types/instrument";
4
4
  import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../utils/exchanges";
5
- import { companyNameExtraWords, shortCompanyName } from "./company-name";
5
+ import { companyNameMatch, shortCompanyName } from "./company-name";
6
6
  import { LISTING_SEARCH_LIMIT } from "./search";
7
7
  import { classifyInstrumentKind } from "./search/ranking";
8
8
  import { isProviderBusy, type ProviderBusyError } from "../sources/provider-errors";
@@ -28,10 +28,12 @@ export function isCompanyNameInput(input: string): boolean {
28
28
  /**
29
29
  * The listing a company's name stands for when it is typed where a symbol
30
30
  * goes, as an entry of a comma list ("Whirlpool Corporation" is WHR on
31
- * NYSE). Of search's listings whose names have every word of it (the name is
32
- * also searched without its legal form, which can find more), the company
31
+ * NYSE). Of search's listings whose names have every word of it, or a word
32
+ * it shortens ("philip morris intl" is Philip Morris International; the name
33
+ * is also searched without its legal form, which can find more), the company
33
34
  * whose name says no more than the one typed goes first, so a subsidiary that
34
- * shares the word (Whirlpool of India) does not; then its line on a US
35
+ * shares the word (Whirlpool of India) does not, and of those a name matched
36
+ * word for word before one a short form matched; then its line on a US
35
37
  * exchange, then a plain line before a depositary receipt, the shortest
36
38
  * symbol (WHR before WHR-A) and search's order. Contracts, preferred shares
37
39
  * and notes are never the listing. Null when none matches; when none does
@@ -55,10 +57,10 @@ export async function findListingOfCompanyName(
55
57
  const ranked = results.flatMap((result, order) => {
56
58
  const exchange = canonicalExchange(result.exchange === "SMART" ? result.primaryExchange : result.exchange || result.primaryExchange);
57
59
  const type = result.brokerContract?.secType || result.type || "";
58
- const extra = companyNameExtraWords(name, result.name);
59
- if (!exchange || extra == null || classifyInstrumentKind(type) === "derivative" || NOT_COMMON.test(type)
60
+ const match = companyNameMatch(name, result.name);
61
+ if (!exchange || match == null || classifyInstrumentKind(type) === "derivative" || NOT_COMMON.test(type)
60
62
  || parsePublicTickerKey(result.symbol).exchange) return [];
61
- return [{ result, exchange, rank: [extra, isUsListingExchange(exchange) ? 0 : 1, RECEIPT.test(type) ? 1 : 0, result.symbol.length, order] }];
63
+ return [{ result, exchange, rank: [match.extra, match.abbreviated, isUsListingExchange(exchange) ? 0 : 1, RECEIPT.test(type) ? 1 : 0, result.symbol.length, order] }];
62
64
  }).sort((left, right) => {
63
65
  const at = left.rank.findIndex((value, index) => value !== right.rank[index]);
64
66
  return at < 0 ? 0 : left.rank[at]! - right.rank[at]!;
@@ -56,6 +56,49 @@ export function companyNameExtraWords(typed: string, name: string | null | undef
56
56
  return words.size - wanted.size;
57
57
  }
58
58
 
59
+ /**
60
+ * Whether a typed word is a short form of a name's word, as a list writes
61
+ * one: its first letter and the rest in order, at least three letters
62
+ * (`intl` for international, `hldgs` for holdings, `grp` for group).
63
+ */
64
+ function abbreviates(short: string, word: string): boolean {
65
+ if (short.length < 3 || short.length >= word.length || short[0] !== word[0] || !/^\p{L}+$/u.test(short)) return false;
66
+ let at = 0;
67
+ for (const char of word) {
68
+ if (char === short[at]) at += 1;
69
+ if (at === short.length) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * `companyNameExtraWords` for a name typed with short forms, as a list or a
76
+ * spreadsheet writes it ("philip morris intl" for Philip Morris
77
+ * International Inc.): each typed word is one of the name's, or a short form
78
+ * of another of its words (`abbreviates`). `extra` counts the name's words
79
+ * left unmatched, `abbreviated` the typed words read as short forms, so an
80
+ * exact name ranks before one only a short form reaches. Null when a typed
81
+ * word is in the name neither way, or nothing was typed.
82
+ */
83
+ export function companyNameMatch(typed: string, name: string | null | undefined): { extra: number; abbreviated: number } | null {
84
+ const wanted = companyWords(typed);
85
+ const words = companyWords(name);
86
+ if (wanted.size === 0) return null;
87
+ const matched = new Set<string>();
88
+ let abbreviated = 0;
89
+ for (const word of wanted) {
90
+ if (words.has(word)) {
91
+ matched.add(word);
92
+ continue;
93
+ }
94
+ const long = [...words].find((candidate) => !matched.has(candidate) && !wanted.has(candidate) && abbreviates(word, candidate));
95
+ if (!long) return null;
96
+ matched.add(long);
97
+ abbreviated += 1;
98
+ }
99
+ return { extra: words.size - matched.size, abbreviated };
100
+ }
101
+
59
102
  /** Items grouped by the company their names are, in the order each company first appears. Unnamed items are left out. */
60
103
  export function groupByCompany<T>(items: readonly T[], nameOf: (item: T) => string | null | undefined): T[][] {
61
104
  const groups: T[][] = [];
@@ -19,6 +19,7 @@ import { isQuoteBeforeLatestSession, isQuoteStaleForCurrentSession } from "../ma
19
19
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
20
20
  import { canonicalExchange, isUsListingExchange, parsePublicTickerKey, resolveExchangeTimeZone } from "../utils/exchanges";
21
21
  import { zonedDateKey } from "../utils/zoned-date-time";
22
+ import { delistingLabel } from "../market-data/quotes/delisting";
22
23
 
23
24
  /*
24
25
  * What became of a symbol, read the way `quote` reads it: the listing's own
@@ -33,8 +34,10 @@ const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
33
34
  /**
34
35
  * The exchange-local date a listing last traded, when its quote is stale and
35
36
  * that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
37
+ * A quote that says its listing no longer trades gives its last session.
36
38
  */
37
39
  function endedTradingDate(quote: Quote, now = Date.now()): string | null {
40
+ if (quote.delisting) return quote.delisting.lastTradeDate;
38
41
  const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
39
42
  if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
40
43
  return quoteDateKey(quote, last);
@@ -60,12 +63,18 @@ export function tradingListing(quote: Quote | null | undefined, now = Date.now()
60
63
  return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
61
64
  }
62
65
 
63
- /** "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote that stopped long ago. */
66
+ /**
67
+ * "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote
68
+ * that stopped long ago; "TWTR: Delisted Oct 27, 2022 (last price $53.86)"
69
+ * when the quote says the listing no longer trades.
70
+ */
64
71
  export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
65
72
  const date = endedTradingDate(quote, now);
66
73
  if (!date) return null;
67
74
  const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
68
- return `${label}: no longer trading since ${date} (last price ${price})`;
75
+ return quote.delisting
76
+ ? `${label}: ${delistingLabel(quote.delisting)} (last price ${price})`
77
+ : `${label}: no longer trading since ${date} (last price ${price})`;
69
78
  }
70
79
 
71
80
  /**
@@ -81,11 +90,17 @@ export interface ListingFate {
81
90
  /**
82
91
  * The line a command with nothing to serve for the symbol says: `quote`'s
83
92
  * change line when its company left it ("FB now lists ...; Meta Platforms
84
- * trades as META (changed 2022-06-09)"), else, for a listing that stopped,
85
- * "TWTR stopped trading 2022-10-27; no history or options served". Null
86
- * for a symbol that trades as it always has.
93
+ * trades as META (changed 2022-06-09)"), else, for a listing the service
94
+ * says no longer trades, "TWTR: Delisted Oct 27, 2022", and for another
95
+ * that stopped, "XYZ stopped trading 2022-10-27; no history or options
96
+ * served". Null for a symbol that trades as it always has.
87
97
  */
88
98
  note: string | null;
99
+ /**
100
+ * "TWTR: Delisted Oct 27, 2022" when the listing's quote says it no longer
101
+ * trades: its history is served, ending on that session, and says so.
102
+ */
103
+ delisted?: string;
89
104
  /**
90
105
  * The change when the symbol's company trades under another symbol now
91
106
  * and its own quote shows nothing new listed under it (`movedAwayChange`):
@@ -156,9 +171,10 @@ export function describeListingFate(
156
171
  const abroad = !!quote && !!bareListingNote({ symbol, request: { exchange: listing.exchange ?? "" } }, quote);
157
172
  const current = tradingListing(quote, now);
158
173
  const change = abroad ? null : describeTickerChange(symbol, changes, current);
159
- const note = tickerChangeLine(change) ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
174
+ const delisted = quote?.delisting && !abroad ? `${symbol}: ${delistingLabel(quote.delisting)}` : undefined;
175
+ const note = tickerChangeLine(change) ?? delisted ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
160
176
  const formerIssuer = abroad ? null : describeFormerIssuer(symbol, changes, current);
161
- return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer };
177
+ return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer, ...(delisted ? { delisted } : {}) };
162
178
  }
163
179
 
164
180
  /**
@@ -200,7 +216,8 @@ export async function loadListingFate(
200
216
 
201
217
  /**
202
218
  * The lines a command says about a symbol's fate beside what it served. With
203
- * nothing served, `fate.note`. With data, `quote`'s change line when the
219
+ * nothing served, `fate.note`. With data, `fate.delisted` for a listing that
220
+ * no longer trades ("TWTR: Delisted Oct 27, 2022"), `quote`'s change line when the
204
221
  * symbol's company left it (FB's bars are the fund listed there now), and, for
205
222
  * bars from before the symbol's holder took it (META's from 2021), that they
206
223
  * are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
@@ -211,11 +228,43 @@ export function listingFateNotes(fate: ListingFate, served: { served: boolean; f
211
228
  const first = served.firstDate?.slice(0, 10);
212
229
  const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
213
230
  return [
231
+ // What it served ends on the listing's last session, and says so.
232
+ fate.delisted ?? null,
214
233
  tickerChangeLine(change),
215
234
  earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
216
235
  ].filter((line): line is string => !!line);
217
236
  }
218
237
 
238
+ /**
239
+ * A report's notes and errors beside what became of its symbol, worded as
240
+ * `quote`, `history` and `options` word it: with nothing served, the fate is
241
+ * why ("TWTR: Delisted Oct 27, 2022", "FB now lists ...; Meta Platforms
242
+ * trades as META") and stands in for the errors the empty load raised ("Not
243
+ * a ticker", "No options provider"); with data, `listingFateNotes` beside
244
+ * the errors. A symbol whose company moved, with nothing listed under it now,
245
+ * has nothing of its own to serve and throws `MovedSymbolError`.
246
+ */
247
+ export function listingFateReport(
248
+ fate: ListingFate,
249
+ served: { served: boolean; firstDate?: string | null },
250
+ errors: readonly string[],
251
+ ): { notes: string[]; errors: string[] } {
252
+ if (fate.moved) throw new MovedSymbolError(fate.moved);
253
+ const notes = listingFateNotes(fate, served);
254
+ return { notes, errors: !served.served && notes.length > 0 ? [] : [...errors] };
255
+ }
256
+
257
+ /**
258
+ * A report that served nothing for its symbol (`listingFateReport`): what
259
+ * became of the symbol is the one reason it is empty, said once in place of
260
+ * every table's "No data." and of the load's errors; the errors alone when
261
+ * nothing became of it.
262
+ */
263
+ export function listingFateEmpty(fate: ListingFate, errors: readonly string[]): { empty?: string; errors: string[] } {
264
+ const report = listingFateReport(fate, { served: false }, errors);
265
+ return report.notes.length ? { empty: report.notes.join(" "), errors: report.errors } : { errors: report.errors };
266
+ }
267
+
219
268
  /**
220
269
  * What a report or loader with nothing of its own to serve for a symbol
221
270
  * throws when the symbol's company moved (`ListingFate.moved`): its message
@@ -38,6 +38,7 @@ import { readTerminalListingCode } from "../terminal-codes";
38
38
  import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
39
39
  import { settleWithin } from "../../utils/async-deadline";
40
40
  import { companyNameExtraWords } from "../company-name";
41
+ import { delistingLabel } from "../../market-data/quotes/delisting";
41
42
  export type {
42
43
  ResolvedTickerSearch,
43
44
  TickerSearchCandidate,
@@ -58,6 +59,17 @@ const MAX_NAMED_LISTINGS = 6;
58
59
  */
59
60
  export const LISTING_SEARCH_LIMIT = 30;
60
61
 
62
+ /**
63
+ * A listing's name as a choice between listings gives it: a listing that no
64
+ * longer trades says so and when ("First Republic Bank · Last traded Apr 28,
65
+ * 2023"), and drops a name that only repeats its symbol.
66
+ */
67
+ function listingChoiceName(result: InstrumentSearchResult): string {
68
+ if (!result.delisting) return result.name;
69
+ const name = result.name && normalizeTickerSymbol(result.name) !== normalizeTickerSymbol(result.symbol) ? result.name : "";
70
+ return [name, delistingLabel(result.delisting)].filter(Boolean).join(" · ");
71
+ }
72
+
61
73
  export class AmbiguousTickerError extends Error {
62
74
  constructor(readonly query: string, readonly listings: readonly string[], readonly listingNames: Readonly<Record<string, string>> = {}) {
63
75
  // Every listing is a choice: a list cut short could leave out the one meant.
@@ -467,7 +479,7 @@ async function resolveTickerSymbol(
467
479
  }
468
480
  const listings = new Set(matches.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
469
481
  const ambiguity = (query: string) => new AmbiguousTickerError(query, [...listings], Object.fromEntries(matches.map((item) => [
470
- publicTickerKey(item.symbol, listingExchange(item.result!)), item.result!.name,
482
+ publicTickerKey(item.symbol, listingExchange(item.result!)), listingChoiceName(item.result!),
471
483
  ])));
472
484
  if (pickerQuery && listings.size > 1) throw ambiguity(pickerQuery);
473
485
  // A code that names several venues (TSE) the symbol trades on more than one of is a choice.
@@ -485,7 +497,12 @@ async function resolveTickerSymbol(
485
497
  });
486
498
  const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
487
499
  const currency = resolveCurrencyUnit(quote.currency).currency;
488
- if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
500
+ const ended = quote.delisting;
501
+ if (ended) {
502
+ // The symbol's own quote is a US listing that no longer trades (a bare FRC, whose
503
+ // venue is not on record): it is the run search offers with the same last session.
504
+ verified = matches.filter((item) => item.result?.delisting?.lastTradeDate === ended.lastTradeDate);
505
+ } else if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
489
506
  && Number.isFinite(quote.lastUpdated) && quote.lastUpdated > 0) {
490
507
  const sameSecurity = matches.filter((item) => {
491
508
  const result = item.result!;
@@ -663,7 +680,11 @@ async function searchProviderResults(
663
680
 
664
681
  // A sector word ("defense", which the server answers with rows tagged with it) lists its small companies by
665
682
  // name; the server orders "defense ETF" by size, which a name search does not. Those funds lead the list.
666
- const theme = withThemeFunds && !assetClass ? themeQueryWord([...byKey.values()], query) : null;
683
+ // So do they when the funds are asked for by name: "defense etf" searches "defense", whose answer holds
684
+ // only the few funds tagged with it (not ITA), and "gold miners etf" a phrase no symbol is.
685
+ const fundsAsked = assetClass?.code === "ETF" || assetClass?.code === "FUND";
686
+ const theme = withThemeFunds && (!assetClass || fundsAsked)
687
+ && (themeQueryWord([...byKey.values()], query) || (fundsAsked && /\s/.test(query.trim())));
667
688
  if (theme) {
668
689
  try {
669
690
  const { onPartial: _ignored, ...context } = searchContext ?? {};
@@ -123,6 +123,41 @@ export function resolvePriceComparison(
123
123
  };
124
124
  }
125
125
 
126
+ /**
127
+ * The bars a compared leg pans and zooms over: inside the view only its shared
128
+ * dates, as the chart shows them, and every loaded bar past either edge of the
129
+ * view. Clipping the whole buffer to the view left a pan or a zoom out nothing
130
+ * to move into until the next fetch, and none at all toward newer bars.
131
+ */
132
+ export function comparedBufferPoints(
133
+ points: readonly TimeSeriesPoint[],
134
+ shared: { start: number | null; end: number | null },
135
+ view: { start: number | null; end: number | null },
136
+ ): TimeSeriesPoint[] {
137
+ const { start, end } = shared;
138
+ if (start === null || end === null) return [];
139
+ const before = view.start === null ? Number.NEGATIVE_INFINITY : Math.min(start, view.start);
140
+ const after = view.end === null ? Number.POSITIVE_INFINITY : Math.max(end, view.end);
141
+ return points.filter((point) => {
142
+ const time = point.date.getTime();
143
+ return time < before || time > after || (time >= start && time <= end);
144
+ });
145
+ }
146
+
147
+ /** A compared leg's pan buffer around `view`; see comparedBufferPoints. */
148
+ export function bufferPriceComparison(
149
+ series: ResolvedSeries,
150
+ comparison: PriceComparison | null,
151
+ view: { start: number | null; end: number | null },
152
+ ): ResolvedSeries {
153
+ if (!comparison?.seriesIds.includes(series.id)) return series;
154
+ return {
155
+ ...series,
156
+ latestChangePercent: undefined,
157
+ points: comparedBufferPoints(series.points, priceComparisonBoundsForSeries(series, comparison)!, view),
158
+ };
159
+ }
160
+
126
161
  /** Clip only compared legs; raw observations and study inputs stay available in the source cache. */
127
162
  export function clipPriceComparison(series: ResolvedSeries, comparison: PriceComparison | null): ResolvedSeries {
128
163
  if (!comparison?.seriesIds.includes(series.id)) return series;