@gloom-sh/terminal 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -12,7 +12,7 @@ import {
12
12
  import { classifyInstrumentType } from "../tickers/instrument-kind";
13
13
  import { isRoundTheClockCoin } from "../utils/crypto-pair";
14
14
  import { currencyPairCode } from "../utils/currency-pair";
15
- import { currencyUnitLabel } from "../utils/currency-units";
15
+ import { currencyUnitLabel, resolveCurrencyUnit } from "../utils/currency-units";
16
16
  import { getPricePointTimestamp } from "../utils/price-history";
17
17
  import { latestTradeIndex, missingVolumeNotice, pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
18
18
  import { formatUtcTime } from "../utils/utc-time";
@@ -109,6 +109,42 @@ export function cleanFloat32Price(value: number): number {
109
109
  return value;
110
110
  }
111
111
 
112
+ /**
113
+ * The finest step a venue that quotes in a sub-unit prices in, in decimals of
114
+ * that unit: a ten-thousandth of a penny, the smallest tick London's tick
115
+ * tables use (for its lowest-priced shares).
116
+ */
117
+ const SUB_UNIT_TICK_DECIMALS = 4;
118
+ /** The decimals of that sub-unit a table of bars shows: a tenth of a penny, 638.5p as 6.385. */
119
+ const SUB_UNIT_SHOWN_DECIMALS = 1;
120
+
121
+ /**
122
+ * A bar's price as the decimal it is (`cleanFloat32Price`). A price served in
123
+ * a sub-unit and divided into its currency (`divisor` 100: a London line's
124
+ * pence, as pounds) carries the float32 noise of the pence it was quoted in
125
+ * (623.59998p is 6.2359998), so it is cleaned in pence, kept to the venue's
126
+ * finest tick there, then divided: 6.236.
127
+ */
128
+ export function cleanBarPrice(value: number, divisor = 1): number {
129
+ if (!(divisor > 1) || !Number.isFinite(divisor) || !Number.isFinite(value) || value === 0) return cleanFloat32Price(value);
130
+ const subUnit = cleanFloat32Price(value * divisor);
131
+ const places = Math.min(SUB_UNIT_TICK_DECIMALS, decimalPlaces(subUnit));
132
+ const unitPlaces = Math.round(Math.log10(divisor));
133
+ return Number((Number(subUnit.toFixed(places)) / divisor).toFixed(places + unitPlaces));
134
+ }
135
+
136
+ /**
137
+ * How many of the unit a quote's prices were served in make one of the
138
+ * currency it shows them in: 100 for a London line priced in pence and shown
139
+ * in pounds, 1 for any other. A quote in a sub-unit itself (`GBp`) shows the
140
+ * prices as served: they are cleaned as they are.
141
+ */
142
+ export function quotedPriceDivisor(quote: { currency?: string; providerPriceDivisor?: number } | null | undefined): number {
143
+ const divisor = quote?.providerPriceDivisor;
144
+ if (resolveCurrencyUnit(quote?.currency).divisor > 1) return 1;
145
+ return divisor != null && Number.isFinite(divisor) && divisor > 1 ? divisor : 1;
146
+ }
147
+
112
148
  function isUtcMidnight(time: number): boolean {
113
149
  return new Date(time).toISOString().endsWith("T00:00:00.000Z");
114
150
  }
@@ -229,6 +265,8 @@ export function historyRows(
229
265
  currency: string | null = null,
230
266
  unit: string | null = null,
231
267
  volume: HistoryVolume | null = null,
268
+ /** The listing's `quotedPriceDivisor`: prices served in pence are cleaned in pence. */
269
+ priceDivisor = 1,
232
270
  ): HistoryRow[] {
233
271
  const times = points.map(getPricePointTimestamp);
234
272
  const intraday = resolution ? isIntradayResolution(resolution) : looksIntraday(times);
@@ -240,7 +278,7 @@ export function historyRows(
240
278
  const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
241
279
  const values = pricePointValues(point);
242
280
  const flags = pricePointIntegrityFlags(point);
243
- const price = (value: number | null) => value == null ? null : cleanFloat32Price(value);
281
+ const price = (value: number | null) => value == null ? null : cleanBarPrice(value, priceDivisor);
244
282
  return {
245
283
  date: intraday ? iso.replace(".000Z", "Z") : iso.slice(0, 10),
246
284
  open: price(values.open),
@@ -440,8 +478,14 @@ function neededDecimals(value: number, cap: number): number {
440
478
  * row. A known asset kind caps it with the app's price display rules; without
441
479
  * one, the largest price sets a seven significant digit ceiling (BTC keeps
442
480
  * cents, FX keeps pips, sub-cent coins keep their digits). Cents at least above 1.
481
+ * Prices served in a sub-unit (`priceDivisor` 100, a London line's pence) may
482
+ * keep a tenth of it too: 6.385 for 638.5p. Finer ticks stay in the export.
443
483
  */
444
- export function historyPriceDecimals(rows: readonly Pick<HistoryRow, typeof PRICE_KEYS[number]>[], assetCategory?: string): number {
484
+ export function historyPriceDecimals(
485
+ rows: readonly Pick<HistoryRow, typeof PRICE_KEYS[number]>[],
486
+ assetCategory?: string,
487
+ priceDivisor = 1,
488
+ ): number {
445
489
  const values = rows.flatMap((row) => PRICE_KEYS.flatMap((key) => {
446
490
  const value = row[key];
447
491
  return value != null && Number.isFinite(value) && value !== 0 ? [Math.abs(value)] : [];
@@ -450,9 +494,12 @@ export function historyPriceDecimals(rows: readonly Pick<HistoryRow, typeof PRIC
450
494
  const largest = values.reduce((max, value) => Math.max(max, value), 0);
451
495
  const magnitude = Math.floor(Math.log10(largest));
452
496
  const significantCap = largest >= 1 ? Math.max(2, 6 - magnitude) : 6 - magnitude;
453
- const cap = resolveAssetDisplayKind({ assetCategory }) === "other"
497
+ // A line priced in pence is a share or a fund on its exchange, whatever kind its row was saved as.
498
+ const subUnit = priceDivisor > 1;
499
+ const kind = subUnit && resolveAssetDisplayKind({ assetCategory }) === "other" ? "EQUITY" : assetCategory;
500
+ const cap = resolveAssetDisplayKind({ assetCategory: kind }) === "other"
454
501
  ? significantCap
455
- : Math.min(significantCap, marketPriceFractionDigitCeiling(largest, { assetCategory }));
502
+ : Math.min(significantCap, marketPriceFractionDigitCeiling(largest, { assetCategory: kind }) + (subUnit ? SUB_UNIT_SHOWN_DECIMALS : 0));
456
503
  const needed = values.map((value) => neededDecimals(value, cap)).sort((left, right) => left - right);
457
504
  const decimals = needed[Math.floor(0.9 * (needed.length - 1))]!;
458
505
  return largest >= 1 ? Math.max(2, decimals) : decimals;
@@ -470,13 +517,14 @@ interface BarPrices {
470
517
  * The decimals one table of bars prints its prices with (`history`, HP's pane
471
518
  * and `fn HP`): historyPriceDecimals over the float32-cleaned prices, so a
472
519
  * feed's 4013.39990234 reads as the 4013.4 it is and the count follows the
473
- * asset's display rules (two for gold, three for silver, cents for a US equity).
520
+ * asset's display rules (two for gold, three for silver, cents for a US equity,
521
+ * and a London line's pence: 6.385 for 638.5p, with `priceDivisor` 100).
474
522
  */
475
- export function barPriceDecimals(bars: readonly BarPrices[], assetCategory?: string): number {
476
- const clean = (value: number | null | undefined) => value == null ? null : cleanFloat32Price(value);
523
+ export function barPriceDecimals(bars: readonly BarPrices[], assetCategory?: string, priceDivisor = 1): number {
524
+ const clean = (value: number | null | undefined) => value == null ? null : cleanBarPrice(value, priceDivisor);
477
525
  return historyPriceDecimals(bars.flatMap((bar) => bar.close == null ? [] : [{
478
526
  date: "", open: clean(bar.open), high: clean(bar.high), low: clean(bar.low), close: clean(bar.close)!, volume: null,
479
- }]), assetCategory);
527
+ }]), assetCategory, priceDivisor);
480
528
  }
481
529
 
482
530
  /** A price at the table's decimal count (barPriceDecimals), within `maxWidth` when the pane gives one. */
package/src/cli/index.ts CHANGED
@@ -179,6 +179,7 @@ export function createCoreCliCommands(
179
179
  { flags: "--<option> <value>", description: "A function setting; gloom catalog <function> lists them" },
180
180
  { flags: "--require-bot-safe", description: "Fail unless the function has a verified, complete report" },
181
181
  { flags: "--explain", description: "Follow the report with what its terms mean (text and JSON; CSV keeps the rows)" },
182
+ { flags: "--output <path>, --out <path>", description: "Write the report to this file, in the format asked for, and print where it went" },
182
183
  {
183
184
  ...TABLE_SECTION_OPTION,
184
185
  description: `${TABLE_SECTION_OPTION.description} (CALLS keeps --section for the transcript part)`,
@@ -190,6 +191,7 @@ export function createCoreCliCommands(
190
191
  "fn ANR SAN:EPA",
191
192
  "fn CBR --json",
192
193
  "fn WEI --csv --section europe > europe.csv",
194
+ "fn RV NEE,FSLR --csv --output rv.csv",
193
195
  "fn 13F AAPL --view=ticker-holdings",
194
196
  "fn OVME --spot 100 --strike 100 --days 30 --volatility 25",
195
197
  "fn HIVG SPY --event fomc --offset -6 --lookback ALL --csv",
@@ -15,12 +15,12 @@ import {
15
15
  type ListingVenue,
16
16
  } from "../tickers/listing-venues";
17
17
  import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
18
- import { findListingOfCompanyName, isCompanyNameInput, readTerminalCodeArg } from "../tickers/company-name-listing";
18
+ import { findListingOfCompanyName, isCompanyNameInput, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
19
19
  import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
20
20
  import { loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
21
21
  import { quotedListingName } from "../tickers/listing-name";
22
22
  import { cliStyles } from "../utils/cli-output";
23
- import { groupByCompany, sameCompany, shortCompanyName } from "../tickers/company-name";
23
+ import { groupByCompany, namesDepositaryReceipt, sameCompany, sameListedName, shortCompanyName } from "../tickers/company-name";
24
24
  import {
25
25
  canonicalExchange,
26
26
  exchangeCodeVenues,
@@ -35,7 +35,7 @@ import {
35
35
  import { withMarketData } from "./scoped-context";
36
36
  import { failWithFields } from "./errors";
37
37
  import { stripStrayEdges } from "./symbol-args";
38
- import { isNoProviderMessage, isProviderBusy } from "../sources/provider-errors";
38
+ import { isNoProviderMessage, isNoSourceAnswer, isProviderBusy } from "../sources/provider-errors";
39
39
  import { dashedCoinPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
40
40
  import { boardCoinFor, primeBoardCoins } from "./board-coins";
41
41
  import { knownGapMessage } from "./known-gaps";
@@ -49,10 +49,12 @@ import {
49
49
  bloombergMarketHint,
50
50
  endsInLegalForm,
51
51
  isSearchOnlyVenue,
52
+ readLondonDottedSymbol,
52
53
  readNearMissListing,
53
54
  readTerminalListingCode,
54
55
  terminalCodeReadAs,
55
56
  unknownBloombergMarket,
57
+ type NearMissListing,
56
58
  type TerminalListingCode,
57
59
  } from "../tickers/terminal-codes";
58
60
  import { listingAbroadVenue } from "../utils/sec";
@@ -338,6 +340,15 @@ export function isNoProviderError(error: unknown): boolean {
338
340
  return isNoProviderMessage(error instanceof Error ? error.message : String(error ?? ""));
339
341
  }
340
342
 
343
+ /**
344
+ * The router's answer that no source served a request because one failed (a
345
+ * timeout, a rate limit, a refused session) rather than missed: it says
346
+ * nothing about whether the symbol exists, unlike "Not a ticker".
347
+ */
348
+ export function isUnsettledNoProvider(error: unknown): boolean {
349
+ return isNoProviderError(error) && !isNoSourceAnswer(error);
350
+ }
351
+
341
352
  const MAX_OTHER_LISTINGS = 5;
342
353
 
343
354
  type BareListing = Pick<CliListing, "symbol" | "exchange"> & Partial<Pick<CliListing, "saved">>;
@@ -359,33 +370,42 @@ function listsSeveralCompanies(venues: readonly ListingVenue[]): boolean {
359
370
  return groupByCompany(venues, (venue) => venue.name).length > 1;
360
371
  }
361
372
 
362
- /** Every other company that lists the symbol once, at its first venue in search order. */
373
+ /**
374
+ * Every other company or fund that lists the symbol, once, at its first venue
375
+ * in search order. A venue is another's only when its name says so: one with
376
+ * no name, or a depositary receipt named unlike the company (a CEDEAR's short
377
+ * form of its fund's name), is left out rather than guessed at, and one
378
+ * fund's venues are one fund however each words it (`sameListedName`).
379
+ */
363
380
  function otherCompanies(venues: readonly ListingVenue[], company: string): ListingVenue[] {
364
- return groupByCompany(venues, (venue) => venue.name)
365
- .filter((group) => !group.some((venue) => sameCompany(venue.name, company)))
381
+ if (!company.trim()) return [];
382
+ const known = venues.filter((venue) => !namesDepositaryReceipt(venue.name) || sameListedName(venue.name, company));
383
+ return groupByCompany(known, (venue) => venue.name, sameListedName)
384
+ .filter((group) => !group.some((venue) => sameListedName(venue.name, company)))
366
385
  .map((group) => group[0]!);
367
386
  }
368
387
 
369
388
  /**
370
- * "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)": the listing a bare
371
- * symbol several companies list was read on, and where the others list it.
372
- * Null when one company lists it, so a symbol that means one thing says
373
- * nothing.
389
+ * "CAR = Avis Budget Group, NASDAQ (different companies on XETRA, ASX)": the
390
+ * listing a bare symbol several companies list was read on, and where the
391
+ * others list it. Null when no other company is known to list it, so a symbol
392
+ * that means one thing (XLK, and its receipt in Buenos Aires) says nothing.
374
393
  */
375
394
  export function listingChoiceNote(
376
395
  symbol: string,
377
396
  chosen: { exchange: string; name?: string | null },
378
397
  venues: readonly ListingVenue[],
379
398
  ): string | null {
380
- if (!chosen.exchange || !listsSeveralCompanies(venues)) return null;
399
+ if (!chosen.exchange) return null;
381
400
  const chosenVenue = venues.find((venue) => venue.exchange === chosen.exchange);
382
401
  const name = chosen.name?.trim() || chosenVenue?.name || "";
383
402
  const others = otherCompanies(venues, chosenVenue?.name || name).map((venue) => venue.exchange)
384
403
  .filter((exchange) => exchange !== chosen.exchange);
385
404
  if (others.length === 0) return null;
386
405
  const more = others.length - MAX_OTHER_LISTINGS;
387
- const also = `${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloom search ${symbol}` : ""}`;
388
- return `${symbol} = ${name ? `${shortCompanyName(name)}, ` : ""}${chosen.exchange} (also ${also})`;
406
+ const named = `${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloom search ${symbol}` : ""}`;
407
+ const which = others.length === 1 ? "a different company" : "different companies";
408
+ return `${symbol} = ${name ? `${shortCompanyName(name)}, ` : ""}${chosen.exchange} (${which} on ${named})`;
389
409
  }
390
410
 
391
411
  // Quotes of these never name a company's listing, so they are never a choice between companies.
@@ -409,34 +429,50 @@ export function listingReadNotes(listing: Pick<ListingArg, "readAs">): string[]
409
429
  return listing.readAs ? [listing.readAs] : [];
410
430
  }
411
431
 
432
+ /**
433
+ * The coin line for a bare coin ticker (SOL, ETH) a command read as a listing that is
434
+ * not the coin, from the quote it was read on (`notTheCoinNote`). A list, so a command can
435
+ * spread it into its warnings. A named listing (`ETH:ARCA`, `--exchange`) asked for that
436
+ * listing on purpose and gets none.
437
+ */
438
+ export function coinListingNotes(
439
+ listing: Pick<CliListing, "symbol" | "exchange">,
440
+ quote: Pick<Quote, "name" | "instrumentType" | "stale" | "delisting"> | null | undefined,
441
+ ): string[] {
442
+ const note = listing.exchange ? null : notTheCoinNote(listing.symbol, quote);
443
+ return note ? [note] : [];
444
+ }
445
+
412
446
  /**
413
447
  * `listingChoiceNote` for a listing a command resolved, from the quote it was
414
448
  * read on: only a bare symbol went where the data service chose. A named
415
449
  * listing whose code names several venues says which it was read as
416
- * (`listingReadNotes`). A list, so a command can spread it into its warnings.
450
+ * (`listingReadNotes`). A bare coin ticker read as another listing ends with
451
+ * the coin's pair (`coinListingNotes`). A list, so a command can spread it into
452
+ * its warnings.
417
453
  */
418
454
  export async function listingChoiceNotes(
419
455
  listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs">,
420
456
  deps: ListingDeps,
421
- quote: Pick<Quote, "name" | "listingExchangeName" | "exchangeName" | "instrumentType"> | null | undefined,
457
+ quote: Pick<Quote, "name" | "listingExchangeName" | "exchangeName" | "instrumentType" | "stale" | "delisting"> | null | undefined,
422
458
  /** The venues `loadBareVenues` already started; looked up here when not given. */
423
459
  venues?: Promise<ListingVenue[]> | null,
424
460
  ): Promise<string[]> {
425
- if (!isBareListing(listing)) return listingReadNotes(listing);
426
- if (!quote || UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return [];
461
+ const coin = coinListingNotes(listing, quote);
462
+ if (!isBareListing(listing)) return [...listingReadNotes(listing), ...coin];
463
+ if (!quote || UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return coin;
427
464
  const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
428
- if (!exchange) return [];
465
+ if (!exchange) return coin;
429
466
  const listed = await (venues ?? loadBareVenues(listing, deps) ?? []);
430
467
  const note = listingChoiceNote(listing.symbol, { exchange, name: quote.name }, listed);
431
- return note ? [note] : [];
468
+ return [...note ? [note] : [], ...coin];
432
469
  }
433
470
 
434
471
  /**
435
472
  * `listingChoiceNotes` for a command with no quote of its own to read the
436
473
  * listing from (`fn`): it loads the listing's quote, and only for a bare
437
474
  * symbol, so a named or saved listing costs nothing. A bare coin ticker read
438
- * as a fund (BTC, the Grayscale trust) ends with `quote`'s note naming the
439
- * coin's pair.
475
+ * as another listing (BTC, the Grayscale trust) ends with the coin's pair.
440
476
  */
441
477
  export async function quotedListingChoiceNotes(
442
478
  listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs">,
@@ -444,9 +480,7 @@ export async function quotedListingChoiceNotes(
444
480
  ): Promise<string[]> {
445
481
  const venues = loadBareVenues(listing, deps);
446
482
  if (!venues) return listingReadNotes(listing);
447
- const quote = await loadListingQuote(deps.dataProvider, listing);
448
- const coin = notTheCoinNote(listing.symbol, quote);
449
- return [...await listingChoiceNotes(listing, deps, quote, venues), ...coin ? [coin] : []];
483
+ return listingChoiceNotes(listing, deps, await loadListingQuote(deps.dataProvider, listing), venues);
450
484
  }
451
485
 
452
486
  /**
@@ -563,31 +597,43 @@ export function futuresTryLine(symbol: string, request: ListingDataRequest): str
563
597
  return `${future.symbol} ${future.name} future${retry ? ` (try: ${retry})` : ""}`;
564
598
  }
565
599
 
600
+ /** A venue's listing as its key names it: under its own symbol there (BP:LSE for `BP.`), else the one asked for. */
601
+ function venueKey(symbol: string, venue: Pick<ListingVenue, "exchange" | "symbol">): string {
602
+ return `${venue.symbol ?? symbol}:${venue.exchange}`;
603
+ }
604
+
566
605
  function describeOtherListings(
567
606
  listing: BareListing,
568
607
  { exchange, resolved, others, choose }: BareListingVenues,
569
608
  request: ListingDataRequest,
609
+ /** Listings just asked that had none: named as having none, never offered to try. */
610
+ without: readonly ListingVenue[] = [],
570
611
  ): OtherListingsMessage | null {
571
612
  const future = futuresTryLine(listing.symbol, request);
572
- if (others.length === 0 && !future) return null;
613
+ const offered = others.filter((venue) => !without.includes(venue));
614
+ if (offered.length === 0 && !future && without.length === 0) return null;
573
615
  const name = resolved?.name || listing.saved?.metadata.name?.trim() || "";
574
- const lines = others.slice(0, MAX_OTHER_LISTINGS).map((venue) => {
575
- const key = `${listing.symbol}:${venue.exchange}`;
616
+ const lines = offered.slice(0, MAX_OTHER_LISTINGS).map((venue) => {
617
+ const key = venueKey(listing.symbol, venue);
576
618
  const retry = retryCommand(request, key);
577
619
  return `${key}${venue.name ? ` ${venue.name}` : ""}${retry ? ` (try: ${retry})` : ""}`;
578
620
  });
579
- if (others.length > MAX_OTHER_LISTINGS) lines.push(`and ${others.length - MAX_OTHER_LISTINGS} more: gloom search ${listing.symbol}`);
621
+ if (offered.length > MAX_OTHER_LISTINGS) lines.push(`and ${offered.length - MAX_OTHER_LISTINGS} more: gloom search ${listing.symbol}`);
580
622
  if (future) lines.push(future);
581
- const heading = choose ? "Listings" : "Other listings";
623
+ // Every other listing was asked and had nothing: they are named as such, with nothing to try.
624
+ const heading = choose ? "Listings" : lines.length === 0 ? `No ${request.noun} on its other listings either` : "Other listings";
625
+ const named = lines.length === 0 ? without : offered;
626
+ const details = lines.length === 0 ? `${heading}: ${without.map((venue) => venueKey(listing.symbol, venue)).join(", ")}.`
627
+ : lines.length === 1 ? `${heading}: ${lines[0]}` : [`${heading}:`, ...lines.map((line) => ` ${line}`)].join("\n");
582
628
  return {
583
629
  // The symbol went to no venue, so none of them is said to lack the data.
584
630
  message: choose
585
631
  ? `Choose an exchange for ${listing.symbol}.`
586
632
  : `${listing.symbol} resolved to ${name ? `${name} (${exchange})` : exchange}, which has no ${request.noun}.`,
587
- details: lines.length === 1 ? `${heading}: ${lines[0]}` : [`${heading}:`, ...lines.map((line) => ` ${line}`)].join("\n"),
633
+ details,
588
634
  heading,
589
- listings: [...others.map((venue) => `${listing.symbol}:${venue.exchange}`), ...future ? [futuresContractByCode(listing.symbol)!.symbol] : []],
590
- choices: others.slice(0, MAX_OTHER_LISTINGS).map((venue) => ({ key: `${listing.symbol}:${venue.exchange}`, name: venue.name || null })),
635
+ listings: [...named.map((venue) => venueKey(listing.symbol, venue)), ...future ? [futuresContractByCode(listing.symbol)!.symbol] : []],
636
+ choices: offered.slice(0, MAX_OTHER_LISTINGS).map((venue) => ({ key: venueKey(listing.symbol, venue), name: venue.name || null })),
591
637
  choose,
592
638
  };
593
639
  }
@@ -612,30 +658,44 @@ async function otherListingsMessage(
612
658
  return venues && describeOtherListings(listing, venues, request);
613
659
  }
614
660
 
615
- /** The listing on one venue, as `SYM:EXCH` names it. */
616
- function namedListing(listing: CliListing, exchange: string): CliListing {
617
- const key = `${listing.symbol}:${exchange}`;
661
+ /** The listing on one venue, as `SYM:EXCH` names it, under the venue's own symbol when search lists one. */
662
+ function namedListing(listing: CliListing, venue: Pick<ListingVenue, "exchange" | "symbol">): CliListing {
663
+ const { exchange } = venue;
664
+ const symbol = venue.symbol ?? listing.symbol;
665
+ const key = `${symbol}:${exchange}`;
618
666
  const saved = canonicalExchange(listing.saved?.metadata.exchange) === exchange ? listing.saved : null;
619
- return { symbol: listing.symbol, exchange, key, saved, request: { symbol: key, exchange } };
667
+ return { symbol, exchange, key, saved, request: { symbol: key, exchange } };
620
668
  }
621
669
 
670
+ /**
671
+ * Each listing's quote, null where it has none; `undefined` where the request
672
+ * itself failed, which says nothing of whether it has one.
673
+ */
622
674
  async function quoteListings(
623
675
  dataProvider: DataProvider,
624
676
  listings: readonly CliListing[],
625
677
  refresh: boolean,
626
678
  datesChangeBasis = false,
627
- ): Promise<Array<Quote | null>> {
679
+ ): Promise<Array<Quote | null | undefined>> {
628
680
  if (listings.length === 0) return [];
629
681
  const targets = listings.map((listing) => listing.request);
630
682
  if (dataProvider.getQuotesBatch) {
631
683
  const results = await dataProvider.getQuotesBatch(targets, { forceRefresh: refresh, ...(datesChangeBasis ? { datesChangeBasis } : {}) })
632
- .catch(logFailure("cli.listing-arg", "Read listing quote batch failed", []));
633
- return targets.map((target, index) => (results.find((result) => result.target === target) ?? results[index])?.quote ?? null);
684
+ .catch(logFailure("cli.listing-arg", "Read listing quote batch failed", null));
685
+ if (!results) return targets.map(() => undefined);
686
+ return targets.map((target, index) => {
687
+ const result = results.find((entry) => entry.target === target) ?? results[index];
688
+ // No source quotes it, which is an answer; any other failure is none.
689
+ return result?.quote ?? (result && (result.error == null || isNoProviderError(result.error)) ? null : undefined);
690
+ });
634
691
  }
635
692
  return Promise.all(targets.map((target) => dataProvider.getQuote(target.symbol, target.exchange, {
636
693
  cacheMode: refresh ? "refresh" : "default",
637
694
  ...(datesChangeBasis ? { datesChangeBasis } : {}),
638
- }).catch(logFailure("cli.listing-arg", "Read listing quote failed", null))));
695
+ }).catch((error: unknown) => {
696
+ logFailure("cli.listing-arg", "Read listing quote failed")(error);
697
+ return isNoProviderError(error) ? null : undefined;
698
+ })));
639
699
  }
640
700
 
641
701
  export type BareListingQuote =
@@ -674,8 +734,8 @@ export async function quoteBareListing(
674
734
  const other = describeOtherListings(listing, venues, request);
675
735
  return other && { kind: "missing", ...other };
676
736
  }
677
- const first = [namedListing(listing, venues.exchange)];
678
- const rest = venues.others.map((venue) => namedListing(listing, venue.exchange));
737
+ const first = [namedListing(listing, venues.resolved ?? { exchange: venues.exchange })];
738
+ const rest = venues.others.map((venue) => namedListing(listing, venue));
679
739
  let tried = first;
680
740
  let quotes = await quoteListings(deps.dataProvider, first, refresh, datesChangeBasis);
681
741
  if (!quotes[0] && rest.length > 0) {
@@ -684,7 +744,9 @@ export async function quoteBareListing(
684
744
  }
685
745
  const found = quotes.findIndex(Boolean);
686
746
  if (found < 0) {
687
- const other = describeOtherListings(listing, venues, request);
747
+ // A listing just asked that had no quote is no listing to try.
748
+ const without = venues.others.filter((_venue, index) => quotes[index + 1] === null);
749
+ const other = describeOtherListings(listing, venues, request, without);
688
750
  return other && { kind: "missing", ...other };
689
751
  }
690
752
  const answer = tried[found]!;
@@ -697,7 +759,8 @@ export async function quoteBareListing(
697
759
  kind: "quoted",
698
760
  listing: answer,
699
761
  quote: quotes[found]!,
700
- note: chosenListingNote(listing.symbol, answer.exchange, others, without),
762
+ // Under the venue's own symbol (BP:LSE for `BP.`), the note names the listing, not only its venue.
763
+ note: chosenListingNote(listing.symbol, answer.symbol === listing.symbol ? answer.exchange : answer.key, others, without),
701
764
  };
702
765
  }
703
766
 
@@ -708,6 +771,8 @@ export interface ReadListing {
708
771
  exchange: string;
709
772
  /** The listing's company, from its quote. */
710
773
  name: string | null;
774
+ /** A bare coin ticker read as a listing that is not the coin (`coinListingNotes`); null otherwise. */
775
+ coinNote: string | null;
711
776
  }
712
777
 
713
778
  /**
@@ -728,10 +793,14 @@ export async function readBareListing(
728
793
  const listing = await resolveCliListing(symbol, null, deps);
729
794
  if (listing.exchange) return null;
730
795
  const answer = await loadListingQuoteAnswer(deps.dataProvider, listing);
731
- if (answer.quote) return { symbol: listing.symbol, exchange: listing.request.exchange, name: answer.quote.name?.trim() || null };
796
+ if (answer.quote) {
797
+ return { symbol: listing.symbol, exchange: listing.request.exchange, name: answer.quote.name?.trim() || null, coinNote: coinListingNotes(listing, answer.quote)[0] ?? null };
798
+ }
732
799
  if (!isNoProviderError(answer.error)) return null;
733
800
  const other = await quoteBareListing(listing, deps, request, { refresh });
734
- if (other?.kind === "quoted") return { symbol: listing.symbol, exchange: other.listing.exchange, name: other.quote.name?.trim() || null };
801
+ if (other?.kind === "quoted") {
802
+ return { symbol: listing.symbol, exchange: other.listing.exchange, name: other.quote.name?.trim() || null, coinNote: coinListingNotes(listing, other.quote)[0] ?? null };
803
+ }
735
804
  if (other?.choose) throw new ListingArgError(other.message, other.details, other.choices);
736
805
  return null;
737
806
  }
@@ -1096,7 +1165,9 @@ export async function resolveCliListing(
1096
1165
  const terminal = named ? null : read;
1097
1166
  // "AAPL GP" is written as a code, with a market code no venue answers to and no company's name: one entry, never AAPL and GP.
1098
1167
  if (!terminal && !named) await refuseUnknownMarketCode(typed, deps);
1099
- let parsed = named ?? parseListingArg(raw, exchangeOption, { ...options, terminalCode: terminal });
1168
+ // `BP.` is London's code for BP: its London line where London lists it, and `GE.` a US ticker with a stray dot.
1169
+ const dotted = terminal || named || exchangeOption?.trim() ? null : readLondonDottedSymbol(typed);
1170
+ let parsed = named ?? (dotted ? await londonDottedListing(dotted, deps) : parseListingArg(raw, exchangeOption, { ...options, terminalCode: terminal }));
1100
1171
  if (!parsed.symbol) throw new ListingArgError("Ticker symbol is required.");
1101
1172
  // $VIX is Schwab's code for ^VIX, unless a security trades as VIX: $AAPL is a cashtag.
1102
1173
  const index = named || parsed.exchange ? null : await readDollarIndexCode(raw, deps);
@@ -1140,6 +1211,21 @@ export async function resolveCliListing(
1140
1211
  };
1141
1212
  }
1142
1213
 
1214
+ /**
1215
+ * A two-letter symbol written with London's trailing dot (`BP.`, `RR.`) as its
1216
+ * London line, said as "Read BP. as BP:LSE.", where search lists it on LSE or
1217
+ * its quote there says it trades. Elsewhere the dot was a slip after the
1218
+ * ticker, dropped as a stray edge is (`GE.` is GE).
1219
+ */
1220
+ async function londonDottedListing(dotted: NearMissListing, deps: ListingDeps): Promise<ListingArg> {
1221
+ const london = { symbol: dotted.symbol, exchange: "LSE" };
1222
+ const venues = await listingVenues(dotted.symbol, deps).catch(logFailure("cli.listing-arg", "Read listing venues failed", []));
1223
+ const listed = venues.some((venue) => venue.exchange === london.exchange) || await quotedOnVenue(london, deps);
1224
+ if (!listed) return parseListingArg(dotted.symbol);
1225
+ const key = `${london.symbol}:${london.exchange}`;
1226
+ return { ...parseListingArg(key), readAs: `Read ${dotted.typed} as ${key}.` };
1227
+ }
1228
+
1143
1229
  /**
1144
1230
  * Fails a line written as a Bloomberg code whose market code no venue answers
1145
1231
  * to (`AAPL GP`), suggesting the code of the symbol's own listing (`AAPL
@@ -1235,17 +1321,23 @@ const ONE_WORD_NAME = /^\p{L}[\p{L}\p{N}&'’.-]*$/u;
1235
1321
  * for anything else, and when search names no company or is too busy to say.
1236
1322
  */
1237
1323
  export async function oneWordCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<ListingArg | null> {
1324
+ const found = await findOneWordCompany(typed, deps);
1325
+ return found ? companyListingArg(typed, found) : null;
1326
+ }
1327
+
1328
+ /** The company `oneWordCompanyName` reads a word as, before it is read as a listing. */
1329
+ async function findOneWordCompany(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<CompanyNameListing | null> {
1238
1330
  const word = typed.trim();
1239
1331
  if (!ONE_WORD_NAME.test(word) || knownGapMessage(word) || isMajorCoinName(word)) return null;
1240
- const named = await readCompanyName(word, deps, { oneWord: true }).catch((error: unknown) => {
1332
+ const found = await findCompanyName(word, deps, { oneWord: true }).catch((error: unknown) => {
1241
1333
  if (!isProviderBusy(error)) throw error;
1242
1334
  // Intentional ignore: a busy search says nothing about the name, so the word keeps its own "Not a ticker".
1243
1335
  return null;
1244
1336
  });
1245
- if (!named) return null;
1337
+ if (!found) return null;
1246
1338
  // The board is read only for a word that names a company, and once for the run (`primeBoardCoins`).
1247
1339
  if (/^[A-Za-z0-9]+$/.test(word)) await primeBoardCoins([`Not a ticker: ${word.toUpperCase()}.`]);
1248
- return boardCoinFor(word) ? null : named;
1340
+ return boardCoinFor(word) ? null : found;
1249
1341
  }
1250
1342
 
1251
1343
  function isMajorCoinName(word: string): boolean {
@@ -1253,9 +1345,16 @@ function isMajorCoinName(word: string): boolean {
1253
1345
  return [...MAJOR_COIN_NAMES.values()].some((name) => name.toLowerCase() === wanted);
1254
1346
  }
1255
1347
 
1348
+ function findCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<CompanyNameListing | null> {
1349
+ return findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, ...options });
1350
+ }
1351
+
1256
1352
  async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<ListingArg | null> {
1257
- const found = await findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, ...options });
1258
- if (!found) return null;
1353
+ const found = await findCompanyName(typed, deps, options);
1354
+ return found ? companyListingArg(typed, found) : null;
1355
+ }
1356
+
1357
+ function companyListingArg(typed: string, found: CompanyNameListing): ListingArg {
1259
1358
  const key = `${found.symbol}:${found.exchange}`;
1260
1359
  // A fund or an index carrying the name asks about the company's stock: "Did you mean JPM (JPMorgan Chase & Co.)?".
1261
1360
  const meant = found.meant ? { symbol: found.meant.symbol, name: found.meant.name } : null;
@@ -1263,6 +1362,35 @@ async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvi
1263
1362
  return { ...parseListingArg(key), readAs: `Read "${typed.trim()}" as ${key} (${found.name}).${question}`, ...meant ? { meant } : {} };
1264
1363
  }
1265
1364
 
1365
+ /**
1366
+ * The listing search names for a company's name typed where a ticker goes,
1367
+ * as a sentence offers it: `PLTR (Palantir Technologies Inc.)`, a listing
1368
+ * abroad with its venue (`SIE:XETRA (Siemens AG)`), a fund or an index that
1369
+ * carries the name as the company's stock it means. Null when the words name
1370
+ * no company, search is busy, or the listing found is the word itself: a
1371
+ * real ticker is never a company's name. Never throws.
1372
+ */
1373
+ export async function companyNameSuggestion(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<{ symbol: string; name: string } | null> {
1374
+ const words = typed.trim().replace(/^\$/, "");
1375
+ if (!words) return null;
1376
+ try {
1377
+ const found = isCompanyNameInput(words) ? await findCompanyName(words, deps).catch(busyAsMiss) : await findOneWordCompany(words, deps);
1378
+ const listing = found?.meant ?? found;
1379
+ if (!listing || listing.symbol.toUpperCase() === words.toUpperCase()) return null;
1380
+ return { symbol: isUsListingExchange(listing.exchange) ? listing.symbol : `${listing.symbol}:${listing.exchange}`, name: listing.name };
1381
+ } catch (error) {
1382
+ logFailure("cli.listing-arg", "Search a company name for a suggestion failed")(error);
1383
+ return null;
1384
+ }
1385
+ }
1386
+
1387
+ /** A search too busy to answer is no company, as a name that matches none. */
1388
+ function busyAsMiss(error: unknown): null {
1389
+ if (!isProviderBusy(error)) throw error;
1390
+ // Intentional ignore: a busy search says nothing about the name, so no company is suggested.
1391
+ return null;
1392
+ }
1393
+
1266
1394
  /** `resolveCliListing` for a command: a bad exchange fails the command with its message. */
1267
1395
  export async function requireCliListing(
1268
1396
  raw: string,