gloomberb 0.16.6 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -15,6 +15,7 @@ import { currencyUnitLabel } from "../utils/currency-units";
15
15
  import { getPricePointTimestamp } from "../utils/price-history";
16
16
  import { pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
17
17
  import { formatUtcTime } from "../utils/utc-time";
18
+ import { historyCoverageNotice } from "../sources/history-coverage";
18
19
 
19
20
  const PRICE_KEYS = ["open", "high", "low", "close"] as const;
20
21
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -24,6 +25,13 @@ const INTRADAY_RESOLUTIONS = ["1m", "5m", "15m", "30m", "45m", "1h"] as const;
24
25
  /** A first bar later than this after the range's start is a late start, not a weekend or holiday. */
25
26
  const LATE_START_MIN_MS = 10 * DAY_MS;
26
27
  const LATE_START_MIN_BARS = 3;
28
+ /** A bar with more than this many calendar periods (`barPeriod`) since the one before follows a gap in the series. */
29
+ const BREAK_GAP_PERIODS = 3;
30
+ /** A close more than this many times the close before it, or less than its hundredth, follows a jump. */
31
+ const BREAK_JUMP_RATIO = 100;
32
+ /** Flag codes for a break before a bar, whose prices stay as served; every other code blanks them. */
33
+ const BREAK_FLAGS: ReadonlySet<string> = new Set(["gap", "jump"]);
34
+ const FIRST_MONDAY_MS = Date.UTC(1970, 0, 5);
27
35
 
28
36
  const BAR_INTERVAL_LABELS: Record<ManualChartResolution, string> = {
29
37
  "1m": "1-minute",
@@ -50,7 +58,10 @@ export interface HistoryRow {
50
58
  unit: string | null;
51
59
  /** Bar size, in the codes of `resolution`: 5m, 1h, 1d, 1wk, 1mo. */
52
60
  interval: ManualChartResolution | null;
53
- /** Each way the bar's prices contradict each other, such as `high<open`; null when they agree. */
61
+ /**
62
+ * Each way the bar's prices contradict each other, such as `high<open`, and
63
+ * `gap` or `jump` for a break in the series just before it; null when none.
64
+ */
54
65
  flag: string | null;
55
66
  /** The bar's period had not ended when it was served: the close is the latest price. */
56
67
  partial?: true;
@@ -122,10 +133,59 @@ function historyInterval(times: readonly number[], resolution: ManualChartResolu
122
133
  return days >= 25 ? "1mo" : days >= 6 ? "1wk" : "1d";
123
134
  }
124
135
 
136
+ /**
137
+ * The calendar period a bar falls in, numbered so that consecutive periods
138
+ * differ by one: its month for monthly bars, its week (Monday to Sunday, UTC)
139
+ * for weekly and anything finer. Daily and intraday bars are counted in weeks
140
+ * because a market's own closures leave holes that are no break in the
141
+ * series: nights, weekends, holidays (Lunar New Year and Golden Week close
142
+ * Chinese markets for up to nine days, Eid the Gulf markets about as long),
143
+ * and a thin listing that goes a week or two without a trade. A daily series
144
+ * then breaks exactly where its weekly bars would.
145
+ */
146
+ function barPeriod(time: number, interval: ManualChartResolution): number {
147
+ if (interval !== "1mo") return Math.floor((time - FIRST_MONDAY_MS) / (7 * DAY_MS));
148
+ const date = new Date(time);
149
+ return date.getUTCFullYear() * 12 + date.getUTCMonth();
150
+ }
151
+
152
+ /**
153
+ * The breaks before each bar: `gap` after more than three calendar periods
154
+ * with no bar (`barPeriod`), `jump` when its close is over 100 times the last
155
+ * close before it, or under a hundredth of it. Closes that are blank, zero or
156
+ * negative are skipped.
157
+ */
158
+ function historyBreakFlags(
159
+ times: readonly number[],
160
+ closes: readonly (number | null)[],
161
+ interval: ManualChartResolution | null,
162
+ ): string[][] {
163
+ let previousTime: number | null = null;
164
+ let previousClose: number | null = null;
165
+ return times.map((time, index) => {
166
+ const flags: string[] = [];
167
+ if (interval && Number.isFinite(time)) {
168
+ if (previousTime != null && barPeriod(time, interval) - barPeriod(previousTime, interval) > BREAK_GAP_PERIODS) flags.push("gap");
169
+ previousTime = time;
170
+ }
171
+ const close = closes[index];
172
+ if (close != null && close > 0) {
173
+ if (previousClose != null && Math.max(close / previousClose, previousClose / close) > BREAK_JUMP_RATIO) flags.push("jump");
174
+ previousClose = close;
175
+ }
176
+ return flags;
177
+ });
178
+ }
179
+
180
+ function flagCodes(row: Pick<HistoryRow, "flag">): string[] {
181
+ return row.flag ? row.flag.split(",") : [];
182
+ }
183
+
125
184
  /**
126
185
  * Intraday bars keep their UTC time; daily and longer bars are trading dates.
127
186
  * A bar whose prices contradict each other is blanked exactly as HP blanks it,
128
- * and says why in `flag`.
187
+ * and says why in `flag`. A bar after a gap or a jump in the series keeps its
188
+ * prices and is flagged `gap` or `jump`.
129
189
  */
130
190
  export function historyRows(
131
191
  points: readonly PricePoint[],
@@ -137,7 +197,7 @@ export function historyRows(
137
197
  const times = points.map(getPricePointTimestamp);
138
198
  const intraday = resolution ? isIntradayResolution(resolution) : looksIntraday(times);
139
199
  const interval = historyInterval(times, resolution);
140
- return points.map((point, index) => {
200
+ const rows = points.map((point, index) => {
141
201
  const time = times[index]!;
142
202
  const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
143
203
  const values = pricePointValues(point);
@@ -163,6 +223,11 @@ export function historyRows(
163
223
  : {}),
164
224
  };
165
225
  });
226
+ const breaks = historyBreakFlags(times, rows.map((row) => row.close), interval);
227
+ return rows.map((row, index) => {
228
+ const flags = [...flagCodes(row), ...breaks[index]!];
229
+ return flags.length > 0 ? { ...row, flag: flags.join(",") } : row;
230
+ });
166
231
  }
167
232
 
168
233
  /**
@@ -292,16 +357,28 @@ function historyDate(value: string): string {
292
357
  return value.length > 10 ? formatUtcTime(value) : value;
293
358
  }
294
359
 
295
- /** The facts line under a history heading: "Currency USD", "daily bars", "2025-10-09 to 2026-10-08", "251 bars". */
296
- export function historyFacts(unit: HistoryUnit, rows: readonly Pick<HistoryRow, "date" | "interval">[]): string[] {
360
+ /** The first bar after the series' last gap or jump, which reads on without a break; null when it has none. */
361
+ export function historyUnbrokenFrom(rows: readonly Pick<HistoryRow, "date" | "flag">[]): string | null {
362
+ const last = rows.findLastIndex((row) => flagCodes(row).some((code) => BREAK_FLAGS.has(code)));
363
+ return last >= 0 ? rows[last]!.date : null;
364
+ }
365
+
366
+ /**
367
+ * The facts line under a history heading: "Currency USD", "daily bars",
368
+ * "2025-10-09 to 2026-10-08", "251 bars", and "unbroken from 1992-12-01"
369
+ * after a gap or jump.
370
+ */
371
+ export function historyFacts(unit: HistoryUnit, rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[]): string[] {
297
372
  const first = rows[0]?.date;
298
373
  const last = rows.at(-1)?.date;
299
374
  const interval = rows[0]?.interval;
375
+ const unbroken = historyUnbrokenFrom(rows);
300
376
  return [
301
377
  unit.kind === "pair" ? unit.unit! : unit.kind === "points" ? "Index points" : `Currency ${unit.unit ?? "unknown"}`,
302
378
  interval ? `${BAR_INTERVAL_LABELS[interval]} bars` : "",
303
379
  first && last ? (first === last ? historyDate(first) : `${historyDate(first)} to ${historyDate(last)}`) : "",
304
380
  `${rows.length} ${rows.length === 1 ? "bar" : "bars"}`,
381
+ unbroken ? `unbroken from ${historyDate(unbroken)}` : "",
305
382
  ].filter(Boolean);
306
383
  }
307
384
 
@@ -333,22 +410,102 @@ export function historyNotes(
333
410
  return notes;
334
411
  }
335
412
 
413
+ /**
414
+ * "Price history starts 1992-12-01; old-shekel quotes before 1992 are not
415
+ * served.": the source serves nothing before a date the range reaches back
416
+ * past, and why when it says. Null for a range that starts after it.
417
+ */
418
+ export function historyCoverageWarning(
419
+ range: TimeRange,
420
+ coverage: { coverageStart?: string; coverageNote?: string },
421
+ now = Date.now(),
422
+ ): string | null {
423
+ return historyCoverageNotice(coverage.coverageStart, subtractTimeRange(new Date(now), range).getTime(), coverage.coverageNote);
424
+ }
425
+
336
426
  /** Gaps listed by date before the rest are only counted. */
337
427
  const LISTED_GAPS = 3;
338
428
 
429
+ function listItems(items: readonly string[]): string {
430
+ return items.length > 1 ? `${items.slice(0, -1).join(", ")} and ${items.at(-1)}` : items[0] ?? "";
431
+ }
432
+
433
+ function utcDay(time: number): string {
434
+ return new Date(time).toISOString().slice(0, 10);
435
+ }
436
+
437
+ /** The bars flagged `code`, each with the bar before it. */
438
+ function breaksFlagged<Row extends Pick<HistoryRow, "flag">>(rows: readonly Row[], code: string): Array<[Row, Row]> {
439
+ return rows.flatMap((row, index) => index > 0 && flagCodes(row).includes(code) ? [[rows[index - 1]!, row] as [Row, Row]] : []);
440
+ }
441
+
442
+ /**
443
+ * "No bars for 1980-08 to 1992-11 (148 months)": the periods with no bar
444
+ * before each bar flagged `gap`, the latest few by date and the rest counted.
445
+ */
446
+ function historyBreakGapNote(rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[]): string | null {
447
+ const interval = rows[0]?.interval;
448
+ const gaps = interval ? breaksFlagged(rows, "gap") : [];
449
+ if (!interval || gaps.length === 0) return null;
450
+ const monthly = interval === "1mo";
451
+ const spans = gaps.map(([before, after]) => {
452
+ const [from, to] = [Date.parse(before.date), Date.parse(after.date)];
453
+ const periods = barPeriod(to, interval) - barPeriod(from, interval) - 1;
454
+ const length = `${periods} ${monthly ? "month" : "week"}${periods === 1 ? "" : "s"}`;
455
+ if (monthly) {
456
+ const month = (time: number, step: number) => {
457
+ const date = new Date(time);
458
+ return utcDay(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1)).slice(0, 7);
459
+ };
460
+ return `${month(from, 1)} to ${month(to, -1)} (${length})`;
461
+ }
462
+ return `${utcDay(from + (interval === "1wk" ? 7 : 1) * DAY_MS)} to ${utcDay(to - DAY_MS)} (${length})`;
463
+ });
464
+ const shown = spans.slice(-LISTED_GAPS);
465
+ const earlier = spans.length - shown.length;
466
+ return `No bars for ${listItems([...earlier > 0 ? [`${earlier} earlier ${earlier === 1 ? "gap" : "gaps"}`] : [], ...shown])}`;
467
+ }
468
+
469
+ const closeFormat = new Intl.NumberFormat("en-US", { maximumSignificantDigits: 7, useGrouping: false });
470
+
471
+ /**
472
+ * "Close rises 564x from 0.0049 on 1980-07-01 to 2.763 on 1992-12-01": each
473
+ * bar flagged `jump`, against the last close before it. Neutral on the cause,
474
+ * which can be a change of unit, a different series or a real collapse.
475
+ */
476
+ export function historyJumpNote(rows: readonly Pick<HistoryRow, "date" | "close" | "flag">[]): string | null {
477
+ const jumps: string[] = [];
478
+ let previous: { date: string; close: number } | null = null;
479
+ for (const row of rows) {
480
+ if (row.close == null || !(row.close > 0)) continue;
481
+ if (previous && flagCodes(row).includes("jump")) {
482
+ const rises = row.close > previous.close;
483
+ const ratio = Math.round(rises ? row.close / previous.close : previous.close / row.close).toLocaleString("en-US");
484
+ jumps.push(`${rises ? "rises" : "falls"} ${ratio}x from ${closeFormat.format(previous.close)} on ${historyDate(previous.date)}`
485
+ + ` to ${closeFormat.format(row.close)} on ${historyDate(row.date)}`);
486
+ }
487
+ previous = { date: row.date, close: row.close };
488
+ }
489
+ if (jumps.length === 0) return null;
490
+ const shown = jumps.slice(-LISTED_GAPS);
491
+ const earlier = jumps.length - shown.length;
492
+ return `Close ${earlier > 0 ? `jumps over 100x at ${earlier} earlier ${earlier === 1 ? "bar" : "bars"}, then ` : ""}${listItems(shown)}`;
493
+ }
494
+
339
495
  /**
340
496
  * "No bar for 2026-10-10": the days a coin, which trades every day, has no
341
497
  * daily bar for between its first and last bar. Days before the first bar are
342
498
  * before the listing. A market that closes skips weekends and holidays, so its
343
- * gaps say nothing and get no note.
499
+ * gaps say nothing on their own: for it, and for a coin's other bar sizes, the
500
+ * note names the gaps flagged `gap`, "No bars for 1980-08 to 1992-11 (148 months)".
344
501
  */
345
502
  export function historyGapNote(
346
- rows: readonly Pick<HistoryRow, "date" | "interval">[],
503
+ rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[],
347
504
  market: { listing: string; exchange?: string; instrumentType?: string },
348
505
  ): string | null {
349
506
  const everyDay = isRoundTheClockCoin(market.listing, market.exchange)
350
507
  || classifyInstrumentType(market.instrumentType) === "crypto";
351
- if (!everyDay || rows[0]?.interval !== "1d") return null;
508
+ if (!everyDay || rows[0]?.interval !== "1d") return historyBreakGapNote(rows);
352
509
  const days = new Set(rows.map((row) => Date.parse(`${row.date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite));
353
510
  const sorted = [...days].sort((left, right) => left - right);
354
511
  const gaps: Array<[number, number]> = [];
@@ -366,15 +523,19 @@ export function historyGapNote(
366
523
  ...earlier > 0 ? [`${earlier} earlier ${earlier === 1 ? "day" : "days"}`] : [],
367
524
  ...shown.map(([from, to]) => from === to ? date(from) : `${date(from)} to ${date(to)}`),
368
525
  ];
369
- const list = listed.length > 1 ? `${listed.slice(0, -1).join(", ")} and ${listed.at(-1)}` : listed[0];
370
- return `${length(gaps) === 1 ? "No bar" : "No bars"} for ${list}`;
526
+ return `${length(gaps) === 1 ? "No bar" : "No bars"} for ${listItems(listed)}`;
371
527
  }
372
528
 
373
- /** "37 of 251 bars have high below open or close in the source data; ...", or null when every bar agrees. */
529
+ /**
530
+ * "37 of 251 bars have high below open or close in the source data; ...", or
531
+ * null when every bar agrees. Bars flagged only for a gap or jump before them
532
+ * keep their prices and are not counted.
533
+ */
374
534
  export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
375
- const flagged = rows.filter((row) => row.flag);
535
+ const contradictions = (row: Pick<HistoryRow, "flag">) => flagCodes(row).filter((code) => !BREAK_FLAGS.has(code));
536
+ const flagged = rows.filter((row) => contradictions(row).length > 0);
376
537
  if (flagged.length === 0) return null;
377
- const codes = new Set(flagged.flatMap((row) => row.flag!.split(",")));
538
+ const codes = new Set(flagged.flatMap(contradictions));
378
539
  const kinds = [
379
540
  codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
380
541
  codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Where data finer than a minute is: nowhere as bars. GIP draws 1-minute
3
- * bars and QR lists each trade, timestamped to the millisecond.
3
+ * bars, and QR lists each trade of a US listing. `history --interval`, GIP's
4
+ * `--chartResolution 1s` and QR on a listing abroad all say this.
4
5
  */
5
6
  export function subMinuteDataMessage(symbol = "<ticker>"): string {
6
- return `No second or tick bars; spread series are daily. 1-minute bars: gloomberb fn GIP ${symbol}. `
7
- + `Individual trades with ms timestamps: gloomberb fn QR ${symbol}.`;
7
+ return `No second or tick bars; 1-minute bars: fn GIP ${symbol}; trades exist for US listings only.`;
8
+ }
9
+
10
+ /** A bar size finer than a minute, as people type one: 1s, 30s, 5sec, 500ms, tick, ticks, tick-by-tick, seconds. */
11
+ export function isSubMinuteInterval(value: string): boolean {
12
+ return /^(\d+(\.\d+)?\s*(ms|msecs?|millis(econds?)?|s|secs?|seconds?)|\d*\s*ticks?|tick[\s-]*by[\s-]*tick|seconds?|sub[\s-]*(minute|second)s?)$/i
13
+ .test(value.trim());
8
14
  }
9
15
 
10
16
  /**
@@ -22,6 +28,11 @@ const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; message: string }> =
22
28
  "carbon price", "carbon prices", "carbon credits", "emission allowances", "emissions allowances"],
23
29
  message: "EU carbon (EUA): the auction price is in EPWR, chart CAP:carbon:eua. Traded proxies: CO2:LSE, KRBN.",
24
30
  },
31
+ {
32
+ terms: ["EU gas storage", "European gas storage", "Europe gas storage", "EU gas inventories", "European gas inventories",
33
+ "AGSI", "AGSI+", "GIE AGSI", "gas storage Europe"],
34
+ message: "European gas storage: EPWR GAS, or EPWR GAS DE for one country; chart CAP:gas-storage:eu. US storage is NGS.",
35
+ },
25
36
  {
26
37
  terms: ["tick", "ticks", "tick data", "tick bars", "tick by tick", "second", "seconds", "second bars", "1s", "1 second",
27
38
  "1 second bars", "sub-minute", "sub minute bars", "subsecond"],
@@ -3,7 +3,15 @@ import type { Quote } from "../types/financials";
3
3
  import type { CliCommandContext } from "../types/plugin";
4
4
  import type { TickerRecord } from "../types/ticker";
5
5
  import { perpetualListingOfMarket, typedPerpetualListing } from "../tickers/perpetual-listing";
6
- import { listingVenues, quotedOnVenue, readSharedCode, type ListingDeps, type ListingStore, type ListingVenue } from "../tickers/listing-venues";
6
+ import {
7
+ listingVenues,
8
+ quotedOnVenue,
9
+ readAmbiguousCurrencyPair,
10
+ readSharedCode,
11
+ type ListingDeps,
12
+ type ListingStore,
13
+ type ListingVenue,
14
+ } from "../tickers/listing-venues";
7
15
  import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
8
16
  import {
9
17
  describeTickerChange,
@@ -33,6 +41,7 @@ import {
33
41
  import { withMarketData } from "./scoped-context";
34
42
  import { isNoProviderMessage } from "../sources/provider-errors";
35
43
  import { dashedCoinPair } from "../utils/crypto-pair";
44
+ import { currencyPairSpelling } from "../utils/currency-pair";
36
45
  import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
37
46
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
38
47
  import { zonedDateKey } from "../utils/zoned-date-time";
@@ -75,7 +84,11 @@ export interface ListingArg {
75
84
  * the command takes several symbols and `--exchange` only fills in the ones
76
85
  * without an exchange of their own. A code that names several venues (TSE)
77
86
  * agrees with any of them. A futures board code no listed security uses is
78
- * its future: TTF is TTF=F, while NG stays NovaGold.
87
+ * its future: TTF is TTF=F, while NG stays NovaGold. A currency pair is the
88
+ * one listing the service has for it, however it is written: EUR/USD,
89
+ * EURUSD:CCY and JPY=X are EURUSD=X and USDJPY=X, so every spelling asks for
90
+ * the same quote. USD-NGN and USDNGN could be a security's symbol, so
91
+ * `resolveCliListing` asks search before reading them as USDNGN=X.
79
92
  */
80
93
  export function parseListingArg(
81
94
  raw: string,
@@ -106,6 +119,8 @@ export function parseListingArg(
106
119
  // TSE and --exchange JPX agree on Tokyo.
107
120
  if (agreed.length === 1) [exchange, code] = [agreed[0]!, agreed[0]!];
108
121
  }
122
+ const pair = currencyPairSpelling(parsed.symbol, exchange);
123
+ if (pair && !pair.ambiguous) return { symbol: pair.symbol, exchange: "", key: pair.symbol };
109
124
  const shared = isSharedExchangeCode(code) ? { sharedCode: code } : {};
110
125
  if (exchange) return { symbol: parsed.symbol, exchange, key: `${parsed.symbol}:${exchange}`, ...shared };
111
126
  // A futures board code no security uses is its future: TTF is TTF=F.
@@ -855,6 +870,9 @@ export async function resolveCliListing(
855
870
  ): Promise<CliListing> {
856
871
  let parsed = parseListingArg(raw, exchangeOption, options);
857
872
  if (!parsed.symbol) throw new ListingArgError("Ticker symbol is required.");
873
+ // USD-NGN and USDNGN are the pair USDNGN=X unless a security is listed under those letters.
874
+ const pair = parsed.exchange ? null : await readAmbiguousCurrencyPair(parsed.symbol, deps);
875
+ if (pair) parsed = { symbol: pair, exchange: "", key: pair };
858
876
  try {
859
877
  await checkListingExchange(parsed, deps);
860
878
  } catch (error) {
@@ -0,0 +1,159 @@
1
+ import type { DataProvider } from "../types/data-provider";
2
+ import type { TickerRecord } from "../types/ticker";
3
+ import { LISTING_SEARCH_LIMIT, searchTickerCandidates, type TickerSearchCandidate } from "../tickers/search";
4
+ import { mapWithConcurrency } from "../utils/concurrency";
5
+ import { TickerNotFoundError } from "./ticker-resolution";
6
+
7
+ /** How long a suggestion waits for search; a slow search leaves the plain "not found" as it was. */
8
+ const SUGGESTION_TIMEOUT_MS = 6_000;
9
+ /** Searches in flight at once while a list's missing symbols are looked up. */
10
+ const SUGGESTION_CONCURRENCY = 4;
11
+ /** A list with more missing symbols than this is an import with stale codes, not a typo; it gets no suggestions and no extra searches. */
12
+ export const MAX_SUGGESTED_SYMBOLS = 10;
13
+ /** Symbols shorter than this are too short for one edit to leave them recognisable. */
14
+ const MIN_TYPO_LENGTH = 4;
15
+
16
+ export interface ListingSuggestion {
17
+ /** The listing as `search` prints it: USDNGN=X. */
18
+ symbol: string;
19
+ name: string;
20
+ }
21
+
22
+ interface SuggestionDeps {
23
+ store: { loadAllTickers(): Promise<TickerRecord[]> };
24
+ dataProvider: DataProvider;
25
+ /** Replaces the search, for a caller that already has the candidates. */
26
+ search?: (query: string) => Promise<TickerSearchCandidate[]>;
27
+ timeoutMs?: number;
28
+ }
29
+
30
+ /** The text of a symbol or a name with its case, spaces, punctuation and a leading `$` gone: `USD-NGN` and `USD/NGN` are `USDNGN`. */
31
+ function compact(text: string): string {
32
+ return text.toUpperCase().replace(/[^\p{L}\p{N}]/gu, "");
33
+ }
34
+
35
+ /** A symbol as people type it: without the `=X` or `=F` a pair or a future carries, and without its venue (`BTCUSDT:BYBIT`). */
36
+ function typedForm(symbol: string): string {
37
+ return compact(symbol.split(":")[0]!.replace(/=[A-Z]?$/i, ""));
38
+ }
39
+
40
+ /** The edits (insert, delete, replace, swap two neighbours) between two strings, up to two; anything further apart is 2. */
41
+ function editDistance(left: string, right: string): number {
42
+ if (left === right) return 0;
43
+ if (Math.abs(left.length - right.length) > 1) return 2;
44
+ const rows: number[][] = Array.from({ length: left.length + 1 }, (_, row) => [row, ...new Array<number>(right.length).fill(0)]);
45
+ for (let column = 1; column <= right.length; column += 1) rows[0]![column] = column;
46
+ for (let row = 1; row <= left.length; row += 1) {
47
+ for (let column = 1; column <= right.length; column += 1) {
48
+ const same = left[row - 1] === right[column - 1] ? 0 : 1;
49
+ let best = Math.min(rows[row - 1]![column]! + 1, rows[row]![column - 1]! + 1, rows[row - 1]![column - 1]! + same);
50
+ if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) {
51
+ best = Math.min(best, rows[row - 2]![column - 2]! + 1);
52
+ }
53
+ rows[row]![column] = best;
54
+ }
55
+ }
56
+ return rows[left.length]![right.length]!;
57
+ }
58
+
59
+ function candidateName(candidate: TickerSearchCandidate): string {
60
+ return candidate.detail.split(" | ")[0]?.trim() ?? "";
61
+ }
62
+
63
+ /** 0 when the candidate is the typed symbol with other punctuation (`USD_NGN` for `USDNGN=X`) or named so, 1 for one slip of the keys, null for anything else. */
64
+ function nearness(typed: string, candidate: TickerSearchCandidate): number | null {
65
+ const wanted = compact(typed);
66
+ if (!wanted) return null;
67
+ const symbol = typedForm(candidate.label || candidate.symbol);
68
+ if (wanted === symbol || wanted === compact(candidateName(candidate))) return 0;
69
+ return wanted.length >= MIN_TYPO_LENGTH && symbol.length >= MIN_TYPO_LENGTH && editDistance(wanted, symbol) === 1 ? 1 : null;
70
+ }
71
+
72
+ /**
73
+ * The search hit a symbol that was not found most likely meant, or null.
74
+ * Only a real near match counts: the same letters and digits with other
75
+ * punctuation, or one slip in a symbol of four characters or more. The first
76
+ * result is never taken for being first, and a tie between different symbols
77
+ * is no suggestion at all.
78
+ */
79
+ export function nearestListing(typed: string, candidates: readonly TickerSearchCandidate[]): ListingSuggestion | null {
80
+ const typedKey = typed.trim().replace(/^\$/, "").toUpperCase();
81
+ const near = candidates
82
+ .filter((candidate) => (candidate.label || candidate.symbol).toUpperCase() !== typedKey)
83
+ .flatMap((candidate) => {
84
+ const tier = nearness(typed, candidate);
85
+ return tier == null ? [] : [{ candidate, tier }];
86
+ });
87
+ if (near.length === 0) return null;
88
+ const best = Math.min(...near.map((entry) => entry.tier));
89
+ const top = near.filter((entry) => entry.tier === best).map((entry) => entry.candidate);
90
+ if (new Set(top.map((candidate) => typedForm(candidate.label || candidate.symbol))).size > 1) return null;
91
+ const first = top[0]!;
92
+ return { symbol: first.label || first.symbol, name: candidateName(first) };
93
+ }
94
+
95
+ /** The suggestion as a line says it: `USDNGN=X (US Dollar/Nigerian Naira)`. */
96
+ export function describeSuggestion({ symbol, name }: ListingSuggestion): string {
97
+ return name && name.toUpperCase() !== symbol.toUpperCase() ? `${symbol} (${name})` : symbol;
98
+ }
99
+
100
+ /** The search the `search` command runs for a query: saved tickers and the data service's matches, as many as it lists. */
101
+ async function searchAsCommand(query: string, { store, dataProvider }: SuggestionDeps): Promise<TickerSearchCandidate[]> {
102
+ const tickers = new Map((await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const));
103
+ return searchTickerCandidates({
104
+ query,
105
+ tickers,
106
+ dataProvider,
107
+ localLimit: 6,
108
+ totalLimit: LISTING_SEARCH_LIMIT,
109
+ searchContext: { limit: LISTING_SEARCH_LIMIT },
110
+ });
111
+ }
112
+
113
+ /**
114
+ * The listing search finds for a symbol that resolved to nothing, when it is a
115
+ * near match; null when search finds nothing near, fails or is slow. It never
116
+ * throws and never waits longer than the timeout.
117
+ */
118
+ export async function suggestListing(input: string, deps: SuggestionDeps): Promise<ListingSuggestion | null> {
119
+ const query = input.trim().replace(/^\$/, "");
120
+ if (!query) return null;
121
+ let timer: ReturnType<typeof setTimeout> | undefined;
122
+ try {
123
+ const candidates = await Promise.race([
124
+ Promise.resolve().then(() => (deps.search ?? ((text: string) => searchAsCommand(text, deps)))(query)),
125
+ new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), deps.timeoutMs ?? SUGGESTION_TIMEOUT_MS); }),
126
+ ]);
127
+ return candidates ? nearestListing(query, candidates) : null;
128
+ } catch {
129
+ return null;
130
+ } finally {
131
+ if (timer) clearTimeout(timer);
132
+ }
133
+ }
134
+
135
+ /** The answer for a symbol search lists nothing for, with nothing else said (a company that left its symbol, a wrong exchange, a stopped listing each say more). */
136
+ const PLAIN_NO_MATCH = /^No ticker match found for ".*"\.$/s;
137
+
138
+ /** Whether the message is the plain no-match answer, the one a suggestion can sit beside. */
139
+ export function isPlainNoMatch(message: string | null | undefined): boolean {
140
+ return message != null && PLAIN_NO_MATCH.test(message);
141
+ }
142
+
143
+ /**
144
+ * The error a single symbol failed with, and when it is search finding no
145
+ * listing for the symbol, that error with the nearest listing named in its
146
+ * message: `No ticker match found for "USD_NGN"; did you mean USDNGN=X (US
147
+ * Dollar/Nigerian Naira)?`. Any other error comes back as it is.
148
+ */
149
+ export async function withSuggestion(error: unknown, input: string, deps: SuggestionDeps): Promise<unknown> {
150
+ if (!(error instanceof TickerNotFoundError) || !isPlainNoMatch(error.message)) return error;
151
+ const suggestion = await suggestListing(input, deps);
152
+ return suggestion ? new TickerNotFoundError(`${error.message.replace(/\.$/, "")}; did you mean ${describeSuggestion(suggestion)}?`) : error;
153
+ }
154
+
155
+ /** The suggestion of each input, null where there is none; none at all for more than `MAX_SUGGESTED_SYMBOLS`. */
156
+ export async function suggestListings(inputs: readonly string[], deps: SuggestionDeps): Promise<Array<ListingSuggestion | null>> {
157
+ if (inputs.length > MAX_SUGGESTED_SYMBOLS) return inputs.map(() => null);
158
+ return mapWithConcurrency(inputs, SUGGESTION_CONCURRENCY, (input) => suggestListing(input, deps));
159
+ }
@@ -26,3 +26,16 @@ export async function smokePluginHost(): Promise<void> {
26
26
  const { smokePluginHostLoad } = await import("../plugins/host-smoke");
27
27
  await smokePluginHostLoad();
28
28
  }
29
+
30
+ export const SHOT_VIEW_SMOKE_COMMAND = "__gloomberb-smoke-shot-view";
31
+
32
+ /**
33
+ * Reads the page `shot` and rendered `fn` reports draw panes in. The release
34
+ * binary has no sources to build it from, so it carries a prebuilt copy; run
35
+ * from outside a checkout, this fails when that copy is missing or incomplete
36
+ * rather than on a user's first capture.
37
+ */
38
+ export async function smokeShotView(): Promise<void> {
39
+ const { loadShotView } = await import("./desktop-pane-shot-view");
40
+ await loadShotView();
41
+ }
@@ -2,7 +2,8 @@ import { notATickerSymbol } from "../sources/provider-errors";
2
2
  import { splitSymbolArgs } from "./symbol-args";
3
3
 
4
4
  // "Not a ticker: APPLE." as the router words it; the symbol ends at the sentence's own period.
5
- const NOT_A_TICKER_SENTENCE = /Not a ticker: ((?:(?!\.\s)[^\n`\x1b])+?)\.(?=\s|\x1b|$)(?! Try `gloomberb (?:search|fred) )/g;
5
+ // One that already names what to try (a search, a FRED series, the pair search offered) is left alone.
6
+ const NOT_A_TICKER_SENTENCE = /Not a ticker: ((?:(?!\.\s)[^\n`\x1b])+?)\.(?=\s|\x1b|$)(?! Try `gloomberb (?:search|fred) | Did you mean )/g;
6
7
 
7
8
  /**
8
9
  * The symbol as the user typed it on the command line (`Apple`), not as the router saw it (`APPLE`).
@@ -46,6 +47,17 @@ export function withSearchHints(text: string, args: readonly string[], isFredSer
46
47
  : text;
47
48
  }
48
49
 
50
+ /**
51
+ * "Not a ticker: USD_NGN. Did you mean USDNGN=X (US Dollar/Nigerian Naira)?",
52
+ * the listing search suggests, which takes the place of the search hint.
53
+ */
54
+ export function withListingSuggestion(message: string, suggestion: string): string {
55
+ const symbol = notATickerSymbol(message);
56
+ if (!symbol || message.includes(" Did you mean ")) return message;
57
+ const sentence = `Not a ticker: ${symbol}.`;
58
+ return message.replace(sentence, `${sentence} Did you mean ${suggestion}?`);
59
+ }
60
+
49
61
  /** Whether an error message says the symbol is not a ticker. */
50
62
  export function isNotATickerMessage(message: string | null | undefined): boolean {
51
63
  return message != null && notATickerSymbol(message) !== null;
@@ -140,19 +140,24 @@ export function capabilityHasOption(capability: PaneFunctionCapability, key: str
140
140
  return resolveOptionDef(capability, key) !== undefined;
141
141
  }
142
142
 
143
- function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string): string {
143
+ function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string, argument: string | null): string {
144
144
  const token = optionToken(rawValue);
145
145
  const match = option.values?.find((candidate) => (
146
146
  optionToken(candidate.value) === token
147
147
  || candidate.aliases?.some((alias) => optionToken(alias) === token)
148
148
  ));
149
149
  if (match) return match.value;
150
+ const hint = option.invalidValueHint?.(rawValue, argument);
150
151
  throw new Error(
151
- `Invalid --${option.key} value "${rawValue}". Use one of: ${option.values?.map(({ value }) => value).join(", ") ?? ""}.`,
152
+ `Invalid --${option.key} value "${rawValue}".${hint ? ` ${hint}` : ""} Use one of: ${option.values?.map(({ value }) => value).join(", ") ?? ""}.`,
152
153
  );
153
154
  }
154
155
 
155
- function normalizeOptionValue(option: PaneFunctionOptionDef, rawValue: string | true): string | number | boolean {
156
+ function normalizeOptionValue(
157
+ option: PaneFunctionOptionDef,
158
+ rawValue: string | true,
159
+ argument: string | null = null,
160
+ ): string | number | boolean {
156
161
  if (option.type === "boolean") {
157
162
  if (rawValue === true) return true;
158
163
  if (/^true$/i.test(rawValue)) return true;
@@ -161,7 +166,7 @@ function normalizeOptionValue(option: PaneFunctionOptionDef, rawValue: string |
161
166
  }
162
167
  if (rawValue === true) throw new Error(`--${option.key} requires a value.`);
163
168
  if (option.normalize) return option.normalize(rawValue);
164
- if (option.type === "enum") return normalizeEnumValue(option, rawValue);
169
+ if (option.type === "enum") return normalizeEnumValue(option, rawValue, argument);
165
170
  if (option.type === "integer") {
166
171
  const value = Number(rawValue);
167
172
  if (!Number.isInteger(value)) throw new Error(`--${option.key} must be an integer.`);
@@ -179,14 +184,17 @@ function normalizeOptionValue(option: PaneFunctionOptionDef, rawValue: string |
179
184
  export function normalizeCapabilityOptions(
180
185
  capability: PaneFunctionCapability,
181
186
  options: Record<string, string | true>,
182
- /** `code` is what the user types for the function (`CDX`), which an error names; the capability id stands in when there is none. */
183
- settings: { strict?: boolean; code?: string } = {},
187
+ /**
188
+ * `code` is what the user types for the function (`CDX`), which an error names; the capability id stands in when
189
+ * there is none. `argument` is what the function was given (`RMS.AX`), which an option's pointer names.
190
+ */
191
+ settings: { strict?: boolean; code?: string; argument?: string | null } = {},
184
192
  ): NormalizedPaneFunctionOptions {
185
193
  if (!capability.botSafe) {
186
194
  // A rendered view takes any pane setting; the options it declares are checked.
187
195
  return Object.fromEntries(Object.entries(options).map(([key, value]) => {
188
196
  const option = resolveOptionDef(capability, key);
189
- return option ? [option.key, normalizeOptionValue(option, value)] : [key, value];
197
+ return option ? [option.key, normalizeOptionValue(option, value, settings.argument)] : [key, value];
190
198
  }));
191
199
  }
192
200
 
@@ -206,7 +214,7 @@ export function normalizeCapabilityOptions(
206
214
  `${settings.code ?? capability.id} does not support --${rawKey}.${supported ? ` Supported options: ${supported}.` : " It has no options."}`,
207
215
  );
208
216
  }
209
- normalized[option.key] = normalizeOptionValue(option, rawValue);
217
+ normalized[option.key] = normalizeOptionValue(option, rawValue, settings.argument);
210
218
  }
211
219
  return normalized;
212
220
  }