gloomberb 0.16.4 → 0.16.5

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 (554) hide show
  1. package/README.md +14 -91
  2. package/package.json +4 -1
  3. package/src/{plugins/builtin/cloud → account}/auth-form.tsx +9 -9
  4. package/src/{plugins/builtin/cloud → account}/auth-model.ts +4 -4
  5. package/src/account/billing.ts +57 -0
  6. package/src/{plugins/builtin/cloud → account}/device-signin-dialog.tsx +18 -12
  7. package/src/{plugins/builtin/cloud → account}/device-signin.ts +2 -2
  8. package/src/{plugins/builtin/cloud → account}/email-auth-attempt.ts +2 -2
  9. package/src/account/session.ts +16 -0
  10. package/src/{plugins/builtin/cloud → account}/upgrade-dialog.tsx +16 -15
  11. package/src/{plugins/builtin/cloud → account}/upgrade-personalization.ts +7 -7
  12. package/src/{plugins/builtin/cloud → account}/upgrade-tickers.ts +6 -6
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/data.ts +24 -2
  15. package/src/api-client/earnings.ts +6 -0
  16. package/src/api-client/futures-curve.ts +56 -0
  17. package/src/api-client/index.ts +4 -1
  18. package/src/api-client/market-discovery.ts +10 -1
  19. package/src/api-client/paths.ts +7 -0
  20. package/src/api-client/research-activity.ts +15 -10
  21. package/src/api-client/types.ts +17 -1
  22. package/src/api-client/yield-curves.ts +8 -0
  23. package/src/app/global-shortcuts.ts +15 -1
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +7 -3
  25. package/src/architecture/core-builtin-allowlist.json +4 -13
  26. package/src/brokers/signed-in/sign-in-dialog.tsx +11 -5
  27. package/src/capabilities/chart-series.ts +11 -1
  28. package/src/cli/commands/crypto-hints.ts +7 -0
  29. package/src/cli/commands/fred-series.ts +61 -0
  30. package/src/cli/commands/market.ts +407 -104
  31. package/src/cli/commands/news-hints.ts +62 -0
  32. package/src/cli/commands/news-search.ts +53 -0
  33. package/src/cli/commands/options-leaps.ts +5 -2
  34. package/src/cli/commands/overview.ts +93 -20
  35. package/src/cli/commands/remote.ts +2 -1
  36. package/src/cli/commands/search.ts +82 -9
  37. package/src/cli/commands/system.ts +55 -8
  38. package/src/cli/commands/ticker.ts +88 -11
  39. package/src/cli/context.ts +45 -14
  40. package/src/cli/desktop-pane-shot.ts +59 -7
  41. package/src/cli/earnings-calendar.ts +71 -0
  42. package/src/cli/freshness.ts +18 -7
  43. package/src/cli/glossary.ts +429 -0
  44. package/src/cli/history-rows.ts +48 -7
  45. package/src/cli/index-roots.ts +7 -0
  46. package/src/cli/index.ts +21 -7
  47. package/src/cli/known-gaps.ts +27 -0
  48. package/src/cli/listing-arg.ts +417 -56
  49. package/src/cli/not-a-ticker.ts +12 -5
  50. package/src/cli/pane-functions/access-gate.ts +5 -0
  51. package/src/cli/pane-functions/capabilities.ts +3 -2
  52. package/src/cli/pane-functions/catalog.ts +35 -5
  53. package/src/cli/pane-functions/data.ts +12 -8
  54. package/src/cli/pane-functions/discovery.ts +1 -0
  55. package/src/cli/pane-functions/dom.ts +20 -6
  56. package/src/cli/pane-functions/freshness.ts +363 -43
  57. package/src/cli/pane-functions/function-help.ts +12 -5
  58. package/src/cli/pane-functions/headless.ts +45 -17
  59. package/src/cli/pane-functions/index.ts +63 -10
  60. package/src/cli/pane-functions/market-session.ts +22 -8
  61. package/src/cli/pane-functions/options.ts +63 -22
  62. package/src/cli/pane-functions/report-notices.ts +25 -7
  63. package/src/cli/pane-functions/report.ts +15 -8
  64. package/src/cli/pane-functions/resolver.ts +49 -12
  65. package/src/cli/pane-functions/screenshot.ts +110 -23
  66. package/src/cli/report-tables.ts +3 -0
  67. package/src/cli/result.ts +4 -1
  68. package/src/cli/staged-tickers.ts +52 -0
  69. package/src/cli/symbol-args.ts +9 -0
  70. package/src/cli/symbol-list.ts +172 -0
  71. package/src/cli/ticker-resolution.ts +223 -30
  72. package/src/cli/types.ts +5 -0
  73. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  74. package/src/components/chart/composite/composite-chart.tsx +40 -6
  75. package/src/components/chart/composite/legend.tsx +49 -2
  76. package/src/components/chart/composite/panel-surface.tsx +44 -17
  77. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  78. package/src/components/command-bar/panel/index.tsx +11 -3
  79. package/src/components/command-bar/panel/runtime.ts +3 -0
  80. package/src/components/command-bar/panel/types.ts +2 -0
  81. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  82. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  83. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  84. package/src/components/command-bar/workflow/tickers.ts +23 -4
  85. package/src/components/layout/detached-pane-shell.tsx +9 -0
  86. package/src/components/layout/floating-pane.tsx +3 -0
  87. package/src/components/layout/header.tsx +20 -5
  88. package/src/components/layout/pane/content.tsx +7 -13
  89. package/src/components/layout/pane/footer/index.tsx +6 -2
  90. package/src/components/layout/pane/footer/model.ts +35 -1
  91. package/src/components/layout/pane/header.tsx +24 -1
  92. package/src/components/layout/pane/index.tsx +12 -2
  93. package/src/components/layout/pane/sidebar.tsx +78 -10
  94. package/src/components/layout/shell/active-drag.ts +21 -5
  95. package/src/components/layout/shell/drag/index.ts +39 -9
  96. package/src/components/layout/shell/drag/live.ts +61 -51
  97. package/src/components/layout/shell/drag/runtime.ts +54 -12
  98. package/src/components/layout/shell/index.tsx +25 -7
  99. package/src/components/layout/shell/menu.ts +5 -0
  100. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  101. package/src/components/layout/shell/pane/layers.tsx +192 -45
  102. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  103. package/src/components/markdown-text.tsx +62 -20
  104. package/src/components/metric-treemap/heat-surface.tsx +10 -1
  105. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  106. package/src/components/onboarding/account-step/index.tsx +1 -1
  107. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  108. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  109. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  110. package/src/components/onboarding/onboarding-wizard.tsx +2 -2
  111. package/src/components/onboarding/use-onboarding-upgrade.ts +3 -3
  112. package/src/components/onboarding/wizard-account.ts +2 -2
  113. package/src/components/pane-template-wizard.tsx +9 -1
  114. package/src/components/price-selector-dialog.tsx +11 -2
  115. package/src/components/sign-in-gate.tsx +3 -3
  116. package/src/components/ticker/list-table-view.tsx +24 -1
  117. package/src/components/ui/action-row.tsx +2 -1
  118. package/src/components/ui/button.tsx +7 -1
  119. package/src/components/ui/choice-dialog.tsx +6 -4
  120. package/src/components/ui/confirm-dialog.tsx +28 -18
  121. package/src/components/ui/data-table/index.tsx +17 -6
  122. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  123. package/src/components/ui/data-table/types.ts +12 -0
  124. package/src/components/ui/dialog-actions.tsx +64 -0
  125. package/src/components/ui/display.tsx +17 -3
  126. package/src/components/ui/frame.tsx +35 -2
  127. package/src/components/ui/icon.tsx +2 -0
  128. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  129. package/src/data/cached-query.ts +2 -0
  130. package/src/data/config/store/normalize.ts +7 -2
  131. package/src/data/ticker-repository.ts +5 -0
  132. package/src/data/ticker-store.ts +15 -0
  133. package/src/docs/content.ts +215 -0
  134. package/src/docs/load.ts +139 -0
  135. package/src/docs/requests.ts +39 -0
  136. package/src/docs/snapshot.chunk.ts +8 -0
  137. package/src/docs/snapshot.json +7416 -0
  138. package/src/docs/topics.ts +216 -0
  139. package/src/i18n/es.ts +61 -4
  140. package/src/i18n/ja.ts +61 -4
  141. package/src/i18n/ko.ts +61 -4
  142. package/src/i18n/zh-cn.ts +61 -4
  143. package/src/i18n/zh-tw.ts +61 -4
  144. package/src/layout/desks.ts +1 -1
  145. package/src/layout/pane-manager/dock-tree.ts +18 -0
  146. package/src/layout/pane-manager/index.ts +1 -0
  147. package/src/market-data/coordinator/chart.ts +10 -4
  148. package/src/market-data/coordinator/index.ts +6 -3
  149. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  150. package/src/market-data/futures-contract.ts +151 -0
  151. package/src/market-data/hooks.tsx +10 -2
  152. package/src/market-data/market/format.ts +39 -21
  153. package/src/market-data/market/freshness.ts +81 -18
  154. package/src/market-data/market/status.ts +19 -23
  155. package/src/market-data/published-apac-sessions.ts +23 -1
  156. package/src/market-data/quotes/contributions.ts +3 -0
  157. package/src/market-data/quotes/metadata.ts +7 -24
  158. package/src/market-data/quotes/resolution.ts +2 -0
  159. package/src/market-data/quotes/screener-live-quotes.ts +5 -4
  160. package/src/market-data/result-types.ts +2 -0
  161. package/src/plugins/builtin/account-management/model.ts +1 -56
  162. package/src/plugins/builtin/account-management/pane.tsx +6 -4
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  164. package/src/plugins/builtin/alerts/cli.ts +313 -47
  165. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  166. package/src/plugins/builtin/alerts/events.ts +23 -2
  167. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  168. package/src/plugins/builtin/alerts/history.ts +2 -0
  169. package/src/plugins/builtin/alerts/index.tsx +41 -9
  170. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  171. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  172. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  173. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  174. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  175. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  176. package/src/plugins/builtin/alerts/targets.ts +92 -0
  177. package/src/plugins/builtin/analytics/index.tsx +4 -2
  178. package/src/plugins/builtin/analytics/risk-headless.ts +30 -12
  179. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  180. package/src/plugins/builtin/analytics/risk-model.ts +35 -5
  181. package/src/plugins/builtin/analytics/risk-pane.tsx +15 -8
  182. package/src/plugins/builtin/attention/index.tsx +2 -0
  183. package/src/plugins/builtin/attention/pane.tsx +3 -1
  184. package/src/plugins/builtin/attention/series.ts +26 -0
  185. package/src/plugins/builtin/awards/index.tsx +2 -0
  186. package/src/plugins/builtin/awards/pane.tsx +3 -1
  187. package/src/plugins/builtin/awards/series.ts +29 -0
  188. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  189. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  190. package/src/plugins/builtin/cds/client.ts +70 -13
  191. package/src/plugins/builtin/cds/headless.ts +116 -19
  192. package/src/plugins/builtin/cds/index.tsx +3 -1
  193. package/src/plugins/builtin/cds/model.ts +193 -31
  194. package/src/plugins/builtin/cds/pane.tsx +56 -14
  195. package/src/plugins/builtin/cds/series.ts +19 -0
  196. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  197. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  198. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  199. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  200. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  201. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  202. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  203. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  204. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  205. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  206. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  207. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  208. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  209. package/src/plugins/builtin/chat/controller/index.ts +2 -0
  210. package/src/plugins/builtin/cloud/askg/controller.ts +111 -19
  211. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  212. package/src/plugins/builtin/cloud/askg/manifest.ts +32 -7
  213. package/src/plugins/builtin/cloud/askg/model.ts +27 -8
  214. package/src/plugins/builtin/cloud/askg/protocol.ts +10 -1
  215. package/src/plugins/builtin/cloud/askg/tool-display.ts +3 -0
  216. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  217. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  218. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  219. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  220. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  221. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +3 -0
  222. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +77 -9
  223. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +6 -1
  224. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +9 -0
  225. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +35 -11
  226. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  227. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  228. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  229. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  230. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  231. package/src/plugins/builtin/company-attention/model.ts +26 -7
  232. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  233. package/src/plugins/builtin/company-attention/series.ts +52 -0
  234. package/src/plugins/builtin/company-kpis/pane.tsx +4 -2
  235. package/src/plugins/builtin/composite-plugins.ts +19 -13
  236. package/src/plugins/builtin/cot/index.tsx +3 -1
  237. package/src/plugins/builtin/cot/model.ts +40 -11
  238. package/src/plugins/builtin/cot/series.ts +27 -0
  239. package/src/plugins/builtin/credit-boards/index.tsx +2 -0
  240. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  241. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  242. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  243. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  244. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  245. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  246. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  247. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  248. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  249. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  250. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  251. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  252. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  253. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  254. package/src/plugins/builtin/dividend-yield/index.tsx +2 -0
  255. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  256. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  257. package/src/plugins/builtin/docs/index.tsx +68 -0
  258. package/src/plugins/builtin/docs/pane.tsx +426 -0
  259. package/src/plugins/builtin/docs/targets.ts +72 -0
  260. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  261. package/src/plugins/builtin/doe/index.tsx +2 -0
  262. package/src/plugins/builtin/doe/series.ts +62 -0
  263. package/src/plugins/builtin/earnings/client.ts +3 -2
  264. package/src/plugins/builtin/earnings/format.ts +1 -5
  265. package/src/plugins/builtin/earnings/headless.ts +58 -6
  266. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  267. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  268. package/src/plugins/builtin/earnings/history.tsx +7 -3
  269. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  270. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  271. package/src/plugins/builtin/econ/index.tsx +2 -0
  272. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  273. package/src/plugins/builtin/econ-statistics/headless.ts +19 -4
  274. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  276. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  277. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  278. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  279. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  280. package/src/plugins/builtin/futures/index.tsx +43 -3
  281. package/src/plugins/builtin/futures/model.ts +1 -1
  282. package/src/plugins/builtin/futures/returns.ts +1 -1
  283. package/src/plugins/builtin/futures/series.ts +13 -0
  284. package/src/plugins/builtin/futures/table.tsx +1 -1
  285. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  286. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  287. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  288. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  289. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  290. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  291. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  292. package/src/plugins/builtin/gpu/board.tsx +13 -10
  293. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  294. package/src/plugins/builtin/gpu/client.ts +19 -1
  295. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  296. package/src/plugins/builtin/gpu/headless.ts +56 -25
  297. package/src/plugins/builtin/gpu/history.tsx +3 -3
  298. package/src/plugins/builtin/gpu/index.tsx +2 -0
  299. package/src/plugins/builtin/gpu/model.ts +101 -16
  300. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  301. package/src/plugins/builtin/gpu/series.ts +38 -0
  302. package/src/plugins/builtin/help/function-card.tsx +25 -6
  303. package/src/plugins/builtin/help/index.tsx +17 -8
  304. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  305. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  306. package/src/plugins/builtin/holders/beneficial-view.tsx +55 -11
  307. package/src/plugins/builtin/holders/client.ts +22 -3
  308. package/src/plugins/builtin/holders/format.ts +18 -3
  309. package/src/plugins/builtin/holders/headless.ts +36 -133
  310. package/src/plugins/builtin/holders/pane.tsx +11 -4
  311. package/src/plugins/builtin/holders/report-header.ts +40 -5
  312. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  313. package/src/plugins/builtin/insider/client.ts +35 -20
  314. package/src/plugins/builtin/insider/headless.ts +41 -11
  315. package/src/plugins/builtin/insider/index.tsx +26 -9
  316. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  317. package/src/plugins/builtin/insider/issuer.ts +49 -0
  318. package/src/plugins/builtin/insider/model.ts +3 -0
  319. package/src/plugins/builtin/iv-history/client.ts +14 -4
  320. package/src/plugins/builtin/iv-history/headless.ts +7 -4
  321. package/src/plugins/builtin/iv-history/index.tsx +2 -0
  322. package/src/plugins/builtin/iv-history/model.ts +111 -7
  323. package/src/plugins/builtin/iv-history/screen-pane.tsx +5 -9
  324. package/src/plugins/builtin/iv-history/series.ts +29 -0
  325. package/src/plugins/builtin/jobs/headless.ts +28 -3
  326. package/src/plugins/builtin/jobs/index.tsx +3 -1
  327. package/src/plugins/builtin/jobs/series.ts +19 -0
  328. package/src/plugins/builtin/kelly-sizer/pane.tsx +2 -1
  329. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  330. package/src/plugins/builtin/macro-day/client.ts +44 -3
  331. package/src/plugins/builtin/macro-day/headless.ts +13 -10
  332. package/src/plugins/builtin/macro-day/model.ts +56 -10
  333. package/src/plugins/builtin/macro-day/pane.tsx +30 -20
  334. package/src/plugins/builtin/market-heatmap/model.ts +19 -11
  335. package/src/plugins/builtin/market-heatmap/portfolio.ts +2 -2
  336. package/src/plugins/builtin/market-movers/client.ts +7 -5
  337. package/src/plugins/builtin/market-movers/index.tsx +3 -2
  338. package/src/plugins/builtin/market-movers/model.ts +30 -0
  339. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  340. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  341. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  342. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  343. package/src/plugins/builtin/members/headless.ts +7 -2
  344. package/src/plugins/builtin/members/model.ts +31 -3
  345. package/src/plugins/builtin/members/pane.tsx +3 -6
  346. package/src/plugins/builtin/mna/headless.ts +15 -3
  347. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  348. package/src/plugins/builtin/money-markets/series.ts +41 -0
  349. package/src/plugins/builtin/notes/cli.ts +12 -7
  350. package/src/plugins/builtin/options/view.tsx +1 -1
  351. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  352. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  353. package/src/plugins/builtin/options-positioning/headless.ts +1 -0
  354. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  355. package/src/plugins/builtin/options-scenario/client.ts +2 -1
  356. package/src/plugins/builtin/options-scenario/headless.ts +1 -0
  357. package/src/plugins/builtin/options-scenario/hedge.ts +2 -1
  358. package/src/plugins/builtin/perps/client.ts +40 -6
  359. package/src/plugins/builtin/perps/equity-row.tsx +33 -11
  360. package/src/plugins/builtin/perps/freshness.ts +66 -0
  361. package/src/plugins/builtin/perps/headless.ts +45 -30
  362. package/src/plugins/builtin/perps/index.tsx +18 -1
  363. package/src/plugins/builtin/perps/model.ts +20 -5
  364. package/src/plugins/builtin/perps/pane.tsx +76 -20
  365. package/src/plugins/builtin/perps/series.ts +76 -0
  366. package/src/plugins/builtin/plugin-marketplace/pane.tsx +67 -61
  367. package/src/plugins/builtin/plugin-module.ts +3 -0
  368. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  369. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  370. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  371. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +331 -65
  372. package/src/plugins/builtin/portfolio-list/cli/render.ts +200 -31
  373. package/src/plugins/builtin/portfolio-list/cli/shared.ts +24 -0
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  375. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  376. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  377. package/src/plugins/builtin/portfolio-list/headless.ts +34 -4
  378. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  379. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  381. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  382. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  383. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  384. package/src/plugins/builtin/power/client.ts +16 -0
  385. package/src/plugins/builtin/power/evidence.ts +6 -2
  386. package/src/plugins/builtin/power/headless.ts +71 -7
  387. package/src/plugins/builtin/power/index.tsx +4 -1
  388. package/src/plugins/builtin/power/model.ts +72 -1
  389. package/src/plugins/builtin/power/pane.tsx +32 -9
  390. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  391. package/src/plugins/builtin/realized-vol/headless.ts +5 -0
  392. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -2
  393. package/src/plugins/builtin/research/event-model.ts +68 -24
  394. package/src/plugins/builtin/research/events-headless.ts +2 -1
  395. package/src/plugins/builtin/research/headless.ts +2 -1
  396. package/src/plugins/builtin/seasonality/client.ts +15 -7
  397. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  398. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  399. package/src/plugins/builtin/seasonality/model.ts +119 -0
  400. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  401. package/src/plugins/builtin/series-inventory.ts +91 -0
  402. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  403. package/src/plugins/builtin/shared/command-tab.ts +30 -12
  404. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  405. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  406. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  407. package/src/plugins/builtin/shared/open-description.ts +29 -0
  408. package/src/plugins/builtin/shared/report-freshness.ts +9 -0
  409. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  410. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  411. package/src/plugins/builtin/short-interest/index.tsx +2 -0
  412. package/src/plugins/builtin/short-interest/series.ts +36 -0
  413. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  414. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  415. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  416. package/src/plugins/builtin/short-volume/index.tsx +2 -0
  417. package/src/plugins/builtin/short-volume/series.ts +23 -0
  418. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  419. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  420. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  421. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  422. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  423. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  424. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  425. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  426. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  427. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  428. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  429. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  430. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  431. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  432. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  433. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  434. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  435. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  436. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  437. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  438. package/src/plugins/builtin/treasury-auctions/headless.ts +16 -2
  439. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  440. package/src/plugins/builtin/treasury-auctions/model.ts +8 -0
  441. package/src/plugins/builtin/vol-surface/headless.ts +1 -0
  442. package/src/plugins/builtin/volatility/index.tsx +2 -0
  443. package/src/plugins/builtin/volatility/series.ts +84 -0
  444. package/src/plugins/builtin/world-venue-map/geo-series.ts +26 -1
  445. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  446. package/src/plugins/builtin/world-venue-map/index.ts +2 -1
  447. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  448. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  449. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  450. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  451. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  452. package/src/plugins/builtin/yield-curve/index.tsx +3 -1
  453. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  454. package/src/plugins/catalog-browser.ts +2 -1
  455. package/src/plugins/registry/index.ts +89 -4
  456. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  457. package/src/portfolio/position-value.ts +40 -0
  458. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  459. package/src/public/time-series.ts +17 -0
  460. package/src/react/auto-refresh.ts +13 -3
  461. package/src/remote/app-host.tsx +20 -4
  462. package/src/remote/confirm-change.tsx +85 -0
  463. package/src/remote/controller-utils.ts +8 -1
  464. package/src/remote/controller.ts +123 -6
  465. package/src/remote/pane-screenshot.ts +47 -0
  466. package/src/remote/schema.ts +62 -0
  467. package/src/remote/types.ts +30 -0
  468. package/src/remote/watchlist.ts +306 -0
  469. package/src/renderers/browser/main.tsx +3 -2
  470. package/src/renderers/cloudflare/worker.ts +1 -1
  471. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  472. package/src/renderers/dom/data-table/index.tsx +33 -6
  473. package/src/renderers/dom/data-table/row.tsx +27 -1
  474. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  475. package/src/renderers/dom/desktop/controls.tsx +30 -23
  476. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  477. package/src/renderers/dom/desktop/icons.tsx +7 -0
  478. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  479. package/src/renderers/dom/host/box.tsx +3 -0
  480. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  481. package/src/renderers/dom/host/live-frame.ts +6 -2
  482. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  483. package/src/renderers/dom/host/size-observer.ts +26 -0
  484. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  485. package/src/renderers/dom/host/text.tsx +1 -1
  486. package/src/renderers/dom/styles.css +61 -0
  487. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +17 -2
  488. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +19 -11
  489. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  490. package/src/renderers/electrobun/view/main.tsx +2 -0
  491. package/src/series/capability.ts +75 -0
  492. package/src/series/csv.ts +160 -0
  493. package/src/series/files.ts +107 -0
  494. package/src/series/registry.ts +198 -0
  495. package/src/series/resolve.ts +107 -0
  496. package/src/series/standalone.ts +48 -0
  497. package/src/series/types.ts +145 -0
  498. package/src/series/validate.ts +171 -0
  499. package/src/sources/gloomberb-cloud/index.ts +7 -7
  500. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  501. package/src/sources/history-result.ts +1 -0
  502. package/src/sources/listing-symbols.ts +4 -1
  503. package/src/sources/provider-router/batches.ts +4 -1
  504. package/src/sources/provider-router/cache.ts +4 -2
  505. package/src/sources/provider-router/cached-routes.ts +1 -1
  506. package/src/sources/provider-router/financial-routes.ts +1 -0
  507. package/src/sources/provider-router/financials.ts +13 -0
  508. package/src/sources/provider-router/history.ts +43 -25
  509. package/src/sources/provider-router/search.ts +1 -0
  510. package/src/sync/core-contributors.ts +1 -0
  511. package/src/tickers/company-name.ts +75 -0
  512. package/src/tickers/instrument-kind.ts +25 -0
  513. package/src/tickers/open-target.ts +3 -2
  514. package/src/tickers/search/index.ts +45 -7
  515. package/src/tickers/search/ranking.ts +33 -17
  516. package/src/tickers/search/types.ts +7 -1
  517. package/src/tickers/us-listing.ts +34 -0
  518. package/src/time-series/hooks.ts +9 -0
  519. package/src/time-series/resolve.ts +95 -16
  520. package/src/time-series/types.ts +4 -2
  521. package/src/time-series/use-chart-resolution.ts +32 -0
  522. package/src/types/config.ts +6 -0
  523. package/src/types/data-provider.ts +5 -0
  524. package/src/types/financials.ts +52 -1
  525. package/src/types/headless.ts +61 -0
  526. package/src/types/plugin.ts +19 -0
  527. package/src/types/price-history.ts +7 -0
  528. package/src/types/ticker.ts +14 -0
  529. package/src/ui/host.tsx +18 -0
  530. package/src/ui/live-resize.ts +81 -0
  531. package/src/ui/live-stretch.ts +121 -0
  532. package/src/ui/measured-cells.ts +58 -0
  533. package/src/ui/pane-fill.ts +11 -0
  534. package/src/utils/basis-points.ts +17 -0
  535. package/src/utils/cli-output.ts +7 -1
  536. package/src/utils/concurrency.ts +25 -0
  537. package/src/utils/crypto-pair.ts +20 -0
  538. package/src/utils/currency-pair.ts +19 -0
  539. package/src/utils/decimal-comma.ts +19 -0
  540. package/src/utils/dom-screenshot-css.ts +265 -0
  541. package/src/utils/dom-screenshot-rasters.ts +276 -0
  542. package/src/utils/dom-screenshot.ts +437 -52
  543. package/src/utils/exchange-rate-snapshot.ts +6 -2
  544. package/src/utils/exchanges.ts +107 -1
  545. package/src/utils/format.ts +16 -3
  546. package/src/utils/fundamentals.ts +66 -0
  547. package/src/utils/futures-generic.ts +6 -12
  548. package/src/utils/fx-market-hours.ts +15 -5
  549. package/src/utils/price-history.ts +99 -42
  550. package/src/utils/sec-eps-basis.ts +24 -11
  551. package/src/utils/sec.ts +61 -7
  552. package/src/utils/utc-time.ts +51 -4
  553. package/src/plugins/builtin/fx-matrix/freshness.ts +0 -65
  554. /package/src/{plugins/builtin/cloud → tickers}/starter-symbols.ts +0 -0
@@ -2,19 +2,29 @@ import type { DataProvider } from "../types/data-provider";
2
2
  import type { Quote } from "../types/financials";
3
3
  import type { CliCommandContext } from "../types/plugin";
4
4
  import type { TickerRecord } from "../types/ticker";
5
- import { AmbiguousContractError, AmbiguousTickerError, listingChoiceQuery, resolveTickerSearch } from "../tickers/search";
5
+ import { AmbiguousContractError, AmbiguousTickerError, LISTING_SEARCH_LIMIT, listingChoiceQuery, resolveTickerSearch } from "../tickers/search";
6
+ import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
6
7
  import { cliStyles } from "../utils/cli-output";
8
+ import { groupByCompany, sameCompany, shortCompanyName } from "../tickers/company-name";
7
9
  import {
8
10
  canonicalExchange,
11
+ exchangeCodeVenues,
9
12
  exchangeLabel,
10
13
  isKnownExchangeCode,
14
+ isSharedExchangeCode,
11
15
  isUsListingExchange,
12
16
  parsePublicTickerKey,
13
17
  publicTickerKey,
14
18
  resolveExchangeTimeZone,
19
+ typedExchangeCode,
15
20
  } from "../utils/exchanges";
16
21
  import { withMarketData } from "./scoped-context";
17
22
  import { isNoProviderMessage } from "../sources/provider-errors";
23
+ import { dashedCoinPair } from "../utils/crypto-pair";
24
+ import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
25
+ import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
26
+ import { zonedDateKey } from "../utils/zoned-date-time";
27
+ import { bareFuturesCode, futuresContractByCode } from "../market-data/futures-board";
18
28
 
19
29
  /** The `--exchange` option of every command that takes a ticker. */
20
30
  export const EXCHANGE_OPTION = {
@@ -36,6 +46,14 @@ export interface ListingArg {
36
46
  exchange: string;
37
47
  /** SAN:EPA, or SAN when no exchange was named. */
38
48
  key: string;
49
+ /**
50
+ * The code as typed when it names several venues (TSE: Toronto or Tokyo),
51
+ * until `resolveCliListing` reads it as the one the symbol trades on.
52
+ * `exchange` is meanwhile the code's first venue.
53
+ */
54
+ sharedCode?: string;
55
+ /** What a shared code was read as, when not its first venue: "Read 7203:TSE as 7203:JPX: ...". */
56
+ readAs?: string;
39
57
  }
40
58
 
41
59
  /**
@@ -43,26 +61,43 @@ export interface ListingArg {
43
61
  * an exchange go through the app's alias table, so `SAN:XPAR` is `SAN:EPA`.
44
62
  * A symbol's own exchange and `--exchange` that disagree are an error, unless
45
63
  * the command takes several symbols and `--exchange` only fills in the ones
46
- * without an exchange of their own.
64
+ * without an exchange of their own. A code that names several venues (TSE)
65
+ * agrees with any of them. A futures board code no listed security uses is
66
+ * its future: TTF is TTF=F, while NG stays NovaGold.
47
67
  */
48
68
  export function parseListingArg(
49
69
  raw: string,
50
70
  exchangeOption?: string | null,
51
71
  { ownExchangeWins = false }: { ownExchangeWins?: boolean } = {},
52
72
  ): ListingArg {
53
- const option = canonicalExchange(exchangeOption ?? "");
73
+ const optionCode = (exchangeOption ?? "").trim().toUpperCase();
74
+ const option = canonicalExchange(optionCode);
54
75
  let typed = raw.trim().replace(/^\$/, "").toUpperCase();
55
76
  // `SAN:` asks which listing; with --exchange it has its answer.
56
77
  if (option && typed.endsWith(":")) typed = typed.slice(0, -1);
57
- const parsed = parsePublicTickerKey(typed);
58
- if (parsed.exchange && option && parsed.exchange !== option && !ownExchangeWins) {
59
- throw new ListingArgError(
60
- `${typed} names exchange ${parsed.exchange}, but --exchange names ${option}.`,
61
- `Name the exchange once, as ${parsed.symbol}:${parsed.exchange} or ${parsed.symbol} --exchange ${option}.`,
62
- );
78
+ const venueKey = parsePublicTickerKey(typed);
79
+ // BTC/USD, as crypto venues write a pair, is the coin's own BTC-USD.
80
+ const parsed = { ...venueKey, symbol: dashedCoinPair(venueKey.symbol) ?? venueKey.symbol };
81
+ const ownCode = parsed.exchange ? typedExchangeCode(typed) : "";
82
+ let exchange = parsed.exchange ?? option;
83
+ let code = ownCode || optionCode;
84
+ if (parsed.exchange && option) {
85
+ const named = exchangeCodeVenues(optionCode);
86
+ const agreed = exchangeCodeVenues(ownCode).filter((venue) => named.includes(venue));
87
+ if (agreed.length === 0 && !ownExchangeWins) {
88
+ throw new ListingArgError(
89
+ `${typed} names exchange ${parsed.exchange}, but --exchange names ${option}.`,
90
+ `Name the exchange once, as ${parsed.symbol}:${parsed.exchange} or ${parsed.symbol} --exchange ${option}.`,
91
+ );
92
+ }
93
+ // TSE and --exchange JPX agree on Tokyo.
94
+ if (agreed.length === 1) [exchange, code] = [agreed[0]!, agreed[0]!];
63
95
  }
64
- const exchange = parsed.exchange ?? option;
65
- return { symbol: parsed.symbol, exchange, key: exchange ? `${parsed.symbol}:${exchange}` : parsed.symbol };
96
+ const shared = isSharedExchangeCode(code) ? { sharedCode: code } : {};
97
+ if (exchange) return { symbol: parsed.symbol, exchange, key: `${parsed.symbol}:${exchange}`, ...shared };
98
+ // A futures board code no security uses is its future: TTF is TTF=F.
99
+ const symbol = bareFuturesCode(parsed.symbol)?.symbol ?? parsed.symbol;
100
+ return { symbol, exchange, key: symbol };
66
101
  }
67
102
 
68
103
  interface ListingStore {
@@ -83,7 +118,9 @@ export interface ListingVenue {
83
118
 
84
119
  /**
85
120
  * The venues a symbol trades on and the company on each, from the same
86
- * listing picker the command bar opens for `SAN:`.
121
+ * listing picker the command bar opens for `SAN:`. This is the one venue list
122
+ * every command names; whether a venue it leaves out trades the symbol is the
123
+ * listing's own quote's to say (`notTradedMessage`).
87
124
  */
88
125
  export async function listingVenues(symbol: string, { store, dataProvider }: ListingDeps): Promise<ListingVenue[]> {
89
126
  const query = `${symbol}:`;
@@ -91,7 +128,9 @@ export async function listingVenues(symbol: string, { store, dataProvider }: Lis
91
128
  const tickers = new Map((await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const));
92
129
  let venues: ListingVenue[];
93
130
  try {
94
- const resolved = await resolveTickerSearch({ query, activeTicker: null, tickers, dataProvider });
131
+ const resolved = await resolveTickerSearch({
132
+ query, activeTicker: null, tickers, dataProvider, searchContext: { limit: LISTING_SEARCH_LIMIT },
133
+ });
95
134
  const only = !resolved ? null
96
135
  : resolved.kind === "local" ? { exchange: resolved.ticker.metadata.exchange, name: resolved.ticker.metadata.name }
97
136
  : {
@@ -114,6 +153,60 @@ function tradesOn(symbol: string, venues: readonly ListingVenue[]): string {
114
153
  return venues.length > 0 ? ` ${symbol} trades on: ${venues.map((venue) => venue.exchange).join(", ")}.` : "";
115
154
  }
116
155
 
156
+ /** "Toronto Stock Exchange (TSX) or Tokyo Stock Exchange (JPX)". */
157
+ function venueChoices(venues: readonly string[]): string {
158
+ const labels = venues.map(exchangeLabel);
159
+ return labels.length > 1 ? `${labels.slice(0, -1).join(", ")} or ${labels.at(-1)}` : labels[0] ?? "";
160
+ }
161
+
162
+ /**
163
+ * A listing typed with a code that names several venues (7203:TSE), read as
164
+ * the one of them its symbol trades on: the venues the `7203:` picker lists,
165
+ * else the one its own quote prices (search can leave a venue out). The
166
+ * listing says so in `readAs` when that is not the code's first venue. A
167
+ * symbol on several of them asks which; one on none of them says where it
168
+ * trades. When neither search nor a quote knows the symbol, the code's first
169
+ * venue stands and the request's own failure speaks.
170
+ */
171
+ async function settleSharedCode(listing: ListingArg, deps: ListingDeps): Promise<ListingArg> {
172
+ const code = listing.sharedCode;
173
+ if (!code) return listing;
174
+ const meanings = exchangeCodeVenues(code);
175
+ const venues = await listingVenues(listing.symbol, deps).catch(() => []);
176
+ let traded = meanings.filter((venue) => venues.some((entry) => entry.exchange === venue));
177
+ if (traded.length === 0) {
178
+ const quoted = await Promise.all(meanings.map((exchange) => quotedOnVenue({ symbol: listing.symbol, exchange }, deps)));
179
+ traded = meanings.filter((_venue, index) => quoted[index]);
180
+ }
181
+ const { symbol } = listing;
182
+ if (traded.length > 1) {
183
+ throw new ListingArgError(
184
+ `${code} is ${venueChoices(meanings)}, and ${symbol} trades on ${traded.join(" and ")}.`,
185
+ `Name one: ${traded.map((venue) => `${symbol}:${venue}`).join(" or ")}.`,
186
+ );
187
+ }
188
+ if (traded.length === 0) {
189
+ if (venues.length === 0) return listing;
190
+ throw new ListingArgError(`${code} is ${venueChoices(meanings)}, and ${symbol} trades on none of them.${tradesOn(symbol, venues)}`);
191
+ }
192
+ const exchange = traded[0]!;
193
+ const key = `${symbol}:${exchange}`;
194
+ const readAs = exchange === meanings[0] ? {}
195
+ : { readAs: `Read ${symbol}:${code} as ${key}: ${code} is ${venueChoices(meanings)}, and ${symbol} trades on ${exchange}.` };
196
+ return { symbol, exchange, key, ...readAs };
197
+ }
198
+
199
+ /**
200
+ * The keys a listing can be saved under: SAN:EPA, or for a code that names
201
+ * several venues (7203:TSE) each of theirs, as local rows are looked up before
202
+ * the code is read.
203
+ */
204
+ export function listingKeys(listing: ListingArg): string[] {
205
+ return listing.sharedCode
206
+ ? exchangeCodeVenues(listing.sharedCode).map((exchange) => `${listing.symbol}:${exchange}`)
207
+ : [listing.key];
208
+ }
209
+
117
210
  /**
118
211
  * An exchange code the app does not know is accepted only when the symbol is
119
212
  * listed there, so a typo never falls back to another venue's line.
@@ -132,12 +225,31 @@ async function checkListingExchange(listing: ListingArg, deps: ListingDeps): Pro
132
225
  );
133
226
  }
134
227
 
228
+ /**
229
+ * Whether the listing's own quote prices it on a venue abroad, as `quote
230
+ * SYM:EXCH` reads it. Search can leave a venue out (CAR's ASX line); a quote
231
+ * there settles that it trades. The US exchanges share one symbol, so a quote
232
+ * named for any of them says only that the symbol has a US line: there,
233
+ * search's venues stand.
234
+ */
235
+ async function quotedOnVenue(listing: Pick<ListingArg, "symbol" | "exchange">, { dataProvider }: ListingDeps): Promise<boolean> {
236
+ if (isUsListingExchange(listing.exchange)) return false;
237
+ try {
238
+ const quote = await dataProvider.getQuote(`${listing.symbol}:${listing.exchange}`, listing.exchange);
239
+ return Number.isFinite(quote.price) && quote.price !== 0
240
+ && canonicalExchange(quote.listingExchangeName || quote.exchangeName) === listing.exchange;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+
135
246
  /**
136
247
  * "SAN does not trade on TSX. SAN trades on: NYSE, EPA." for a listing whose
137
248
  * exchange the symbol is not listed on, from the same picker `SAN:` opens.
138
- * Null when no exchange was named, when the symbol is listed there, or when its
139
- * venues cannot be found, so the request's own failure stands. It searches, so
140
- * ask only after a request came back empty.
249
+ * A venue the picker leaves out but the listing's quote prices (CAR:ASX) is
250
+ * traded, as `quote` says. Null when no exchange was named, when the symbol is
251
+ * listed there, or when its venues cannot be found, so the request's own
252
+ * failure stands. It searches, so ask only after a request came back empty.
141
253
  */
142
254
  export async function notTradedMessage(
143
255
  listing: Pick<ListingArg, "symbol" | "exchange">,
@@ -146,6 +258,7 @@ export async function notTradedMessage(
146
258
  if (!listing.exchange) return null;
147
259
  const venues = await listingVenues(listing.symbol, deps).catch(() => []);
148
260
  if (venues.length === 0 || venues.some((venue) => venue.exchange === listing.exchange)) return null;
261
+ if (await quotedOnVenue(listing, deps)) return null;
149
262
  return `${listing.symbol} does not trade on ${listing.exchange}.${tradesOn(listing.symbol, venues)}`;
150
263
  }
151
264
 
@@ -165,6 +278,11 @@ export interface ListingDataRequest {
165
278
  command: string;
166
279
  /** What the listing has none of: "history", "a quote". */
167
280
  noun: string;
281
+ /**
282
+ * The retry for one listing, when the key does not go right after the
283
+ * command (`gloomberb watchlist add Growth SAN:EPA`); null names the listings alone.
284
+ */
285
+ retry?: ((key: string) => string) | null;
168
286
  }
169
287
 
170
288
  /**
@@ -181,60 +299,254 @@ type BareListing = Pick<CliListing, "symbol" | "exchange"> & Partial<Pick<CliLis
181
299
 
182
300
  /** Where a bare symbol went and where else it trades, in the order they are tried and named. */
183
301
  interface BareListingVenues {
184
- /** Canonical code of the venue the symbol went to. */
302
+ /** Canonical code of the venue the symbol went to; "" when it went nowhere (`choose`). */
185
303
  exchange: string;
186
304
  /** That venue as search lists it; null for a saved venue search no longer lists. */
187
305
  resolved: ListingVenue | null;
188
- /** The symbol's other venues, in search order. */
306
+ /** The symbol's other venues, in search order; every venue when `choose`. */
189
307
  others: ListingVenue[];
308
+ /** Several companies list the symbol, so no venue is the one it means: the user picks. */
309
+ choose: boolean;
310
+ }
311
+
312
+ /** Whether the venues' companies differ by name. A venue with no name is no other company. */
313
+ function listsSeveralCompanies(venues: readonly ListingVenue[]): boolean {
314
+ return groupByCompany(venues, (venue) => venue.name).length > 1;
315
+ }
316
+
317
+ /** Every other company that lists the symbol once, at its first venue in search order. */
318
+ function otherCompanies(venues: readonly ListingVenue[], company: string): ListingVenue[] {
319
+ return groupByCompany(venues, (venue) => venue.name)
320
+ .filter((group) => !group.some((venue) => sameCompany(venue.name, company)))
321
+ .map((group) => group[0]!);
322
+ }
323
+
324
+ /**
325
+ * "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)": the listing a bare
326
+ * symbol several companies list was read on, and where the others list it.
327
+ * Null when one company lists it, so a symbol that means one thing says
328
+ * nothing.
329
+ */
330
+ export function listingChoiceNote(
331
+ symbol: string,
332
+ chosen: { exchange: string; name?: string | null },
333
+ venues: readonly ListingVenue[],
334
+ ): string | null {
335
+ if (!chosen.exchange || !listsSeveralCompanies(venues)) return null;
336
+ const chosenVenue = venues.find((venue) => venue.exchange === chosen.exchange);
337
+ const name = chosen.name?.trim() || chosenVenue?.name || "";
338
+ const others = otherCompanies(venues, chosenVenue?.name || name).map((venue) => venue.exchange)
339
+ .filter((exchange) => exchange !== chosen.exchange);
340
+ if (others.length === 0) return null;
341
+ const more = others.length - MAX_OTHER_LISTINGS;
342
+ const also = `${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloomberb search ${symbol}` : ""}`;
343
+ return `${symbol} = ${name ? `${shortCompanyName(name)}, ` : ""}${chosen.exchange} (also ${also})`;
344
+ }
345
+
346
+ // Quotes of these never name a company's listing, so they are never a choice between companies.
347
+ const UNLISTED_TYPES = new Set(["FUTURE", "INDEX", "CURRENCY", "CRYPTOCURRENCY", "OPTION"]);
348
+
349
+ /** Whether the data service chose the listing: a bare symbol, with no venue named and none saved. */
350
+ function isBareListing(listing: Pick<CliListing, "exchange" | "saved">): boolean {
351
+ return !listing.exchange && !listing.saved;
352
+ }
353
+
354
+ /**
355
+ * The venues of a bare listing, started beside its data request so naming the
356
+ * choice costs no wait; null for a listing whose venue was named or saved.
357
+ */
358
+ export function loadBareVenues(listing: Pick<CliListing, "symbol" | "exchange" | "saved">, deps: ListingDeps): Promise<ListingVenue[]> | null {
359
+ return isBareListing(listing) ? listingVenues(listing.symbol, deps).catch(() => []) : null;
360
+ }
361
+
362
+ /** What the resolver read a typed listing as (7203:TSE as 7203:JPX), for a command's warnings. */
363
+ export function listingReadNotes(listing: Pick<ListingArg, "readAs">): string[] {
364
+ return listing.readAs ? [listing.readAs] : [];
365
+ }
366
+
367
+ /**
368
+ * `listingChoiceNote` for a listing a command resolved, from the quote it was
369
+ * read on: only a bare symbol went where the data service chose. A named
370
+ * listing whose code names several venues says which it was read as
371
+ * (`listingReadNotes`). A list, so a command can spread it into its warnings.
372
+ */
373
+ export async function listingChoiceNotes(
374
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs">,
375
+ deps: ListingDeps,
376
+ quote: Pick<Quote, "name" | "listingExchangeName" | "exchangeName" | "instrumentType"> | null | undefined,
377
+ /** The venues `loadBareVenues` already started; looked up here when not given. */
378
+ venues?: Promise<ListingVenue[]> | null,
379
+ ): Promise<string[]> {
380
+ if (!isBareListing(listing)) return listingReadNotes(listing);
381
+ if (!quote || UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return [];
382
+ const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
383
+ if (!exchange) return [];
384
+ const listed = await (venues ?? loadBareVenues(listing, deps) ?? []);
385
+ const note = listingChoiceNote(listing.symbol, { exchange, name: quote.name }, listed);
386
+ return note ? [note] : [];
387
+ }
388
+
389
+ /**
390
+ * `listingChoiceNotes` for a command with no quote of its own to read the
391
+ * listing from (`fn`): it loads the listing's quote, and only for a bare
392
+ * symbol, so a named or saved listing costs nothing.
393
+ */
394
+ export async function quotedListingChoiceNotes(
395
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs">,
396
+ deps: ListingDeps,
397
+ ): Promise<string[]> {
398
+ const venues = loadBareVenues(listing, deps);
399
+ if (!venues) return listingReadNotes(listing);
400
+ return listingChoiceNotes(listing, deps, await loadListingQuote(deps.dataProvider, listing), venues);
190
401
  }
191
402
 
192
- /** A bare symbol goes to its saved listing, else to the one search lists first. Null when it named its exchange. */
403
+ /**
404
+ * The same company's line on a US exchange, under the same symbol: SAN:MCE's
405
+ * New York receipt. Null when the US line is another company (SAN:EPA is
406
+ * Sanofi, SAN in New York is Banco Santander) or there is none.
407
+ */
408
+ export async function usLineOfListing(
409
+ listing: Pick<ListingArg, "symbol" | "exchange">,
410
+ company: string | null | undefined,
411
+ deps: ListingDeps,
412
+ ): Promise<ListingVenue | null> {
413
+ if (!listing.exchange || isUsListingExchange(listing.exchange) || !company) return null;
414
+ const venues = await listingVenues(listing.symbol, deps).catch(() => []);
415
+ return venues.find((venue) => isUsListingExchange(venue.exchange) && sameCompany(venue.name, company)) ?? null;
416
+ }
417
+
418
+ /**
419
+ * The company's own listing on a US exchange: its line under the same symbol
420
+ * (SAN:MCE's New York receipt), else the one search finds by its name
421
+ * (Sanofi's SNY for SAN:EPA, Toyota's TM for 7203:JPX). Funds and contracts
422
+ * that carry its name are not its listing. Null for a listing in the US or
423
+ * with no venue, and when the company has no US line search knows.
424
+ */
425
+ async function usListingOfCompany(
426
+ listing: Pick<ListingArg, "symbol" | "exchange">,
427
+ company: string | null | undefined,
428
+ deps: ListingDeps,
429
+ ): Promise<UsListing | null> {
430
+ const sameSymbol = await usLineOfListing(listing, company, deps);
431
+ if (sameSymbol) return { ...sameSymbol, symbol: listing.symbol };
432
+ if (!listing.exchange || isUsListingExchange(listing.exchange)) return null;
433
+ return findUsListingOfCompany(deps.dataProvider, company);
434
+ }
435
+
436
+ /**
437
+ * What a function whose data covers US listings only (the US earnings board,
438
+ * US options and trades, FINRA short interest) says for a listing abroad,
439
+ * instead of a bare error or another company's numbers under the same
440
+ * symbol: "Time and sales covers US listings only, and SAN:EPA is Sanofi on
441
+ * Euronext Paris (EPA).", then the company's US listing with the command that
442
+ * asks for it ("Sanofi's US listing: SNY (try: gloomberb fn TAS SNY)"), or that
443
+ * none was found.
444
+ */
445
+ export async function usListingsOnlyMessage(
446
+ what: string,
447
+ listing: Pick<CliListing, "symbol" | "exchange" | "key" | "request" | "saved">,
448
+ deps: ListingDeps,
449
+ retry: (key: string) => string | null,
450
+ ): Promise<{ message: string; details: string }> {
451
+ const quote = await loadListingQuote(deps.dataProvider, listing);
452
+ const company = listingIdentity(listing, quote).name;
453
+ const named = company ? `${shortCompanyName(company)} on ${exchangeLabel(listing.exchange)}` : exchangeLabel(listing.exchange);
454
+ const us = await usListingOfCompany(listing, company, deps);
455
+ const owner = company ? shortCompanyName(company) : listing.key;
456
+ const command = us ? retry(us.symbol) : null;
457
+ return {
458
+ message: `${what} covers US listings only, and ${listing.key} is ${named}.`,
459
+ details: us
460
+ ? `${owner}'s US listing: ${us.symbol}${command ? ` (try: ${command})` : ""}`
461
+ : `No US listing of ${owner} was found.`,
462
+ };
463
+ }
464
+
465
+ /**
466
+ * A bare symbol goes to its saved listing, else to the one search lists first,
467
+ * unless several companies list it (2222 is Kotobuki Spirits in Tokyo and Saudi
468
+ * Aramco in Riyadh): then it asks, and none of the venues is the answer. Null
469
+ * when it named its exchange.
470
+ */
193
471
  async function bareListingVenues(listing: BareListing, deps: ListingDeps): Promise<BareListingVenues | null> {
194
472
  if (listing.exchange) return null;
195
473
  const venues = await listingVenues(listing.symbol, deps).catch(() => []);
196
474
  const savedExchange = canonicalExchange(listing.saved?.metadata.exchange);
475
+ if (!savedExchange && listsSeveralCompanies(venues)) return { exchange: "", resolved: null, others: venues, choose: true };
197
476
  const resolved = venues.find((venue) => venue.exchange === savedExchange) ?? (savedExchange ? null : venues[0] ?? null);
198
477
  const exchange = resolved?.exchange ?? savedExchange;
199
478
  if (!exchange) return null;
200
- return { exchange, resolved, others: venues.filter((venue) => venue !== resolved && venue.exchange !== savedExchange) };
479
+ return { exchange, resolved, others: venues.filter((venue) => venue !== resolved && venue.exchange !== savedExchange), choose: false };
201
480
  }
202
481
 
203
482
  interface OtherListingsMessage {
204
483
  message: string;
205
484
  details: string;
485
+ /** What `listings` are, as a sentence names them: "Other listings", or "Listings" when none is the one the symbol went to. */
486
+ heading: string;
206
487
  /** SAN:EPA, for each other listing. */
207
- others: string[];
488
+ listings: string[];
489
+ }
490
+
491
+ /**
492
+ * The command that asks again for another listing: `gloomberb history SAN:EPA
493
+ * --range 5Y` keeps the options. Null where the request names listings alone.
494
+ */
495
+ function retryCommand(request: ListingDataRequest, key: string): string | null {
496
+ if (request.retry !== undefined) return request.retry?.(key) ?? null;
497
+ const [command, ...options] = request.command.split(" ");
498
+ return ["gloomberb", command, key, ...options].join(" ");
499
+ }
500
+
501
+ /**
502
+ * "GC=F Gold future (try: gloomberb quote GC=F)", under a bare futures board
503
+ * code a security keeps (GC, NG) when that security has no data: the future is
504
+ * the other thing the code names. Null for any other symbol.
505
+ */
506
+ export function futuresTryLine(symbol: string, request: ListingDataRequest): string | null {
507
+ const future = futuresContractByCode(symbol);
508
+ if (!future) return null;
509
+ const retry = retryCommand(request, future.symbol);
510
+ return `${future.symbol} ${future.name} future${retry ? ` (try: ${retry})` : ""}`;
208
511
  }
209
512
 
210
513
  function describeOtherListings(
211
514
  listing: BareListing,
212
- { exchange, resolved, others }: BareListingVenues,
515
+ { exchange, resolved, others, choose }: BareListingVenues,
213
516
  request: ListingDataRequest,
214
517
  ): OtherListingsMessage | null {
215
- if (others.length === 0) return null;
518
+ const future = futuresTryLine(listing.symbol, request);
519
+ if (others.length === 0 && !future) return null;
216
520
  const name = resolved?.name || listing.saved?.metadata.name?.trim() || "";
217
- const [command, ...options] = request.command.split(" ");
218
- const retry = (key: string) => ["gloomberb", command, key, ...options].join(" ");
219
521
  const lines = others.slice(0, MAX_OTHER_LISTINGS).map((venue) => {
220
522
  const key = `${listing.symbol}:${venue.exchange}`;
221
- return `${key}${venue.name ? ` ${venue.name}` : ""} (try: ${retry(key)})`;
523
+ const retry = retryCommand(request, key);
524
+ return `${key}${venue.name ? ` ${venue.name}` : ""}${retry ? ` (try: ${retry})` : ""}`;
222
525
  });
223
526
  if (others.length > MAX_OTHER_LISTINGS) lines.push(`and ${others.length - MAX_OTHER_LISTINGS} more: gloomberb search ${listing.symbol}`);
527
+ if (future) lines.push(future);
528
+ const heading = choose ? "Listings" : "Other listings";
224
529
  return {
225
- message: `${listing.symbol} resolved to ${name ? `${name} (${exchange})` : exchange}, which has no ${request.noun}.`,
226
- details: lines.length === 1 ? `Other listings: ${lines[0]}` : ["Other listings:", ...lines.map((line) => ` ${line}`)].join("\n"),
227
- others: others.map((venue) => `${listing.symbol}:${venue.exchange}`),
530
+ // The symbol went to no venue, so none of them is said to lack the data.
531
+ message: choose
532
+ ? `Choose an exchange for ${listing.symbol}.`
533
+ : `${listing.symbol} resolved to ${name ? `${name} (${exchange})` : exchange}, which has no ${request.noun}.`,
534
+ details: lines.length === 1 ? `${heading}: ${lines[0]}` : [`${heading}:`, ...lines.map((line) => ` ${line}`)].join("\n"),
535
+ heading,
536
+ listings: [...others.map((venue) => `${listing.symbol}:${venue.exchange}`), ...future ? [futuresContractByCode(listing.symbol)!.symbol] : []],
228
537
  };
229
538
  }
230
539
 
231
540
  /**
232
541
  * For a bare symbol whose request found no source: the listing it went to and
233
542
  * the symbol's other listings, each with the command that asks for it, such
234
- * as "2222 resolved to Kotobuki Spirits Co., Ltd. (JPX), which has no
235
- * history." and "2222:TADAWUL Saudi Arabian Oil Co. (try: gloomberb history
236
- * 2222:TADAWUL)". Null when the symbol named its exchange or has no other
237
- * listing. It searches, so ask only after a request failed.
543
+ * as "SXR8 resolved to iShares Core S&P 500 UCITS ETF USD (Acc) (XETRA), which has
544
+ * no history." and "SXR8:BUD iShares Core S&P 500 UCITS ETF USD (Acc) (try:
545
+ * gloomberb history SXR8:BUD)". When several
546
+ * companies list the code, it went to none of them: "Choose an exchange for
547
+ * 2222." and every listing with its retry command, as `2222:` asks. Null when
548
+ * the symbol named its exchange or has no other listing. It searches, so ask
549
+ * only after a request failed.
238
550
  */
239
551
  async function otherListingsMessage(
240
552
  listing: BareListing,
@@ -282,15 +594,15 @@ export type BareListingQuote =
282
594
  /**
283
595
  * The quote for a bare symbol no source quoted. The data service reads a
284
596
  * symbol with no US listing on its home listing abroad (`bareListingNote`),
285
- * so this is left for one it reads on none, as a code several companies list
286
- * (2222 is Kotobuki Spirits in Tokyo and Saudi Aramco in Riyadh), and for a
287
- * home listing with no quote. Search knows where the symbol trades: each
288
- * listing is asked again by its own key (2222:JPX), as a named one is. The
289
- * listing the symbol resolved to goes first, then the others together, and
290
- * the first in search order with a quote is the answer. When none has one,
291
- * the `otherListingsMessage` failure. Null when the symbol named its exchange
292
- * (a named venue never switches) or search knows no listing of it. It
293
- * searches, so ask only after a request failed.
597
+ * so this is left for one it reads on none, and for a home listing with no
598
+ * quote. Search knows where the symbol trades: each listing is asked again by
599
+ * its own key (SXR8:XETRA), as a named one is. The listing the symbol resolved
600
+ * to goes first, then the others together, and the first in search order with a
601
+ * quote is the answer. When none has one, the `otherListingsMessage` failure.
602
+ * A code several companies list (2222) is never answered from one of them: it is
603
+ * that failure, asking which. Null when the symbol named its exchange (a named
604
+ * venue never switches) or search knows no listing of it. It searches, so ask
605
+ * only after a request failed.
294
606
  */
295
607
  export async function quoteBareListing(
296
608
  listing: CliListing,
@@ -300,6 +612,10 @@ export async function quoteBareListing(
300
612
  ): Promise<BareListingQuote | null> {
301
613
  const venues = await bareListingVenues(listing, deps);
302
614
  if (!venues) return null;
615
+ if (venues.choose) {
616
+ const other = describeOtherListings(listing, venues, request);
617
+ return other && { kind: "missing", ...other };
618
+ }
303
619
  const first = [namedListing(listing, venues.exchange)];
304
620
  const rest = venues.others.map((venue) => namedListing(listing, venue.exchange));
305
621
  let tried = first;
@@ -319,19 +635,49 @@ export async function quoteBareListing(
319
635
  const others = [...first, ...rest]
320
636
  .filter((entry, index) => entry !== answer && (index >= tried.length || quotes[index]))
321
637
  .map((entry) => entry.exchange);
638
+ return {
639
+ kind: "quoted",
640
+ listing: answer,
641
+ quote: quotes[found]!,
642
+ note: chosenListingNote(listing.symbol, answer.exchange, others, without),
643
+ };
644
+ }
645
+
646
+ /**
647
+ * "IEO -> BATS (other listings: IEX, BMV)": the venue a bare symbol went to,
648
+ * the venues it was not quoted on, and the ones it also trades on.
649
+ */
650
+ export function chosenListingNote(symbol: string, exchange: string, others: readonly string[], without: readonly string[] = []): string {
322
651
  const more = others.length - MAX_OTHER_LISTINGS;
323
652
  const aside = [
324
653
  without.length > 0 ? `no quote on ${without.join(", ")}` : "",
325
654
  others.length > 0
326
- ? `other listings: ${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloomberb search ${listing.symbol}` : ""}`
655
+ ? `other listings: ${others.slice(0, MAX_OTHER_LISTINGS).join(", ")}${more > 0 ? ` and ${more} more: gloomberb search ${symbol}` : ""}`
327
656
  : "",
328
657
  ].filter(Boolean).join("; ");
329
- return {
330
- kind: "quoted",
331
- listing: answer,
332
- quote: quotes[found]!,
333
- note: `${listing.symbol} -> ${answer.exchange}${aside ? ` (${aside})` : ""}`,
334
- };
658
+ return `${symbol} -> ${exchange}${aside ? ` (${aside})` : ""}`;
659
+ }
660
+
661
+ // A quote this much older than now, and stale, is a listing that stopped trading, not a quiet day.
662
+ const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
663
+
664
+ /**
665
+ * The exchange-local date a listing last traded, when its quote is stale and
666
+ * that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
667
+ */
668
+ function endedTradingDate(quote: Quote, now = Date.now()): string | null {
669
+ const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
670
+ if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
671
+ const zone = resolveExchangeTimeZone(quote.listingExchangeName || quote.exchangeName) ?? "UTC";
672
+ return zonedDateKey(last, zone);
673
+ }
674
+
675
+ /** "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote that stopped long ago. */
676
+ export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
677
+ const date = endedTradingDate(quote, now);
678
+ if (!date) return null;
679
+ const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
680
+ return `${label}: no longer trading since ${date} (last price ${price})`;
335
681
  }
336
682
 
337
683
  // The zones of the US exchanges; a venue in another is abroad.
@@ -393,6 +739,14 @@ type ListingForLoad = Pick<ListingArg, "symbol" | "exchange"> & Partial<Pick<Cli
393
739
  /** The saved ticker for a listing: under its own key, or a saved symbol whose venue it is. */
394
740
  export async function loadSavedListing(store: ListingStore, listing: ListingArg): Promise<TickerRecord | null> {
395
741
  if (!listing.exchange) return store.loadTicker(listing.symbol);
742
+ if (listing.sharedCode) {
743
+ // 7203:TSE is the row saved on any venue TSE names.
744
+ for (const exchange of exchangeCodeVenues(listing.sharedCode)) {
745
+ const saved = await loadSavedListing(store, { symbol: listing.symbol, exchange, key: `${listing.symbol}:${exchange}` });
746
+ if (saved) return saved;
747
+ }
748
+ return null;
749
+ }
396
750
  for (const key of new Set([listing.key, publicTickerKey(listing.symbol, listing.exchange), listing.symbol])) {
397
751
  const ticker = await store.loadTicker(key);
398
752
  if (ticker && (key === listing.key || canonicalExchange(ticker.metadata.exchange) === listing.exchange)) return ticker;
@@ -416,10 +770,14 @@ export async function resolveCliListing(
416
770
  deps: ListingDeps,
417
771
  options: { ownExchangeWins?: boolean } = {},
418
772
  ): Promise<CliListing> {
419
- const listing = parseListingArg(raw, exchangeOption, options);
420
- if (!listing.symbol) throw new ListingArgError("Ticker symbol is required.");
421
- await checkListingExchange(listing, deps);
773
+ const parsed = parseListingArg(raw, exchangeOption, options);
774
+ if (!parsed.symbol) throw new ListingArgError("Ticker symbol is required.");
775
+ await checkListingExchange(parsed, deps);
776
+ let listing = await settleSharedCode(parsed, deps);
422
777
  const saved = await loadSavedListing(deps.store, listing);
778
+ // A code nothing could read stays the venue it is saved on.
779
+ const savedExchange = canonicalExchange(saved?.metadata.exchange);
780
+ if (listing.sharedCode && savedExchange) listing = { symbol: listing.symbol, exchange: savedExchange, key: `${listing.symbol}:${savedExchange}` };
423
781
  return {
424
782
  ...listing,
425
783
  saved,
@@ -456,8 +814,11 @@ export async function requireListingArg(
456
814
  ): Promise<ListingArg> {
457
815
  try {
458
816
  const listing = parseListingArg(raw, exchangeOption);
459
- if (listing.symbol.endsWith(":") || (listing.exchange && !isKnownExchangeCode(listing.exchange))) {
460
- await withMarketData(ctx, (market) => checkListingExchange(listing, market));
817
+ if (listing.symbol.endsWith(":") || (listing.exchange && !isKnownExchangeCode(listing.exchange)) || listing.sharedCode) {
818
+ return await withMarketData(ctx, async (market) => {
819
+ await checkListingExchange(listing, market);
820
+ return settleSharedCode(listing, market);
821
+ });
461
822
  }
462
823
  return listing;
463
824
  } catch (error) {
@@ -475,7 +836,7 @@ export interface ListingIdentity {
475
836
  }
476
837
 
477
838
  export function listingIdentity(
478
- listing: CliListing,
839
+ listing: Pick<CliListing, "symbol" | "exchange" | "saved">,
479
840
  quote?: Pick<Quote, "name" | "listingExchangeName" | "exchangeName"> | null,
480
841
  ): ListingIdentity {
481
842
  const quoteExchange = canonicalExchange(quote?.listingExchangeName || quote?.exchangeName);
@@ -486,7 +847,7 @@ export function listingIdentity(
486
847
  }
487
848
 
488
849
  /** The listing's own quote, for its company name and venue; null when it has none. */
489
- export async function loadListingQuote(dataProvider: DataProvider, listing: CliListing): Promise<Quote | null> {
850
+ export async function loadListingQuote(dataProvider: DataProvider, listing: Pick<CliListing, "request">): Promise<Quote | null> {
490
851
  try {
491
852
  return await dataProvider.getQuote(listing.request.symbol, listing.request.exchange);
492
853
  } catch {