gloomberb 0.17.4 → 0.17.6

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 (384) hide show
  1. package/package.json +3 -1
  2. package/patches/electrobun@1.18.1.patch +80 -0
  3. package/src/api-client/chat.ts +9 -0
  4. package/src/api-client/commodity-balances.ts +77 -0
  5. package/src/api-client/data.ts +35 -3
  6. package/src/api-client/earnings.ts +10 -0
  7. package/src/api-client/index.ts +5 -0
  8. package/src/api-client/insider-signals.ts +75 -0
  9. package/src/api-client/market-discovery.ts +1 -1
  10. package/src/api-client/normalizers.ts +1 -0
  11. package/src/api-client/paths.ts +9 -0
  12. package/src/api-client/research-activity.ts +10 -3
  13. package/src/api-client/socket.ts +10 -0
  14. package/src/api-client/types.ts +25 -1
  15. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
  16. package/src/app/runtime/broker-import.ts +23 -21
  17. package/src/app/runtime/plugin-bindings.ts +13 -7
  18. package/src/app/runtime/update.ts +64 -4
  19. package/src/app.tsx +2 -0
  20. package/src/architecture/core-builtin-allowlist.json +2 -10
  21. package/src/ask-gloom/pane-context.ts +11 -1
  22. package/src/brokers/broker-portfolio-sync.ts +66 -0
  23. package/src/brokers/sync-broker-instance.ts +75 -41
  24. package/src/cli/commands/market.ts +101 -34
  25. package/src/cli/commands/options-chain.ts +20 -6
  26. package/src/cli/commands/overview.ts +54 -13
  27. package/src/cli/commands/search.ts +1 -1
  28. package/src/cli/commands/system.ts +19 -1
  29. package/src/cli/commands/ticker.ts +24 -4
  30. package/src/cli/context.ts +13 -0
  31. package/src/cli/errors.ts +15 -2
  32. package/src/cli/fx-pair.ts +77 -0
  33. package/src/cli/glossary.ts +34 -4
  34. package/src/cli/history-rows.ts +25 -11
  35. package/src/cli/index.ts +30 -12
  36. package/src/cli/listing-arg.ts +61 -138
  37. package/src/cli/listing-suggestion.ts +3 -21
  38. package/src/cli/pane-functions/catalog.ts +50 -0
  39. package/src/cli/pane-functions/freshness.ts +44 -9
  40. package/src/cli/pane-functions/function-help.ts +9 -9
  41. package/src/cli/pane-functions/headless.ts +19 -7
  42. package/src/cli/pane-functions/index.ts +63 -5
  43. package/src/cli/pane-functions/market-session.ts +18 -3
  44. package/src/cli/pane-functions/resolver.ts +29 -8
  45. package/src/cli/pane-functions/screenshot.ts +2 -0
  46. package/src/cli/report-tables.ts +11 -4
  47. package/src/cli/result.ts +2 -0
  48. package/src/cli/series-search.ts +6 -1
  49. package/src/cli/ticker-resolution.ts +1 -2
  50. package/src/cli/unknown-command.ts +18 -0
  51. package/src/components/chart/composite/composite-chart.tsx +80 -19
  52. package/src/components/chart/composite/format.ts +17 -0
  53. package/src/components/chart/composite/legend.tsx +26 -15
  54. package/src/components/chart/composite/panel-labels.ts +38 -0
  55. package/src/components/chart/composite/panel-series.ts +1 -0
  56. package/src/components/chart/composite/panel-surface.tsx +25 -7
  57. package/src/components/chart/composite/price-axis-labels.tsx +16 -3
  58. package/src/components/chart/composite/scene.ts +60 -2
  59. package/src/components/chart/composite/series-axis-tags.tsx +67 -0
  60. package/src/components/chart/composite/tools.ts +2 -2
  61. package/src/components/chart/composite/types.ts +28 -0
  62. package/src/components/chart/core/view-reset.ts +46 -0
  63. package/src/components/command-bar/commands/collection.ts +1 -1
  64. package/src/components/command-bar/commands/direct/index.ts +48 -1
  65. package/src/components/command-bar/commands/registry.ts +8 -0
  66. package/src/components/command-bar/list/model.ts +7 -0
  67. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  68. package/src/components/command-bar/panel/prompt-binding.ts +8 -0
  69. package/src/components/command-bar/routes/root/command-items.ts +17 -2
  70. package/src/components/command-bar/routes/root/pick-prefetch.ts +38 -0
  71. package/src/components/command-bar/routes/root/provider-search.ts +6 -0
  72. package/src/components/command-bar/routes/root/runtime.ts +3 -0
  73. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -1
  74. package/src/components/command-bar/routes/root/shortcut-items.ts +1 -1
  75. package/src/components/command-bar/routes/ticker-search/actions.ts +34 -0
  76. package/src/components/command-bar/surface/index.tsx +7 -1
  77. package/src/components/command-bar/workflow/collection-actions.ts +6 -5
  78. package/src/components/command-bar/workflow/ops.ts +8 -5
  79. package/src/components/command-bar/workflow/pane-reuse.ts +3 -2
  80. package/src/components/command-bar/workflow/tickers.ts +1 -1
  81. package/src/components/form-modal/confirm.tsx +1 -0
  82. package/src/components/form-modal/request.ts +2 -0
  83. package/src/components/layout/detached-pane-shell.tsx +6 -1
  84. package/src/components/layout/pane/content.tsx +11 -8
  85. package/src/components/layout/pane/sidebar.tsx +24 -11
  86. package/src/components/layout/pane/ticker-fate-gate.tsx +56 -0
  87. package/src/components/layout/shell/index.tsx +5 -1
  88. package/src/components/layout/shell/menu.ts +8 -0
  89. package/src/components/layout/shell/pane/capture.ts +29 -12
  90. package/src/components/onboarding/wizard-model.ts +1 -1
  91. package/src/components/onboarding/wizard-positions.ts +18 -10
  92. package/src/components/ticker/badge/format.ts +64 -15
  93. package/src/components/ticker/badge/index.tsx +32 -11
  94. package/src/components/ticker/badge/list.tsx +4 -3
  95. package/src/components/ticker/badge/text.tsx +2 -2
  96. package/src/components/ticker/list-table-view.tsx +8 -1
  97. package/src/components/ui/confirm-dialog.tsx +30 -4
  98. package/src/components/ui/data-table/opentui/index.tsx +61 -20
  99. package/src/components/ui/data-table/opentui/model.ts +104 -0
  100. package/src/components/ui/fields.tsx +5 -0
  101. package/src/components/ui/inline-quick-add.tsx +4 -1
  102. package/src/components/ui/query-bar-keys.tsx +29 -5
  103. package/src/components/ui/query-bar.tsx +29 -4
  104. package/src/components/ui/windowed-list.tsx +27 -0
  105. package/src/data/config/store/normalize.ts +8 -0
  106. package/src/docs/snapshot.json +248 -5
  107. package/src/i18n/es.ts +3 -1
  108. package/src/i18n/ja.ts +3 -1
  109. package/src/i18n/ko.ts +3 -1
  110. package/src/i18n/zh-cn.ts +3 -1
  111. package/src/i18n/zh-tw.ts +3 -1
  112. package/src/layout-marketplace/cloud.ts +16 -5
  113. package/src/layout-marketplace/gallery.tsx +4 -4
  114. package/src/layout-marketplace/linked-sync.ts +9 -3
  115. package/src/layout-marketplace/linked.ts +24 -3
  116. package/src/layout-marketplace/model.ts +39 -4
  117. package/src/market-data/coordinator/events.ts +5 -1
  118. package/src/market-data/coordinator/index.ts +38 -1
  119. package/src/market-data/coordinator/quotes.ts +4 -0
  120. package/src/market-data/futures-contract.ts +53 -0
  121. package/src/market-data/hooks.tsx +225 -30
  122. package/src/market-data/market/bar-period.ts +71 -0
  123. package/src/market-data/market/format.ts +14 -2
  124. package/src/market-data/market/freshness.ts +28 -1
  125. package/src/market-data/market/status.ts +152 -13
  126. package/src/market-data/model-dividend-yield.ts +76 -0
  127. package/src/market-data/quotes/freshness.ts +30 -1
  128. package/src/{plugins/builtin/portfolio-list → market-data/quotes}/live-valuation.ts +6 -6
  129. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  130. package/src/plugins/builtin/analytics/metrics.ts +8 -4
  131. package/src/plugins/builtin/analytics/risk-headless.ts +12 -5
  132. package/src/plugins/builtin/analytics/risk-model.ts +74 -15
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +6 -6
  134. package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
  135. package/src/plugins/builtin/broker-manager/model.ts +2 -2
  136. package/src/plugins/builtin/builtin-plugin-meta.ts +0 -1
  137. package/src/plugins/builtin/chart-composer/editor/controller.ts +14 -10
  138. package/src/plugins/builtin/chart-composer/formula.ts +233 -0
  139. package/src/plugins/builtin/chart-composer/headless.ts +27 -2
  140. package/src/plugins/builtin/chart-composer/index.tsx +3 -2
  141. package/src/plugins/builtin/chart-composer/pane.tsx +41 -10
  142. package/src/plugins/builtin/chart-composer/presets.ts +91 -2
  143. package/src/plugins/builtin/chart-composer/quick-add.tsx +9 -2
  144. package/src/plugins/builtin/chart-composer/series-catalog.ts +44 -11
  145. package/src/plugins/builtin/chart-composer/series-expression.ts +1 -1
  146. package/src/plugins/builtin/chart-composer/settings.ts +16 -4
  147. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  148. package/src/plugins/builtin/chat/content/channel-navigation.ts +17 -6
  149. package/src/plugins/builtin/chat/content/footer.tsx +5 -2
  150. package/src/plugins/builtin/chat/content/index.tsx +12 -5
  151. package/src/plugins/builtin/chat/content/mentions.ts +50 -21
  152. package/src/plugins/builtin/chat/content/scroll.ts +12 -5
  153. package/src/plugins/builtin/chat/content/snapshot.ts +11 -0
  154. package/src/plugins/builtin/chat/content/transcript.tsx +48 -37
  155. package/src/plugins/builtin/chat/content/whole-message-copy.ts +44 -2
  156. package/src/plugins/builtin/chat/controller/index.ts +88 -17
  157. package/src/plugins/builtin/chat/controller/mention-candidates.ts +104 -0
  158. package/src/plugins/builtin/chat/controller/messages.ts +14 -2
  159. package/src/plugins/builtin/chat/controller/realtime.ts +9 -0
  160. package/src/plugins/builtin/chat/controller/state.ts +25 -0
  161. package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
  162. package/src/plugins/builtin/chat/controller/view.ts +4 -0
  163. package/src/plugins/builtin/chat/conversation-recency.ts +73 -0
  164. package/src/plugins/builtin/chat/conversations.ts +12 -5
  165. package/src/plugins/builtin/chat/index.tsx +1 -1
  166. package/src/plugins/builtin/chat/layout.ts +23 -5
  167. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  168. package/src/plugins/builtin/chat/share/flow.tsx +18 -9
  169. package/src/plugins/builtin/chat/sidebar-rows.ts +18 -5
  170. package/src/plugins/builtin/chat/sidebar.tsx +6 -1
  171. package/src/plugins/builtin/chat/unread-badge.ts +38 -7
  172. package/src/plugins/builtin/cloud/askg/manifest.ts +4 -3
  173. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
  174. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +16 -5
  175. package/src/plugins/builtin/commodities-energy/index.tsx +23 -4
  176. package/src/plugins/builtin/commodity-balances/client.ts +51 -193
  177. package/src/plugins/builtin/commodity-balances/headless.ts +10 -3
  178. package/src/plugins/builtin/commodity-balances/model.ts +5 -180
  179. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -0
  180. package/src/plugins/builtin/custom-view/index.tsx +1 -1
  181. package/src/plugins/builtin/custom-view/loader.ts +57 -7
  182. package/src/plugins/builtin/custom-view/sources.ts +146 -0
  183. package/src/plugins/builtin/custom-view/view-spec.ts +37 -1
  184. package/src/plugins/builtin/earnings/board-model.ts +33 -3
  185. package/src/plugins/builtin/earnings/board-table.ts +59 -14
  186. package/src/plugins/builtin/earnings/board.tsx +36 -3
  187. package/src/plugins/builtin/earnings/headless.ts +24 -5
  188. package/src/plugins/builtin/earnings/history-model.ts +9 -1
  189. package/src/plugins/builtin/earnings/history-table.ts +16 -5
  190. package/src/plugins/builtin/earnings/history.tsx +40 -10
  191. package/src/plugins/builtin/earnings/index.tsx +3 -1
  192. package/src/plugins/builtin/earnings/no-earnings.ts +52 -0
  193. package/src/plugins/builtin/econ-statistics/detail.tsx +4 -1
  194. package/src/plugins/builtin/econ-statistics/headless.ts +5 -0
  195. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  196. package/src/plugins/builtin/econ-statistics/view.ts +4 -0
  197. package/src/plugins/builtin/futures/model.ts +5 -4
  198. package/src/plugins/builtin/futures/series.ts +38 -1
  199. package/src/plugins/builtin/futures-curve/client.ts +4 -4
  200. package/src/plugins/builtin/futures-curve/headless.ts +33 -8
  201. package/src/plugins/builtin/futures-curve/index.tsx +4 -4
  202. package/src/plugins/builtin/futures-curve/model.ts +109 -6
  203. package/src/plugins/builtin/futures-curve/pane.tsx +24 -13
  204. package/src/plugins/builtin/insider/headless.ts +74 -4
  205. package/src/plugins/builtin/insider/index.tsx +99 -5
  206. package/src/plugins/builtin/insider/signals-client.ts +69 -0
  207. package/src/plugins/builtin/insider/signals-model.ts +146 -0
  208. package/src/plugins/builtin/insider/signals.tsx +176 -0
  209. package/src/plugins/builtin/iv-history/client.ts +41 -2
  210. package/src/plugins/builtin/iv-history/headless.ts +24 -16
  211. package/src/plugins/builtin/iv-history/screen-pane.tsx +25 -7
  212. package/src/plugins/builtin/jodi/client.ts +42 -205
  213. package/src/plugins/builtin/jodi/headless.ts +13 -4
  214. package/src/plugins/builtin/jodi/model.ts +18 -43
  215. package/src/plugins/builtin/jodi/pane.tsx +5 -0
  216. package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
  217. package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
  218. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  219. package/src/plugins/builtin/macro-day/release-table.ts +3 -3
  220. package/src/plugins/builtin/market-movers/client.ts +32 -6
  221. package/src/plugins/builtin/market-movers/headless.ts +7 -2
  222. package/src/plugins/builtin/market-movers/model.ts +64 -3
  223. package/src/plugins/builtin/market-movers/screener.ts +20 -9
  224. package/src/plugins/builtin/market-valuation/detail.tsx +7 -2
  225. package/src/plugins/builtin/market-valuation/headless.ts +17 -4
  226. package/src/plugins/builtin/market-valuation/pane.tsx +7 -3
  227. package/src/plugins/builtin/market-valuation/view.ts +10 -0
  228. package/src/plugins/builtin/mna/model.ts +2 -1
  229. package/src/plugins/builtin/options-positioning/client.ts +10 -0
  230. package/src/plugins/builtin/options-positioning/headless.ts +88 -13
  231. package/src/plugins/builtin/options-positioning/index.tsx +2 -2
  232. package/src/plugins/builtin/options-positioning/model.ts +47 -0
  233. package/src/plugins/builtin/options-positioning/pane.tsx +55 -26
  234. package/src/plugins/builtin/options-scenario/charts.tsx +13 -4
  235. package/src/plugins/builtin/options-scenario/client.ts +111 -17
  236. package/src/plugins/builtin/options-scenario/collar.ts +141 -0
  237. package/src/plugins/builtin/options-scenario/csp.ts +1 -1
  238. package/src/plugins/builtin/options-scenario/freshness.ts +27 -0
  239. package/src/plugins/builtin/options-scenario/headless.ts +28 -12
  240. package/src/plugins/builtin/options-scenario/index.tsx +1 -1
  241. package/src/plugins/builtin/options-scenario/model.ts +55 -5
  242. package/src/plugins/builtin/options-scenario/pane.tsx +24 -5
  243. package/src/plugins/builtin/perps/client.ts +77 -6
  244. package/src/plugins/builtin/perps/headless.ts +25 -14
  245. package/src/plugins/builtin/perps/index.tsx +1 -1
  246. package/src/plugins/builtin/perps/model.ts +32 -0
  247. package/src/plugins/builtin/perps/pane.tsx +5 -4
  248. package/src/plugins/builtin/portfolio-list/allocation.ts +9 -4
  249. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +39 -20
  250. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  251. package/src/plugins/builtin/portfolio-list/cli/shared.ts +15 -7
  252. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +1 -1
  253. package/src/plugins/builtin/portfolio-list/column-values.ts +4 -3
  254. package/src/plugins/builtin/portfolio-list/command-bar.ts +1 -1
  255. package/src/plugins/builtin/portfolio-list/headless.ts +10 -3
  256. package/src/plugins/builtin/portfolio-list/pane/index.tsx +36 -4
  257. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  258. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +35 -6
  259. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  260. package/src/plugins/builtin/portfolio-list/settings.ts +22 -0
  261. package/src/plugins/builtin/portfolio-list/since-last-seen.ts +197 -0
  262. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  263. package/src/plugins/builtin/portfolio-list/table.tsx +14 -2
  264. package/src/plugins/builtin/rate-path/model.ts +1 -1
  265. package/src/plugins/builtin/rate-path/pane.tsx +2 -1
  266. package/src/plugins/builtin/research/corporate-actions-pane.tsx +14 -5
  267. package/src/plugins/builtin/research/event-model.ts +41 -49
  268. package/src/plugins/builtin/research/event-sources.ts +1 -1
  269. package/src/plugins/builtin/research/headless.ts +1 -1
  270. package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
  271. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
  272. package/src/plugins/builtin/reverse-dcf/headless.ts +25 -9
  273. package/src/plugins/builtin/reverse-dcf/index.tsx +4 -1
  274. package/src/plugins/builtin/reverse-dcf/model.ts +39 -5
  275. package/src/plugins/builtin/reverse-dcf/pane.tsx +16 -6
  276. package/src/plugins/builtin/sectors/client.ts +46 -19
  277. package/src/plugins/builtin/sectors/sector-model.ts +22 -10
  278. package/src/plugins/builtin/shared/earnings-report.ts +48 -1
  279. package/src/plugins/builtin/shared/history-span.ts +55 -0
  280. package/src/plugins/builtin/shared/report-freshness.ts +20 -11
  281. package/src/plugins/builtin/shared/series-detail-chart.tsx +5 -2
  282. package/src/plugins/builtin/shared/series-list-detail.tsx +7 -3
  283. package/src/plugins/builtin/shared/ticker-surface.ts +16 -2
  284. package/src/plugins/builtin/shared/use-quote-board.ts +12 -5
  285. package/src/plugins/builtin/themes/headless.ts +15 -2
  286. package/src/plugins/builtin/thirteenf/api.ts +60 -43
  287. package/src/plugins/builtin/thirteenf/data.ts +76 -35
  288. package/src/plugins/builtin/thirteenf/headless.ts +2 -0
  289. package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
  290. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +85 -17
  291. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +92 -0
  292. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +26 -8
  293. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -2
  294. package/src/plugins/builtin/ticker-detail/headless.ts +95 -22
  295. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  296. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  297. package/src/plugins/builtin/world-venue-map/headless.ts +17 -3
  298. package/src/plugins/builtin/yield-curve/curves.ts +15 -1
  299. package/src/plugins/builtin/yield-curve/headless.ts +17 -7
  300. package/src/plugins/builtin/yield-curve/history.ts +2 -0
  301. package/src/plugins/builtin/yield-curve/index.tsx +23 -18
  302. package/src/plugins/builtin/yield-curve/series.ts +19 -9
  303. package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
  304. package/src/plugins/builtin/yield-curve/world.ts +4 -0
  305. package/src/plugins/registry/host-actions.ts +3 -0
  306. package/src/{plugins/builtin/portfolio-list → portfolio}/mutations.ts +6 -6
  307. package/src/portfolio/valuation-basis.ts +4 -21
  308. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  309. package/src/remote/controller.ts +27 -7
  310. package/src/remote/schema.ts +25 -9
  311. package/src/renderers/browser/app-services.ts +7 -1
  312. package/src/renderers/browser/config-host.ts +55 -2
  313. package/src/renderers/browser/first-screen.ts +193 -0
  314. package/src/renderers/browser/main.tsx +59 -7
  315. package/src/renderers/cloudflare/http-proxy.ts +21 -13
  316. package/src/renderers/dom/desktop/controls.tsx +2 -0
  317. package/src/renderers/dom/desktop/query-bar.tsx +1 -1
  318. package/src/renderers/dom/dom-ui-host.tsx +2 -0
  319. package/src/renderers/dom/windowed-list.tsx +201 -0
  320. package/src/renderers/dom/windowed-rows.ts +65 -0
  321. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
  322. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -7
  323. package/src/renderers/electrobun/bun/desktop/http-stream.ts +21 -6
  324. package/src/renderers/electrobun/bun/desktop/update.ts +132 -14
  325. package/src/renderers/electrobun/shared/protocol.ts +7 -4
  326. package/src/renderers/electrobun/view/http-fetch.ts +12 -1
  327. package/src/renderers/electrobun/view/update-host.ts +2 -2
  328. package/src/shares/bug-report-dialog.tsx +162 -0
  329. package/src/shares/bug-report-flow.tsx +84 -0
  330. package/src/shares/bug-report.ts +42 -0
  331. package/src/shares/capture-flow.ts +19 -0
  332. package/src/shares/capture-preview.tsx +129 -0
  333. package/src/shares/chat.ts +10 -1
  334. package/src/shares/dialog.tsx +16 -91
  335. package/src/shares/flow.tsx +12 -16
  336. package/src/shares/personal-pane.ts +35 -1
  337. package/src/sources/listing-symbols.ts +8 -4
  338. package/src/state/app/context/index.tsx +13 -0
  339. package/src/state/app/tab-cursor.ts +85 -0
  340. package/src/sync/core-contributors.ts +206 -59
  341. package/src/telemetry/usage-counts.ts +16 -0
  342. package/src/test-support/config-store.ts +31 -0
  343. package/src/test-support/moved-symbols.ts +64 -0
  344. package/src/theme/colors.ts +30 -0
  345. package/src/tickers/listing-fate.ts +242 -0
  346. package/src/tickers/open-target.ts +61 -11
  347. package/src/tickers/search/index.ts +45 -3
  348. package/src/tickers/search/ranking.ts +19 -2
  349. package/src/tickers/search/types.ts +3 -0
  350. package/src/tickers/search/upsert.ts +22 -7
  351. package/src/tickers/ticker-changes.ts +26 -9
  352. package/src/time-series/bar-spacing.ts +19 -0
  353. package/src/time-series/chart-data.ts +37 -13
  354. package/src/time-series/field-catalog.ts +5 -2
  355. package/src/time-series/forward-valuation.ts +10 -7
  356. package/src/time-series/fundamentals.ts +49 -11
  357. package/src/time-series/hooks.ts +2 -0
  358. package/src/time-series/live-bars.ts +3 -1
  359. package/src/time-series/parsed-history-cache.ts +2 -0
  360. package/src/time-series/price-units.ts +69 -0
  361. package/src/time-series/resolve.ts +71 -11
  362. package/src/time-series/session-bar.ts +138 -0
  363. package/src/time-series/spec.ts +20 -3
  364. package/src/time-series/studies.ts +211 -12
  365. package/src/time-series/transforms.ts +2 -0
  366. package/src/time-series/types.ts +11 -1
  367. package/src/time-series/valuation-price.ts +13 -0
  368. package/src/types/config.ts +24 -1
  369. package/src/types/financials.ts +2 -0
  370. package/src/types/headless.ts +27 -0
  371. package/src/types/plugin.ts +7 -0
  372. package/src/ui/host.tsx +5 -0
  373. package/src/updater/channel.ts +51 -0
  374. package/src/updater/github-releases.ts +85 -16
  375. package/src/updater/index.ts +128 -17
  376. package/src/utils/asset-category.ts +20 -0
  377. package/src/utils/datetime-format.ts +7 -1
  378. package/src/utils/decimal-comma.ts +83 -3
  379. package/src/utils/fundamentals.ts +7 -2
  380. package/src/utils/http-proxy-response.ts +66 -9
  381. package/src/utils/plugin-proxy-hosts.json +1 -5
  382. package/src/utils/semver.ts +47 -2
  383. package/src/utils/typo-distance.ts +22 -0
  384. package/src/plugins/builtin/shared/zip-text.ts +0 -46
@@ -13,16 +13,8 @@ import {
13
13
  type ListingVenue,
14
14
  } from "../tickers/listing-venues";
15
15
  import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
16
- import {
17
- describeTickerChange,
18
- quotedListing,
19
- startTickerChangeLookup,
20
- tickerChangeLine,
21
- type CurrentListing,
22
- type TickerChange,
23
- type TickerChangeLookup,
24
- } from "../tickers/ticker-changes";
25
- import type { TickerChanges } from "../types/ticker-changes";
16
+ import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
17
+ import { loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
26
18
  import { quotedListingName } from "../tickers/listing-name";
27
19
  import { cliStyles } from "../utils/cli-output";
28
20
  import { groupByCompany, sameCompany, shortCompanyName } from "../tickers/company-name";
@@ -35,17 +27,14 @@ import {
35
27
  isUsListingExchange,
36
28
  parsePublicTickerKey,
37
29
  publicTickerKey,
38
- resolveExchangeTimeZone,
39
30
  typedExchangeCode,
40
31
  } from "../utils/exchanges";
41
32
  import { withMarketData } from "./scoped-context";
33
+ import { failWithFields } from "./errors";
42
34
  import { stripStrayEdges } from "./symbol-args";
43
35
  import { isNoProviderMessage } from "../sources/provider-errors";
44
36
  import { dashedCoinPair } from "../utils/crypto-pair";
45
37
  import { currencyPairSpelling } from "../utils/currency-pair";
46
- import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
47
- import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
48
- import { zonedDateKey } from "../utils/zoned-date-time";
49
38
  import { bareFuturesCode, futuresContractByCode } from "../market-data/futures-board";
50
39
 
51
40
  /** The `--exchange` option of every command that takes a ticker. */
@@ -260,7 +249,9 @@ export interface ListingDataRequest {
260
249
  * What became of the symbol (`loadListingFate`), for a command a listing
261
250
  * that stopped trading has nothing for (history, options): a request no
262
251
  * source serves fails with `fate.note` instead of the source's miss, and the
263
- * change line leads any listings the failure names.
252
+ * change line leads any listings the failure names. A symbol whose company
253
+ * moved, with nothing new listed under it, fails naming its new symbol
254
+ * whatever came back (`failMovedSymbol`).
264
255
  */
265
256
  fate?: () => Promise<ListingFate>;
266
257
  }
@@ -472,7 +463,7 @@ interface OtherListingsMessage {
472
463
  * The command that asks again for another listing: `gloomberb history SAN:EPA
473
464
  * --range 5Y` keeps the options. Null where the request names listings alone.
474
465
  */
475
- function retryCommand(request: ListingDataRequest, key: string): string | null {
466
+ function retryCommand(request: Pick<ListingDataRequest, "command" | "retry">, key: string): string | null {
476
467
  if (request.retry !== undefined) return request.retry?.(key) ?? null;
477
468
  const [command, ...options] = request.command.split(" ");
478
469
  return ["gloomberb", command, key, ...options].join(" ");
@@ -638,95 +629,23 @@ export function chosenListingNote(symbol: string, exchange: string, others: read
638
629
  return `${symbol} -> ${exchange}${aside ? ` (${aside})` : ""}`;
639
630
  }
640
631
 
641
- // A quote this much older than now, and stale, is a listing that stopped trading, not a quiet day.
642
- const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
643
-
644
- /**
645
- * The exchange-local date a listing last traded, when its quote is stale and
646
- * that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
647
- */
648
- function endedTradingDate(quote: Quote, now = Date.now()): string | null {
649
- const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
650
- if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
651
- const zone = resolveExchangeTimeZone(quote.listingExchangeName || quote.exchangeName) ?? "UTC";
652
- return zonedDateKey(last, zone);
653
- }
654
-
655
- /** What trades under a symbol now, from its quote: nothing when it has none or stopped long ago. */
656
- export function tradingListing(quote: Quote | null | undefined, now = Date.now()): CurrentListing | null {
657
- return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
658
- }
659
-
660
- /** "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote that stopped long ago. */
661
- export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
662
- const date = endedTradingDate(quote, now);
663
- if (!date) return null;
664
- const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
665
- return `${label}: no longer trading since ${date} (last price ${price})`;
666
- }
667
-
668
632
  /**
669
- * What became of a symbol, from what `quote` reads it by: the listing's own
670
- * quote, which says when it stopped trading, and the symbol's ticker changes
671
- * beside that quote (FB became META).
633
+ * "HFC became DINO on 2022-03-15: `gloomberb history DINO`": a symbol whose
634
+ * company moved, with the command that asks for it under its new symbol.
672
635
  */
673
- export interface ListingFate {
674
- /** The exchange-local date the listing last traded, when its quote stopped long ago (TWTR's 2022-10-27). */
675
- endedOn: string | null;
676
- /** The symbol's change as `quote` reads it; null when it has none, or was read on a listing abroad. */
677
- change: TickerChange | null;
678
- /**
679
- * The line a command with nothing to serve for the symbol says: `quote`'s
680
- * change line when its company left it ("FB now lists ...; Meta Platforms
681
- * trades as META (changed 2022-06-09)"), else, for a listing that stopped,
682
- * "TWTR stopped trading 2022-10-27; no history or options served". Null
683
- * for a symbol that trades as it always has.
684
- */
685
- note: string | null;
686
- }
687
-
688
- const NO_FATE: ListingFate = { endedOn: null, change: null, note: null };
689
- /** How long a fate whose change lookup started late waits for it. */
690
- const LATE_CHANGE_WAIT_MS = 2_000;
691
-
692
- /** `ListingFate` from a listing's quote (null when it has none) and its ticker changes. */
693
- export function describeListingFate(
694
- listing: { symbol: string; exchange?: string | null },
695
- quote: Quote | null,
696
- changes: TickerChanges | null,
697
- now = Date.now(),
698
- ): ListingFate {
699
- const symbol = listing.symbol.trim().toUpperCase();
700
- const endedOn = quote ? endedTradingDate(quote, now) : null;
701
- // A bare symbol read on a listing abroad (SXR8 -> XETRA) is not the US symbol the changes are of.
702
- const abroad = !!quote && !!bareListingNote({ symbol, request: { symbol, exchange: listing.exchange ?? "" } }, quote);
703
- const change = abroad ? null : describeTickerChange(symbol, changes, tradingListing(quote, now));
704
- const note = tickerChangeLine(change) ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
705
- return { endedOn, change, note };
636
+ export function movedSymbolLine(change: MovedTickerChange, retry: string | null): string {
637
+ return retry ? `${change.note}: \`${retry}\`` : change.note;
706
638
  }
707
639
 
708
640
  /**
709
- * `describeListingFate` for a listing: its quote is `quote` when the command
710
- * has it, else loaded; its changes come from `changes` when the command started
711
- * that lookup beside its own request, else from one started here. Fails soft:
712
- * a source that does not answer says nothing became of it.
641
+ * Fails a command that has nothing to serve for a symbol whose company
642
+ * moved (`ListingFate.moved`) with `movedSymbolLine`; `--json` reads the
643
+ * change as quote rows carry it (`tickerChange`) and the command that asks
644
+ * for the new symbol (`retry`). Exits as any failure does.
713
645
  */
714
- export async function loadListingFate(
715
- listing: { symbol: string; exchange?: string | null; request?: { symbol: string; exchange: string } },
716
- dataProvider: Pick<DataProvider, "getQuote" | "getTickerChanges">,
717
- options: { quote?: Quote | null | Promise<Quote | null>; changes?: TickerChangeLookup } = {},
718
- ): Promise<ListingFate> {
719
- const request = listing.request ?? { symbol: listing.symbol, exchange: listing.exchange ?? "" };
720
- const lookup = options.changes ?? startTickerChangeLookup(dataProvider, listing.symbol, listing.exchange);
721
- try {
722
- const [quote, changes] = await Promise.all([
723
- options.quote !== undefined ? options.quote : dataProvider.getQuote(request.symbol, request.exchange).catch(() => null),
724
- options.changes ? lookup.settle() : lookup.settle(LATE_CHANGE_WAIT_MS),
725
- ]);
726
- return describeListingFate(listing, quote, changes);
727
- } catch {
728
- return NO_FATE;
729
- }
646
+ export function failMovedSymbol(change: MovedTickerChange, request: Pick<ListingDataRequest, "command" | "retry">): never {
647
+ const retry = retryCommand(request, change.tradesAs);
648
+ return failWithFields(movedSymbolLine(change, retry), "ticker_moved", { tickerChange: change, retry });
730
649
  }
731
650
 
732
651
  /**
@@ -745,41 +664,28 @@ export function listingFateLoader(
745
664
  }
746
665
 
747
666
  /**
748
- * The lines a command says about a symbol's fate beside what it served. With
749
- * nothing served, `fate.note`. With data, `quote`'s change line when the
750
- * symbol's company left it (FB's bars are the fund listed there now), and, for
751
- * bars from before the symbol's holder took it (META's from 2021), that they
752
- * are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
667
+ * A command's load for one listing beside its fate (`listingFateLoader`): a
668
+ * symbol whose company moved serves nothing, whatever came back or failed,
669
+ * and the command fails naming its new symbol (`failMovedSymbol`). A loader
670
+ * that finds that itself (`MovedSymbolError`) fails the same way.
753
671
  */
754
- export function listingFateNotes(fate: ListingFate, served: { served: boolean; firstDate?: string | null }): string[] {
755
- if (!served.served) return fate.note ? [fate.note] : [];
756
- const { change } = fate;
757
- const first = served.firstDate?.slice(0, 10);
758
- const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
759
- return [
760
- tickerChangeLine(change),
761
- earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
762
- ].filter((line): line is string => !!line);
763
- }
764
-
765
- // The zones of the US exchanges; a venue in another is abroad.
766
- const US_TIME_ZONES = new Set(["America/New_York", "America/Chicago"]);
767
-
768
- /**
769
- * "SXR8 -> XETRA" for a bare symbol the data service read on its home listing
770
- * abroad, as it reads a symbol with no US listing whose listings belong to one
771
- * company or fund: a row that shows the symbol as typed says which listing it
772
- * is. Null for a named or saved venue, and for a venue the app does not place
773
- * outside the US (a US exchange, OTC, a futures exchange).
774
- */
775
- export function bareListingNote(
776
- listing: Pick<CliListing, "symbol" | "request">,
777
- quote: Pick<Quote, "listingExchangeName" | "exchangeName"> | null | undefined,
778
- ): string | null {
779
- if (listing.request.exchange || !quote || !/^[A-Z0-9]{1,10}$/.test(listing.symbol)) return null;
780
- const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
781
- const zone = resolveExchangeTimeZone(venue);
782
- return zone && !US_TIME_ZONES.has(zone) && !isUsListingExchange(venue) ? `${listing.symbol} -> ${venue}` : null;
672
+ export async function refuseMovedListing<T>(
673
+ fate: (() => Promise<ListingFate>) | null,
674
+ request: Pick<ListingDataRequest, "command" | "retry">,
675
+ load: () => Promise<T>,
676
+ ): Promise<T> {
677
+ let value: T;
678
+ try {
679
+ value = await load();
680
+ } catch (error) {
681
+ if (error instanceof MovedSymbolError) failMovedSymbol(error.change, request);
682
+ const moved = (await fate?.())?.moved;
683
+ if (moved) failMovedSymbol(moved, request);
684
+ throw error;
685
+ }
686
+ const moved = (await fate?.())?.moved;
687
+ if (moved) failMovedSymbol(moved, request);
688
+ return value;
783
689
  }
784
690
 
785
691
  /**
@@ -802,8 +708,9 @@ export function tradingBoardNote(
802
708
  * calls what it returned empty, and a listing's exchange is one its symbol does
803
709
  * not trade on, the command ends in that message instead of the request's own
804
710
  * "unavailable". With `request`, a bare symbol no source serves ends naming
805
- * its other listings (`otherListingsMessage`). A request that succeeds costs no
806
- * extra lookup.
711
+ * its other listings (`otherListingsMessage`), and with `request.fate` a symbol
712
+ * whose company moved ends naming its new symbol, served or not. A request
713
+ * that succeeds costs no extra lookup beyond the fate the command reads anyway.
807
714
  */
808
715
  export async function loadForListing<T>(
809
716
  listings: ListingForLoad | readonly ListingForLoad[],
@@ -823,12 +730,20 @@ export async function loadForListing<T>(
823
730
  await checkVenues();
824
731
  if (request && all.length === 1 && isNoProviderError(error)) {
825
732
  const [other, fate] = await Promise.all([otherListingsMessage(all[0]!, deps, request), request.fate?.() ?? null]);
733
+ // Nothing served either way: a company that moved is under its new symbol, before any other listing.
734
+ const moved = movedAwayChange(fate?.change);
735
+ if (moved) failMovedSymbol(moved, request);
826
736
  if (other) ctx.fail(other.message, [fate?.note, other.details].filter(Boolean).join("\n"));
827
737
  if (fate?.note) ctx.fail(`${fate.note}.`);
828
738
  }
829
739
  throw error;
830
740
  }
831
741
  if (isMiss?.(value)) await checkVenues();
742
+ // Data came back, but the symbol's company moved and nothing new lists there: the data is another security's.
743
+ if (request?.fate && all.length === 1) {
744
+ const { moved } = await request.fate();
745
+ if (moved) failMovedSymbol(moved, request);
746
+ }
832
747
  return value;
833
748
  }
834
749
 
@@ -969,10 +884,18 @@ export function listingIdentity(
969
884
 
970
885
  /** The listing's own quote, for its company name and venue; null when it has none. */
971
886
  export async function loadListingQuote(dataProvider: DataProvider, listing: Pick<CliListing, "request">): Promise<Quote | null> {
887
+ return (await loadListingQuoteAnswer(dataProvider, listing)).quote;
888
+ }
889
+
890
+ /** The listing's own quote, or the error its request failed with: "Not a ticker: PALANTIR." for a name typed as a symbol. */
891
+ export async function loadListingQuoteAnswer(
892
+ dataProvider: DataProvider,
893
+ listing: Pick<CliListing, "request">,
894
+ ): Promise<{ quote: Quote | null; error?: unknown }> {
972
895
  try {
973
- return await dataProvider.getQuote(listing.request.symbol, listing.request.exchange);
974
- } catch {
975
- return null;
896
+ return { quote: await dataProvider.getQuote(listing.request.symbol, listing.request.exchange) };
897
+ } catch (error) {
898
+ return { quote: null, error };
976
899
  }
977
900
  }
978
901
 
@@ -5,6 +5,7 @@ import { mapWithConcurrency } from "../utils/concurrency";
5
5
  import { isUsListingExchange } from "../utils/exchanges";
6
6
  import { STARTER_SYMBOLS } from "../tickers/starter-symbols";
7
7
  import { TickerNotFoundError } from "./ticker-resolution";
8
+ import { typoDistance } from "../utils/typo-distance";
8
9
 
9
10
  /** How long a suggestion waits for search; a slow search leaves the plain "not found" as it was. */
10
11
  const SUGGESTION_TIMEOUT_MS = 6_000;
@@ -39,25 +40,6 @@ function typedForm(symbol: string): string {
39
40
  return compact(symbol.split(":")[0]!.replace(/=[A-Z]?$/i, ""));
40
41
  }
41
42
 
42
- /** The edits (insert, delete, replace, swap two neighbours) between two strings, up to two; anything further apart is 2. */
43
- function editDistance(left: string, right: string): number {
44
- if (left === right) return 0;
45
- if (Math.abs(left.length - right.length) > 1) return 2;
46
- const rows: number[][] = Array.from({ length: left.length + 1 }, (_, row) => [row, ...new Array<number>(right.length).fill(0)]);
47
- for (let column = 1; column <= right.length; column += 1) rows[0]![column] = column;
48
- for (let row = 1; row <= left.length; row += 1) {
49
- for (let column = 1; column <= right.length; column += 1) {
50
- const same = left[row - 1] === right[column - 1] ? 0 : 1;
51
- let best = Math.min(rows[row - 1]![column]! + 1, rows[row]![column - 1]! + 1, rows[row - 1]![column - 1]! + same);
52
- if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) {
53
- best = Math.min(best, rows[row - 2]![column - 2]! + 1);
54
- }
55
- rows[row]![column] = best;
56
- }
57
- }
58
- return rows[left.length]![right.length]!;
59
- }
60
-
61
43
  function candidateName(candidate: TickerSearchCandidate): string {
62
44
  return candidate.detail.split(" | ")[0]?.trim() ?? "";
63
45
  }
@@ -68,7 +50,7 @@ function nearness(typed: string, candidate: TickerSearchCandidate): number | nul
68
50
  if (!wanted) return null;
69
51
  const symbol = typedForm(candidate.label || candidate.symbol);
70
52
  if (wanted === symbol || wanted === compact(candidateName(candidate))) return 0;
71
- return wanted.length >= MIN_TYPO_LENGTH && symbol.length >= MIN_TYPO_LENGTH && editDistance(wanted, symbol) === 1 ? 1 : null;
53
+ return wanted.length >= MIN_TYPO_LENGTH && symbol.length >= MIN_TYPO_LENGTH && typoDistance(wanted, symbol) === 1 ? 1 : null;
72
54
  }
73
55
 
74
56
  /**
@@ -154,7 +136,7 @@ export async function suggestUsTicker(typed: string, deps: SuggestionDeps): Prom
154
136
  const saved = (await deps.store.loadAllTickers().catch(() => [])).filter((ticker) => isUsListingExchange(ticker.metadata.exchange));
155
137
  const near = new Set([...STARTER_SYMBOLS, ...saved.map((ticker) => ticker.metadata.ticker)]
156
138
  .map((symbol) => symbol.trim().toUpperCase())
157
- .filter((symbol) => compact(symbol).length >= MIN_TYPO_LENGTH && editDistance(wanted, compact(symbol)) === 1));
139
+ .filter((symbol) => compact(symbol).length >= MIN_TYPO_LENGTH && typoDistance(wanted, compact(symbol)) === 1));
158
140
  if (near.size !== 1) return null;
159
141
  const [symbol] = near;
160
142
  const candidates = await searchCandidates(symbol!, deps);
@@ -26,6 +26,7 @@ import {
26
26
  } from "../../utils/cli-output";
27
27
  import { renderGlossaryEntries, type GlossaryLookup } from "../glossary";
28
28
  import { renderSeriesMatches, type SeriesMatch } from "../series-search";
29
+ import { typoDistance } from "../../utils/typo-distance";
29
30
 
30
31
  export interface PaneFunctionCatalog {
31
32
  panes: ReadonlyMap<string, PaneDef>;
@@ -152,6 +153,55 @@ export function findFunctionCode(registry: PaneFunctionCatalog, token: string):
152
153
  };
153
154
  }
154
155
 
156
+ /** A function code a typed one is a slip away from: what to type, and the function's name. */
157
+ export interface NearFunctionCode {
158
+ code: string;
159
+ label: string;
160
+ }
161
+
162
+ /** Typed names shorter than this are too short for one slip to leave them recognisable (`HQ` is a slip from a dozen codes). */
163
+ const MIN_CODE_TYPO_LENGTH = 3;
164
+ /**
165
+ * A word typed as a command is as likely a ticker as a slip from a code
166
+ * (`PLTR`, `META`, `QQQ`): it is read as a slip only from this length and
167
+ * when its first letter is the code's.
168
+ */
169
+ const MIN_COMMAND_WORD_TYPO_LENGTH = 4;
170
+ /** Near codes an unknown one is answered with; more than this many is no suggestion. */
171
+ const NEAR_CODE_LIMIT = 3;
172
+
173
+ /**
174
+ * The function codes one slip of the keys from a name that opens no function
175
+ * (a letter swapped, missing, extra or wrong: `RCDF` for RDCF, `HPP` for HP),
176
+ * among the mnemonics the command bar takes, each named by the code it is
177
+ * near (`DES` for Ticker Research, whose code is T). Empty when none is that
178
+ * close, the name is too short to tell, or so many are that none stands out.
179
+ * A word typed as a command (`commandWord`, `gloomberb RCDF PLTR`) is held to
180
+ * MIN_COMMAND_WORD_TYPO_LENGTH and the code's first letter, so a ticker typed
181
+ * there is not taken for a code.
182
+ */
183
+ export function nearFunctionCodes(
184
+ registry: PaneFunctionCatalog,
185
+ name: string,
186
+ { commandWord = false }: { commandWord?: boolean } = {},
187
+ ): NearFunctionCode[] {
188
+ const wanted = normalizeLookupToken(name);
189
+ if (wanted.length < (commandWord ? MIN_COMMAND_WORD_TYPO_LENGTH : MIN_CODE_TYPO_LENGTH)) return [];
190
+ const near = new Map<PaneTemplateDef, string>();
191
+ for (const template of registry.paneTemplates.values()) {
192
+ const prefix = template.shortcut?.prefix;
193
+ if (!prefix) continue;
194
+ const code = [prefix, ...template.shortcut?.aliases ?? []].find((candidate) => {
195
+ const known = normalizeLookupToken(candidate);
196
+ return (!commandWord || known[0] === wanted[0]) && typoDistance(wanted, known) === 1;
197
+ });
198
+ if (code) near.set(template, code.toUpperCase());
199
+ }
200
+ if (near.size > NEAR_CODE_LIMIT) return [];
201
+ return [...near].map(([template, code]) => ({ code, label: template.label }))
202
+ .sort((left, right) => left.code.localeCompare(right.code));
203
+ }
204
+
155
205
  function sampleArgForTemplate(template: PaneTemplateDef): string {
156
206
  switch (template.shortcut?.argKind) {
157
207
  case "ticker":
@@ -44,6 +44,12 @@ export interface ReportFreshness {
44
44
  basis?: string;
45
45
  /** Set when the dates are the start of a period: the status line then reads `Sep 2026 (monthly)`. */
46
46
  periodicity?: "monthly" | "quarterly" | "annual";
47
+ /**
48
+ * The newest bar's week or month had not ended, and the last day it covers
49
+ * so far (`asOf`, when known): the status line reads `month to date, through
50
+ * Thu 15 Oct 2026`.
51
+ */
52
+ periodToDate?: { period: "week" | "month"; through?: string };
47
53
  /** The local date of the session close `asOf` is, when the newest observation is its venue's latest close. */
48
54
  asOfClose?: string;
49
55
  /**
@@ -346,11 +352,20 @@ function declaredSources(declared: HeadlessPaneFreshness, now: number): Array<{
346
352
  });
347
353
  }
348
354
 
355
+ /** A declared bar period still forming, its last day read as a date; null for anything else. */
356
+ function readPeriodToDate(declared: HeadlessPaneFreshness["periodToDate"], now: number): { period: "week" | "month"; through: ReportTime | null } | null {
357
+ if (declared?.period !== "week" && declared?.period !== "month") return null;
358
+ const through = declared.through ? parseReportTime(declared.through) : null;
359
+ return { period: declared.period, through: through?.dateOnly && through.time <= now + FUTURE_TOLERANCE_MS ? through : null };
360
+ }
361
+
349
362
  function resolveFreshness({ declared, signals, observations, now }: FreshnessInputs): ReportFreshness {
350
363
  const sources = declaredSources(declared, now);
351
364
  // Sources date the data by their own observations, as a declared as-of does.
352
365
  const declaredAsOf = observedTime({ asOf: declared.asOf }, now, ["asOf"]) ?? newest(sources.map(({ time }) => time));
353
- const asOf = declaredAsOf ?? newest([...observations.units, ...observations.extra]);
366
+ // A bar still forming covers its period through a later day than the one it is dated by.
367
+ const periodToDate = readPeriodToDate(declared.periodToDate, now);
368
+ const asOf = declaredAsOf ?? periodToDate?.through ?? newest([...observations.units, ...observations.extra]);
354
369
  // The oldest source, not the newest, says how far behind the view can be.
355
370
  const agedFrom = sources.length ? oldest(sources.map(({ time }) => time))
356
371
  : declared.oldestAge ? oldest(observations.units) ?? asOf : null;
@@ -443,6 +458,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
443
458
  ...(partial ?? {}),
444
459
  ...(status === "not-a-feed" && declared.basis ? { basis: declared.basis } : {}),
445
460
  ...(periodicity ? { periodicity } : {}),
461
+ ...(periodToDate && !declaredAsOf ? { periodToDate: { period: periodToDate.period, ...(periodToDate.through ? { through: isoTime(periodToDate.through) } : {}) } } : {}),
446
462
  ...(asOfClose ? { asOfClose } : {}),
447
463
  ...(tradingDayMarket ? { tradingDayMarket } : {}),
448
464
  ...(market ? { market } : {}),
@@ -550,6 +566,16 @@ function periodText(date: string, periodicity: NonNullable<ReportFreshness["peri
550
566
  : `${year}`;
551
567
  }
552
568
 
569
+ /**
570
+ * A bar still forming: `month to date, through Thu 15 Oct 2026`, or with no
571
+ * day to name, the period from its start, `Oct 2026 to date` or `week of Mon
572
+ * 12 Oct 2026 to date`.
573
+ */
574
+ export function periodToDateText({ period, through }: NonNullable<ReportFreshness["periodToDate"]>, asOf: string): string {
575
+ if (through) return `${period} to date, through ${dayText(through)}`;
576
+ return period === "month" ? `${periodText(asOf, "monthly")} to date` : `week of ${dayText(asOf)} to date`;
577
+ }
578
+
553
579
  /** The period a FRED series frequency ("Monthly", "Quarterly") publishes in; null for daily and weekly series, whose dates are days. */
554
580
  export function periodicityOf(frequency: string | null | undefined): ReportFreshness["periodicity"] | null {
555
581
  const word = frequency?.trim().toLowerCase() ?? "";
@@ -674,7 +700,8 @@ function asOfText(freshness: ReportFreshness, short: boolean, local: boolean, un
674
700
  const market = freshness.tradingDayMarket;
675
701
  const period = freshness.periodicity;
676
702
  const time = parseReportTime(freshness.asOf);
677
- const asOf = period ? `${periodText(freshness.asOf, period)} (${period})`
703
+ const asOf = freshness.periodToDate ? periodToDateText(freshness.periodToDate, freshness.asOf)
704
+ : period ? `${periodText(freshness.asOf, period)} (${period})`
678
705
  : freshness.asOfClose
679
706
  ? `${tradingDayText(freshness.asOfClose, market)} close`
680
707
  : time?.dateOnly ? tradingDayText(freshness.asOf, market)
@@ -706,16 +733,22 @@ function sourcesAsOfParts(freshness: ReportFreshness, sources: readonly ReportSo
706
733
  * When the first venue opens again: its UTC time when that is later the same
707
734
  * UTC day, else its weekday at the venue (`Mon`), with the date a week out.
708
735
  */
709
- function reopenText(market: ReportMarket, now: number, local: boolean): string | null {
736
+ function reopenText(market: ReportMarket, now: number, local: boolean): { text: string; time: boolean } | null {
710
737
  if (!market.reopensAt || !market.reopensOn) return null;
711
738
  const at = Date.parse(market.reopensAt);
712
739
  if (!Number.isFinite(at)) return null;
713
740
  const iso = new Date(at).toISOString();
714
- if (iso.slice(0, 10) === new Date(now).toISOString().slice(0, 10)) return `${iso.slice(11, 16)} UTC${local ? localTimeSuffix(at) : ""}`;
715
- return at - now > WEEKDAY_ONLY_MS ? dayText(market.reopensOn) : WEEKDAYS[new Date(`${market.reopensOn}T00:00:00Z`).getUTCDay()]!;
741
+ if (iso.slice(0, 10) === new Date(now).toISOString().slice(0, 10)) return { text: `${iso.slice(11, 16)} UTC${local ? localTimeSuffix(at) : ""}`, time: true };
742
+ return { text: at - now > WEEKDAY_ONLY_MS ? dayText(market.reopensOn) : WEEKDAYS[new Date(`${market.reopensOn}T00:00:00Z`).getUTCDay()]!, time: false };
716
743
  }
717
744
 
718
- /** `markets closed until Mon`; `short` is the compact form a narrow capture falls back to (`reopens Mon`). */
745
+ /**
746
+ * `markets closed until Mon`; `short` is the compact form a narrow capture
747
+ * falls back to (`reopens Mon`). A time is always the regular session's open,
748
+ * and says so (`pre-market open · regular session opens 13:30 UTC`), so it is
749
+ * never read as when the pre-market opens, nor a US market as closed until
750
+ * then while its pre-market trades first.
751
+ */
719
752
  function marketText(market: ReportMarket, now: number, short: boolean, local: boolean): string {
720
753
  const reopen = reopenText(market, now, local);
721
754
  switch (market.state) {
@@ -724,10 +757,12 @@ function marketText(market: ReportMarket, now: number, short: boolean, local: bo
724
757
  case "after-hours":
725
758
  return "after hours";
726
759
  case "pre-market":
727
- return reopen ? `pre-market, opens ${reopen}` : "pre-market";
760
+ if (!reopen) return "pre-market open";
761
+ return short ? `pre-market · regular opens ${reopen.text}` : `pre-market open · regular session opens ${reopen.text}`;
728
762
  case "closed":
729
- if (short) return reopen ? `reopens ${reopen}` : "closed";
730
- return reopen ? `markets closed until ${reopen}` : "markets closed";
763
+ if (!reopen) return short ? "closed" : "markets closed";
764
+ if (reopen.time) return short ? `regular opens ${reopen.text}` : `markets closed · regular session opens ${reopen.text}`;
765
+ return short ? `reopens ${reopen.text}` : `markets closed until ${reopen.text}`;
731
766
  default: {
732
767
  const _exhaustive: never = market.state;
733
768
  return _exhaustive;
@@ -68,10 +68,10 @@ const QUOTES: FunctionFreshness = { free: "15 minutes delayed, crypto real-time"
68
68
  /** US option chains and the quotes behind them. */
69
69
  const OPTIONS: FunctionFreshness = { free: DELAYED, pro: REAL_TIME };
70
70
  const FX: FunctionFreshness = { free: DELAYED, pro: REAL_TIME };
71
- /** Open interest settles once a session; spot and the quotes behind gamma follow the plan. */
71
+ /** Open interest is counted once a session and reaches Gloom about two sessions late; spot and the quotes behind gamma follow the plan. */
72
72
  const OPX_DATA: FunctionFreshness = {
73
- free: "Open interest as of the prior session; spot and gamma 15 minutes delayed",
74
- pro: "Open interest as of the prior session; spot and gamma real-time",
73
+ free: "Open interest about two sessions old, its date in the footer; spot and gamma 15 minutes delayed",
74
+ pro: "Open interest about two sessions old, its date in the footer; spot and gamma real-time",
75
75
  };
76
76
  const NEWS: FunctionFreshness = { free: "12 hours delayed", pro: REAL_TIME };
77
77
  const US_SCANNER: FunctionFreshness = { free: DELAYED, pro: REAL_TIME };
@@ -175,7 +175,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
175
175
  bloomberg: ["DDIS"],
176
176
  },
177
177
  HP: {
178
- summary: "Daily open, high, low, close and volume in a scrollable table.",
178
+ summary: "Open, high, low, close and volume in a scrollable table: daily bars over 3M to start, weekly for 5Y and monthly for ALL. The newest bar is marked to date while its day, week or month runs on.",
179
179
  usage: ["HP AAPL"],
180
180
  keys: [key("t", " range")],
181
181
  data: QUOTES,
@@ -241,14 +241,14 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
241
241
  bloomberg: ["OMON"],
242
242
  },
243
243
  OPX: {
244
- summary: "Open interest by strike for one expiry with max pain and spot marked, every expiry's open interest, put/call ratio and max pain, and dealer gamma (GEX) by strike with its flip level and a range for the dealer assumption. MAXPAIN opens it too.",
244
+ summary: "Open interest by strike for one expiry with max pain and spot marked, every expiry's open interest, put/call ratio, max pain and net dealer gamma, and dealer gamma (GEX) by strike with its flip level, call and put walls and a range for the dealer assumption. MAXPAIN opens it too.",
245
245
  usage: ["OPX", "OPX SPY", "OPX SPX"],
246
246
  keys: [TABS, OPEN],
247
247
  data: OPX_DATA,
248
248
  bloomberg: ["OPX"],
249
249
  },
250
250
  GEX: {
251
- summary: "Net dealer gamma by strike across every expiry or one, with the total, the flip level and a range for the dealer assumption. The GEX tab of OPX.",
251
+ summary: "Net dealer gamma by strike across every expiry or one, with the total, the flip level, the call and put walls and a range for the dealer assumption. The GEX tab of OPX.",
252
252
  usage: ["GEX", "GEX SPY", "GEX SPX"],
253
253
  keys: [TABS],
254
254
  data: OPX_DATA,
@@ -756,7 +756,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
756
756
  bloomberg: ["GLCO"],
757
757
  },
758
758
  CTM: {
759
- summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote.",
759
+ summary: "A futures root's listed contracts as a curve against a week and a month ago, with roll yield, contango or backwardation, and each contract's price, open interest and volume. Takes a FUT root, VX or a CME crypto root (BTC, ETH, SOL, XRP), which also shows each contract's premium to spot and annualised basis against the USD pair quote: Ann Basis = (Last / spot - 1) × 365 / days to expiry, simple actual/365.",
760
760
  usage: ["CTM GC", "CTM BTC"],
761
761
  keys: [key("d", "ate"), key("c", "urrent"), STEP],
762
762
  data: same("Delayed, usually 10 minutes; VIX at settlement"),
@@ -1068,8 +1068,8 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
1068
1068
  bloomberg: ["13F"],
1069
1069
  },
1070
1070
  INS: {
1071
- summary: "Insider buys, sells, awards and gifts from Form 4 filings, with insider name, size and price. US equities.",
1072
- usage: ["INS NVDA"],
1071
+ summary: "Insider buys, sells, awards and gifts from Form 4 filings, with insider name, size and price. US equities. Alone it ranks the companies whose insiders are buying in the open market, with why.",
1072
+ usage: ["INS NVDA", "INS"],
1073
1073
  keys: [OPEN, key("f", "ilter")],
1074
1074
  data: AS_FILED,
1075
1075
  bloomberg: ["INS"],
@@ -286,6 +286,7 @@ function serializeBundleSection(
286
286
  title: section.title,
287
287
  columns: serializableColumns(columns),
288
288
  rows: section.rows,
289
+ ...(section.unit ? { unit: section.unit } : {}),
289
290
  ...(section.empty && section.rows.length === 0 ? { empty: section.empty } : {}),
290
291
  };
291
292
  }
@@ -410,9 +411,20 @@ function seriesPointExportDate(series: HeadlessSeries, date: SeriesPointDate): s
410
411
  return isDateOnly(date) ? date as string : new Date(time).toISOString().replace(".000Z", "Z");
411
412
  }
412
413
 
414
+ /**
415
+ * A latest value as the series report prints it: percentages and multiples
416
+ * (P/E, EV/EBITDA, one series over another) to two decimals, which a multiple
417
+ * under 0.1 trades for three significant digits so a small ratio is not
418
+ * flattened to 0.03. The exports keep every digit. Any other unit prints as
419
+ * loaded, without float noise.
420
+ */
413
421
  function seriesValue(series: HeadlessSeries, value: number | null): string {
414
422
  if (value == null) return "-";
415
- return series.unit?.trim() === "%" && Number.isFinite(value) ? value.toFixed(2) : displayNumber(value);
423
+ if (!Number.isFinite(value)) return displayNumber(value);
424
+ const unit = series.unit?.trim();
425
+ if (unit === "%") return value.toFixed(2);
426
+ if (unit?.toLowerCase() === "x") return value !== 0 && Math.abs(value) < 0.1 ? value.toPrecision(3) : value.toFixed(2);
427
+ return displayNumber(value);
416
428
  }
417
429
 
418
430
  function renderRows(
@@ -467,7 +479,7 @@ function renderBundle(
467
479
  renderSection(section.title),
468
480
  "rows" in section && section.rows
469
481
  ? renderRows(section.textRows ?? section.rows, section.textColumns ?? section.columns, definition.columns, section.empty)
470
- : renderEntries(section.entries),
482
+ : renderEntries(section.textEntries ?? section.entries),
471
483
  ]);
472
484
  }
473
485
 
@@ -618,11 +630,11 @@ export function headlessReportTables(
618
630
  break;
619
631
  }
620
632
  case "bundle":
621
- tables = (result as HeadlessBundleResult).sections.map((section) => (
622
- "rows" in section && section.rows
623
- ? exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows)
624
- : exportEntriesTable(section.title, section.entries)
625
- ));
633
+ tables = (result as HeadlessBundleResult).sections.map((section) => {
634
+ if (!("rows" in section && section.rows)) return exportEntriesTable(section.title, section.entries);
635
+ const table = exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows);
636
+ return section.unit?.trim() ? { ...table, identity: { currencyOrUnit: section.unit.trim() } } : table;
637
+ });
626
638
  break;
627
639
  case "series": {
628
640
  const seriesResult = result as HeadlessSeriesResult;