gloomberb 0.18.4 → 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 (600) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +31 -6
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -0,0 +1,203 @@
1
+ import type { MarketDataRequestContext } from "../../types/data-provider";
2
+ import type { PricePoint, Quote } from "../../types/financials";
3
+ import { abortable, settleWithin, withDeadline } from "../../utils/async-deadline";
4
+ import { canonicalExchange } from "../../utils/exchanges";
5
+ import { logFailure } from "../../utils/log-failure";
6
+ import { dailyBarDate } from "../../utils/price-history";
7
+ import { classifyInstrumentType } from "../../tickers/instrument-kind";
8
+ import { changeBasisSessionDate, isChangeBasisDatedWithoutCloses, quoteChangeBasis } from "./change-basis";
9
+
10
+ /** History closes come as float32 (30.93000030517578) where the quote says 30.93: one close. */
11
+ const SAME_CLOSE_RELATIVE = 1e-5;
12
+ /**
13
+ * How long a reading that named no day stands before the closes are read
14
+ * again: a history a session behind catches up, a failed read is retried.
15
+ */
16
+ const UNDATED_RETRY_MS = 10 * 60_000;
17
+ /** Readings kept at once; the oldest goes first. */
18
+ const MAX_READINGS = 2_000;
19
+ /** Daily histories read at once. */
20
+ const MAX_READS_IN_FLIGHT = 6;
21
+ /** How long one read may take before its slot goes to the next: a hung read must not stall the rest. */
22
+ const READ_DEADLINE_MS = 10_000;
23
+
24
+ /** The listing a quote was asked for, as its daily closes are read: its context names a broker's contract. */
25
+ export interface DatedCloseListing {
26
+ symbol: string;
27
+ exchange: string;
28
+ context?: MarketDataRequestContext;
29
+ }
30
+
31
+ /** A listing's recent daily closes (a month is enough). */
32
+ export type ReadDailyCloses = (listing: DatedCloseListing) => Promise<readonly Pick<PricePoint, "date" | "close">[]>;
33
+
34
+ export interface PreviousCloseDatesOptions {
35
+ /** Called with the latest copy of a quote, dated, when a read lands that names its day. */
36
+ onDated?: (listing: DatedCloseListing, quote: Quote) => void;
37
+ now?: () => number;
38
+ maxReadsInFlight?: number;
39
+ readDeadlineMs?: number;
40
+ }
41
+
42
+ interface Reading {
43
+ date: string | null;
44
+ at: number;
45
+ }
46
+
47
+ /** One listing whatever spelling of its exchange a request used. */
48
+ export function datedCloseListingKey(listing: DatedCloseListing): string {
49
+ return `${listing.symbol.trim().toUpperCase()}|${canonicalExchange(listing.exchange)}`;
50
+ }
51
+
52
+ function sameClose(left: number, right: number): boolean {
53
+ return Math.abs(left - right) <= Math.max(Math.abs(left), Math.abs(right)) * SAME_CLOSE_RELATIVE;
54
+ }
55
+
56
+ /**
57
+ * What dates a quote's previous close by its listing's daily closes: the
58
+ * listing, the session the move is dated to and the close itself. Null for a
59
+ * quote something else dates (its venue's calendar, a coin's UTC day, a
60
+ * future's terms, a close the source dated), one with no move from a previous
61
+ * close, and an option, whose chain would read a history per contract. The
62
+ * venue's calendar is checked first, since this runs on every tick.
63
+ */
64
+ function readingKey(quote: Quote | null | undefined, listing: DatedCloseListing, now: number): string | null {
65
+ if (!quote || isChangeBasisDatedWithoutCloses(quote, now) || classifyInstrumentType(quote.instrumentType) === "option") return null;
66
+ if (quoteChangeBasis(quote)?.kind !== "previous close") return null;
67
+ const previousClose = quote.previousClose;
68
+ const sessionDate = changeBasisSessionDate(quote);
69
+ if (previousClose == null || !Number.isFinite(previousClose) || !sessionDate) return null;
70
+ return `${datedCloseListingKey(listing)}|${sessionDate}|${previousClose}`;
71
+ }
72
+
73
+ /**
74
+ * The day of the latest daily close before the move's session, when that
75
+ * close is the quote's previous close; null otherwise, so a gap in the
76
+ * history never names the wrong day.
77
+ */
78
+ function latestCloseBefore(points: readonly Pick<PricePoint, "date" | "close">[], quote: Quote): string | null {
79
+ const sessionDate = changeBasisSessionDate(quote)!;
80
+ const exchange = quote.listingExchangeName ?? quote.exchangeName ?? "";
81
+ const before = points
82
+ .map((point) => ({ time: new Date(point.date).getTime(), close: point.close }))
83
+ .filter((point) => Number.isFinite(point.time) && Number.isFinite(point.close))
84
+ .map((point) => ({ date: dailyBarDate(point.time, exchange), close: point.close }))
85
+ .filter((point) => point.date < sessionDate)
86
+ .sort((left, right) => left.date.localeCompare(right.date))
87
+ .at(-1);
88
+ return before && sameClose(before.close, quote.previousClose!) ? before.date : null;
89
+ }
90
+
91
+ function withDate<T extends Quote | null | undefined>(quote: T, date: string | null | undefined): T {
92
+ return date && quote ? { ...quote, previousCloseDate: date } : quote;
93
+ }
94
+
95
+ /**
96
+ * Dates the previous close a quote's move is from where the venue's calendar
97
+ * cannot (LSE and Helsinki publish none the app carries), by the listing's
98
+ * own daily closes, so every surface that reads quotes through the app's
99
+ * quote router names the day (`quoteChangeBasis`). The closes are read once
100
+ * per listing, session and close, and the answer is kept: a refresh, a tick
101
+ * or a cached read of the same quote is dated without another read. A few
102
+ * reads run at once, each for at most ten seconds.
103
+ */
104
+ export class PreviousCloseDates {
105
+ private readonly readings = new Map<string, Reading>();
106
+ private readonly pending = new Map<string, Promise<string | null>>();
107
+ /** The latest copy of each quote a read is pending for, which the read dates when it lands. */
108
+ private readonly latestCopies = new Map<string, { quote: Quote; listing: DatedCloseListing }>();
109
+ private readonly waiting: Array<() => void> = [];
110
+ private active = 0;
111
+ private readonly now: () => number;
112
+ private readonly maxReadsInFlight: number;
113
+ private readonly readDeadlineMs: number;
114
+
115
+ constructor(private readonly readCloses: ReadDailyCloses, private readonly options: PreviousCloseDatesOptions = {}) {
116
+ this.now = options.now ?? (() => Date.now());
117
+ this.maxReadsInFlight = options.maxReadsInFlight ?? MAX_READS_IN_FLIGHT;
118
+ this.readDeadlineMs = options.readDeadlineMs ?? READ_DEADLINE_MS;
119
+ }
120
+
121
+ /** The quote dated by what was already read for it; as it was when nothing was. Never reads. */
122
+ known<T extends Quote | null | undefined>(quote: T, listing: DatedCloseListing): T {
123
+ const key = readingKey(quote, listing, this.now());
124
+ return key ? withDate(quote, this.readings.get(key)?.date) : quote;
125
+ }
126
+
127
+ /**
128
+ * The quote dated by what was already read for it, starting the read when
129
+ * no reading for it stands. Never waits: the read dates the copies after
130
+ * it, and `onDated` the latest one.
131
+ */
132
+ observe<T extends Quote | null | undefined>(quote: T, listing: DatedCloseListing): T {
133
+ const key = readingKey(quote, listing, this.now());
134
+ if (!key) return quote;
135
+ const reading = this.readings.get(key);
136
+ if (!this.stands(reading)) void this.start(key, quote!, listing);
137
+ return withDate(quote, reading?.date);
138
+ }
139
+
140
+ /**
141
+ * Starts the reads these quotes need and waits for them, at most `waitMs`
142
+ * in all and no longer than `signal` lets it; `known` then dates them.
143
+ */
144
+ async settle(
145
+ entries: ReadonlyArray<{ quote: Quote; listing: DatedCloseListing }>,
146
+ { waitMs, signal }: { waitMs: number; signal?: AbortSignal },
147
+ ): Promise<void> {
148
+ if (signal?.aborted) return;
149
+ const reads: Promise<string | null>[] = [];
150
+ for (const { quote, listing } of entries) {
151
+ const key = readingKey(quote, listing, this.now());
152
+ if (key && !this.stands(this.readings.get(key))) reads.push(this.start(key, quote, listing));
153
+ }
154
+ if (reads.length) await settleWithin(abortable(Promise.all(reads), signal, "Stopped waiting for daily closes"), waitMs);
155
+ }
156
+
157
+ /** Whether a reading answers without another read: a day it named, or no day named lately. */
158
+ private stands(reading: Reading | undefined): boolean {
159
+ return !!reading && (!!reading.date || this.now() - reading.at < UNDATED_RETRY_MS);
160
+ }
161
+
162
+ private start(key: string, quote: Quote, listing: DatedCloseListing): Promise<string | null> {
163
+ this.latestCopies.set(key, { quote, listing });
164
+ return this.pending.get(key) ?? this.read(key, quote, listing);
165
+ }
166
+
167
+ private read(key: string, quote: Quote, listing: DatedCloseListing): Promise<string | null> {
168
+ const read = this.inSlot(() => withDeadline(Promise.resolve().then(() => this.readCloses(listing)), this.readDeadlineMs, "Reading daily closes timed out"))
169
+ .then((points) => latestCloseBefore(points, quote))
170
+ .catch(logFailure("market-data.dated-previous-close", "Read daily closes failed", null))
171
+ .then((date) => {
172
+ this.pending.delete(key);
173
+ this.readings.delete(key);
174
+ if (this.readings.size >= MAX_READINGS) this.readings.delete(this.readings.keys().next().value!);
175
+ this.readings.set(key, { date, at: this.now() });
176
+ const latest = this.latestCopies.get(key);
177
+ this.latestCopies.delete(key);
178
+ if (date && latest && this.options.onDated) {
179
+ try {
180
+ this.options.onDated(latest.listing, withDate(latest.quote, date));
181
+ } catch (error) {
182
+ logFailure("market-data.dated-previous-close", "Passing on a dated quote failed")(error);
183
+ }
184
+ }
185
+ return date;
186
+ });
187
+ this.pending.set(key, read);
188
+ return read;
189
+ }
190
+
191
+ private async inSlot<T>(work: () => Promise<T>): Promise<T> {
192
+ if (this.active < this.maxReadsInFlight) this.active += 1;
193
+ else await new Promise<void>((resolve) => this.waiting.push(resolve));
194
+ try {
195
+ return await work();
196
+ } finally {
197
+ // A finished read hands its slot to the next one waiting.
198
+ const next = this.waiting.shift();
199
+ if (next) next();
200
+ else this.active -= 1;
201
+ }
202
+ }
203
+ }
@@ -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,
@@ -0,0 +1,68 @@
1
+ import { canonicalExchange } from "../../utils/exchanges";
2
+ import { zonedDateTimeParts, zonedWallClockToUtcMs } from "../../utils/zoned-date-time";
3
+
4
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
5
+ const FRIDAY = 5;
6
+
7
+ /**
8
+ * CME Group's Globex week runs from Sunday 17:00 Central to Friday 16:00
9
+ * Central, 17:00 in New York, which ICE Futures U.S. keeps too
10
+ * (cmegroup.com/trading-hours, ice.com). Nothing trades between, whatever
11
+ * state a quote stamped before the Friday close still carries. The venues
12
+ * have no other calendar here: their daily breaks and holiday sessions vary
13
+ * by contract, so the quote's own state says those.
14
+ */
15
+ const WEEK_ZONE = "America/Chicago";
16
+ const WEEK_CLOSE_MINUTES = 16 * 60;
17
+ const WEEK_OPEN_MINUTES = 17 * 60;
18
+
19
+ /**
20
+ * The futures venues the weekly gap covers, and whether the week's first open
21
+ * is theirs to name: every Globex contract opens by Sunday 17:00 Central, but
22
+ * an ICE U.S. one opens when its own hours say, from Sunday evening (cotton)
23
+ * to Monday morning (coffee), so the venue has no one reopen to give.
24
+ */
25
+ const WEEK_VENUES: Readonly<Record<string, { reopens: boolean }>> = {
26
+ CME: { reopens: true },
27
+ CBT: { reopens: true },
28
+ NYM: { reopens: true },
29
+ CMX: { reopens: true },
30
+ NYB: { reopens: false },
31
+ };
32
+
33
+ export interface FuturesWeekendClosure {
34
+ /** The Friday close that began it. */
35
+ closedAt: number;
36
+ /** The Sunday open that ends it, when the venue's contracts all open then or later. */
37
+ reopensAt?: number;
38
+ /** That open's local date, `YYYY-MM-DD` (always a Sunday). */
39
+ reopensOn?: string;
40
+ }
41
+
42
+ /**
43
+ * The weekend gap of a futures venue's week that `time` falls in, from the
44
+ * Friday close to the Sunday open; null on any other instant, and for a venue
45
+ * that is not a futures exchange here. Only ever the closed side: a moment
46
+ * outside it is not known to be open.
47
+ */
48
+ export function futuresWeekendClosure(exchange: string | undefined, time: number): FuturesWeekendClosure | null {
49
+ const venue = WEEK_VENUES[canonicalExchange(exchange)];
50
+ if (!venue || !Number.isFinite(time)) return null;
51
+ const { year, month, day } = zonedDateTimeParts(time, WEEK_ZONE);
52
+ const today = Date.UTC(year, month - 1, day) / MS_PER_DAY;
53
+ const weekday = new Date(today * MS_PER_DAY).getUTCDay();
54
+ // The latest Friday on or before the local date: a Monday to Thursday reads last week's, whose gap has ended.
55
+ const friday = today - ((weekday - FRIDAY + 7) % 7);
56
+ const at = (days: number, minutes: number) => {
57
+ const date = new Date(days * MS_PER_DAY);
58
+ return zonedWallClockToUtcMs(WEEK_ZONE, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(),
59
+ Math.floor(minutes / 60), minutes % 60, 0);
60
+ };
61
+ const closedAt = at(friday, WEEK_CLOSE_MINUTES);
62
+ const reopensAt = at(friday + 2, WEEK_OPEN_MINUTES);
63
+ if (time < closedAt || time >= reopensAt) return null;
64
+ return {
65
+ closedAt,
66
+ ...(venue.reopens ? { reopensAt, reopensOn: new Date((friday + 2) * MS_PER_DAY).toISOString().slice(0, 10) } : {}),
67
+ };
68
+ }
@@ -6,6 +6,7 @@ import { quoteTradingDay } from "../quotes/day-range";
6
6
  import { isQuoteBeforeLatestSession } from "../quotes/freshness";
7
7
  import { delistingLabel, delistingStateLabel } from "../quotes/delisting";
8
8
  import { preMarketOpenOn, regularSessionCloseOn } from "./freshness";
9
+ import { futuresWeekendClosure } from "./futures-week";
9
10
 
10
11
  const CLOSED_CHANGE_MUTING_RATIO = 0.55;
11
12
  const US_SESSION_TIME = new Intl.DateTimeFormat("en-US", {
@@ -97,6 +98,17 @@ function closeLabel(date: string): string {
97
98
  return `${WEEKDAYS[new Date(`${date}T00:00:00Z`).getUTCDay()]} close`;
98
99
  }
99
100
 
101
+ /**
102
+ * The market state a quote's venue is in now. A futures venue's weekend gap
103
+ * closes every contract, so it outranks the state a quote stamped before the
104
+ * Friday close (a stale print reading REGULAR on a Sunday); every other quote
105
+ * keeps the state its source reported.
106
+ */
107
+ function quoteMarketState(quote: Quote | null | undefined, now = Date.now()): MarketState | undefined {
108
+ if (!quote?.marketState) return undefined;
109
+ return futuresWeekendClosure(quote.listingExchangeName || quote.exchangeName, now) ? "CLOSED" : quote.marketState;
110
+ }
111
+
100
112
  /**
101
113
  * A quote's market state as a status cell names it (marketStateLabel), STALE
102
114
  * for a print older than its venue's latest session, and the close it stands
@@ -109,7 +121,7 @@ export function quoteMarketStateLabel(quote: Quote | null | undefined, now = Dat
109
121
  if (!quote?.marketState) return null;
110
122
  if (isQuoteBeforeLatestSession(quote, now)) return STALE_SESSION_LABEL;
111
123
  const awaiting = preMarketCloseAwaitingPrint(quote);
112
- return awaiting ? closeLabel(awaiting.date) : marketStateLabel(quote.marketState);
124
+ return awaiting ? closeLabel(awaiting.date) : marketStateLabel(quoteMarketState(quote, now)!);
113
125
  }
114
126
 
115
127
  /** An extended session's column, named as marketStateLabel names its state. */
@@ -216,7 +228,7 @@ function sessionLabel(quote: Quote | null | undefined, now: number, standingClos
216
228
  if (isQuoteBeforeLatestSession(quote, now)) return STALE_SESSION_LABEL;
217
229
  if (overnightSessionDisplay(quote)) return "OVERNIGHT";
218
230
  const awaiting = standingClose ? preMarketCloseAwaitingPrint(quote) : null;
219
- return awaiting ? closeLabel(awaiting.date) : marketStateLabel(quote.marketState);
231
+ return awaiting ? closeLabel(awaiting.date) : marketStateLabel(quoteMarketState(quote, now)!);
220
232
  }
221
233
 
222
234
  /**
@@ -230,7 +242,7 @@ export function quoteSessionColor(quote: Quote | null | undefined, palette: Them
230
242
  if (!quote?.marketState) return palette.textDim;
231
243
  if (isQuoteBeforeLatestSession(quote, now)) return palette.warning;
232
244
  if (overnightSessionDisplay(quote)) return marketStateColor("POST", palette);
233
- return marketStateColor(preMarketCloseAwaitingPrint(quote) ? "CLOSED" : quote.marketState, palette);
245
+ return marketStateColor(preMarketCloseAwaitingPrint(quote) ? "CLOSED" : quoteMarketState(quote, now)!, palette);
234
246
  }
235
247
 
236
248
  /**
@@ -239,7 +251,7 @@ export function quoteSessionColor(quote: Quote | null | undefined, palette: Them
239
251
  * with no known session rather than an open market.
240
252
  */
241
253
  export function quoteDotMarketState(quote: Quote | null | undefined, now = Date.now()): MarketState | undefined {
242
- return quote && !quote.delisting && !isQuoteBeforeLatestSession(quote, now) ? quote.marketState : undefined;
254
+ return quote && !quote.delisting && !isQuoteBeforeLatestSession(quote, now) ? quoteMarketState(quote, now) : undefined;
243
255
  }
244
256
 
245
257
  /** Closed prices are final snapshots, so they should not look live or directional. */
@@ -1,3 +1,6 @@
1
+ import { isRoundTheClockCoin } from "../utils/crypto-pair";
2
+ import { currencyPairCode, isCurrencyCode } from "../utils/currency-pair";
3
+ import { parsePublicTickerKey } from "../utils/exchanges";
1
4
  import { parseFuturesGeneric } from "../utils/futures-generic";
2
5
 
3
6
  /** A US-listed fund whose options track an instrument that has none of its own. */
@@ -32,23 +35,24 @@ const FUTURES_ALTERNATIVES: Readonly<Record<string, ListedOptionsAlternative>> =
32
35
  ZN: { symbol: "IEF", name: "iShares 7-10 Year Treasury Bond ETF" },
33
36
  "6E": { symbol: "FXE", name: "Invesco CurrencyShares Euro Trust" },
34
37
  "6J": { symbol: "FXY", name: "Invesco CurrencyShares Japanese Yen Trust" },
38
+ "6B": { symbol: "FXB", name: "Invesco CurrencyShares British Pound Sterling Trust" },
35
39
  BTC: { symbol: "IBIT", name: "iShares Bitcoin Trust" },
36
40
  ETH: { symbol: "ETHA", name: "iShares Ethereum Trust" },
37
41
  };
38
42
 
39
43
  const MONTH_CODE_CONTRACT = /^([A-Z0-9]{1,3}?)[FGHJKMNQUVXZ]\d{1,4}$/;
40
44
 
41
- /** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. */
42
- 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 {
43
47
  const ticker = symbol.trim().toUpperCase().replace(/[.:][A-Z]+$/, "");
44
48
  if (ticker.endsWith("=F")) {
45
49
  const body = ticker.slice(0, -2);
46
- return FUTURES_ALTERNATIVES[body] ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
50
+ return known(body) ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
47
51
  }
48
52
  const generic = parseFuturesGeneric(ticker);
49
53
  if (generic) return generic.root;
50
54
  const contract = MONTH_CODE_CONTRACT.exec(ticker)?.[1];
51
- return contract && FUTURES_ALTERNATIVES[contract] ? contract : null;
55
+ return contract && known(contract) ? contract : null;
52
56
  }
53
57
 
54
58
  /** The listed fund whose options stand in for an instrument with no chain, if there is one. */
@@ -67,3 +71,136 @@ export function isOptionsUnavailableError(error: unknown): boolean {
67
71
  export function optionsUnavailableTitle(symbol: string): string {
68
72
  return `Options are not available for ${symbol.trim().toUpperCase()}.`;
69
73
  }
74
+
75
+ /**
76
+ * The coins whose options Gloom Cloud serves, from Deribit, as BTC-USD and
77
+ * ETH-USD: the standard options path (options, OMON, OPX, OVDV).
78
+ */
79
+ const COIN_OPTION_UNDERLYINGS: Readonly<Record<string, string>> = { BTC: "BTC-USD", ETH: "ETH-USD" };
80
+
81
+ /** BTC-USD or ETH-USD for a coin pair whose options are listed (on the coin board or bare); null otherwise. */
82
+ export function listedCoinOptions(symbol: string): string | null {
83
+ const parsed = parsePublicTickerKey(symbol.trim().toUpperCase());
84
+ if (!isRoundTheClockCoin(parsed.symbol, parsed.exchange)) return null;
85
+ const pair = /^([A-Z0-9]+)-USD$/.exec(parsed.symbol)?.[1];
86
+ return pair ? COIN_OPTION_UNDERLYINGS[pair] ?? null : null;
87
+ }
88
+
89
+ /** A coin (SOL-USD, DOGE-USD) nothing lists options on: only BTC and ETH have them. */
90
+ export function isCoinWithoutOptions(symbol: string): boolean {
91
+ const parsed = parsePublicTickerKey(symbol.trim().toUpperCase());
92
+ return isRoundTheClockCoin(parsed.symbol, parsed.exchange) && listedCoinOptions(symbol) == null;
93
+ }
94
+
95
+ /**
96
+ * What exists instead for a coin without options, worded for the CLI:
97
+ * BTC and ETH options, and the bitcoin-linked US listings with their own,
98
+ * each as the command that reads it (`options`, or `fn OPX`).
99
+ */
100
+ export function coinOptionsAlternativesText(command = "options"): string {
101
+ const run = (symbol: string) => `gloomberb ${command} ${symbol}`;
102
+ return `Listed crypto options cover BTC and ETH: ${run("BTC-USD")}, ${run("ETH-USD")}.`
103
+ + ` Bitcoin-linked US listings have their own: ${run("IBIT")}.`;
104
+ }
105
+
106
+ /** The same for a pane, which has no commands to give. */
107
+ export const COIN_OPTIONS_HINT = "Listed crypto options cover BTC-USD and ETH-USD; bitcoin-linked US funds such as IBIT list their own.";
108
+
109
+ /**
110
+ * An options function's `refuseSymbol`: a coin nothing lists options on
111
+ * (SOL-USD) is answered before any request, with what exists instead.
112
+ */
113
+ export function coinWithoutOptionsRefusal(code: string) {
114
+ return (key: string): { message: string; details: string } | null => isCoinWithoutOptions(key)
115
+ ? { message: optionsUnavailableTitle(parsePublicTickerKey(key.trim().toUpperCase()).symbol), details: coinOptionsAlternativesText(`fn ${code}`) }
116
+ : null;
117
+ }
118
+
119
+ /**
120
+ * A CLI options function's `symbolAlias` (`gloomberb fn OPX BTC`, `shot`): a
121
+ * bare BTC or ETH is read as the coin, with a note naming the Grayscale fund's
122
+ * listing (BTC:ARCA) for its own options (`code` is the function, OPX). The
123
+ * command bar and panes keep a bare BTC as the fund, as everywhere else.
124
+ */
125
+ export function coinOptionsAlias(code: string) {
126
+ return (typed: string): { symbol: string; note: string } | null => {
127
+ const key = typed.trim().toUpperCase();
128
+ const pair = COIN_OPTION_UNDERLYINGS[key];
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;
130
+ };
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
+ }
@@ -20,7 +20,7 @@ const PRICE_DENOMINATED_DESCRIPTIVE_FIELDS: ReadonlySet<string> = new Set(["high
20
20
  * reconciled separately (reconcileQuoteDayRange).
21
21
  */
22
22
  const TRADING_DAY_FIELDS = [
23
- "changeSessionDate", "previousClose", "open", "volume", "mark", "bid", "ask", "bidSize", "askSize",
23
+ "changeSessionDate", "previousClose", "previousCloseDate", "open", "volume", "mark", "bid", "ask", "bidSize", "askSize",
24
24
  "lastTradePrice", "lastTradeTime",
25
25
  ] as const;
26
26
 
@@ -36,6 +36,12 @@ function isOtherTradingDay(current: Quote, next: Quote): boolean {
36
36
  return currentDay !== null && nextDay !== null && nextDay !== currentDay;
37
37
  }
38
38
 
39
+ /** Whether two observations are known to describe the same trading day. */
40
+ function isSameTradingDay(current: Quote, next: Quote): boolean {
41
+ const currentDay = quoteTradingDay(current);
42
+ return currentDay !== null && currentDay === quoteTradingDay(next);
43
+ }
44
+
39
45
  function inferQuoteProviderId(quote: Quote | QuoteContribution): string {
40
46
  if (quote.providerId?.trim()) return quote.providerId;
41
47
  return "quote";
@@ -195,7 +201,7 @@ export function mergeQuoteContribution(
195
201
  // Close provenance belongs to this observation; never borrow an anchor
196
202
  // from another session, listing or currency when a new quote omits it.
197
203
  ...(!samePriceBasis ? {
198
- previousClose: next.previousClose, bid: next.bid, ask: next.ask,
204
+ previousClose: next.previousClose, previousCloseDate: next.previousCloseDate, bid: next.bid, ask: next.ask,
199
205
  open: next.open, mark: next.mark, high52w: next.high52w, low52w: next.low52w,
200
206
  lastTradePrice: next.lastTradePrice, lastTradeTime: next.lastTradeTime,
201
207
  preMarketPrice: next.preMarketPrice, preMarketChange: next.preMarketChange,
@@ -232,6 +238,18 @@ export function mergeQuoteContribution(
232
238
  delete merged.changeBasis;
233
239
  } else if (next.previousClose === undefined) {
234
240
  delete merged.previousClose;
241
+ delete merged.previousCloseDate;
242
+ }
243
+ // The day a previous close is from is said of that close and that session: a tick that names
244
+ // none keeps it while both are the same, and drops it otherwise. A close can repeat across
245
+ // sessions (a flat day), so the trading day decides, not the close alone.
246
+ if (next.previousCloseDate === undefined) {
247
+ const sameClose = next.previousClose === undefined || next.previousClose === current.previousClose;
248
+ if (samePriceBasis && sameClose && merged.previousClose === current.previousClose && current.previousCloseDate && isSameTradingDay(current, next)) {
249
+ merged.previousCloseDate = current.previousCloseDate;
250
+ } else {
251
+ delete merged.previousCloseDate;
252
+ }
235
253
  }
236
254
 
237
255
  if (isOtherTradingDay(current, next)) {
@@ -221,6 +221,8 @@ function assignDailyChangeFields(
221
221
  }
222
222
 
223
223
  assignField(target, provenance, "previousClose", referenceProvider);
224
+ // The day that close is from comes with it, from the same source.
225
+ if (referenceProvider.previousCloseDate) target.previousCloseDate = referenceProvider.previousCloseDate;
224
226
  const price = target.price;
225
227
  const previousClose = target.previousClose;
226
228
  if (!finitePositiveNumber(price) || !finitePositiveNumber(previousClose)) {
@@ -500,6 +502,7 @@ export function resolveCanonicalQuote(
500
502
  // What that move is from when it is not previousClose; absent when it is.
501
503
  ...(resolved.changeBasis ? { changeBasis: resolved.changeBasis } : {}),
502
504
  previousClose: resolved.previousClose as Quote["previousClose"],
505
+ ...(resolved.previousCloseDate ? { previousCloseDate: resolved.previousCloseDate } : {}),
503
506
  regularClose: priceProvider?.regularClose,
504
507
  regularCloseSessionDate: priceProvider?.regularClose != null ? priceProvider.regularCloseSessionDate : undefined,
505
508
  regularChange: priceProvider?.regularClose != null ? priceProvider.regularChange : undefined,
@@ -6,6 +6,7 @@ import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
6
6
  import { normalizePriceHistory } from "../utils/price-history";
7
7
  import { resolveTickerFinancialsQuoteState } from "./quotes/resolution";
8
8
  import { instrumentIdentityKey } from "../utils/instrument-identity";
9
+ import { withSavedCopyMarkOf } from "../data/saved-copies";
9
10
 
10
11
  export function toMarketDataContext(instrument: InstrumentRef): MarketDataRequestContext {
11
12
  return {
@@ -123,6 +124,8 @@ export function buildTickerFinancialsSnapshot(
123
124
 
124
125
  return {
125
126
  ...resolved,
127
+ // The quote shown is a saved copy when the newest quote answered was one.
128
+ ...(resolved.quote ? { quote: withSavedCopyMarkOf(resolved.quote, quote ?? snapshot?.quote) } : {}),
126
129
  priceHistory: normalizePriceHistory(priceHistory ?? snapshot?.priceHistory ?? []),
127
130
  };
128
131
  }