@gloom-sh/terminal 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
package/src/i18n/zh-cn.ts CHANGED
@@ -1129,6 +1129,19 @@ export const zhCN: Record<string, string> = {
1129
1129
  "{symbol}'s figures are not collected yet; the next read stores them.": "尚未采集 {symbol} 的数据;下次读取时会保存。",
1130
1130
  "{symbol} files with the SEC as a trust or pool, not as an open-end fund, and its sponsor's site is not one Gloom reads.": "{symbol} 在 SEC 登记为信托或资金池,而非开放式基金,且 Gloom 不读取其发起人的网站。",
1131
1131
  "No figures for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets list none for it yet.": "暂无 {symbol} 的数据:Gloom 不读取其发起人的网站,SEC 的招募说明书和 N-PORT 数据集中也尚未列出。",
1132
+ "est. {range}": "估计 {range}",
1133
+ "Shares outstanding": "流通份额",
1134
+ "1M flows": "1个月资金流",
1135
+ "Structure": "结构",
1136
+ "Closed-end fund": "封闭式基金",
1137
+ "BDC": "BDC",
1138
+ "est.": "估计",
1139
+ "{count} shares": "{count} 份",
1140
+ "No figures for {symbol}: it files with the SEC as a closed-end fund or BDC, but neither the filings Gloom reads nor its sponsor's site state any for it yet.": "暂无 {symbol} 的数据:它以封闭式基金或 BDC 身份向 SEC 申报,但 Gloom 读取的申报文件和其发起人的网站均尚未列出任何数据。",
1141
+ "No NAV for {symbol}: {publisher} publishes figures for it, but no NAV.": "暂无 {symbol} 的资产净值:{publisher} 公布了其数据,但未公布资产净值。",
1142
+ "No NAV for {symbol}: its SEC filings state net assets but no NAV per share, and its sponsor's NAV is not one Gloom reads.": "暂无 {symbol} 的资产净值:其 SEC 申报文件列出了净资产,但未列出每股资产净值,Gloom 也不读取其发起人公布的资产净值。",
1143
+ "No NAV for {symbol}: its sponsor's site is not one Gloom reads, and a trust files no NAV with the SEC's fund data sets.": "暂无 {symbol} 的资产净值:Gloom 不读取其发起人的网站,信托也不在 SEC 的基金数据集中申报资产净值。",
1144
+ "No NAV for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets state no NAV.": "暂无 {symbol} 的资产净值:Gloom 不读取其发起人的网站,SEC 的招募说明书和 N-PORT 数据集中也没有资产净值。",
1132
1145
  "No figures for {symbol}: it lists as a fund, but the SEC's lists do not name it yet and its sponsor's site is not one Gloom reads.": "暂无 {symbol} 的数据:它以基金身份上市,但 SEC 的名单尚未收录,且 Gloom 不读取其发起人的网站。",
1133
1146
  "No fund facts for {symbol}: it is not on the SEC's lists of US-listed funds.": "{symbol} 没有基金概况:它不在 SEC 的美国上市基金名单中。",
1134
1147
  "No fund facts for {symbol} yet.": "{symbol} 暂无基金概况。",
@@ -1474,8 +1487,11 @@ export const zhCN: Record<string, string> = {
1474
1487
  "Answers cite filings, calls and news.": "回答引用公告、电话会议和新闻。",
1475
1488
  "{ticker} is open. {shortcut}, then AP, adds more.": "{ticker} 已打开。{shortcut} 然后输入 AP 可添加更多。",
1476
1489
  "Later: {shortcut}, then AP.": "之后:{shortcut},然后输入 AP。",
1477
- "Enter: next field, then add.": "Enter:下一字段,最后添加。",
1478
- "Enter to add a ticker.": "按 Enter 添加代码。",
1490
+ "Enter: next field, then add": "Enter:下一字段,最后添加",
1491
+ "Add one ticker to continue": "添加一个代码即可继续",
1492
+ "Add one ticker to continue.": "添加一个代码即可继续。",
1493
+ "Esc, then Enter: continue": "Esc,然后 Enter:继续",
1494
+ "Enter to add a ticker": "按 Enter 添加代码",
1479
1495
  " already added": " 已添加",
1480
1496
  "Credentials stay on this device.": "凭据仅保存在此设备上。",
1481
1497
  "Ask Gloom": "Ask Gloom",
package/src/i18n/zh-tw.ts CHANGED
@@ -1129,6 +1129,19 @@ export const zhTW: Record<string, string> = {
1129
1129
  "{symbol}'s figures are not collected yet; the next read stores them.": "尚未蒐集 {symbol} 的數據;下次讀取時會儲存。",
1130
1130
  "{symbol} files with the SEC as a trust or pool, not as an open-end fund, and its sponsor's site is not one Gloom reads.": "{symbol} 在 SEC 登記為信託或資金池,而非開放式基金,且 Gloom 不讀取其發起人的網站。",
1131
1131
  "No figures for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets list none for it yet.": "暫無 {symbol} 的數據:Gloom 不讀取其發起人的網站,SEC 的公開說明書與 N-PORT 資料集中也尚未列出。",
1132
+ "est. {range}": "估計 {range}",
1133
+ "Shares outstanding": "流通單位數",
1134
+ "1M flows": "1個月資金流",
1135
+ "Structure": "結構",
1136
+ "Closed-end fund": "封閉式基金",
1137
+ "BDC": "BDC",
1138
+ "est.": "估計",
1139
+ "{count} shares": "{count} 單位",
1140
+ "No figures for {symbol}: it files with the SEC as a closed-end fund or BDC, but neither the filings Gloom reads nor its sponsor's site state any for it yet.": "暫無 {symbol} 的數據:它以封閉式基金或 BDC 身分向 SEC 申報,但 Gloom 讀取的申報文件與其發起人的網站均尚未列出任何數據。",
1141
+ "No NAV for {symbol}: {publisher} publishes figures for it, but no NAV.": "暫無 {symbol} 的資產淨值:{publisher} 公布了其數據,但未公布資產淨值。",
1142
+ "No NAV for {symbol}: its SEC filings state net assets but no NAV per share, and its sponsor's NAV is not one Gloom reads.": "暫無 {symbol} 的資產淨值:其 SEC 申報文件列出了淨資產,但未列出每股資產淨值,Gloom 也不讀取其發起人公布的資產淨值。",
1143
+ "No NAV for {symbol}: its sponsor's site is not one Gloom reads, and a trust files no NAV with the SEC's fund data sets.": "暫無 {symbol} 的資產淨值:Gloom 不讀取其發起人的網站,信託也不在 SEC 的基金資料集中申報資產淨值。",
1144
+ "No NAV for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets state no NAV.": "暫無 {symbol} 的資產淨值:Gloom 不讀取其發起人的網站,SEC 的公開說明書與 N-PORT 資料集中也沒有資產淨值。",
1132
1145
  "No figures for {symbol}: it lists as a fund, but the SEC's lists do not name it yet and its sponsor's site is not one Gloom reads.": "暫無 {symbol} 的數據:它以基金身分上市,但 SEC 的名單尚未收錄,且 Gloom 不讀取其發起人的網站。",
1133
1146
  "No fund facts for {symbol}: it is not on the SEC's lists of US-listed funds.": "{symbol} 沒有基金概況:它不在 SEC 的美國上市基金名單中。",
1134
1147
  "No fund facts for {symbol} yet.": "{symbol} 暫無基金概況。",
@@ -1474,8 +1487,11 @@ export const zhTW: Record<string, string> = {
1474
1487
  "Answers cite filings, calls and news.": "回答引用申報、法說會和新聞。",
1475
1488
  "{ticker} is open. {shortcut}, then AP, adds more.": "{ticker} 已開啟。{shortcut} 然後輸入 AP 可新增更多。",
1476
1489
  "Later: {shortcut}, then AP.": "之後:{shortcut},然後輸入 AP。",
1477
- "Enter: next field, then add.": "Enter:下一欄位,最後新增。",
1478
- "Enter to add a ticker.": "按 Enter 新增代號。",
1490
+ "Enter: next field, then add": "Enter:下一欄位,最後新增",
1491
+ "Add one ticker to continue": "新增一個代號即可繼續",
1492
+ "Add one ticker to continue.": "新增一個代號即可繼續。",
1493
+ "Esc, then Enter: continue": "Esc,然後 Enter:繼續",
1494
+ "Enter to add a ticker": "按 Enter 新增代號",
1479
1495
  " already added": " 已新增",
1480
1496
  "Credentials stay on this device.": "憑證僅保存在此裝置上。",
1481
1497
  "Ask Gloom": "Ask Gloom",
@@ -41,9 +41,9 @@ export function ratioCount(ratio: number): string {
41
41
  return Number.isInteger(ratio) ? String(ratio) : String(Number(ratio.toPrecision(6)));
42
42
  }
43
43
 
44
- /** What a home line says of its receipt: `1 GGAL ADR = 10 of these shares`. */
44
+ /** What a home line says of its receipt: `1 GGAL ADR = 10 of these shares`, and `1 of these shares` for one. */
45
45
  function homeLineRatioText(pair: DepositaryPair): string {
46
- return `1 ${pair.receipt} ADR = ${ratioCount(pair.ratio)} of these share${pair.ratio === 1 ? "" : "s"}`;
46
+ return `1 ${pair.receipt} ADR = ${ratioCount(pair.ratio)} of these shares`;
47
47
  }
48
48
 
49
49
  /**
@@ -0,0 +1,50 @@
1
+ import { apiClient } from "../api-client";
2
+ import type { FundFactsPayload, FundFlows } from "../api-client/fund-facts";
3
+ import { isBondFactsShape } from "./bond-funds";
4
+
5
+ /**
6
+ * A fund facts answer as every reader takes it (the ETF pane, DES, CMP,
7
+ * `ticker`, `fundamentals`): each figure dated and published, the parts a
8
+ * server before them leaves out still readable, anything malformed refused.
9
+ */
10
+
11
+ const fact = (value: unknown) => value === null || (typeof value === "object" && value !== null && "value" in value && "source" in value);
12
+ const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
13
+
14
+ function isMetalHoldingsShape(value: FundFactsPayload["metalHoldings"]): boolean {
15
+ return value == null || ((value.metal === "gold" || value.metal === "silver") && typeof value.asOf === "string" && finite(value.ounces)
16
+ && (value.tonnes === null || finite(value.tonnes)) && typeof value.source?.publisher === "string"
17
+ && (value.change === null || (typeof value.change?.since === "string" && finite(value.change.ounces))));
18
+ }
19
+
20
+ /** Absent from servers before it; otherwise a dated factor with its reset period. */
21
+ const leverageShape = (value: FundFactsPayload["leverage"]) => value === undefined || value === null
22
+ || (fact(value) && typeof value.value === "number" && Number.isFinite(value.value) && (value.reset === "daily" || value.reset === "monthly"));
23
+
24
+ /** A NAV's estimate range, when the sponsor published one: absent, or two ends in order. */
25
+ const estimateShape = (nav: FundFactsPayload["nav"]) => !nav?.estimate
26
+ || (finite(nav.estimate.low) && finite(nav.estimate.high) && nav.estimate.low <= nav.estimate.high);
27
+
28
+ /** Absent from servers before them; otherwise a dated count, and a change between two dated counts. */
29
+ const sharesShape = (value: FundFactsPayload["sharesOutstanding"]) => value == null || (fact(value) && finite(value.value));
30
+ const flowsShape = (value: FundFlows | null | undefined) => value == null
31
+ || (finite(value.shares) && (value.value === null || finite(value.value)) && typeof value.from === "string" && typeof value.asOf === "string");
32
+
33
+ export function validateFundFacts(data: FundFactsPayload): FundFactsPayload {
34
+ if (!data || typeof data.ticker !== "string" || typeof data.covered !== "boolean"
35
+ || ![data.expenseRatio, data.nav, data.premiumDiscount, data.netAssets, data.sponsor, data.inception, data.index].every(fact)
36
+ || !estimateShape(data.nav) || !sharesShape(data.sharesOutstanding) || !flowsShape(data.flows)
37
+ || !isBondFactsShape(data.bond) || !leverageShape(data.leverage) || !isMetalHoldingsShape(data.metalHoldings)) {
38
+ throw new Error("Unreadable fund facts response");
39
+ }
40
+ return data;
41
+ }
42
+
43
+ /** `GET /cloud/fund-facts/:ticker`, read and checked. */
44
+ export async function readFundFacts(
45
+ symbol: string,
46
+ client: Pick<typeof apiClient, "getFundFacts"> = apiClient,
47
+ signal?: AbortSignal,
48
+ ): Promise<FundFactsPayload> {
49
+ return validateFundFacts(await (signal ? client.getFundFacts(symbol, { signal }) : client.getFundFacts(symbol)));
50
+ }
@@ -4,6 +4,7 @@ import type { Quote } from "../../types/financials";
4
4
  import { canonicalExchange } from "../../utils/exchanges";
5
5
  import { isFiniteNumber } from "../../utils/guards";
6
6
  import { formatUtcDayTime } from "../../utils/utc-time";
7
+ import { fxPrintTradingDay } from "../../utils/fx-market-hours";
7
8
  import { zonedDateKey } from "../../utils/zoned-date-time";
8
9
  import { futuresChangeBasis, futuresSettleText, isFuturesInstrument } from "../futures-contract";
9
10
  import { quoteTradingDay } from "../quotes/day-range";
@@ -94,6 +95,18 @@ function isCurrencyPair(quote: Quote): boolean {
94
95
  || kindFromListingSyntax(quote.symbol, quote.listingExchangeName || quote.exchangeName) === "currency";
95
96
  }
96
97
 
98
+ /**
99
+ * The trading day a quote describes (quoteTradingDay); for a currency pair,
100
+ * whose venue has no clock, the FX trading day of its print, which turns at
101
+ * 17:00 New York, so every pair on a board is dated alike. Null for a pair's
102
+ * print made while FX is shut, which belongs to no trading day.
103
+ */
104
+ function tradingDayOf(quote: Quote): string | null {
105
+ const day = quoteTradingDay(quote);
106
+ if (day || !isCurrencyPair(quote) || !Number.isFinite(quote.lastUpdated) || quote.lastUpdated <= 0) return day;
107
+ return fxPrintTradingDay(quote.lastUpdated);
108
+ }
109
+
97
110
  /** A coin on the spot crypto venue: its day is the UTC day. */
98
111
  function isSpotCoin(quote: Quote): boolean {
99
112
  return quoteVenue(quote) === "CCC";
@@ -154,7 +167,7 @@ export function isChangeBasisDatedWithoutCloses(quote: Quote, now = Date.now()):
154
167
  * nothing dates it.
155
168
  */
156
169
  export function changeBasisSessionDate(quote: Quote): string | null {
157
- const date = regularSessionDisplayDate(quote) ?? quoteTradingDay(quote);
170
+ const date = regularSessionDisplayDate(quote) ?? tradingDayOf(quote);
158
171
  return date && ISO_DATE.test(date) ? date : null;
159
172
  }
160
173
 
@@ -179,7 +192,7 @@ export function quoteChangeBasis(
179
192
  if (!display || !isFiniteNumber(display.change)) return null;
180
193
  if (quote.changeBasis === "24h" || isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return { kind: "24h" };
181
194
  if (isSpotCoin(quote)) return { kind: "utc day" };
182
- const sessionDate = (session === "active" ? quote.changeSessionDate : regularSessionDisplayDate(quote)) ?? quoteTradingDay(quote);
195
+ const sessionDate = (session === "active" ? quote.changeSessionDate : regularSessionDisplayDate(quote)) ?? tradingDayOf(quote);
183
196
  if (isFuturesInstrument(quote.symbol, quote.instrumentType)) {
184
197
  const basis = futuresChangeBasis(terms ?? { settlement: null }, { ...display, sessionDate }, quoteVenue(quote));
185
198
  return basis ? { kind: basis.kind, date: basis.session } : { kind: "previous close" };
@@ -211,7 +224,7 @@ export function quoteSettleText(
211
224
  export function quotePreviousCloseDate(quote: Quote | null | undefined): string | null {
212
225
  if (!quote || !isFiniteNumber(quote.previousClose) || quote.changeBasis != null) return null;
213
226
  if (isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return null;
214
- const sessionDate = quote.changeSessionDate ?? quoteTradingDay(quote);
227
+ const sessionDate = quote.changeSessionDate ?? tradingDayOf(quote);
215
228
  if (!sessionDate || !ISO_DATE.test(sessionDate)) return null;
216
229
  if (isSpotCoin(quote)) return dayBefore(sessionDate);
217
230
  if (isFuturesInstrument(quote.symbol, quote.instrumentType)) {
@@ -628,6 +628,31 @@ function currencyMarks(currency: string, normalizedCurrency: string, code = fals
628
628
  return code ? { prefix: "", suffix: ` ${normalizedCurrency}` } : { prefix: getCurrencySymbol(normalizedCurrency), suffix: "" };
629
629
  }
630
630
 
631
+ /**
632
+ * A London line shown in pounds and served in the pence it trades in. The
633
+ * code is compared in its own case: `GBp` (and `GBX`) is a price already in
634
+ * pence, which says so itself (630.8p) and needs no pence figure beside it.
635
+ */
636
+ export function isPenceQuotedLine(quote: Pick<Quote, "currency" | "providerPriceDivisor"> | null | undefined): boolean {
637
+ return quote?.currency?.trim() === "GBP" && quote.providerPriceDivisor === 100;
638
+ }
639
+
640
+ /**
641
+ * A London line's price in the pence it trades in, as the exchange and its
642
+ * announcements write it: `630.8p` for £6.308, with the decimals of a penny
643
+ * the pounds are shown to (`poundDecimals`) beyond two. Null for a price not
644
+ * quoted in pence.
645
+ */
646
+ export function penceFigure(
647
+ value: number | null | undefined,
648
+ quote: Pick<Quote, "currency" | "providerPriceDivisor"> | null | undefined,
649
+ poundDecimals: number,
650
+ ): string | null {
651
+ if (value == null || !Number.isFinite(value) || !isPenceQuotedLine(quote)) return null;
652
+ const digits = Math.max(0, Math.min(4, poundDecimals - 2));
653
+ return `${getNumberFormatter(digits, digits, true).format(value * 100)}${minorUnitSign("GBp")}`;
654
+ }
655
+
631
656
  export function formatMarketPriceWithCurrency(
632
657
  value: number | undefined,
633
658
  currency = "USD",
@@ -32,7 +32,8 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
32
32
  TSXV: 9 * 60 + 30,
33
33
  CSE: 9 * 60 + 30,
34
34
  FWB2: 8 * 60,
35
- XETRA: 8 * 60,
35
+ // Continuous trading opens at 09:00 Berlin time, after the opening auction; before it is pre-trading.
36
+ XETRA: 9 * 60,
36
37
  LSE: 8 * 60,
37
38
  EPA: 9 * 60,
38
39
  AMS: 9 * 60,
@@ -1,4 +1,5 @@
1
1
  import { isRoundTheClockCoin } from "../utils/crypto-pair";
2
+ import { currencyPairCode, isCurrencyCode } from "../utils/currency-pair";
2
3
  import { parsePublicTickerKey } from "../utils/exchanges";
3
4
  import { parseFuturesGeneric } from "../utils/futures-generic";
4
5
 
@@ -34,23 +35,24 @@ const FUTURES_ALTERNATIVES: Readonly<Record<string, ListedOptionsAlternative>> =
34
35
  ZN: { symbol: "IEF", name: "iShares 7-10 Year Treasury Bond ETF" },
35
36
  "6E": { symbol: "FXE", name: "Invesco CurrencyShares Euro Trust" },
36
37
  "6J": { symbol: "FXY", name: "Invesco CurrencyShares Japanese Yen Trust" },
38
+ "6B": { symbol: "FXB", name: "Invesco CurrencyShares British Pound Sterling Trust" },
37
39
  BTC: { symbol: "IBIT", name: "iShares Bitcoin Trust" },
38
40
  ETH: { symbol: "ETHA", name: "iShares Ethereum Trust" },
39
41
  };
40
42
 
41
43
  const MONTH_CODE_CONTRACT = /^([A-Z0-9]{1,3}?)[FGHJKMNQUVXZ]\d{1,4}$/;
42
44
 
43
- /** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. */
44
- function futuresRoot(symbol: string): string | null {
45
+ /** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. `known` says which roots a bare contract may carry. */
46
+ function futuresRoot(symbol: string, known: (root: string) => boolean = (root) => !!FUTURES_ALTERNATIVES[root]): string | null {
45
47
  const ticker = symbol.trim().toUpperCase().replace(/[.:][A-Z]+$/, "");
46
48
  if (ticker.endsWith("=F")) {
47
49
  const body = ticker.slice(0, -2);
48
- return FUTURES_ALTERNATIVES[body] ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
50
+ return known(body) ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
49
51
  }
50
52
  const generic = parseFuturesGeneric(ticker);
51
53
  if (generic) return generic.root;
52
54
  const contract = MONTH_CODE_CONTRACT.exec(ticker)?.[1];
53
- return contract && FUTURES_ALTERNATIVES[contract] ? contract : null;
55
+ return contract && known(contract) ? contract : null;
54
56
  }
55
57
 
56
58
  /** The listed fund whose options stand in for an instrument with no chain, if there is one. */
@@ -127,3 +129,78 @@ export function coinOptionsAlias(code: string) {
127
129
  return pair ? { symbol: pair, note: `${key} read as ${pair}, the coin's options. For the US fund that trades as ${key}: gloomberb fn ${code} ${key}:ARCA` } : null;
128
130
  };
129
131
  }
132
+
133
+ /**
134
+ * The CME currency futures, each as the pair it settles on, in the order the
135
+ * pair is quoted: 6E is EUR/USD, 6J USD/JPY. No chain of options on any of
136
+ * them is served here, as none is on the spot pair.
137
+ */
138
+ const FX_FUTURES_PAIRS: Readonly<Record<string, string>> = {
139
+ "6E": "EURUSD", "6J": "USDJPY", "6B": "GBPUSD", "6A": "AUDUSD", "6C": "USDCAD", "6S": "USDCHF", "6N": "NZDUSD", "6M": "USDMXN",
140
+ };
141
+
142
+ /** The US-listed currency funds whose options stand in for an FX pair's volatility, by the currency they hold. */
143
+ const FX_VOL_FUNDS: Readonly<Record<string, string>> = { EUR: "FXE", JPY: "FXY", GBP: "FXB" };
144
+ /** The fund that holds the dollar against a basket, for a pair none of the single-currency funds holds. */
145
+ const DOLLAR_FUND = "UUP";
146
+ const FX_VOL_FUND_LIST = [...Object.values(FX_VOL_FUNDS), DOLLAR_FUND];
147
+
148
+ /** The pair a symbol is: a spot pair in any spelling (EURUSD=X, EUR/USD), or a currency future (6E=F, 6EZ26, 6E1) as the pair it settles on; null for anything else. */
149
+ export function fxVolatilityPair(symbol: string, exchange?: string): string | null {
150
+ const spot = currencyPairCode(symbol, exchange);
151
+ // Metals quoted against a currency (XAUUSD=X) are no currency pair.
152
+ if (spot) return isCurrencyCode(spot.slice(0, 3)) && isCurrencyCode(spot.slice(3)) ? spot : null;
153
+ const root = futuresRoot(symbol, (candidate) => !!FX_FUTURES_PAIRS[candidate]);
154
+ return root ? FX_FUTURES_PAIRS[root] ?? null : null;
155
+ }
156
+
157
+ /**
158
+ * The listed fund that stands in for an FX pair's volatility: the one that
159
+ * holds the pair's euro, yen or pound, else the dollar index fund.
160
+ */
161
+ function fxVolatilityFunds(pair: string): string[] {
162
+ const held = [pair.slice(0, 3), pair.slice(3)].map((code) => FX_VOL_FUNDS[code]).filter((fund): fund is string => !!fund);
163
+ return held.length > 0 ? held : FX_VOL_FUND_LIST;
164
+ }
165
+
166
+ /**
167
+ * What exists instead for the volatility of an FX pair or currency future,
168
+ * worded as one line for the CLI: OTC FX volatility is not served, the
169
+ * currency funds' options are the nearest listed thing (the one that holds
170
+ * the pair's euro, yen or pound, else any of them), and HVT reads the pair's
171
+ * realized volatility. Null for anything that is no FX pair. `code` is the
172
+ * function asked (OVDV), `symbol` as typed.
173
+ */
174
+ function fxVolatilityAnswer(code: string, symbol: string): string | null {
175
+ const pair = fxVolatilityPair(symbol);
176
+ if (!pair) return null;
177
+ const typed = symbol.trim().toUpperCase();
178
+ const funds = fxVolatilityFunds(pair);
179
+ // VCA screens every fund; the others read the pair's own, or the dollar's when it has none.
180
+ const read = code === "VCA" ? funds.join(",") : funds === FX_VOL_FUND_LIST ? DOLLAR_FUND : funds[0]!;
181
+ return `OTC FX volatility is not covered for ${typed}. Currency-ETF options are the listed proxy: `
182
+ + `${funds.join(", ")} (gloom fn ${code} ${read}). Realized volatility: gloom fn HVT ${typed}.`;
183
+ }
184
+
185
+ /**
186
+ * The same for a pane, which has no commands to give: what is not covered,
187
+ * the funds whose options stand in, and HVT for realized volatility.
188
+ */
189
+ export function fxVolatilityNote(symbol: string, exchange?: string): string | null {
190
+ const pair = fxVolatilityPair(symbol, exchange);
191
+ if (!pair) return null;
192
+ return `OTC FX volatility is not covered for ${symbol.trim().toUpperCase()}. Currency-ETF options are the listed proxy: `
193
+ + `${fxVolatilityFunds(pair).join(", ")}. HVT shows the pair's realized volatility.`;
194
+ }
195
+
196
+ /**
197
+ * A volatility function's `refuseSymbol` for FX (OVDV, VCA, HIVG): an FX pair
198
+ * or currency future is answered before any request, with one line saying
199
+ * OTC FX volatility is not covered and what to read instead.
200
+ */
201
+ export function fxVolatilityRefusal(code: string) {
202
+ return (key: string): { message: string; details?: string } | null => {
203
+ const message = fxVolatilityAnswer(code, key);
204
+ return message ? { message } : null;
205
+ };
206
+ }
@@ -1,7 +1,7 @@
1
1
  import { logFailure } from "../utils/log-failure";
2
2
  import type { NewsCapability } from "../capabilities";
3
3
  import type { ConnectionHealthRegistry } from "../core/connection-health";
4
- import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
4
+ import type { NewsAccess, NewsArticle, NewsQuery, NewsQueryState } from "./types";
5
5
  import { isAppVisible, subscribeAppVisibility } from "../state/app/activity";
6
6
  import {
7
7
  DEFAULT_GLOBAL_QUERY,
@@ -28,6 +28,18 @@ export interface NewsServiceOptions {
28
28
  connectionHealth?: ConnectionHealthRegistry;
29
29
  /** Defaults to the app's visibility; injectable for tests. */
30
30
  visibility?: { isVisible(): boolean; subscribe(listener: () => void): () => void };
31
+ /**
32
+ * The session the sources read news with. Its key changes when what the
33
+ * service answers changes: a session restored at launch, a sign-in or
34
+ * sign-out, an upgrade. Every open query is then read again and its stories
35
+ * replaced, so a feed read without the session is not left on screen.
36
+ */
37
+ session?: NewsSessionSignal;
38
+ }
39
+
40
+ export interface NewsSessionSignal {
41
+ key(): string;
42
+ subscribe(listener: () => void): () => void;
31
43
  }
32
44
 
33
45
  export type NewsQueryListener = (state: NewsQueryState) => void;
@@ -50,17 +62,30 @@ interface SourceFetchResult {
50
62
  failedSourceIds: string[];
51
63
  errors: string[];
52
64
  nextCursor: string | null;
65
+ access: NewsAccess | null;
53
66
  }
54
67
 
55
68
  interface NewsQueryEntry {
56
69
  query: NewsQuery;
57
70
  state: NewsQueryState;
58
71
  inFlight: Promise<NewsQueryState> | null;
72
+ /** The session generation the read in flight went out under. */
73
+ inFlightGeneration: number;
59
74
  loadMoreInFlight: Promise<void> | null;
75
+ loadMoreGeneration: number;
76
+ /** The session generation the stories on screen were read under. */
77
+ generation: number;
60
78
  refs: number;
61
79
  lastAccessedAt: number;
62
80
  }
63
81
 
82
+ /** Held back beats live: one source the service held back is enough to say the list is not the live wire. */
83
+ function combineNewsAccess(values: ReadonlyArray<NewsAccess | null | undefined>): NewsAccess | null {
84
+ if (values.includes("delayed")) return "delayed";
85
+ if (values.includes("realtime")) return "realtime";
86
+ return null;
87
+ }
88
+
64
89
  function newsCapabilityPriority(source: NewsCapability): number {
65
90
  return source.priority ?? 1000;
66
91
  }
@@ -85,6 +110,12 @@ export class NewsService {
85
110
  private readonly visibility: NonNullable<NewsServiceOptions["visibility"]>;
86
111
  private lastPollAt = 0;
87
112
  private unsubscribeVisibility: (() => void) | null = null;
113
+ private readonly session?: NewsSessionSignal;
114
+ private sessionKey: string | null = null;
115
+ /** Bumped when the session changes; a read from an older generation is dropped. */
116
+ private sessionGeneration = 0;
117
+ private sessionRefetchQueued = false;
118
+ private unsubscribeSession: (() => void) | null = null;
88
119
 
89
120
  constructor(options: NewsServiceOptions = {}) {
90
121
  const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
@@ -94,6 +125,7 @@ export class NewsService {
94
125
  this.now = options.now ?? Date.now;
95
126
  this.connectionHealth = options.connectionHealth;
96
127
  this.visibility = options.visibility ?? { isVisible: isAppVisible, subscribe: subscribeAppVisibility };
128
+ this.session = options.session;
97
129
  }
98
130
 
99
131
  register(source: NewsCapability): () => void {
@@ -125,6 +157,10 @@ export class NewsService {
125
157
  this.clearPollTimer();
126
158
  this.scheduleNextPoll();
127
159
  });
160
+ if (this.session) {
161
+ this.sessionKey = this.session.key();
162
+ this.unsubscribeSession = this.session.subscribe(() => this.handleSessionChange());
163
+ }
128
164
  this.scheduleNextPoll();
129
165
  }
130
166
 
@@ -132,9 +168,46 @@ export class NewsService {
132
168
  this.polling = false;
133
169
  this.unsubscribeVisibility?.();
134
170
  this.unsubscribeVisibility = null;
171
+ this.unsubscribeSession?.();
172
+ this.unsubscribeSession = null;
135
173
  this.clearPollTimer();
136
174
  }
137
175
 
176
+ /**
177
+ * A new session answers with other news: the live wire once a Pro session
178
+ * is in place, the held-back one after signing out. Reads still out under
179
+ * the old session are dropped, open queries are read again and replace
180
+ * their stories rather than merge into them, and closed ones are forgotten.
181
+ * A token and its user arrive as separate changes, so the reads wait for
182
+ * the end of the tick and go out once.
183
+ */
184
+ private handleSessionChange(): void {
185
+ if (!this.session) return;
186
+ const key = this.session.key();
187
+ if (key === this.sessionKey) return;
188
+ this.sessionKey = key;
189
+ this.sessionGeneration++;
190
+ if (this.sessionRefetchQueued) return;
191
+ this.sessionRefetchQueued = true;
192
+ queueMicrotask(() => {
193
+ this.sessionRefetchQueued = false;
194
+ if (!this.polling) return;
195
+ let dropped = false;
196
+ for (const [queryKey, entry] of this.queries) {
197
+ if (entry.refs > 0) continue;
198
+ this.queries.delete(queryKey);
199
+ dropped = true;
200
+ }
201
+ if (dropped) {
202
+ this.rebuildArticlePool();
203
+ this.notify();
204
+ }
205
+ for (const entry of this.queries.values()) {
206
+ void this.refreshQuery(entry.query, true);
207
+ }
208
+ });
209
+ }
210
+
138
211
  private pollInFlight = false;
139
212
 
140
213
  private clearPollTimer(): void {
@@ -226,6 +299,7 @@ export class NewsService {
226
299
  return;
227
300
  }
228
301
 
302
+ const generation = this.sessionGeneration;
229
303
  const request = (async () => {
230
304
  entry.state = { ...entry.state, loadingMore: true };
231
305
  this.notify();
@@ -234,6 +308,12 @@ export class NewsService {
234
308
  ...normalized,
235
309
  cursor: entry.state.nextCursor ?? undefined,
236
310
  });
311
+ // An older page from another session must not join the list the new session read.
312
+ if (generation !== this.sessionGeneration || entry.generation !== generation) {
313
+ entry.state = { ...entry.state, loadingMore: false };
314
+ this.notify();
315
+ return;
316
+ }
237
317
  entry.state = {
238
318
  ...entry.state,
239
319
  loadingMore: false,
@@ -253,6 +333,7 @@ export class NewsService {
253
333
  }
254
334
  })();
255
335
  entry.loadMoreInFlight = request;
336
+ entry.loadMoreGeneration = generation;
256
337
  await request;
257
338
  }
258
339
 
@@ -318,7 +399,9 @@ export class NewsService {
318
399
  showLoading: boolean,
319
400
  ): Promise<NewsQueryState> {
320
401
  const entry = this.getOrCreateQueryEntry(query);
321
- if (entry.inFlight) return entry.inFlight;
402
+ const generation = this.sessionGeneration;
403
+ // A read that went out under an older session answers for that session: a new one goes out beside it.
404
+ if (entry.inFlight && entry.inFlightGeneration === generation) return entry.inFlight;
322
405
 
323
406
  const current = entry.state;
324
407
  if (showLoading) {
@@ -333,14 +416,17 @@ export class NewsService {
333
416
  const promise = (async () => {
334
417
  try {
335
418
  const result = await this.fetchFromSources(query);
419
+ if (generation !== this.sessionGeneration) return this.currentRead(entry);
336
420
  if (result.sourceIds.length === 0 && result.failedSourceIds.length > 0) {
337
421
  throw new Error("News sources unavailable.");
338
422
  }
339
- if (entry.loadMoreInFlight) return entry.state;
423
+ if (entry.loadMoreInFlight && entry.loadMoreGeneration === generation) return entry.state;
340
424
  const incoming = filterNewsArticlesForQuery(dedupeNewsArticles(result.articles), query);
425
+ // Stories read under another session are another feed (held back, or live): the new read replaces them.
426
+ const sameSession = entry.generation === generation;
341
427
  // The stories a coin's fallback found are not the pair's own: once stories are filed under it, they replace them.
342
- const existing = incoming.length > 0 && entry.state.articles.length > 0
343
- && (incoming[0]!.searchedFor ?? null) !== (entry.state.articles[0]!.searchedFor ?? null)
428
+ const existing = !sameSession || (incoming.length > 0 && entry.state.articles.length > 0
429
+ && (incoming[0]!.searchedFor ?? null) !== (entry.state.articles[0]!.searchedFor ?? null))
344
430
  ? []
345
431
  : entry.state.articles;
346
432
  const incomingIds = new Set(incoming.map((article) => article.id));
@@ -363,13 +449,17 @@ export class NewsService {
363
449
  sourceIds: result.sourceIds,
364
450
  nextCursor: hasOlderPages ? entry.state.nextCursor : result.nextCursor,
365
451
  loadingMore: entry.state.loadingMore,
452
+ // A page that does not say keeps what this session's earlier answer said.
453
+ access: result.access ?? (sameSession ? entry.state.access ?? null : null),
366
454
  };
367
455
  entry.state = state;
456
+ entry.generation = generation;
368
457
  entry.lastAccessedAt = this.now();
369
458
  this.rebuildArticlePool();
370
459
  this.notify();
371
460
  return state;
372
461
  } catch (error) {
462
+ if (generation !== this.sessionGeneration) return this.currentRead(entry);
373
463
  const state: NewsQueryState = {
374
464
  ...current,
375
465
  phase: "error",
@@ -380,15 +470,21 @@ export class NewsService {
380
470
  this.notify();
381
471
  return state;
382
472
  } finally {
383
- entry.inFlight = null;
473
+ if (entry.inFlightGeneration === generation) entry.inFlight = null;
384
474
  this.pruneInactiveQueries();
385
475
  }
386
476
  })();
387
477
 
388
478
  entry.inFlight = promise;
479
+ entry.inFlightGeneration = generation;
389
480
  return promise;
390
481
  }
391
482
 
483
+ /** What a dropped read answers its caller: the current session's read when one is out, else the state as it is. */
484
+ private currentRead(entry: NewsQueryEntry): Promise<NewsQueryState> | NewsQueryState {
485
+ return entry.inFlight && entry.inFlightGeneration === this.sessionGeneration ? entry.inFlight : entry.state;
486
+ }
487
+
392
488
  private getOrCreateQueryEntry(query: NewsQuery): NewsQueryEntry {
393
489
  const key = buildNewsQueryKey(query);
394
490
  const now = this.now();
@@ -402,7 +498,10 @@ export class NewsService {
402
498
  query,
403
499
  state: createIdleNewsQueryState(),
404
500
  inFlight: null,
501
+ inFlightGeneration: this.sessionGeneration,
405
502
  loadMoreInFlight: null,
503
+ loadMoreGeneration: this.sessionGeneration,
504
+ generation: this.sessionGeneration,
406
505
  refs: 0,
407
506
  lastAccessedAt: now,
408
507
  };
@@ -451,7 +550,7 @@ export class NewsService {
451
550
  private async readSourcePage(
452
551
  source: NewsCapability,
453
552
  query: NewsQuery,
454
- ): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null }> {
553
+ ): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null; access?: NewsAccess | null }> {
455
554
  if (source.provider.fetchNewsPage) {
456
555
  const page = await this.trackSourceRequest(
457
556
  source,
@@ -462,6 +561,7 @@ export class NewsService {
462
561
  articles: page.articles.map((article) => markDetailCapableArticle(source, article)),
463
562
  nextCursor: page.nextCursor ?? null,
464
563
  error: page.error,
564
+ access: page.access,
465
565
  };
466
566
  }
467
567
  const articles = (await this.trackSourceRequest(
@@ -484,6 +584,7 @@ export class NewsService {
484
584
  failedSourceIds,
485
585
  errors: page.error ? [page.error] : [],
486
586
  nextCursor: page.nextCursor,
587
+ access: page.access ?? null,
487
588
  };
488
589
  if (page.articles.length > 0) return result;
489
590
  firstEmpty ??= result;
@@ -491,7 +592,7 @@ export class NewsService {
491
592
  failedSourceIds.push(newsCapabilitySourceId(source));
492
593
  }
493
594
  }
494
- return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds, errors: [], nextCursor: null };
595
+ return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds, errors: [], nextCursor: null, access: null };
495
596
  }
496
597
 
497
598
  private async fetchMergedNews(query: NewsQuery, sources: NewsCapability[]): Promise<SourceFetchResult> {
@@ -505,6 +606,7 @@ export class NewsService {
505
606
  const errors: string[] = [];
506
607
  const sourceIds: string[] = [];
507
608
  const failedSourceIds: string[] = [];
609
+ const access: Array<NewsAccess | null | undefined> = [];
508
610
  let nextCursor: string | null = null;
509
611
  settled.forEach((result, index) => {
510
612
  if (result.status !== "fulfilled") {
@@ -515,9 +617,10 @@ export class NewsService {
515
617
  articles.push(...result.value.page.articles);
516
618
  if (result.value.page.error) errors.push(result.value.page.error);
517
619
  sourceIds.push(newsCapabilitySourceId(result.value.source));
620
+ access.push(result.value.page.access);
518
621
  nextCursor ??= result.value.page.nextCursor;
519
622
  });
520
- return { articles, sourceIds, failedSourceIds, errors, nextCursor };
623
+ return { articles, sourceIds, failedSourceIds, errors, nextCursor, access: combineNewsAccess(access) };
521
624
  }
522
625
 
523
626
  private trackSourceRequest<T>(
@@ -550,6 +653,7 @@ export class NewsService {
550
653
  sourceIds: [...new Set([...entry.state.sourceIds, newsCapabilitySourceId(source)])],
551
654
  nextCursor: entry.state.nextCursor,
552
655
  loadingMore: false,
656
+ access: entry.state.access ?? null,
553
657
  };
554
658
  changed = true;
555
659
  }