gloomberb 0.18.2 → 0.18.4

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 (954) hide show
  1. package/README.md +8 -8
  2. package/README.zh-CN.md +11 -11
  3. package/bin/{gloomberb → gloom} +3 -1
  4. package/package.json +6 -2
  5. package/patches/electrobun@1.18.1.patch +106 -5
  6. package/src/account/checkout-handoff.tsx +225 -0
  7. package/src/account/device-signin-dialog.tsx +2 -2
  8. package/src/account/device-signin.ts +2 -2
  9. package/src/account/upgrade-dialog.tsx +1 -1
  10. package/src/api-client/askg.ts +2 -2
  11. package/src/api-client/auth.ts +2 -2
  12. package/src/api-client/central-bank-rates.ts +6 -0
  13. package/src/api-client/collections.ts +5 -1
  14. package/src/api-client/cot.ts +2 -0
  15. package/src/api-client/crypto-etf-flows.ts +37 -0
  16. package/src/api-client/crypto-markets.ts +60 -0
  17. package/src/api-client/data.ts +99 -5
  18. package/src/api-client/doe.ts +15 -0
  19. package/src/api-client/equity-screener.ts +28 -0
  20. package/src/api-client/exchange-announcements.ts +38 -0
  21. package/src/api-client/fund-facts.ts +39 -0
  22. package/src/api-client/futures-curve.ts +9 -1
  23. package/src/api-client/hiring.ts +301 -0
  24. package/src/api-client/index.ts +24 -5
  25. package/src/api-client/market-discovery.ts +14 -0
  26. package/src/api-client/members.ts +6 -0
  27. package/src/api-client/mna.ts +6 -0
  28. package/src/api-client/paths.ts +18 -1
  29. package/src/api-client/request.ts +26 -7
  30. package/src/api-client/short-side-publications.ts +37 -0
  31. package/src/api-client/socket.ts +64 -4
  32. package/src/api-client/swap-curve.ts +35 -0
  33. package/src/api-client/telemetry.ts +17 -0
  34. package/src/api-client/themes.ts +7 -1
  35. package/src/api-client/types.ts +90 -2
  36. package/src/app/global-shortcuts.ts +1 -16
  37. package/src/app/pane-runtime/plugin-bindings.ts +9 -2
  38. package/src/app/runtime/desktop-deeplink.ts +5 -5
  39. package/src/app/runtime/plugin-bindings.ts +2 -2
  40. package/src/app/runtime/startup.ts +13 -0
  41. package/src/app/runtime/update.ts +28 -8
  42. package/src/app/star-prompt/status-line.tsx +2 -2
  43. package/src/architecture/core-builtin-allowlist.json +5 -2
  44. package/src/architecture/pane-tabs-allowlist.json +65 -0
  45. package/src/architecture/ui-literals-allowlist.json +3 -3
  46. package/src/architecture/ui-tokens-allowlist.json +1 -1
  47. package/src/brokers/broker-portfolio-sync.ts +37 -0
  48. package/src/brokers/directory.ts +11 -0
  49. package/src/brokers/logos.ts +3 -1
  50. package/src/brokers/signed-in/adapter.ts +304 -26
  51. package/src/brokers/signed-in/client.ts +14 -2
  52. package/src/brokers/signed-in/connect.ts +10 -2
  53. package/src/brokers/sync-broker-instance.ts +9 -1
  54. package/src/capabilities/chart-series.ts +22 -6
  55. package/src/cli/bank-section.ts +1 -1
  56. package/src/cli/base-converter.ts +23 -14
  57. package/src/cli/central-bank-events.ts +1 -1
  58. package/src/cli/command-options.ts +59 -4
  59. package/src/cli/commands/api.ts +9 -6
  60. package/src/cli/commands/broker.ts +2 -2
  61. package/src/cli/commands/command-utils.ts +2 -2
  62. package/src/cli/commands/crypto-hints.ts +7 -20
  63. package/src/cli/commands/fred-series.ts +4 -2
  64. package/src/cli/commands/keybindings.ts +2 -2
  65. package/src/cli/commands/market.ts +1189 -296
  66. package/src/cli/commands/news-hints.ts +2 -2
  67. package/src/cli/commands/news-search.ts +59 -6
  68. package/src/cli/commands/options-leaps.ts +41 -26
  69. package/src/cli/commands/overview.ts +121 -48
  70. package/src/cli/commands/plugins.ts +9 -9
  71. package/src/cli/commands/remote.ts +13 -10
  72. package/src/cli/commands/search.ts +64 -44
  73. package/src/cli/commands/system.ts +27 -19
  74. package/src/cli/commands/themes.ts +1 -1
  75. package/src/cli/commands/ticker.ts +272 -70
  76. package/src/cli/context.ts +3 -2
  77. package/src/cli/desktop-pane-shot-view.ts +2 -2
  78. package/src/cli/desktop-pane-shot.ts +1 -1
  79. package/src/cli/earnings-calendar.ts +4 -4
  80. package/src/cli/entry.ts +18 -1
  81. package/src/cli/errors.ts +20 -2
  82. package/src/cli/exit-codes.ts +1 -1
  83. package/src/cli/export-source.ts +10 -4
  84. package/src/cli/freshness.ts +2 -8
  85. package/src/cli/fx-forward.ts +130 -0
  86. package/src/cli/fx-pair.ts +10 -9
  87. package/src/cli/glossary.ts +64 -20
  88. package/src/cli/help.ts +20 -14
  89. package/src/cli/history-export.ts +126 -0
  90. package/src/cli/history-rows.ts +196 -43
  91. package/src/cli/index-roots.ts +5 -45
  92. package/src/cli/index.ts +32 -18
  93. package/src/cli/issuer-membership.ts +103 -0
  94. package/src/cli/known-gaps.ts +118 -25
  95. package/src/cli/listing-arg.ts +315 -43
  96. package/src/cli/name-entries.ts +73 -22
  97. package/src/cli/native-smoke.ts +2 -0
  98. package/src/cli/not-a-ticker.ts +19 -9
  99. package/src/cli/opentui-libc.ts +5 -0
  100. package/src/cli/options.ts +12 -0
  101. package/src/cli/pane-functions/capabilities.ts +77 -20
  102. package/src/cli/pane-functions/catalog.ts +40 -23
  103. package/src/cli/pane-functions/discovery.ts +1 -1
  104. package/src/cli/pane-functions/dom.ts +12 -8
  105. package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
  106. package/src/cli/pane-functions/freshness.ts +174 -23
  107. package/src/cli/pane-functions/function-help.ts +66 -27
  108. package/src/cli/pane-functions/headless.ts +48 -16
  109. package/src/cli/pane-functions/index.ts +82 -21
  110. package/src/cli/pane-functions/options.ts +22 -2
  111. package/src/cli/pane-functions/report-notices.ts +12 -4
  112. package/src/cli/pane-functions/report.ts +5 -5
  113. package/src/cli/pane-functions/resolver.ts +65 -14
  114. package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
  115. package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
  116. package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
  117. package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
  118. package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
  119. package/src/cli/pane-functions/screenshot-payload.ts +453 -0
  120. package/src/cli/pane-functions/screenshot-render.ts +316 -0
  121. package/src/cli/pane-functions/screenshot-types.ts +162 -0
  122. package/src/cli/pane-functions/shot-options.ts +8 -3
  123. package/src/cli/peso-dollar-rates.ts +79 -0
  124. package/src/cli/quote-answer.ts +38 -3
  125. package/src/cli/report-tables.ts +190 -27
  126. package/src/cli/restore-plugins.ts +29 -14
  127. package/src/cli/result.ts +122 -25
  128. package/src/cli/series-search.ts +5 -5
  129. package/src/cli/statement-filings.ts +19 -0
  130. package/src/cli/suffixed-listing-rows.ts +49 -0
  131. package/src/cli/symbol-args.ts +50 -15
  132. package/src/cli/symbol-cells.ts +120 -0
  133. package/src/cli/symbol-list.ts +226 -60
  134. package/src/cli/symbol-rows.ts +131 -25
  135. package/src/cli/ticker-resolution.ts +108 -11
  136. package/src/cli/treasury-yields.ts +5 -5
  137. package/src/cli/types.ts +2 -0
  138. package/src/cli/uncovered-instrument.ts +3 -3
  139. package/src/cli/unknown-command.ts +6 -6
  140. package/src/components/chart/composite/chart-layout.ts +131 -0
  141. package/src/components/chart/composite/composite-chart.tsx +387 -21
  142. package/src/components/chart/composite/drawing-store.ts +26 -6
  143. package/src/components/chart/composite/interactions.ts +9 -3
  144. package/src/components/chart/composite/legend.tsx +75 -3
  145. package/src/components/chart/composite/levels.ts +5 -0
  146. package/src/components/chart/composite/panel-gestures.ts +41 -0
  147. package/src/components/chart/composite/panel-labels.ts +2 -1
  148. package/src/components/chart/composite/panel-surface.tsx +316 -11
  149. package/src/components/chart/composite/price-axis-labels.tsx +27 -3
  150. package/src/components/chart/composite/rasterizer.ts +44 -19
  151. package/src/components/chart/composite/scene.ts +16 -4
  152. package/src/components/chart/composite/text-renderer.ts +48 -21
  153. package/src/components/chart/composite/toolbar.tsx +52 -5
  154. package/src/components/chart/composite/types.ts +19 -0
  155. package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
  156. package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
  157. package/src/components/chart/composite/use-composite-viewport.ts +12 -3
  158. package/src/components/chart/composite/value-scale.ts +149 -0
  159. package/src/components/chart/composite/volume-profile-paint.ts +6 -3
  160. package/src/components/chart/core/pointer.ts +2 -0
  161. package/src/components/chart-table/index.ts +2 -0
  162. package/src/components/chart-table/read.tsx +74 -0
  163. package/src/components/command-bar/commands/collection-list.ts +41 -19
  164. package/src/components/command-bar/commands/collection.ts +9 -3
  165. package/src/components/command-bar/commands/direct/index.ts +1 -1
  166. package/src/components/command-bar/commands/direct/runtime.ts +3 -2
  167. package/src/components/command-bar/pane-templates/items.ts +59 -6
  168. package/src/components/command-bar/pane-templates/workflow.ts +10 -1
  169. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  170. package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
  171. package/src/components/command-bar/routes/root/provider-search.ts +42 -11
  172. package/src/components/command-bar/routes/root/results.ts +18 -4
  173. package/src/components/command-bar/routes/root/runtime.ts +15 -3
  174. package/src/components/command-bar/routes/root/search-providers.ts +25 -2
  175. package/src/components/command-bar/routes/root/selection.ts +6 -1
  176. package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
  177. package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
  178. package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
  179. package/src/components/command-bar/selection-runtime.ts +50 -10
  180. package/src/components/command-bar/surface/index.tsx +24 -2
  181. package/src/components/command-bar/view-model.ts +7 -0
  182. package/src/components/command-bar/workflow/broker.ts +2 -2
  183. package/src/components/command-bar/workflow/collection-actions.ts +46 -7
  184. package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
  185. package/src/components/command-bar/workflow/fields.ts +8 -2
  186. package/src/components/command-bar/workflow/ops.ts +18 -4
  187. package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
  188. package/src/components/command-bar/workflow/submit.ts +3 -2
  189. package/src/components/command-bar/workflow/tickers.ts +5 -1
  190. package/src/components/company-logo.tsx +39 -0
  191. package/src/components/data-table/sections.ts +4 -2
  192. package/src/components/data-table/view.tsx +23 -3
  193. package/src/components/form-modal/confirm.tsx +2 -1
  194. package/src/components/form-modal/request.ts +5 -0
  195. package/src/components/form-modal/routes.ts +4 -1
  196. package/src/components/index.ts +13 -6
  197. package/src/components/layout/detached-pane-shell.tsx +1 -0
  198. package/src/components/layout/floating-pane.tsx +1 -0
  199. package/src/components/layout/header.tsx +234 -60
  200. package/src/components/layout/market-summary.ts +9 -6
  201. package/src/components/layout/pane/capture-status.ts +1 -1
  202. package/src/components/layout/pane/footer/model.ts +11 -5
  203. package/src/components/layout/pane/header.tsx +10 -3
  204. package/src/components/layout/pane/index.tsx +1 -0
  205. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  206. package/src/components/layout/shell/chrome.ts +19 -6
  207. package/src/components/layout/shell/index.tsx +2 -1
  208. package/src/components/layout/shell/pane/layers.tsx +19 -1
  209. package/src/components/layout/status-bar.tsx +2 -2
  210. package/src/components/market-board/index.tsx +11 -3
  211. package/src/components/onboarding/onboarding-wizard.tsx +1 -0
  212. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
  213. package/src/components/onboarding/wizard-broker-sync.ts +4 -1
  214. package/src/components/pane-settings-dialog/desktop.tsx +20 -7
  215. package/src/components/pane-settings-dialog/tui.tsx +17 -9
  216. package/src/components/pane-settings-dialog/value.ts +8 -1
  217. package/src/components/pane-settings-dialog.tsx +44 -13
  218. package/src/components/sign-in-code-panel.tsx +27 -19
  219. package/src/components/ui/column-grid.tsx +48 -4
  220. package/src/components/ui/country-flag-art.tsx +180 -0
  221. package/src/components/ui/country-flag.tsx +118 -12
  222. package/src/components/ui/data-table/opentui/index.tsx +61 -2
  223. package/src/components/ui/data-table/opentui/model.ts +109 -2
  224. package/src/components/ui/data-table/types.ts +1 -1
  225. package/src/components/ui/display.tsx +5 -1
  226. package/src/components/ui/index.ts +3 -1
  227. package/src/components/ui/logo-mark.tsx +11 -2
  228. package/src/components/ui/query-bar.tsx +1 -1
  229. package/src/components/ui/row-notes.ts +79 -0
  230. package/src/components/ui/stat-grid.tsx +28 -3
  231. package/src/components/ui/status.tsx +3 -1
  232. package/src/core/app-services.ts +4 -0
  233. package/src/core/state/app/layout.ts +4 -4
  234. package/src/data/cached-query.ts +8 -1
  235. package/src/data/config/chart-settings.ts +3 -3
  236. package/src/data/config/home.ts +10 -2
  237. package/src/data/config/layout.ts +4 -1
  238. package/src/data/config/store/node.ts +4 -0
  239. package/src/data/config/store/normalize.ts +13 -3
  240. package/src/data/config/store/portfolio-as-of-column.ts +2 -2
  241. package/src/data/json-storage.ts +2 -0
  242. package/src/data/saved-copies.ts +84 -0
  243. package/src/docs/snapshot.json +65 -32
  244. package/src/i18n/es.ts +51 -12
  245. package/src/i18n/ja.ts +51 -12
  246. package/src/i18n/ko.ts +51 -12
  247. package/src/i18n/zh-cn.ts +51 -12
  248. package/src/i18n/zh-tw.ts +51 -12
  249. package/src/layout/desks.ts +1 -1
  250. package/src/layout/launch-layout.ts +1 -1
  251. package/src/layout/pane-manager/floating-actions.ts +5 -3
  252. package/src/layout/pane-manager/floating.ts +43 -1
  253. package/src/layout/pane-manager/index.ts +2 -0
  254. package/src/layout/selected-ticker.ts +74 -0
  255. package/src/market-data/depositary-pair.ts +251 -0
  256. package/src/market-data/futures-board.ts +1 -1
  257. package/src/market-data/futures-contract.ts +124 -40
  258. package/src/market-data/fx-closes.ts +17 -2
  259. package/src/market-data/hooks.tsx +19 -11
  260. package/src/market-data/market/change-basis.ts +49 -7
  261. package/src/market-data/market/freshness.ts +34 -5
  262. package/src/market-data/market/period-return.ts +163 -0
  263. package/src/market-data/market/trading-sessions.ts +13 -0
  264. package/src/market-data/market/year-to-date.ts +8 -13
  265. package/src/market-data/performance.ts +20 -14
  266. package/src/market-data/perpetual-premium.ts +32 -0
  267. package/src/market-data/published-apac-sessions.ts +2 -1
  268. package/src/market-data/published-futures-sessions.ts +69 -0
  269. package/src/market-data/quotes/contributions.ts +5 -1
  270. package/src/market-data/quotes/freshness.ts +42 -0
  271. package/src/market-data/quotes/intraday-bar.ts +37 -0
  272. package/src/market-data/quotes/regular-session-valuation.ts +13 -8
  273. package/src/market-data/quotes/resolution.ts +4 -2
  274. package/src/market-data/tax-equivalent.ts +1 -1
  275. package/src/market-data/ticker-price-returns.ts +3 -1
  276. package/src/news/aggregator.ts +5 -1
  277. package/src/news/news-model.ts +4 -0
  278. package/src/news/types.ts +11 -0
  279. package/src/notifications/app-notifier.ts +1 -1
  280. package/src/plugins/auto-update.ts +2 -2
  281. package/src/plugins/builtin/account-management/index.tsx +1 -0
  282. package/src/plugins/builtin/account-management/pane.tsx +9 -8
  283. package/src/plugins/builtin/alerts/alert-engine.ts +30 -18
  284. package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
  285. package/src/plugins/builtin/alerts/cli.ts +485 -76
  286. package/src/plugins/builtin/alerts/command.ts +5 -10
  287. package/src/plugins/builtin/alerts/event-rows.ts +9 -5
  288. package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
  289. package/src/plugins/builtin/alerts/events.ts +108 -5
  290. package/src/plugins/builtin/alerts/fired-log.ts +78 -0
  291. package/src/plugins/builtin/alerts/format.ts +2 -1
  292. package/src/plugins/builtin/alerts/headless.ts +100 -39
  293. package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
  294. package/src/plugins/builtin/alerts/history-rows.ts +89 -0
  295. package/src/plugins/builtin/alerts/index.tsx +45 -18
  296. package/src/plugins/builtin/alerts/iv-level.ts +365 -0
  297. package/src/plugins/builtin/alerts/levels.ts +3 -3
  298. package/src/plugins/builtin/alerts/pane.tsx +97 -29
  299. package/src/plugins/builtin/alerts/price.ts +35 -0
  300. package/src/plugins/builtin/alerts/quotes.ts +24 -0
  301. package/src/plugins/builtin/alerts/research-builder.ts +84 -11
  302. package/src/plugins/builtin/alerts/research-rules.ts +77 -3
  303. package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
  304. package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
  305. package/src/plugins/builtin/alerts/scheduled.ts +188 -29
  306. package/src/plugins/builtin/alerts/storage.ts +13 -10
  307. package/src/plugins/builtin/alerts/targets.ts +12 -3
  308. package/src/plugins/builtin/analytics/index.tsx +8 -2
  309. package/src/plugins/builtin/analytics/metrics.ts +1 -1
  310. package/src/plugins/builtin/analytics/risk-book.ts +215 -0
  311. package/src/plugins/builtin/analytics/risk-client.ts +13 -6
  312. package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
  313. package/src/plugins/builtin/analytics/risk-math.ts +6 -3
  314. package/src/plugins/builtin/analytics/risk-model.ts +297 -104
  315. package/src/plugins/builtin/analytics/risk-options.ts +17 -2
  316. package/src/plugins/builtin/analytics/risk-pane.tsx +107 -43
  317. package/src/plugins/builtin/attention/pane.tsx +22 -16
  318. package/src/plugins/builtin/awards/headless.ts +7 -3
  319. package/src/plugins/builtin/awards/model.ts +18 -0
  320. package/src/plugins/builtin/awards/pane.tsx +12 -9
  321. package/src/plugins/builtin/backtest/export.ts +1 -1
  322. package/src/plugins/builtin/backtest/headless.ts +3 -3
  323. package/src/plugins/builtin/backtest/rules.ts +41 -5
  324. package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
  325. package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
  326. package/src/plugins/builtin/broker-manager/footer.ts +27 -11
  327. package/src/plugins/builtin/broker-manager/index.tsx +21 -12
  328. package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
  329. package/src/plugins/builtin/broker-manager/model.ts +12 -2
  330. package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
  331. package/src/plugins/builtin/broker-manager/table.ts +28 -12
  332. package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
  333. package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
  334. package/src/plugins/builtin/broker-trading/controller.ts +45 -13
  335. package/src/plugins/builtin/broker-trading/dock.tsx +24 -4
  336. package/src/plugins/builtin/broker-trading/headless.ts +2 -1
  337. package/src/plugins/builtin/broker-trading/index.tsx +7 -1
  338. package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
  339. package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
  340. package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
  341. package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
  342. package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
  343. package/src/plugins/builtin/broker-trading/pane.tsx +115 -26
  344. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
  345. package/src/plugins/builtin/broker-trading/ticket.tsx +122 -36
  346. package/src/plugins/builtin/buildout/cells.tsx +4 -0
  347. package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
  348. package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
  349. package/src/plugins/builtin/buildout/pane/index.tsx +34 -18
  350. package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
  351. package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
  352. package/src/plugins/builtin/catalysts/client.ts +2 -2
  353. package/src/plugins/builtin/catalysts/detail.tsx +3 -2
  354. package/src/plugins/builtin/catalysts/index.tsx +1 -1
  355. package/src/plugins/builtin/catalysts/model.ts +49 -4
  356. package/src/plugins/builtin/catalysts/pane.tsx +9 -11
  357. package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
  358. package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
  359. package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
  360. package/src/plugins/builtin/changelog/index.tsx +1 -0
  361. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +1 -0
  362. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
  363. package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
  364. package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
  365. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  366. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
  367. package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
  368. package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
  369. package/src/plugins/builtin/chart-composer/headless.ts +253 -35
  370. package/src/plugins/builtin/chart-composer/index.tsx +12 -2
  371. package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
  372. package/src/plugins/builtin/chart-composer/pane.tsx +128 -28
  373. package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
  374. package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
  375. package/src/plugins/builtin/chart-composer/session-history.ts +15 -21
  376. package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
  377. package/src/plugins/builtin/chart-composer/settings.ts +31 -19
  378. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +5 -6
  379. package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
  380. package/src/plugins/builtin/chat/attachments/model.ts +3 -1
  381. package/src/plugins/builtin/chat/content/index.tsx +1 -1
  382. package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
  383. package/src/plugins/builtin/chat/people.ts +1 -1
  384. package/src/plugins/builtin/clinical-trials/client.ts +5 -2
  385. package/src/plugins/builtin/clinical-trials/model.ts +13 -4
  386. package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
  387. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
  388. package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
  389. package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
  390. package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
  391. package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
  392. package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
  393. package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
  394. package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
  395. package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
  396. package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
  397. package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
  398. package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
  399. package/src/plugins/builtin/commodity-balances/pane.tsx +14 -8
  400. package/src/plugins/builtin/company-attention/app-rank.tsx +11 -10
  401. package/src/plugins/builtin/company-attention/building-model.ts +222 -0
  402. package/src/plugins/builtin/company-attention/building.tsx +172 -0
  403. package/src/plugins/builtin/company-attention/client.ts +117 -1
  404. package/src/plugins/builtin/company-attention/evidence.ts +16 -3
  405. package/src/plugins/builtin/company-attention/headless.ts +101 -5
  406. package/src/plugins/builtin/company-attention/index.tsx +6 -3
  407. package/src/plugins/builtin/company-attention/model.ts +33 -7
  408. package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
  409. package/src/plugins/builtin/company-attention/moves.tsx +159 -0
  410. package/src/plugins/builtin/company-attention/pane.tsx +89 -31
  411. package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
  412. package/src/plugins/builtin/company-attention/signals.tsx +160 -0
  413. package/src/plugins/builtin/company-kpis/headless.ts +3 -2
  414. package/src/plugins/builtin/company-kpis/model.ts +2 -2
  415. package/src/plugins/builtin/company-kpis/pane.tsx +5 -4
  416. package/src/plugins/builtin/composite-plugins.ts +2 -1
  417. package/src/plugins/builtin/congress-trades/headless.ts +1 -1
  418. package/src/plugins/builtin/connections/index.ts +1 -0
  419. package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
  420. package/src/plugins/builtin/correlation/headless.ts +7 -4
  421. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  422. package/src/plugins/builtin/correlation/settings.ts +1 -0
  423. package/src/plugins/builtin/cot/headless.ts +28 -6
  424. package/src/plugins/builtin/cot/model.ts +12 -0
  425. package/src/plugins/builtin/cot/pane.tsx +7 -5
  426. package/src/plugins/builtin/cpi/headless.ts +5 -1
  427. package/src/plugins/builtin/cpi/pane.tsx +11 -9
  428. package/src/plugins/builtin/credit-boards/model.ts +2 -0
  429. package/src/plugins/builtin/credit-documents/headless.ts +1 -1
  430. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  431. package/src/plugins/builtin/crypto-board/client.ts +86 -0
  432. package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
  433. package/src/plugins/builtin/crypto-board/defi.ts +239 -0
  434. package/src/plugins/builtin/crypto-board/headless.ts +117 -6
  435. package/src/plugins/builtin/crypto-board/index.tsx +8 -3
  436. package/src/plugins/builtin/crypto-board/model.ts +40 -8
  437. package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
  438. package/src/plugins/builtin/crypto-board/series.ts +19 -1
  439. package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
  440. package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
  441. package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
  442. package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
  443. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
  444. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  445. package/src/plugins/builtin/debug/index.tsx +1 -0
  446. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  447. package/src/plugins/builtin/docs/index.tsx +1 -1
  448. package/src/plugins/builtin/docs/pane.tsx +2 -2
  449. package/src/plugins/builtin/docs/targets.ts +1 -1
  450. package/src/plugins/builtin/doe/client.ts +5 -1
  451. package/src/plugins/builtin/doe/headless.ts +12 -6
  452. package/src/plugins/builtin/doe/model.ts +43 -6
  453. package/src/plugins/builtin/doe/pane.tsx +19 -12
  454. package/src/plugins/builtin/earnings/board-model.ts +13 -6
  455. package/src/plugins/builtin/earnings/board-table.ts +4 -1
  456. package/src/plugins/builtin/earnings/board.tsx +253 -50
  457. package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
  458. package/src/plugins/builtin/earnings/calendar.tsx +338 -0
  459. package/src/plugins/builtin/earnings/client.ts +4 -3
  460. package/src/plugins/builtin/earnings/headless.ts +53 -15
  461. package/src/plugins/builtin/earnings/history-model.ts +53 -36
  462. package/src/plugins/builtin/earnings/index.tsx +15 -22
  463. package/src/plugins/builtin/earnings/model.ts +17 -12
  464. package/src/plugins/builtin/earnings/scope.ts +89 -0
  465. package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
  466. package/src/plugins/builtin/earnings-calls/data.ts +146 -2
  467. package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
  468. package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
  469. package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
  470. package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
  471. package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
  472. package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
  473. package/src/plugins/builtin/econ/calendar-model.ts +132 -20
  474. package/src/plugins/builtin/econ/detail-view.tsx +48 -10
  475. package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
  476. package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
  477. package/src/plugins/builtin/econ/index.tsx +112 -25
  478. package/src/plugins/builtin/econ/print-watch.ts +2 -1
  479. package/src/plugins/builtin/econ/types.ts +32 -1
  480. package/src/plugins/builtin/econ-statistics/client.ts +16 -3
  481. package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
  482. package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
  483. package/src/plugins/builtin/econ-statistics/headless.ts +13 -2
  484. package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
  485. package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
  486. package/src/plugins/builtin/econ-statistics/stats.ts +103 -0
  487. package/src/plugins/builtin/econ-statistics/view.ts +16 -1
  488. package/src/plugins/builtin/equity-screener/client.ts +21 -3
  489. package/src/plugins/builtin/equity-screener/headless.ts +77 -5
  490. package/src/plugins/builtin/equity-screener/live.ts +1 -1
  491. package/src/plugins/builtin/equity-screener/model.ts +34 -0
  492. package/src/plugins/builtin/equity-screener/pane.tsx +148 -106
  493. package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
  494. package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
  495. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
  496. package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
  497. package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
  498. package/src/plugins/builtin/exposure/headless.ts +51 -6
  499. package/src/plugins/builtin/exposure/index.tsx +28 -2
  500. package/src/plugins/builtin/exposure/model.ts +7 -0
  501. package/src/plugins/builtin/exposure/pane.tsx +16 -10
  502. package/src/plugins/builtin/fear-greed/index.tsx +1 -1
  503. package/src/plugins/builtin/filing-events/index.tsx +1 -1
  504. package/src/plugins/builtin/futures/model.ts +10 -9
  505. package/src/plugins/builtin/futures/returns.ts +8 -36
  506. package/src/plugins/builtin/futures/table.tsx +1 -1
  507. package/src/plugins/builtin/futures-curve/client.ts +40 -9
  508. package/src/plugins/builtin/futures-curve/headless.ts +39 -17
  509. package/src/plugins/builtin/futures-curve/model.ts +258 -69
  510. package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
  511. package/src/plugins/builtin/fx-matrix/client.ts +15 -8
  512. package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
  513. package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
  514. package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
  515. package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
  516. package/src/plugins/builtin/gpu/board.tsx +4 -1
  517. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  518. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  519. package/src/plugins/builtin/gpu/headless.ts +9 -4
  520. package/src/plugins/builtin/gpu/history.tsx +4 -2
  521. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  522. package/src/plugins/builtin/help/index.tsx +13 -3
  523. package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
  524. package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
  525. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  526. package/src/plugins/builtin/holders/client.ts +15 -2
  527. package/src/plugins/builtin/holders/format.ts +28 -17
  528. package/src/plugins/builtin/holders/headless.ts +48 -8
  529. package/src/plugins/builtin/holders/index-holders.ts +264 -0
  530. package/src/plugins/builtin/holders/pane.tsx +79 -15
  531. package/src/plugins/builtin/holders/report-rows.ts +35 -7
  532. package/src/plugins/builtin/holders/table-model.ts +2 -1
  533. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  534. package/src/plugins/builtin/holders/treemap.tsx +2 -2
  535. package/src/plugins/builtin/insider/base-values.ts +11 -7
  536. package/src/plugins/builtin/insider/client.ts +3 -0
  537. package/src/plugins/builtin/insider/headless.ts +20 -7
  538. package/src/plugins/builtin/insider/index.tsx +17 -8
  539. package/src/plugins/builtin/insider/issuer.ts +34 -4
  540. package/src/plugins/builtin/insider/signals-client.ts +3 -3
  541. package/src/plugins/builtin/insider/signals.tsx +1 -1
  542. package/src/plugins/builtin/insider/table-model.ts +35 -13
  543. package/src/plugins/builtin/insider/table.tsx +5 -2
  544. package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
  545. package/src/plugins/builtin/iv-history/client.ts +20 -0
  546. package/src/plugins/builtin/iv-history/events.ts +145 -0
  547. package/src/plugins/builtin/iv-history/format.ts +3 -0
  548. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  549. package/src/plugins/builtin/iv-history/index.tsx +1 -1
  550. package/src/plugins/builtin/iv-history/model.ts +13 -4
  551. package/src/plugins/builtin/iv-history/series.ts +3 -1
  552. package/src/plugins/builtin/jobs/lock-note.ts +1 -1
  553. package/src/plugins/builtin/jobs/model.ts +4 -15
  554. package/src/plugins/builtin/jobs/pane.tsx +7 -5
  555. package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
  556. package/src/plugins/builtin/jodi/pane.tsx +14 -8
  557. package/src/plugins/builtin/layout-manager/index.tsx +1 -0
  558. package/src/plugins/builtin/macro-day/headless.ts +20 -7
  559. package/src/plugins/builtin/macro-day/index.tsx +3 -2
  560. package/src/plugins/builtin/macro-day/model.ts +29 -8
  561. package/src/plugins/builtin/macro-day/pane.tsx +46 -10
  562. package/src/plugins/builtin/macro-day/releases.ts +55 -13
  563. package/src/plugins/builtin/market-heatmap/data.ts +9 -2
  564. package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
  565. package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
  566. package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
  567. package/src/plugins/builtin/market-heatmap/model.ts +10 -7
  568. package/src/plugins/builtin/market-movers/client.ts +36 -21
  569. package/src/plugins/builtin/market-movers/headless.ts +8 -2
  570. package/src/plugins/builtin/market-movers/index.tsx +16 -10
  571. package/src/plugins/builtin/market-movers/screener.ts +33 -11
  572. package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
  573. package/src/plugins/builtin/market-movers/session-table.ts +4 -2
  574. package/src/plugins/builtin/market-movers/table.ts +4 -1
  575. package/src/plugins/builtin/market-valuation/headless.ts +2 -0
  576. package/src/plugins/builtin/market-valuation/view.ts +5 -0
  577. package/src/plugins/builtin/members/headless.ts +6 -5
  578. package/src/plugins/builtin/members/pane.tsx +11 -4
  579. package/src/plugins/builtin/mna/client.ts +1 -0
  580. package/src/plugins/builtin/mna/detail.tsx +20 -32
  581. package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
  582. package/src/plugins/builtin/mna/distress/model.ts +3 -2
  583. package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
  584. package/src/plugins/builtin/mna/headless.ts +6 -13
  585. package/src/plugins/builtin/mna/model.ts +93 -15
  586. package/src/plugins/builtin/mna/pane.tsx +5 -4
  587. package/src/plugins/builtin/news/headless.ts +12 -6
  588. package/src/plugins/builtin/news/index.tsx +10 -1
  589. package/src/plugins/builtin/news/wire/index.tsx +3 -2
  590. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
  591. package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
  592. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
  593. package/src/plugins/builtin/notes/cli.ts +50 -15
  594. package/src/plugins/builtin/notes/export.ts +17 -6
  595. package/src/plugins/builtin/notes/files.ts +16 -5
  596. package/src/plugins/builtin/openfda/pane.tsx +1 -1
  597. package/src/plugins/builtin/options/carry.ts +2 -2
  598. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  599. package/src/plugins/builtin/options/index.tsx +1 -1
  600. package/src/plugins/builtin/options/model-spot.ts +70 -4
  601. package/src/plugins/builtin/options/view.tsx +51 -23
  602. package/src/plugins/builtin/options-calculator/headless.ts +46 -7
  603. package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
  604. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  605. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  606. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  607. package/src/plugins/builtin/options-scenario/client.ts +142 -29
  608. package/src/plugins/builtin/options-scenario/collar.ts +8 -6
  609. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
  610. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
  611. package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
  612. package/src/plugins/builtin/options-scenario/csp.ts +101 -21
  613. package/src/plugins/builtin/options-scenario/headless.ts +49 -21
  614. package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
  615. package/src/plugins/builtin/options-scenario/index.tsx +28 -3
  616. package/src/plugins/builtin/options-scenario/model.ts +13 -0
  617. package/src/plugins/builtin/options-scenario/pane.tsx +22 -10
  618. package/src/plugins/builtin/perps/equity-row.tsx +11 -3
  619. package/src/plugins/builtin/perps/freshness.ts +8 -5
  620. package/src/plugins/builtin/perps/headless.ts +3 -1
  621. package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
  622. package/src/plugins/builtin/perps/live-rows.ts +161 -0
  623. package/src/plugins/builtin/perps/markets-section.tsx +154 -0
  624. package/src/plugins/builtin/perps/pane.tsx +36 -17
  625. package/src/plugins/builtin/perps/venue-markets.ts +96 -0
  626. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
  627. package/src/plugins/builtin/plugin-marketplace/pane.tsx +145 -60
  628. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +78 -27
  629. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
  630. package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
  631. package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
  632. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
  633. package/src/plugins/builtin/portfolio-list/headless.ts +6 -3
  634. package/src/plugins/builtin/portfolio-list/income.ts +13 -2
  635. package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
  636. package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
  637. package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
  638. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
  639. package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
  640. package/src/plugins/builtin/power/client.ts +6 -3
  641. package/src/plugins/builtin/power/evidence.ts +22 -7
  642. package/src/plugins/builtin/power/gas.tsx +5 -1
  643. package/src/plugins/builtin/power/pane.tsx +44 -32
  644. package/src/plugins/builtin/power/prices.tsx +10 -3
  645. package/src/plugins/builtin/research/analyst-headless.ts +25 -8
  646. package/src/plugins/builtin/research/analyst-model.ts +68 -3
  647. package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
  648. package/src/plugins/builtin/research/event-model.ts +3 -1
  649. package/src/plugins/builtin/research/event-sources.ts +12 -4
  650. package/src/plugins/builtin/research/index.tsx +2 -1
  651. package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
  652. package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
  653. package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
  654. package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
  655. package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
  656. package/src/plugins/builtin/research/relative-valuation-pane.tsx +76 -9
  657. package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
  658. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  659. package/src/plugins/builtin/reverse-dcf/model.ts +10 -5
  660. package/src/plugins/builtin/risk-factors/index.tsx +2 -2
  661. package/src/plugins/builtin/scanner/lock-note.ts +1 -1
  662. package/src/plugins/builtin/seasonality/client.ts +2 -1
  663. package/src/plugins/builtin/sec/forms.ts +13 -0
  664. package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
  665. package/src/plugins/builtin/sec/fund-facts.ts +68 -8
  666. package/src/plugins/builtin/sec/headless.ts +85 -8
  667. package/src/plugins/builtin/sec/index.tsx +58 -12
  668. package/src/plugins/builtin/sectors/client.ts +6 -4
  669. package/src/plugins/builtin/sectors/sector-model.ts +11 -15
  670. package/src/plugins/builtin/series-inventory.ts +2 -0
  671. package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
  672. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  673. package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
  674. package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
  675. package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
  676. package/src/plugins/builtin/shared/earnings-report.ts +3 -3
  677. package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
  678. package/src/plugins/builtin/shared/job-posting.ts +129 -0
  679. package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
  680. package/src/plugins/builtin/shared/report-freshness.ts +3 -0
  681. package/src/plugins/builtin/shared/research-trading.tsx +23 -0
  682. package/src/plugins/builtin/shared/series-cache.ts +2 -0
  683. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  684. package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
  685. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  686. package/src/plugins/builtin/short-interest/client.ts +2 -0
  687. package/src/plugins/builtin/short-interest/headless.ts +41 -12
  688. package/src/plugins/builtin/short-interest/index.tsx +23 -1
  689. package/src/plugins/builtin/short-interest/model.ts +70 -19
  690. package/src/plugins/builtin/short-interest/pane.tsx +19 -2
  691. package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
  692. package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
  693. package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
  694. package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
  695. package/src/plugins/builtin/short-interest/schedule.ts +35 -2
  696. package/src/plugins/builtin/short-interest/surface.tsx +11 -4
  697. package/src/plugins/builtin/short-interest/types.ts +9 -0
  698. package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
  699. package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
  700. package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
  701. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  702. package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
  703. package/src/plugins/builtin/supply-chain/model.ts +51 -3
  704. package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
  705. package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
  706. package/src/plugins/builtin/themes/headless.ts +8 -5
  707. package/src/plugins/builtin/themes/model.ts +14 -4
  708. package/src/plugins/builtin/themes/pane.tsx +24 -5
  709. package/src/plugins/builtin/thirteenf/headless.ts +5 -5
  710. package/src/plugins/builtin/thirteenf/signals.ts +3 -1
  711. package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
  712. package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
  713. package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
  714. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
  715. package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
  716. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
  717. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
  718. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
  719. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
  720. package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
  721. package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
  722. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
  723. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
  724. package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
  725. package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
  726. package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
  727. package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
  728. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
  729. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
  730. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
  731. package/src/plugins/builtin/ticker-detail/quote-footer.ts +8 -3
  732. package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
  733. package/src/plugins/builtin/vol-surface/client.ts +28 -7
  734. package/src/plugins/builtin/vol-surface/headless.ts +43 -15
  735. package/src/plugins/builtin/vol-surface/model.ts +34 -12
  736. package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
  737. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  738. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  739. package/src/plugins/builtin/volatility/model.ts +5 -10
  740. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
  741. package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
  742. package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
  743. package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
  744. package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
  745. package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
  746. package/src/plugins/builtin/world-venue-map/index.ts +3 -26
  747. package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
  748. package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
  749. package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
  750. package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
  751. package/src/plugins/builtin/yield-curve/client.ts +25 -0
  752. package/src/plugins/builtin/yield-curve/curves.ts +7 -4
  753. package/src/plugins/builtin/yield-curve/headless.ts +38 -4
  754. package/src/plugins/builtin/yield-curve/history.ts +4 -3
  755. package/src/plugins/builtin/yield-curve/index.tsx +12 -6
  756. package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
  757. package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
  758. package/src/plugins/bundle.ts +1 -1
  759. package/src/plugins/catalog-browser.ts +2 -1
  760. package/src/plugins/compat.ts +3 -3
  761. package/src/plugins/dependencies.ts +75 -14
  762. package/src/plugins/extracted-plugins.ts +132 -34
  763. package/src/plugins/host-link.ts +149 -34
  764. package/src/plugins/installer.ts +35 -10
  765. package/src/plugins/loader.ts +206 -38
  766. package/src/plugins/manager-node.ts +2 -2
  767. package/src/plugins/marketplace/deactivation.ts +22 -0
  768. package/src/plugins/marketplace/model.ts +2 -2
  769. package/src/plugins/marketplace/packs.ts +1 -1
  770. package/src/plugins/ownership.ts +1 -1
  771. package/src/plugins/peer-retry.ts +155 -0
  772. package/src/plugins/registry/contributions.ts +3 -1
  773. package/src/plugins/registry/index.ts +4 -3
  774. package/src/plugins/registry/plugin-state.ts +3 -0
  775. package/src/plugins/seed-notice.ts +20 -0
  776. package/src/plugins/shortcut-argument.ts +39 -0
  777. package/src/portfolio/allocation.ts +1 -1
  778. package/src/portfolio/collection-name.ts +31 -0
  779. package/src/portfolio/column-values.ts +10 -8
  780. package/src/portfolio/cost-fx.ts +296 -19
  781. package/src/portfolio/manual-lots.ts +75 -0
  782. package/src/portfolio/mutations.ts +60 -22
  783. package/src/portfolio/suffixed-listings.ts +209 -0
  784. package/src/portfolio/totals-currency.ts +24 -0
  785. package/src/public/__snapshots__/public-api.test.ts.snap +14 -0
  786. package/src/public/time-series.ts +11 -8
  787. package/src/remote/schema.ts +1 -1
  788. package/src/renderers/browser/config-host.ts +3 -0
  789. package/src/renderers/browser/main.tsx +23 -3
  790. package/src/renderers/dom/boot-frame-page.ts +187 -0
  791. package/src/renderers/dom/boot-frame.ts +237 -0
  792. package/src/renderers/dom/build-assets.ts +46 -8
  793. package/src/renderers/dom/desktop/controls.tsx +1 -1
  794. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  795. package/src/renderers/dom/dom-ui-host.tsx +4 -29
  796. package/src/renderers/dom/host/ascii-text.ts +16 -2
  797. package/src/renderers/dom/host/box.tsx +43 -2
  798. package/src/renderers/dom/host/chart-surface.tsx +2 -0
  799. package/src/renderers/dom/host/hls.chunk.ts +3 -0
  800. package/src/renderers/dom/host/image-surface.tsx +49 -0
  801. package/src/renderers/dom/host/media-surface.tsx +24 -18
  802. package/src/renderers/dom/host/mouse.ts +30 -0
  803. package/src/renderers/dom/host/size-observer.ts +1 -1
  804. package/src/renderers/dom/host/style.ts +1 -1
  805. package/src/renderers/dom/host/tabs.tsx +16 -5
  806. package/src/renderers/dom/host/top-surface.ts +127 -0
  807. package/src/renderers/dom/styles.css +115 -0
  808. package/src/renderers/dom/test-utils.tsx +2 -1
  809. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
  810. package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
  811. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
  812. package/src/renderers/electrobun/bun/desktop/backend.ts +56 -3
  813. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
  814. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
  815. package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
  816. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  817. package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
  818. package/src/renderers/electrobun/bun/index.ts +11 -1
  819. package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
  820. package/src/renderers/electrobun/bun/window/create.ts +4 -1
  821. package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
  822. package/src/renderers/electrobun/shared/protocol.ts +6 -1
  823. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  824. package/src/renderers/electrobun/view/config-host.ts +3 -0
  825. package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
  826. package/src/renderers/electrobun/view/main.tsx +30 -4
  827. package/src/renderers/electrobun/view/news-source.ts +9 -7
  828. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -2
  829. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  830. package/src/renderers/opentui/browser-open.ts +88 -0
  831. package/src/renderers/opentui/host.tsx +5 -14
  832. package/src/renderers/opentui/image/loader.ts +3 -2
  833. package/src/renderers/opentui/native-smoke.ts +10 -1
  834. package/src/renderers/opentui/pane-image/index.ts +2 -1
  835. package/src/renderers/opentui/render-lib.ts +253 -0
  836. package/src/renderers/opentui/render-timing-smoke.ts +1 -1
  837. package/src/renderers/opentui/start.tsx +8 -1
  838. package/src/renderers/share/layout-view.tsx +2 -2
  839. package/src/renderers/share/main.tsx +4 -4
  840. package/src/renderers/share/view.tsx +1 -1
  841. package/src/series/resolve.ts +22 -6
  842. package/src/series/standalone.ts +30 -2
  843. package/src/series/types.ts +8 -1
  844. package/src/shares/api.ts +4 -1
  845. package/src/shares/pane.ts +1 -1
  846. package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
  847. package/src/sources/gloomberb-cloud/index.ts +39 -16
  848. package/src/sources/gloomberb-cloud/news.ts +39 -0
  849. package/src/sources/gloomberb-cloud/normalizers.ts +14 -1
  850. package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
  851. package/src/sources/listing-symbols.ts +37 -0
  852. package/src/sources/provider-errors.ts +16 -2
  853. package/src/sources/provider-router/financial-routes.ts +21 -3
  854. package/src/sources/provider-router/financials.ts +15 -2
  855. package/src/sources/provider-router/history.ts +30 -8
  856. package/src/sources/provider-router/index.ts +16 -0
  857. package/src/sources/provider-router/primary.ts +1 -1
  858. package/src/state/app/bootstrap.ts +23 -0
  859. package/src/state/hooks/inline-tickers.ts +30 -5
  860. package/src/sync/controller.ts +1 -1
  861. package/src/sync/core-contributors.ts +46 -14
  862. package/src/team/collections-sync.ts +2 -1
  863. package/src/team/collections.ts +178 -17
  864. package/src/telemetry/crash-reports.ts +21 -4
  865. package/src/telemetry/update-failures-node.ts +59 -0
  866. package/src/telemetry/update-failures.ts +307 -0
  867. package/src/test-support/builtin-panes.ts +1 -1
  868. package/src/theme/colors.ts +3 -30
  869. package/src/theme/ui-tokens.ts +37 -1
  870. package/src/tickers/company-name-listing.ts +215 -36
  871. package/src/tickers/company-name.ts +43 -0
  872. package/src/tickers/index-roots.ts +47 -0
  873. package/src/tickers/instrument-label.ts +5 -1
  874. package/src/tickers/listing-fate.ts +30 -0
  875. package/src/tickers/metadata.ts +10 -0
  876. package/src/tickers/perpetual-listing.ts +5 -2
  877. package/src/tickers/search/home-listing.ts +75 -0
  878. package/src/tickers/search/index.ts +76 -27
  879. package/src/tickers/search/ranking.ts +54 -4
  880. package/src/tickers/search/upsert.ts +29 -0
  881. package/src/tickers/terminal-codes.ts +416 -40
  882. package/src/time-series/bar-aggregation.ts +243 -0
  883. package/src/time-series/base-series.ts +307 -0
  884. package/src/time-series/chart-bounds.ts +349 -0
  885. package/src/time-series/chart-seed.ts +142 -0
  886. package/src/time-series/economic.ts +5 -0
  887. package/src/time-series/fundamentals.ts +18 -3
  888. package/src/time-series/history-acquisition.ts +499 -0
  889. package/src/time-series/leverage.ts +100 -0
  890. package/src/time-series/market-resolution.ts +4 -2
  891. package/src/time-series/market.ts +2 -1
  892. package/src/time-series/price-comparison.ts +175 -30
  893. package/src/time-series/range.ts +9 -1
  894. package/src/time-series/resolution.ts +174 -16
  895. package/src/time-series/resolve-context.ts +157 -0
  896. package/src/time-series/resolve.ts +126 -1577
  897. package/src/time-series/series-name.ts +38 -0
  898. package/src/time-series/series-presentation.ts +208 -0
  899. package/src/time-series/spec.ts +11 -6
  900. package/src/time-series/studies.ts +109 -12
  901. package/src/time-series/tail-reconcile.ts +121 -0
  902. package/src/time-series/types.ts +29 -2
  903. package/src/time-series/use-chart-resolution.ts +4 -5
  904. package/src/types/broker.ts +10 -0
  905. package/src/types/config.ts +14 -0
  906. package/src/types/data-provider.ts +20 -0
  907. package/src/types/exchange-rate.ts +6 -0
  908. package/src/types/financials.ts +104 -7
  909. package/src/types/headless.ts +83 -6
  910. package/src/types/instrument.ts +3 -0
  911. package/src/types/issuer-listings.ts +28 -0
  912. package/src/types/plugin.ts +65 -8
  913. package/src/types/price-history.ts +2 -0
  914. package/src/types/ticker.ts +13 -0
  915. package/src/types/trading.ts +59 -1
  916. package/src/ui/ascii-font.ts +13 -1
  917. package/src/ui/host.tsx +6 -1
  918. package/src/updater/command-names.ts +175 -0
  919. package/src/updater/failure.ts +163 -0
  920. package/src/updater/index.ts +97 -28
  921. package/src/utils/calendar-date.ts +32 -15
  922. package/src/utils/cash-runway.ts +241 -0
  923. package/src/utils/central-banks.ts +42 -0
  924. package/src/utils/cli-output.ts +259 -80
  925. package/src/utils/contract-month.ts +16 -0
  926. package/src/utils/crypto-pair.ts +61 -3
  927. package/src/utils/currency-pair.ts +26 -0
  928. package/src/utils/currency-units.ts +29 -0
  929. package/src/utils/datetime-format.ts +10 -0
  930. package/src/utils/debug-log.ts +1 -1
  931. package/src/utils/decimal-comma.ts +39 -6
  932. package/src/utils/exchange-announcements.ts +71 -0
  933. package/src/utils/exchange-rate-snapshot.ts +10 -0
  934. package/src/utils/financial-statements.ts +83 -4
  935. package/src/utils/format.ts +34 -16
  936. package/src/utils/fundamentals.ts +150 -34
  937. package/src/utils/fx-market-hours.ts +10 -1
  938. package/src/utils/latest-financial-period.ts +8 -0
  939. package/src/utils/linux-libc.ts +54 -0
  940. package/src/utils/market-capitalization.ts +111 -15
  941. package/src/utils/max-drawdown.ts +64 -0
  942. package/src/utils/network-failure.ts +83 -0
  943. package/src/utils/option-expiry.ts +139 -7
  944. package/src/utils/price-earnings.ts +11 -5
  945. package/src/utils/price-history.ts +18 -8
  946. package/src/utils/price-window.ts +131 -0
  947. package/src/utils/release-date.ts +47 -0
  948. package/src/utils/sec.ts +71 -0
  949. package/src/utils/throttled-fetch.ts +11 -4
  950. package/src/utils/trailing-eps.ts +67 -0
  951. package/src/utils/typed-text.ts +21 -0
  952. package/src/utils/utc-time.ts +24 -1
  953. package/src/version.ts +6 -0
  954. package/src/cli/pane-functions/screenshot.ts +0 -2122
@@ -1,45 +1,32 @@
1
1
  import { logFailure } from "../utils/log-failure";
2
- import { resolveAssetDisplayKind } from "../market-data/market/format";
3
2
  import { hasValidQuoteObservationTime } from "../market-data/quotes/freshness";
4
3
  import { delistingLabel } from "../market-data/quotes/delisting";
5
- import { SnapshotHistoryUnavailableError } from "../market-data/snapshot-provider";
6
4
  import { financialPeriodCoverage, financialPeriodCoverageWarnings, limitSeriesObservations } from "./financial-period-coverage";
7
- import { HistoryCoverageError, historyCoverageNotice } from "../sources/history-coverage";
8
- import { ProviderMissError } from "../sources/provider-errors";
9
- import { HISTORY_RETENTION_MAX_AGE_MS, canonicalHistoryInterval, historyRetentionNotice, historyStartNotice, isHistoryRetentionError, parseHistoryRecoveryCandidate, type HistoryRecoveryCandidate, type HistoryRetention, type HistoryRetentionError } from "../sources/history-retention";
10
- import { getRouterEntityKey } from "../sources/provider-router/cache";
11
- import { publicListingTarget } from "../sources/listing-target";
12
- import { fetchHistoryResult } from "../sources/history-result";
13
- import type { HistorySession, PriceHistoryResult } from "../types/price-history";
5
+ import { historyCoverageNotice } from "../sources/history-coverage";
6
+ import { historyRetentionNotice, historyStartNotice } from "../sources/history-retention";
14
7
  import type { BrokerContractRef } from "../types/instrument";
15
8
  import { FINANCIAL_VINTAGE_NOTICE, SEC_EPS_BASIS_NOTICE } from "../utils/financial-statements";
16
- import { appendLiveQuotePoint, hasUnknownBondHistoryBasis } from "./chart-data";
17
- import { LiveBarAccumulator } from "./live-bars";
18
- import {
19
- getTimeRangeForDateWindow,
20
- isDateWindowWithinTimeRange,
21
- subtractTimeRange,
22
- } from "./date-window";
9
+ import { subtractTimeRange } from "./date-window";
23
10
  import {
24
11
  CHART_RESOLUTION_STEP_MS,
12
+ chartBarSizeSource,
25
13
  clampTimeRangeToMaxRange,
26
14
  DEFAULT_CHART_RESOLUTION_SUPPORT,
27
- getBestSupportedResolutionForVisibleWindow,
28
- getNextBufferRange,
29
- getSupportedPresetResolution,
30
15
  getSupportMaxRange,
31
16
  intersectChartResolutionSupport,
17
+ isCustomChartResolution,
32
18
  isIntradayResolution,
33
19
  normalizeChartResolutionSupport,
34
20
  type ChartResolutionSupport,
35
21
  type ManualChartResolution,
36
22
  } from "./resolution";
23
+ import { aggregatePriceBars, futuresOpenMinuteOf, withBuiltBarSize } from "./bar-aggregation";
37
24
  import { TIME_RANGES, type TimeRange } from "./range";
38
- import type { DataProvider, MarketDataRequestContext } from "../types/data-provider";
39
- import type { Quote, QuoteMetadata, TickerFinancials } from "../types/financials";
25
+ import type { DataProvider } from "../types/data-provider";
26
+ import type { Quote, TickerFinancials } from "../types/financials";
40
27
  import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../market-data/quotes/metadata";
41
- import type { FredSeriesLoadResult, FredSeriesRequest } from "../sources/gloomberb-cloud/fred-series";
42
- import { extractFredSeries, fredCreditCoverageNotice } from "./economic";
28
+ import type { FredSeriesRequest } from "../sources/gloomberb-cloud/fred-series";
29
+ import { fredCreditCoverageNotice } from "./economic";
43
30
  import {
44
31
  getTimeSeriesField,
45
32
  isFundamentalFieldId,
@@ -48,52 +35,36 @@ import {
48
35
  } from "./field-catalog";
49
36
  import {
50
37
  fundamentalSeriesUsesAvailabilityFallback,
51
- unreportedStatementPeriod,
52
- valuationCurrencyWarning,
53
- valuationPriceIssues,
54
38
  valuationSeriesUsesLiveQuote,
55
39
  valuationSeriesUsesPriceHistory,
56
40
  } from "./fundamentals";
57
41
  import { FORWARD_PE_BASIS_NOTICE, REALIZED_NTM_PE_BASIS_NOTICE } from "./forward-valuation";
58
42
  import { valuationPriceWarning } from "./valuation-price";
59
43
  import { VALUATION_CURRENCY_WARNING_PREFIX } from "./valuation-currency";
60
- import { extractSecuritySeries, collectPriceHistoryIntegrity, chartPriceHistoryIntegrityNotices } from "./market";
61
- import {
62
- activeStudyInputSeriesIds,
63
- maxStudyWarmupPoints,
64
- resolveStudies,
65
- } from "./studies";
66
- import { applyResolvedSeriesTransform } from "./transforms";
44
+ import { chartPriceHistoryIntegrityNotices } from "./market";
45
+ import { maxStudyWarmupPoints, resolveStudies } from "./studies";
67
46
  import { clipSeriesToWindow } from "./alignment";
68
- import { bufferPriceComparison, clipPriceComparison, comparedBufferPoints, priceComparisonBoundsForSeries, priceComparisonSeriesIds, priceObservationWindowFilter, resolvePriceComparison, type PriceComparison } from "./price-comparison";
69
- import { reportingCurrencySeries } from "./reporting-currency";
70
- import { chartQuoteOverrideKeyForSource, compareChartQuoteRecency } from "./live-quotes";
71
- import { chartSeriesSourceKey } from "../capabilities/chart-series";
72
- import { isResolutionFineEnoughForMarketPeriod, resolutionForExplicitMarketPeriods } from "./market-resolution";
73
- import { marketSeriesFrequency } from "./market";
74
47
  import {
75
- rememberParsedPriceHistory,
76
- parsedPriceHistoryKey,
77
- readSeedListingFacts,
78
- rememberSeedListingFacts,
79
- } from "./parsed-history-cache";
48
+ bufferPriceComparison,
49
+ clipPriceComparison,
50
+ priceComparisonBoundsForSeries,
51
+ priceComparisonSeriesIds,
52
+ resolvePriceComparison,
53
+ } from "./price-comparison";
54
+ import { chartQuoteOverrideKeyForSource } from "./live-quotes";
55
+ import { chartSeriesSourceKey } from "../capabilities/chart-series";
56
+ import { rememberSeedListingFacts } from "./parsed-history-cache";
80
57
  import {
81
58
  canonicalExchange,
82
59
  isUsListingExchange,
83
60
  parsePublicTickerKey,
84
- publicTickerKey,
85
- resolveExchangeTimeZone,
86
61
  } from "../utils/exchanges";
87
- import { getPricePointTimestamp, isPriceHistoryStaleForCurrentWindow } from "../utils/price-history";
88
- import { timeZoneLabel, zonedDateKey, zonedWallClockMs } from "../utils/zoned-date-time";
89
- import { formatShortDate } from "../utils/datetime-format";
90
- import { latestSessionStart, latestTradingSessionOpen, listingTimeZone, priorSessionClose } from "../market-data/market/trading-sessions";
91
- import { futuresGenericCaption, futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
62
+ import { isPriceHistoryStaleForCurrentWindow } from "../utils/price-history";
63
+ import { listingTimeZone } from "../market-data/market/trading-sessions";
64
+ import { futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
92
65
  import { classifyInstrumentType, kindFromListingSyntax } from "../tickers/instrument-kind";
93
- import { contractPriceUnit } from "./price-units";
94
- import { INDEX_POINTS_UNIT, isIndexLevel } from "../market-data/market/index-level";
95
- import { isDatasetChart, isOhlcSeriesStyle } from "./spec";
96
- import { SERIES_COLORS } from "../theme/series-colors";
66
+ import { isDatasetChart } from "./spec";
67
+ import { seriesMessageName } from "./series-name";
97
68
  import type {
98
69
  ChartResolutionResult,
99
70
  ChartSeriesSpec,
@@ -101,242 +72,67 @@ import type {
101
72
  ResolvedSeries,
102
73
  TimeSeriesPoint,
103
74
  } from "./types";
104
-
105
- export interface ChartResolveSources {
106
- dataProvider: DataProvider | null;
107
- loadFredSeries: (request: FredSeriesRequest) => Promise<FredSeriesLoadResult>;
108
- now?: Date;
109
- /** Latest streamed quote per security identity, layered over snapshot data. */
110
- quoteOverrides?: ReadonlyMap<string, Quote>;
111
- /**
112
- * Start of the current uninterrupted quote observation. Bars formed from
113
- * quotes inside it are continuous with the loaded history.
114
- */
115
- liveSince?: number;
116
- /** Capture the resolved inputs, including the calculation buffer, for an export. */
117
- onSecurityData?: (series: ChartSeriesSpec, data: TickerFinancials, includesHistory: boolean) => void;
118
- /**
119
- * How often, in milliseconds, a plugin-owned series is loaded again while
120
- * it is charted; nothing for a series that keeps what it loaded.
121
- */
122
- capabilityRefreshMs?: (source: Extract<ChartSeriesSpec["source"], { kind: "capability" }>) => number | undefined;
123
- /**
124
- * What a futures price counts, from its contract terms ("USD / gallon"), so
125
- * the chart states it and a formula can tell gallons from barrels. Null where
126
- * the terms do not say. Without it a future's price is in its currency alone.
127
- */
128
- loadFuturesPriceUnit?: (symbol: string) => Promise<string | null>;
129
- /** Provider-neutral boundary for plugin-owned chart series. */
130
- resolveCapabilitySeries?: (
131
- source: Extract<ChartSeriesSpec["source"], { kind: "capability" }>,
132
- viewport: ChartSpec["viewport"],
133
- spec: ChartSeriesSpec,
134
- ) => Promise<ResolvedSeries>;
135
- }
136
-
137
- export interface ChartResolveOptions {
138
- /** Runtime zoom window used only to choose an adaptive Auto resolution. */
139
- autoViewport?: { start: Date; end: Date } | null;
140
- /** Runtime interaction window used to load history around the visible chart. */
141
- requestViewport?: { start: Date; end: Date } | null;
142
- /** Approximate number of horizontal observations the current surface can use. */
143
- targetPointCount?: number;
144
- /** Resolution currently on screen, kept through small Auto zooms. */
145
- currentResolution?: ManualChartResolution | null;
146
- /**
147
- * Price-only charts paint before slow broker support answers and stand in a
148
- * placeholder list meanwhile. One-shot loaders wait for the real list.
149
- */
150
- awaitResolutionSupport?: boolean;
151
- /** Fired once the real support list lands after a placeholder was used. */
152
- onResolutionSupportSettled?: () => void;
153
- }
154
-
155
- /** A current-window intraday history whose newest bars can be refreshed in place. */
156
- interface LiveTailRequest {
157
- source: Extract<ChartSeriesSpec["source"], { kind: "security" }>;
158
- resolution: ManualChartResolution;
159
- accumulationKey: string;
160
- historySession?: "extended";
161
- }
162
-
163
- /** Recent-window requests for the same history, shared by every chart asking at once. */
164
- const tailRequestsInFlight = new WeakMap<DataProvider, Map<string, Promise<PriceHistoryResult | null>>>();
165
-
166
- /** Raw source data retained while live quotes recompute the chart tail. */
167
- export class ChartResolveCache {
168
- readonly financialsByInstrument = new Map<string, Promise<TickerFinancials | null>>();
169
- readonly quoteMetadataByInstrument = new Map<string, Promise<QuoteMetadata | null>>();
170
- /** Each futures symbol's contract price unit, asked once per chart once known. */
171
- readonly futuresPriceUnits = new Map<string, Promise<string | null>>();
172
- readonly priceHistoryByRequest = new Map<string, Promise<LoadedPriceHistory>>();
173
- readonly priceHistoryExpiryByRequest = new Map<string, number>();
174
- readonly priceHistoryRefreshAfter = new Map<string, number>();
175
- readonly accumulatedPriceHistory = new Map<string, LoadedPriceHistory>();
176
- readonly resolutionSupportByInstrument = new Map<string, Promise<ChartResolutionSupport[]>>();
177
- readonly settledResolutionSupport = new Map<string, ChartResolutionSupport[]>();
178
- readonly rejectedResolutionSupport = new Set<string>();
179
- readonly fredSeriesByRequest = new Map<string, Promise<FredSeriesLoadResult>>();
180
- readonly capabilitySeriesByRequest = new Map<string, Promise<ResolvedSeries>>();
181
- /** Forming bars per accumulated history, kept across live re-resolves. */
182
- readonly liveBars = new Map<string, LiveBarAccumulator>();
183
- /** The loaded history each accumulated history last merged, so an unchanged load is not merged again. */
184
- readonly accumulatedFrom = new Map<string, LoadedPriceHistory>();
185
- /** Histories plotted by the latest resolve that follow the present. */
186
- liveTails: ReadonlyMap<string, LiveTailRequest> = new Map();
187
- tailReconcile: Promise<boolean> | null = null;
188
- /**
189
- * Bumped whenever source data enters the cache. A resolve that ran without a
190
- * bump changed at most what the quotes change: the newest points.
191
- */
192
- revision = 0;
193
-
194
- /** Broker history endpoints are paced per account, so their recent windows are requested less often. */
195
- get liveTailsUseBroker(): boolean {
196
- for (const tail of this.liveTails.values()) if (requestContext(tail.source).brokerId) return true;
197
- return false;
198
- }
199
-
200
- liveBarsFor(accumulationKey: string): LiveBarAccumulator {
201
- let bars = this.liveBars.get(accumulationKey);
202
- if (!bars) {
203
- bars = new LiveBarAccumulator();
204
- this.liveBars.set(accumulationKey, bars);
205
- }
206
- return bars;
207
- }
208
-
209
- /** True once after a streamed quote met a gap between the loaded history and now. */
210
- takeTailReconcileRequest(): boolean {
211
- let requested = false;
212
- for (const bars of this.liveBars.values()) requested = bars.takeReconcileRequest() || requested;
213
- return requested;
214
- }
215
- }
216
-
217
- interface DateBounds {
218
- start: number | null;
219
- /** Inclusive upper bound. */
220
- end: number | null;
221
- }
222
-
223
- interface PriceHistoryRequest {
224
- bounds: DateBounds;
225
- visibleBounds: DateBounds;
226
- explicitWindow: boolean;
227
- /** Dates the chart was given, not a view the user panned or zoomed to. */
228
- authoredWindow: boolean;
229
- fallbackRange: TimeRange;
230
- resolution: ManualChartResolution;
231
- allowProviderDefaultFallback: boolean;
232
- support: readonly ChartResolutionSupport[];
233
- requiredWarmupPoints: number;
234
- historyRequestKey?: string;
235
- /** Ask for pre-market and after-hours bars. */
236
- historySession?: "extended";
237
- }
238
-
239
- interface LoadedPriceHistory extends PriceHistoryResult {
240
- expiresAt?: number;
241
- requestKey?: string;
242
- accumulationKey?: string | null;
243
- /** The first bar the source has at this cadence, once it has said there is nothing older. */
244
- historyStart?: number;
245
- recovery?: {
246
- sourceKey: string;
247
- /** First bar requested from the retained window. */
248
- start: number;
249
- end: number;
250
- requiredWarmupPoints: number;
251
- usableWarmupPoints: number;
252
- /** The validated source evidence the recovery was requested with. */
253
- retention: HistoryRetention;
254
- };
255
- }
256
-
257
- class PriceHistoryAcquisitionError extends Error {
258
- constructor(message: string, readonly retryAt: number) { super(message); }
259
- }
260
-
261
- /** Compatible intervals from different source/session contracts cannot share a capture. */
262
- export function priceHistoryAcquisitionIdentity(result: Pick<PriceHistoryResult, "session" | "sourceKey">): string {
263
- const { observedAt: _observedAt, ...session } = result.session ?? {};
264
- return JSON.stringify([result.sourceKey ?? null, result.session ? session : null]);
265
- }
266
-
267
- /** Merged history keeps the actual acquisition clock belonging to its finite tail. */
268
- export function priceHistoryTailAcquisition<T extends { points: TickerFinancials["priceHistory"] }>(previous: T | undefined, incoming: T): T {
269
- const latest = (value: T) => value.points.reduce((last, point) => {
270
- const time = getPricePointTimestamp(point);
271
- return Number.isFinite(point.close) && Number.isFinite(time) ? Math.max(last, time) : last;
272
- }, Number.NEGATIVE_INFINITY);
273
- return previous && latest(previous) > latest(incoming) ? previous : incoming;
274
- }
275
-
276
- const DAY_MS = 24 * 60 * 60 * 1_000;
277
- const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
278
-
279
- function requestContext(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): MarketDataRequestContext {
280
- return {
281
- brokerId: spec.instrument.instrument?.brokerId ?? spec.instrument.brokerId,
282
- brokerInstanceId: spec.instrument.instrument?.brokerInstanceId ?? spec.instrument.brokerInstanceId,
283
- instrument: spec.instrument.instrument ?? null,
284
- };
285
- }
286
-
287
- function instrumentKey(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
288
- return chartQuoteOverrideKeyForSource(spec);
289
- }
290
-
291
- function instrumentLabel(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
292
- return publicTickerKey(spec.instrument.symbol, spec.instrument.exchange);
293
- }
294
-
295
- /**
296
- * Shown while a current chart draws a saved copy that no refetch brought up
297
- * to date: the last saved bar on the venue's clock, as the axis reads it.
298
- */
299
- function behindHistoryNotice(
300
- source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
301
- points: TickerFinancials["priceHistory"],
302
- now: number,
303
- ): string | null {
304
- const latest = points.findLast((point) => Number.isFinite(point.close) && Number.isFinite(getPricePointTimestamp(point)));
305
- if (!latest) return null;
306
- const zone = listingTimeZone(source.instrument.symbol, source.instrument.exchange) ?? "UTC";
307
- const wall = new Date(zonedWallClockMs(getPricePointTimestamp(latest), zone)).toISOString();
308
- const sameYear = wall.slice(0, 4) === zonedDateKey(now, zone).slice(0, 4);
309
- const day = formatShortDate(wall.slice(0, 10), { utc: true, year: sameYear ? false : "numeric" });
310
- return `${instrumentLabel(source)}: newer bars are unavailable; showing saved bars as of ${day} ${wall.slice(11, 16)} ${timeZoneLabel(zone)}.`;
311
- }
312
-
313
- // A series that fails to load keeps its place with no observations. Dropping it
314
- // made a series the user had added disappear from the legend while it still sat
315
- // in the series editor, with nothing on screen to explain the difference.
316
- function unloadableSeries(spec: ChartSeriesSpec, index: number, warning: string): ResolvedSeries {
317
- const field = spec.source.kind === "security" ? getTimeSeriesField(spec.source.fieldId) : undefined;
318
- const label = spec.source.kind === "security"
319
- ? `${instrumentLabel(spec.source)} ${field?.seriesLabel ?? field?.shortLabel ?? spec.source.fieldId.split(".").at(-1) ?? "Series"}`
320
- : spec.source.kind === "economic"
321
- ? `FRED ${spec.source.seriesId}`
322
- : spec.source.seriesId;
323
- return {
324
- id: spec.id,
325
- label: spec.label?.trim() || label,
326
- color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
327
- unit: field?.unit ?? "",
328
- unitGroup: field?.unitGroup ?? "unknown",
329
- nativeFrequency: field?.nativeFrequency ?? "daily",
330
- dataShape: field?.dataShape ?? "scalar",
331
- style: spec.style,
332
- transform: spec.transform,
333
- axis: spec.axis === "right" ? "right" : "left",
334
- panelId: spec.panelId,
335
- interpolation: spec.interpolation,
336
- warning,
337
- points: [],
338
- };
339
- }
75
+ import {
76
+ type ChartResolveSources,
77
+ type ChartResolveOptions,
78
+ type LiveTailRequest,
79
+ ChartResolveCache,
80
+ type LoadedPriceHistory,
81
+ requestContext,
82
+ instrumentKey,
83
+ instrumentLabel,
84
+ } from "./resolve-context";
85
+ import {
86
+ dateOnly,
87
+ explicitBounds,
88
+ rangeDurationBounds,
89
+ requestedBounds,
90
+ runtimeAutoBounds,
91
+ runtimeRequestBounds,
92
+ sameBounds,
93
+ requestResolution,
94
+ calculationBounds,
95
+ barsAhead,
96
+ trailingRangeForStart,
97
+ filterPoints,
98
+ comparisonDisplayBounds,
99
+ followLatestMarketObservation,
100
+ capabilityRequestBounds,
101
+ endAtNewestObservation,
102
+ latestSessionView,
103
+ } from "./chart-bounds";
104
+ import {
105
+ type PriceHistoryRequest,
106
+ PriceHistoryAcquisitionError,
107
+ priceHistoryAcquisitionIdentity,
108
+ priceHistoryTailAcquisition,
109
+ behindHistoryNotice,
110
+ mergePriceHistoryWindows,
111
+ historyIntersectsBounds,
112
+ clampHistoryBoundsToSupport,
113
+ retainedWarmup,
114
+ loadPriceHistory,
115
+ } from "./history-acquisition";
116
+ import {
117
+ unloadableSeries,
118
+ emptyFinancials,
119
+ chartCalculationSeriesIds,
120
+ latestQuote,
121
+ mergeHistory,
122
+ baseSecuritySeries,
123
+ baseEconomicSeries,
124
+ baseCapabilitySeries,
125
+ staleFredWarning,
126
+ } from "./base-series";
127
+ import {
128
+ resolvePriceHistoryIntegrity,
129
+ assignAxes,
130
+ adoptPriceUnits,
131
+ prepareBaseSeriesForStudies,
132
+ presentationBounds,
133
+ applyStudyPresentationTransforms,
134
+ studyAnchors,
135
+ } from "./series-presentation";
340
136
 
341
137
  function withQuoteExchange(
342
138
  source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
@@ -356,433 +152,6 @@ function withQuoteExchange(
356
152
  };
357
153
  }
358
154
 
359
- function finiteDate(value: string | Date | undefined): Date | null {
360
- if (!value) return null;
361
- const date = value instanceof Date ? new Date(value) : new Date(value);
362
- return Number.isFinite(date.getTime()) ? date : null;
363
- }
364
-
365
- function inclusiveEndDate(value: string | Date | undefined): Date | null {
366
- const parsed = finiteDate(value);
367
- if (!parsed) return null;
368
- return typeof value === "string" && DATE_ONLY_PATTERN.test(value.trim())
369
- ? new Date(parsed.getTime() + DAY_MS - 1)
370
- : parsed;
371
- }
372
-
373
- function dateOnly(date: Date): string {
374
- return date.toISOString().slice(0, 10);
375
- }
376
-
377
- function explicitBounds(spec: ChartSpec): DateBounds | null {
378
- const explicitStart = finiteDate(spec.viewport.dateWindow?.start);
379
- const explicitEnd = inclusiveEndDate(spec.viewport.dateWindow?.end);
380
- if (!explicitStart || !explicitEnd || explicitStart.getTime() > explicitEnd.getTime()) return null;
381
- return { start: explicitStart.getTime(), end: explicitEnd.getTime() };
382
- }
383
-
384
- /** Calendar range limits use the authored dates; observation filters include the final day. */
385
- function rangeDurationBounds(spec: ChartSpec, bounds: DateBounds): DateBounds {
386
- const window = spec.viewport.dateWindow;
387
- if (!window || !DATE_ONLY_PATTERN.test(window.start) || !DATE_ONLY_PATTERN.test(window.end)
388
- || bounds.end !== inclusiveEndDate(window.end)?.getTime()) return bounds;
389
- return { ...bounds, end: finiteDate(window.end)!.getTime() };
390
- }
391
-
392
- function requestedBounds(spec: ChartSpec, latestObservation: Date): DateBounds {
393
- const explicit = explicitBounds(spec);
394
- if (explicit) return explicit;
395
- return {
396
- start: spec.viewport.range === "ALL"
397
- ? null
398
- : subtractTimeRange(latestObservation, spec.viewport.range).getTime(),
399
- end: latestObservation.getTime(),
400
- };
401
- }
402
-
403
- function runtimeAutoBounds(options: ChartResolveOptions): DateBounds | null {
404
- const start = options.autoViewport?.start.getTime();
405
- const end = options.autoViewport?.end.getTime();
406
- return typeof start === "number"
407
- && Number.isFinite(start)
408
- && typeof end === "number"
409
- && Number.isFinite(end)
410
- && start <= end
411
- ? { start, end }
412
- : null;
413
- }
414
-
415
- function runtimeRequestBounds(options: ChartResolveOptions): DateBounds | null {
416
- const start = options.requestViewport?.start.getTime();
417
- const end = options.requestViewport?.end.getTime();
418
- return typeof start === "number"
419
- && Number.isFinite(start)
420
- && typeof end === "number"
421
- && Number.isFinite(end)
422
- && start <= end
423
- ? { start, end }
424
- : null;
425
- }
426
-
427
- function sameBounds(left: DateBounds | null, right: DateBounds | null): boolean {
428
- return left?.start === right?.start && left?.end === right?.end;
429
- }
430
-
431
- function boundsRange(bounds: DateBounds): TimeRange {
432
- if (bounds.start === null || bounds.end === null) return "ALL";
433
- return getTimeRangeForDateWindow({
434
- start: new Date(bounds.start),
435
- end: new Date(bounds.end),
436
- });
437
- }
438
-
439
- function requestResolution(
440
- spec: ChartSpec,
441
- bounds: DateBounds,
442
- calculationSeriesIds: ReadonlySet<string>,
443
- options: ChartResolveOptions,
444
- sharedSupport: readonly ChartResolutionSupport[],
445
- provisionalSupport = false,
446
- ): ManualChartResolution {
447
- const duration = rangeDurationBounds(spec, bounds);
448
- if (spec.viewport.resolution === "auto" && spec.studies.some((study) => study.kind === "realized-vol" && study.visible !== false)) {
449
- // A 252-session estimator must see daily observations even on a long chart.
450
- const maxRange = getSupportMaxRange(sharedSupport, "1d");
451
- if (sharedSupport.length === 0 || provisionalSupport || maxRange === "ALL"
452
- || (maxRange !== null && duration.start !== null && duration.end !== null
453
- && isDateWindowWithinTimeRange(new Date(duration.start), new Date(duration.end), maxRange))) return "1d";
454
- }
455
- if (spec.viewport.resolution !== "auto") {
456
- const maxRange = getSupportMaxRange(sharedSupport, spec.viewport.resolution);
457
- // A placeholder list cannot rule out a manual pick; the real list decides
458
- // on the next pass.
459
- const supported = sharedSupport.length === 0
460
- || provisionalSupport
461
- || maxRange === "ALL"
462
- || (
463
- maxRange !== null
464
- && duration.start !== null
465
- && duration.end !== null
466
- && isDateWindowWithinTimeRange(
467
- new Date(duration.start),
468
- new Date(duration.end),
469
- maxRange,
470
- )
471
- );
472
- if (supported) return spec.viewport.resolution;
473
- }
474
- const runtimeBounds = runtimeAutoBounds(options);
475
- const adaptive = runtimeBounds && runtimeBounds.start !== null && runtimeBounds.end !== null
476
- ? getBestSupportedResolutionForVisibleWindow(
477
- { start: new Date(runtimeBounds.start), end: new Date(runtimeBounds.end) },
478
- sharedSupport,
479
- options.targetPointCount ?? 120,
480
- options.currentResolution,
481
- )
482
- : null;
483
- const preferred = adaptive
484
- ?? getSupportedPresetResolution(
485
- explicitBounds(spec) ? boundsRange(duration) : spec.viewport.range,
486
- provisionalSupport ? [] : sharedSupport,
487
- duration.start !== null && duration.end !== null
488
- ? { start: new Date(duration.start), end: new Date(duration.end) }
489
- : null,
490
- );
491
- const activeSeries = spec.series.filter((entry) => calculationSeriesIds.has(entry.id));
492
- return resolutionForExplicitMarketPeriods(preferred, activeSeries);
493
- }
494
-
495
- function calculationBounds(
496
- spec: ChartSpec,
497
- visibleBounds: DateBounds,
498
- resolution: ManualChartResolution,
499
- ): DateBounds {
500
- if (visibleBounds.start === null) return visibleBounds;
501
- const warmupPoints = maxStudyWarmupPoints(spec.studies);
502
-
503
- const duration = rangeDurationBounds(spec, visibleBounds);
504
- const visibleEnd = duration.end ?? visibleBounds.start;
505
- const bufferedRange = getNextBufferRange(boundsRange(duration));
506
- let start = bufferedRange === "ALL"
507
- ? subtractTimeRange(new Date(visibleEnd), "ALL").getTime()
508
- : subtractTimeRange(new Date(visibleEnd), bufferedRange).getTime();
509
-
510
- if (warmupPoints > 0) {
511
- // Calendar gaps and closed sessions mean N observations often span more
512
- // than N nominal bars. The doubled point window is a bounded safety margin.
513
- const pointWarmup = warmupPoints * CHART_RESOLUTION_STEP_MS[resolution] * 2;
514
- start = Math.min(start, visibleBounds.start - pointWarmup);
515
- }
516
- return { start: Math.min(start, visibleBounds.start), end: visibleBounds.end };
517
- }
518
-
519
- /**
520
- * How far past a view moved into the past its history reaches: half a view,
521
- * as far as the interval's longest window allows with the view still in it.
522
- * Those are the bars a pan forward meets first. Without them, a view whose
523
- * bars were loaded for it alone (a zoom to finer bars, then a pan) ends where
524
- * they do, and a pan forward stops there with nothing to ask for.
525
- */
526
- function barsAhead(view: DateBounds, maxRange: TimeRange | null, now: number): number {
527
- if (view.start === null || view.end === null || view.end >= now) return 0;
528
- const room = maxRange && maxRange !== "ALL"
529
- ? view.start - subtractTimeRange(new Date(view.end), maxRange).getTime()
530
- : Number.POSITIVE_INFINITY;
531
- return Math.max(0, Math.min((view.end - view.start) / 2, room, now - view.end));
532
- }
533
-
534
- function trailingRangeForStart(start: number | null, referenceDate: Date): TimeRange {
535
- if (start === null) return "ALL";
536
- for (const range of TIME_RANGES) {
537
- if (range === "ALL" || start >= subtractTimeRange(referenceDate, range).getTime()) return range;
538
- }
539
- return "ALL";
540
- }
541
-
542
- function exclusiveEnd(bounds: DateBounds): Date | null {
543
- return bounds.end === null ? null : new Date(bounds.end + 1);
544
- }
545
-
546
- function filterPoints(points: readonly TimeSeriesPoint[], bounds: DateBounds): TimeSeriesPoint[] {
547
- return points.filter((point) => {
548
- const time = point.date.getTime();
549
- return Number.isFinite(time)
550
- && (bounds.start === null || time >= bounds.start)
551
- && (bounds.end === null || time <= bounds.end);
552
- });
553
- }
554
-
555
- function resolvePriceHistoryIntegrity(
556
- spec: ChartSpec,
557
- requestedSeries: readonly ResolvedSeries[],
558
- visibleSeries: readonly ResolvedSeries[],
559
- bounds: DateBounds,
560
- resolution: ManualChartResolution | "auto",
561
- dateWindow: ChartSpec["viewport"]["dateWindow"] | null,
562
- ) {
563
- const filters = new Map(requestedSeries.map((entry) => [entry.id, priceObservationWindowFilter(entry, bounds, resolution, dateWindow)]));
564
- const requestedIds = activeStudyInputSeriesIds(spec.studies);
565
- spec.series.filter((entry) => entry.visible !== false).forEach((entry) => requestedIds.add(entry.id));
566
- const selected = requestedSeries.filter((entry) => requestedIds.has(entry.id)).map((entry) => limitSeriesObservations(spec, {
567
- ...entry, points: entry.points.filter((point) => filters.get(entry.id)!(point.date.getTime())),
568
- }));
569
- return [
570
- ...collectPriceHistoryIntegrity(selected, "requested-observation", (id, time) => filters.get(id)!(time)),
571
- ...collectPriceHistoryIntegrity(visibleSeries, "visible-calculation"),
572
- ];
573
- }
574
-
575
- function comparisonDisplayBounds(bounds: DateBounds, comparison: PriceComparison | null): DateBounds {
576
- if (comparison?.alignment !== "session-date" || comparison.start === null || comparison.end === null) return bounds;
577
- // A date-only selection can include a local session that began on the prior
578
- // UTC day. Keep its original timestamp visible in the rendering viewport.
579
- return {
580
- start: bounds.start === null ? null : Math.min(bounds.start, comparison.start),
581
- end: bounds.end === null ? null : Math.max(bounds.end, comparison.end),
582
- };
583
- }
584
-
585
- /**
586
- * An untouched market chart ends at its newest bar. A quote that arrives just
587
- * after the reference time moves it on; a closed market (a night, a weekend)
588
- * moves it back, so the plot does not end in an empty stretch that the first
589
- * pan would then collapse.
590
- */
591
- function followLatestMarketObservation(
592
- bounds: DateBounds,
593
- series: readonly ResolvedSeries[],
594
- ): DateBounds {
595
- if (bounds.end === null) return bounds;
596
- let latest = Number.NEGATIVE_INFINITY;
597
- for (const entry of series) {
598
- if (entry.timeBasis?.kind !== "market") continue;
599
- for (const point of entry.points) {
600
- const timestamp = point.date.getTime();
601
- if (Number.isFinite(timestamp) && timestamp > latest) latest = timestamp;
602
- }
603
- }
604
- if (!Number.isFinite(latest) || latest === bounds.end) return bounds;
605
- const shift = latest - bounds.end;
606
- return {
607
- start: bounds.start === null ? null : bounds.start + shift,
608
- end: latest,
609
- };
610
- }
611
-
612
- /**
613
- * The dates a capability series (CHOKE's transits) is asked for. A view the
614
- * user panned or zoomed keeps the range's dates loaded too: the chart's
615
- * navigation stops at the newest observation it has, so a series asked for
616
- * the moved view alone ended where the view did, and the view could never go
617
- * forward to the newest data again, not even by zooming out.
618
- */
619
- function capabilityRequestBounds(view: DateBounds, range: DateBounds, moved: boolean): DateBounds {
620
- if (!moved) return view;
621
- return {
622
- start: view.start === null || range.start === null ? null : Math.min(view.start, range.start),
623
- end: view.end === null || range.end === null ? null : Math.max(view.end, range.end),
624
- };
625
- }
626
-
627
- /**
628
- * A chart of capability series alone (CHOKE's vessel transits) ends at its
629
- * newest observation, not today: such data is published days late, and an
630
- * axis running on to today would read as days with nothing in them. The
631
- * start stays where the range put it, so no empty span opens on the left.
632
- */
633
- function endAtNewestObservation(bounds: DateBounds, series: readonly ResolvedSeries[]): DateBounds {
634
- if (bounds.end === null) return bounds;
635
- let newest = Number.NEGATIVE_INFINITY;
636
- for (const entry of series) {
637
- for (const point of entry.points) {
638
- const timestamp = point.date.getTime();
639
- if (point.value !== null && Number.isFinite(timestamp) && timestamp > newest) newest = timestamp;
640
- }
641
- }
642
- if (!Number.isFinite(newest) || newest >= bounds.end || (bounds.start !== null && newest <= bounds.start)) return bounds;
643
- return { start: bounds.start, end: newest };
644
- }
645
-
646
- /**
647
- * 1D shows the latest trading session of the chart's first market listing,
648
- * from its open to the newest bar, with the close before it as a reference.
649
- * Null keeps the rolling day: crypto, FX and venues without known hours.
650
- * The reference is the quote's previous close when the quote is from the
651
- * session shown, as the legend's change is; otherwise the last bar of the
652
- * session before.
653
- */
654
- function latestSessionView(
655
- primary: ResolvedSeries | undefined,
656
- marketSeries: readonly ResolvedSeries[],
657
- extendedHours: boolean,
658
- quoteReference: { close: number; time: number } | undefined,
659
- ): { bounds: DateBounds; priorClose: number | null } | null {
660
- if (!primary?.listing) return null;
661
- const bars = primary.points.flatMap((point) => {
662
- const close = point.close ?? point.value;
663
- const time = point.date.getTime();
664
- return typeof close === "number" && Number.isFinite(close) && Number.isFinite(time) ? [{ time, close }] : [];
665
- });
666
- const latest = bars.at(-1)?.time;
667
- if (latest === undefined) return null;
668
- const { symbol, exchange } = primary.listing;
669
- const open = latestSessionStart(symbol, exchange, latest, { extendedHours });
670
- if (open === null) return null;
671
- // The axis starts at the session's first bar, which a quiet pre-market can leave well after the open.
672
- const start = bars.find((bar) => bar.time >= open)?.time ?? open;
673
- // Another listing on the chart can trade later than the primary one.
674
- let end = latest;
675
- for (const entry of marketSeries) {
676
- const last = entry.points.at(-1)?.date.getTime();
677
- if (last !== undefined && Number.isFinite(last) && last > end) end = last;
678
- }
679
- const regularOpen = latestTradingSessionOpen(symbol, exchange, latest);
680
- const quoteOpen = quoteReference ? latestTradingSessionOpen(symbol, exchange, quoteReference.time) : null;
681
- const priorClose = quoteReference && regularOpen !== null && quoteOpen === regularOpen
682
- ? quoteReference.close
683
- : priorSessionClose(bars, open, symbol, exchange);
684
- return { bounds: { start, end }, priorClose };
685
- }
686
-
687
- function emptyFinancials(priceHistory: TickerFinancials["priceHistory"] = []): TickerFinancials {
688
- return { annualStatements: [], quarterlyStatements: [], priceHistory };
689
- }
690
-
691
- function chartCalculationSeriesIds(spec: ChartSpec): Set<string> {
692
- const ids = activeStudyInputSeriesIds(spec.studies);
693
- spec.series.filter(entry => entry.visible !== false).forEach(entry => ids.add(entry.id));
694
- // Hidden primary market data still owns the shared session anchor.
695
- const primary = spec.series.find(entry => entry.source.kind === "security" && isMarketFieldId(entry.source.fieldId));
696
- if (primary) ids.add(primary.id);
697
- return ids;
698
- }
699
-
700
- /** Select the provisional seed cadence before provider resolution support arrives. */
701
- export function chartSeedResolution(spec: ChartSpec, now: Date, options: ChartResolveOptions = {}): ManualChartResolution {
702
- return requestResolution(spec, requestedBounds(spec, now), chartCalculationSeriesIds(spec), options, []);
703
- }
704
-
705
- export function seedChartResolutionResult(
706
- spec: ChartSpec,
707
- historyByInstrument: ReadonlyMap<string, TickerFinancials["priceHistory"]>,
708
- referenceNow?: Date,
709
- options: ChartResolveOptions = {},
710
- ): ChartResolutionResult | null {
711
- let latest = Number.NEGATIVE_INFINITY;
712
- for (const history of historyByInstrument.values()) {
713
- for (const point of history) latest = Math.max(latest, getPricePointTimestamp(point));
714
- }
715
- if (!Number.isFinite(latest)) return null;
716
- const now = referenceNow ?? new Date(latest);
717
- const resolution = chartSeedResolution(spec, now, options);
718
- // The seed is replaced by the settled chart moments later. Draw it with
719
- // what that chart will know (units, served cadence, where its axis ends) so
720
- // the swap changes no label and moves no plot.
721
- const support: ChartResolutionSupport[][] = [];
722
- const series: ResolvedSeries[] = [];
723
- spec.series.forEach((seriesSpec, index) => {
724
- if (seriesSpec.visible === false || seriesSpec.source.kind !== "security") return;
725
- if (!isMarketFieldId(seriesSpec.source.fieldId)) return;
726
- const history = historyByInstrument.get(instrumentKey(seriesSpec.source));
727
- if (!history?.length) return;
728
- const facts = readSeedListingFacts(seriesSpec.source.instrument);
729
- const resolved = baseSecuritySeries(seriesSpec, emptyFinancials(history), index, resolution, facts?.quoteMetadata, facts?.contractUnit);
730
- if (!resolved?.points.length) return;
731
- series.push(resolved);
732
- if (facts?.resolutionSupport?.length) support.push(facts.resolutionSupport);
733
- });
734
- if (series.length === 0) return null;
735
- const requested = requestedBounds(spec, now);
736
- const sessionView = spec.viewport.range === "1D" && !explicitBounds(spec)
737
- ? latestSessionView(series[0], series, spec.viewport.extendedHours === true, undefined)
738
- : null;
739
- const bounds = explicitBounds(spec) || runtimeRequestBounds(options)
740
- ? requested
741
- : sessionView?.bounds ?? followLatestMarketObservation(requested, series);
742
- const primary = series[0]!;
743
- if (sessionView?.priorClose != null && primary.transform === "raw"
744
- && primary.unitGroup.startsWith("price") && primary.unitGroup !== "price:unknown") {
745
- series[0] = { ...primary, priorClose: sessionView.priorClose };
746
- }
747
- const resolutionSupport = support.length === series.length ? intersectChartResolutionSupport(support) : [];
748
- const priceComparison = resolvePriceComparison(spec, series, bounds, resolution);
749
- const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
750
- const comparisonBounds = priceComparison && priceComparison.start !== null
751
- ? priceComparison : bounds;
752
- const baseSeries = series.map((entry) => prepareBaseSeriesForStudies(entry, priceComparisonBoundsForSeries(entry, priceComparison) ?? comparisonBounds));
753
- // Calculate studies from raw buffered inputs, then use the same presentation
754
- // and visible-window rules as the network result.
755
- const studies = resolveStudies(series, spec.studies, resolution);
756
- const axisWarnings: string[] = [];
757
- const bufferedSeries = assignAxes([
758
- ...baseSeries,
759
- ...applyStudyPresentationTransforms(studies.series, spec.studies, series, comparisonBounds, undefined, priceComparison, bounds),
760
- ], [...spec.series, ...spec.studies], axisWarnings).map((entry) => bufferPriceComparison(entry, priceComparison, bounds));
761
- const visible = bufferedSeries.map((entry) => {
762
- const entryBounds = presentationBounds(entry, spec.studies, priceComparison, bounds);
763
- const clipped = entryBounds.start !== null && entryBounds.end !== null
764
- ? clipSeriesToWindow(entry, new Date(entryBounds.start), new Date(entryBounds.end))
765
- : { ...entry, points: filterPoints(entry.points, entryBounds) };
766
- return clipPriceComparison(limitSeriesObservations(spec, clipped), priceComparison);
767
- });
768
- const priceHistoryIntegrity = resolvePriceHistoryIntegrity(spec, series, visible, bounds, resolution, spec.viewport.dateWindow ?? null);
769
- return {
770
- series: visible,
771
- ...(priceHistoryIntegrity.length ? { priceHistoryIntegrity } : {}),
772
- ...(priceComparison ? { priceComparison } : {}),
773
- ...(resolutionSupport.length > 0 ? { resolutionSupport } : {}),
774
- legendSeries: visible,
775
- ...(spec.viewport.maxPoints === undefined ? { bufferedSeries } : {}),
776
- ...((explicitBounds(spec) !== null || spec.viewport.maxPoints === undefined)
777
- && displayBounds.start !== null && displayBounds.end !== null
778
- ? { viewport: { start: new Date(displayBounds.start), end: new Date(displayBounds.end) } } : {}),
779
- loading: true,
780
- errors: studies.errors,
781
- warnings: [...studies.warnings, ...axisWarnings, ...chartPriceHistoryIntegrityNotices(priceHistoryIntegrity), ...(priceComparison ? [priceComparison.notice] : [])],
782
- resolution,
783
- };
784
- }
785
-
786
155
 
787
156
  function chartIsPriceOnly(spec: ChartSpec, calculationSeriesIds: ReadonlySet<string>): boolean {
788
157
  return spec.series.every((entry) => {
@@ -791,385 +160,6 @@ function chartIsPriceOnly(spec: ChartSpec, calculationSeriesIds: ReadonlySet<str
791
160
  });
792
161
  }
793
162
 
794
- function isSortedPriceHistory(points: TickerFinancials["priceHistory"]): boolean {
795
- let previous = Number.NEGATIVE_INFINITY;
796
- for (const point of points) {
797
- const timestamp = getPricePointTimestamp(point);
798
- if (!Number.isFinite(timestamp)) continue;
799
- if (timestamp < previous) return false;
800
- previous = timestamp;
801
- }
802
- return true;
803
- }
804
-
805
- export function mergePriceHistoryWindows(
806
- current: TickerFinancials["priceHistory"],
807
- incoming: TickerFinancials["priceHistory"],
808
- resolution: ManualChartResolution,
809
- ): TickerFinancials["priceHistory"] {
810
- let sorted: TickerFinancials["priceHistory"];
811
- if (!isSortedPriceHistory(current) || !isSortedPriceHistory(incoming)) {
812
- const byTimestamp = new Map<number, TickerFinancials["priceHistory"][number]>();
813
- for (const point of [...current, ...incoming]) {
814
- const timestamp = getPricePointTimestamp(point);
815
- if (Number.isFinite(timestamp)) {
816
- byTimestamp.set(
817
- timestamp,
818
- point.date instanceof Date ? point : { ...point, date: new Date(timestamp) },
819
- );
820
- }
821
- }
822
- sorted = [...byTimestamp.values()].sort(
823
- (left, right) => getPricePointTimestamp(left) - getPricePointTimestamp(right),
824
- );
825
- } else {
826
- sorted = [];
827
- let currentIndex = 0;
828
- let incomingIndex = 0;
829
- // An intraday point strictly inside a later window's bar is that window's
830
- // earlier observation of it, such as a trade-time final print.
831
- const step = isIntradayResolution(resolution) ? CHART_RESOLUTION_STEP_MS[resolution] : 0;
832
- let coveredUntil = Number.NEGATIVE_INFINITY;
833
- const append = (
834
- point: TickerFinancials["priceHistory"][number],
835
- timestamp: number,
836
- ) => {
837
- const normalized = point.date instanceof Date ? point : { ...point, date: new Date(timestamp) };
838
- const previous = sorted.at(-1);
839
- if (previous && getPricePointTimestamp(previous) === timestamp) sorted[sorted.length - 1] = normalized;
840
- else sorted.push(normalized);
841
- };
842
-
843
- while (currentIndex < current.length || incomingIndex < incoming.length) {
844
- const currentPoint = current[currentIndex];
845
- const incomingPoint = incoming[incomingIndex];
846
- const currentTimestamp = currentPoint ? getPricePointTimestamp(currentPoint) : Number.POSITIVE_INFINITY;
847
- const incomingTimestamp = incomingPoint ? getPricePointTimestamp(incomingPoint) : Number.POSITIVE_INFINITY;
848
- if (currentPoint && !Number.isFinite(currentTimestamp)) {
849
- currentIndex += 1;
850
- } else if (incomingPoint && !Number.isFinite(incomingTimestamp)) {
851
- incomingIndex += 1;
852
- } else if (currentPoint && currentTimestamp <= incomingTimestamp) {
853
- if (!(incomingPoint && currentTimestamp < incomingTimestamp && currentTimestamp < coveredUntil)) {
854
- append(currentPoint, currentTimestamp);
855
- }
856
- currentIndex += 1;
857
- } else if (incomingPoint) {
858
- append(incomingPoint, incomingTimestamp);
859
- if (step) coveredUntil = incomingTimestamp + step;
860
- incomingIndex += 1;
861
- }
862
- }
863
- }
864
-
865
- if (isIntradayResolution(resolution)) return sorted;
866
- // Windows can be served by different sources, and they stamp the same session
867
- // differently: local midnight, UTC midnight, or the opening bell. Keying on the
868
- // exact timestamp keeps both copies, so the chart draws every bar twice and
869
- // carries twice the points through every pan. Daily and slower bars are never
870
- // closer than one step apart, so anything closer is the same session; keep the
871
- // copy already plotted to leave existing bars where they are.
872
- const minimumGapMs = CHART_RESOLUTION_STEP_MS[resolution] * 0.8;
873
- const plotted = new Set(current.map(getPricePointTimestamp));
874
- const merged: TickerFinancials["priceHistory"] = [];
875
- for (const point of sorted) {
876
- const previous = merged.at(-1);
877
- if (!previous || getPricePointTimestamp(point) - getPricePointTimestamp(previous) >= minimumGapMs) {
878
- merged.push(point);
879
- continue;
880
- }
881
- if (!plotted.has(getPricePointTimestamp(previous)) && plotted.has(getPricePointTimestamp(point))) {
882
- merged[merged.length - 1] = point;
883
- }
884
- }
885
- return merged;
886
- }
887
-
888
- function historyIntersectsBounds(
889
- history: TickerFinancials["priceHistory"],
890
- bounds: DateBounds,
891
- ): boolean {
892
- if (history.length === 0) return false;
893
- if (bounds.start === null || bounds.end === null) return true;
894
- return history.some((point) => {
895
- const timestamp = getPricePointTimestamp(point);
896
- return Number.isFinite(timestamp) && timestamp >= bounds.start! && timestamp <= bounds.end!;
897
- });
898
- }
899
-
900
- function clampHistoryBoundsToSupport(
901
- bounds: DateBounds,
902
- maxRange: TimeRange | null,
903
- durationEnd: number | null = bounds.end,
904
- ): DateBounds {
905
- if (bounds.start === null || bounds.end === null || !maxRange || maxRange === "ALL") {
906
- return bounds;
907
- }
908
- const supportedStart = subtractTimeRange(new Date(durationEnd ?? bounds.end), maxRange).getTime();
909
- return { start: Math.max(bounds.start, supportedStart), end: bounds.end };
910
- }
911
-
912
- function latestQuote(snapshot: Quote | undefined, override: Quote | undefined): Quote | undefined {
913
- if (!snapshot) return override;
914
- if (!override) return snapshot;
915
- return compareChartQuoteRecency(override, snapshot) >= 0 ? override : snapshot;
916
- }
917
-
918
- function mergeHistory(
919
- financials: TickerFinancials | null,
920
- history: TickerFinancials["priceHistory"],
921
- quoteOverride: Quote | undefined,
922
- now: number,
923
- liveBarResolution?: ManualChartResolution,
924
- exchange?: string,
925
- appendQuote = true,
926
- assetCategory?: string,
927
- live?: { bars: LiveBarAccumulator; since?: number; session?: HistorySession; extendedHours?: boolean },
928
- ): TickerFinancials {
929
- const base = financials ?? emptyFinancials();
930
- const quote = latestQuote(base.quote, quoteOverride);
931
- const unknownHistoryBasis = hasUnknownBondHistoryBasis(quote, assetCategory, base.quoteMetadata?.instrumentType);
932
- if (!appendQuote || unknownHistoryBasis) return { ...base, quote, priceHistory: history };
933
- // Market bars fold every observed quote into the forming bar; other series
934
- // price their latest point from the current quote alone.
935
- const priceHistory = live && liveBarResolution
936
- ? live.bars.apply(history, quote, { now, resolution: liveBarResolution, exchange, liveSince: live.since, session: live.session,
937
- extendedHours: live.extendedHours })
938
- : appendLiveQuotePoint(history, quote, liveBarResolution
939
- ? { now, mode: "ohlc", resolution: liveBarResolution, exchange }
940
- : { now });
941
- return { ...base, quote, priceHistory };
942
- }
943
-
944
- function retainedWarmup(points: TickerFinancials["priceHistory"], visibleStart: number | null): number {
945
- return visibleStart === null ? 0 : points.filter((point) => Number.isFinite(point.close)
946
- && getPricePointTimestamp(point) < visibleStart).length;
947
- }
948
-
949
- function recoveryCandidates(
950
- error: HistoryRetentionError,
951
- provider: DataProvider,
952
- source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
953
- ): readonly HistoryRecoveryCandidate[] {
954
- if (error.candidates.length) return error.candidates;
955
- // A direct Cloud adapter enforces this constraint itself. Routed errors must
956
- // carry the router's actual attempted-source scope, including across RPC.
957
- if (provider.id !== "gloomberb-cloud") return [];
958
- const target = publicListingTarget(source.instrument.symbol, source.instrument.exchange);
959
- const context = requestContext(source);
960
- const candidate = parseHistoryRecoveryCandidate({
961
- sourceKey: `provider:${provider.id}`, retention: error.retention,
962
- request: { symbol: target.symbol, exchange: target.exchange ?? "",
963
- entityKey: getRouterEntityKey(source.instrument.symbol, context.instrument),
964
- brokerId: context.brokerId, brokerInstanceId: context.brokerInstanceId,
965
- interval: error.retention.interval, requestedStart: error.retention.requestedStart,
966
- requestedEnd: error.retention.requestedEnd },
967
- });
968
- return candidate ? [candidate] : [];
969
- }
970
-
971
- /** Requests for older bars one panned window may add when its source answers only the newest part. */
972
- const OLDER_BAR_REQUESTS = 3;
973
- /** A window may start this far before its first bar for closed sessions alone: weekends and holiday weeks. */
974
- const CLOSED_SESSION_GAP_MS = 10 * DAY_MS;
975
- /**
976
- * An answer cut short at the source's cap holds thousands of bars. A shorter
977
- * one that starts late is a young listing or a source that keeps less: it
978
- * holds all there is, and an older request cannot change that.
979
- */
980
- const CAPPED_ANSWER_MIN_BARS = 1_000;
981
-
982
- function firstBarTime(points: TickerFinancials["priceHistory"]): number {
983
- for (const point of points) {
984
- const time = getPricePointTimestamp(point);
985
- if (Number.isFinite(time) && Number.isFinite(point.close)) return time;
986
- }
987
- return Number.NaN;
988
- }
989
-
990
- /**
991
- * A source caps the bars one request returns and answers with the newest, so
992
- * a panned window can come back starting weeks after it was asked to. The
993
- * older part is asked for on its own, from the same source and session, until
994
- * it is covered or the source has nothing older: then the first bar is where
995
- * this listing's history at this cadence starts, and the chart stops there
996
- * instead of asking again. So does an answer that starts late without being
997
- * cut short, or one the source says its coverage starts with.
998
- */
999
- async function withOlderBars(
1000
- provider: DataProvider,
1001
- source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
1002
- request: PriceHistoryRequest,
1003
- result: PriceHistoryResult,
1004
- context: MarketDataRequestContext,
1005
- ): Promise<LoadedPriceHistory> {
1006
- const start = request.bounds.start;
1007
- const end = request.bounds.end;
1008
- if (start === null || end === null || result.resolution !== request.resolution) return result;
1009
- const toleranceMs = Math.max(CLOSED_SESSION_GAP_MS, (end - start) / 5);
1010
- let current: LoadedPriceHistory = result;
1011
- let answerBars = result.points.length;
1012
- for (let attempt = 0; attempt < OLDER_BAR_REQUESTS; attempt += 1) {
1013
- const first = firstBarTime(current.points);
1014
- if (!Number.isFinite(first) || first - start <= toleranceMs) return current;
1015
- // An answer short of the cap that still starts late is everything the source has.
1016
- if (answerBars < CAPPED_ANSWER_MIN_BARS || current.coverageStart) return { ...current, historyStart: first };
1017
- let older: PriceHistoryResult | null;
1018
- try {
1019
- older = await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1020
- { kind: "detail", start: new Date(start), end: new Date(first), interval: request.resolution }, context);
1021
- } catch (error) {
1022
- // The source keeps nothing that old, by its own account; anything else may pass.
1023
- return isHistoryRetentionError(error) || error instanceof ProviderMissError
1024
- ? { ...current, historyStart: first } : current;
1025
- }
1026
- if (!older || older.resolution !== current.resolution
1027
- || priceHistoryAcquisitionIdentity(older) !== priceHistoryAcquisitionIdentity(current)) return current;
1028
- const earlier = older.points.filter((point) => getPricePointTimestamp(point) < first);
1029
- if (!Number.isFinite(firstBarTime(earlier))) return { ...current, historyStart: first };
1030
- current = { ...current, points: [...earlier, ...current.points] };
1031
- answerBars = older.points.length;
1032
- }
1033
- return current;
1034
- }
1035
-
1036
- async function loadPriceHistory(
1037
- provider: DataProvider,
1038
- source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
1039
- request: PriceHistoryRequest,
1040
- ): Promise<LoadedPriceHistory> {
1041
- const context = { ...requestContext(source), historyRequestKey: request.historyRequestKey,
1042
- ...(request.historySession ? { historySession: request.historySession } : {}) };
1043
- let coverageNotice: string | null = null;
1044
- let retentionError: HistoryRetentionError | null = null;
1045
- const observeFailure = (error: unknown) => {
1046
- if (error instanceof SnapshotHistoryUnavailableError) throw error;
1047
- if (error instanceof HistoryCoverageError) coverageNotice ??= error.message;
1048
- if (isHistoryRetentionError(error)) retentionError ??= error;
1049
- };
1050
- const observeCoverage = (result: PriceHistoryResult) => {
1051
- coverageNotice ??= historyCoverageNotice(result.coverageStart, request.visibleBounds.start);
1052
- };
1053
- const accepted = (points: TickerFinancials["priceHistory"]) => points.length > 0
1054
- && (!request.explicitWindow || historyIntersectsBounds(points, request.visibleBounds));
1055
- const detailStart = request.bounds.start === null ? null : new Date(request.bounds.start);
1056
- const detailEnd = exclusiveEnd(request.bounds);
1057
- if (request.explicitWindow && detailStart && detailEnd && (provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
1058
- try {
1059
- const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1060
- { kind: "detail", start: detailStart, end: detailEnd, interval: request.resolution }, context))!;
1061
- const { points } = result;
1062
- observeCoverage(result);
1063
- // Authored dates chart what their source answers; a panned view reaches for the rest.
1064
- if (accepted(points)) return request.authoredWindow ? result : await withOlderBars(provider, source, request, result, context);
1065
- } catch (error) { observeFailure(error); }
1066
- }
1067
- if (provider.getPriceHistoryForResolutionWithMetadata || provider.getPriceHistoryForResolution) {
1068
- try {
1069
- const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1070
- { kind: "resolution", range: request.fallbackRange, resolution: request.resolution }, context))!;
1071
- const { points } = result;
1072
- observeCoverage(result);
1073
- if (accepted(points)) {
1074
- rememberParsedPriceHistory(parsedPriceHistoryKey(source.instrument, request.fallbackRange, request.resolution), points, result);
1075
- return result;
1076
- }
1077
- } catch (error) { observeFailure(error); }
1078
- }
1079
-
1080
- // Only original source/cache exhaustion reaches recovery. Ask the selected
1081
- // source for its retained window; a nominal clock multiplier cannot guarantee
1082
- // enough observations across exchange closures for a study's warmup.
1083
- const retainedFailure = retentionError as HistoryRetentionError | null;
1084
- let recoveryAttempted = false;
1085
- const retryAt = retainedFailure ? retainedFailure.retention.observedAt + HISTORY_RETENTION_MAX_AGE_MS : undefined;
1086
- if (retainedFailure && !coverageNotice && (provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
1087
- const step = CHART_RESOLUTION_STEP_MS[request.resolution];
1088
- const target = publicListingTarget(source.instrument.symbol, source.instrument.exchange);
1089
- const candidate = recoveryCandidates(retainedFailure, provider, source).map((value) => parseHistoryRecoveryCandidate(value))
1090
- .find((value) => value && value.request.symbol === target.symbol
1091
- && value.request.exchange === (target.exchange ?? "")
1092
- && value.request.interval === canonicalHistoryInterval(request.resolution)
1093
- && value.request.entityKey === getRouterEntityKey(source.instrument.symbol, context.instrument)
1094
- && value.request.brokerId === context.brokerId && value.request.brokerInstanceId === context.brokerInstanceId);
1095
- if (candidate && request.visibleBounds.start !== null) {
1096
- // One complete bar inside the advancing source limit absorbs acquisition
1097
- // latency. Keep the source's original exclusive end at its second precision.
1098
- const start = Math.ceil(candidate.retention.availableStart / step) * step + step;
1099
- const end = candidate.retention.requestedEnd;
1100
- const visibleEnd = request.visibleBounds.end === null ? end : Math.floor(request.visibleBounds.end / 1000) * 1000;
1101
- // At an interval the user chose, a preset range or a panned or zoomed
1102
- // view that reaches back past the retained bars, but still reaches them,
1103
- // charts what the source keeps. Auto steps to coarser bars below instead,
1104
- // and authored dates keep failing.
1105
- const retainedOnly = !request.allowProviderDefaultFallback && !request.authoredWindow && visibleEnd >= start;
1106
- if ((retainedOnly || request.visibleBounds.start >= start) && visibleEnd <= end && start < end) {
1107
- recoveryAttempted = true;
1108
- try {
1109
- const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1110
- { kind: "detail", start: new Date(start), end: new Date(end), interval: request.resolution }, { ...context, historyRecovery: candidate }))!;
1111
- const { points } = result;
1112
- observeCoverage(result);
1113
- if (accepted(points)) return { ...result, expiresAt: retryAt, historyStart: firstBarTime(points),
1114
- recovery: { sourceKey: candidate.sourceKey, start, end, requiredWarmupPoints: request.requiredWarmupPoints,
1115
- usableWarmupPoints: retainedWarmup(points, request.visibleBounds.start), retention: candidate.retention } };
1116
- } catch (error) { observeFailure(error); }
1117
- }
1118
- }
1119
- }
1120
-
1121
- const unavailable = () => coverageNotice
1122
- ?? `Requested ${request.resolution} price history is unavailable for ${instrumentLabel(source)}. Choose Auto or a supported interval.`;
1123
- const fail = (message: string): never => {
1124
- if (retryAt !== undefined) throw new PriceHistoryAcquisitionError(message, retryAt);
1125
- throw new Error(message);
1126
- };
1127
- if (!request.allowProviderDefaultFallback || recoveryAttempted) return fail(unavailable());
1128
-
1129
- // After a proved retention failure, one explicit calendar fallback can retain
1130
- // the familiar broader chart with known cadence. Never substitute coarser bars
1131
- // for an authored financial/market period that requires finer observations.
1132
- const coarser = retainedFailure && (provider.getPriceHistoryForResolutionWithMetadata || provider.getPriceHistoryForResolution)
1133
- ? (["1d", "1wk", "1mo"] as const).find((resolution) =>
1134
- CHART_RESOLUTION_STEP_MS[resolution] > CHART_RESOLUTION_STEP_MS[request.resolution]
1135
- && isResolutionFineEnoughForMarketPeriod(resolution, source.period)
1136
- && request.support.some((entry) => entry.resolution === resolution
1137
- && TIME_RANGES.indexOf(entry.maxRange) >= TIME_RANGES.indexOf(request.fallbackRange))) : undefined;
1138
- if (coarser) {
1139
- try {
1140
- const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1141
- { kind: "resolution", range: request.fallbackRange, resolution: coarser }, context))!;
1142
- const { points } = result;
1143
- observeCoverage(result);
1144
- if (accepted(points)) {
1145
- rememberParsedPriceHistory(parsedPriceHistoryKey(source.instrument, request.fallbackRange, coarser), points, result);
1146
- return { ...result, expiresAt: retryAt };
1147
- }
1148
- } catch (error) { observeFailure(error); }
1149
- return fail(unavailable());
1150
- }
1151
- if (retainedFailure) return fail(unavailable());
1152
- if (!provider.getPriceHistoryWithMetadata && source.period && source.period !== "auto" && isMarketFieldId(source.fieldId)) return fail(unavailable());
1153
- let result: PriceHistoryResult;
1154
- try {
1155
- result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
1156
- { kind: "range", range: request.fallbackRange }, context))!;
1157
- if (source.period && source.period !== "auto" && isMarketFieldId(source.fieldId)
1158
- && (result.resolution === null || !isResolutionFineEnoughForMarketPeriod(result.resolution, source.period))) return fail(unavailable());
1159
- } catch (error) {
1160
- return fail(coverageNotice ?? (error instanceof Error ? error.message : String(error)));
1161
- }
1162
- const { points } = result;
1163
- observeCoverage(result);
1164
- if (points.length === 0 && coverageNotice) return fail(coverageNotice);
1165
- if (request.explicitWindow && points.length > 0 && !historyIntersectsBounds(points, request.visibleBounds)) {
1166
- return fail(coverageNotice ?? `Price history for the requested window is unavailable for ${instrumentLabel(source)}.`);
1167
- }
1168
- // A default-only provider remains usable, but its bare array cannot establish
1169
- // an interval for live-bar merging, annualization or a requested-cadence cache.
1170
- return { ...result, expiresAt: retryAt ?? Date.now() + HISTORY_RETENTION_MAX_AGE_MS };
1171
- }
1172
-
1173
163
  /**
1174
164
  * A US stock or fund listing, including one a broker contract routes through
1175
165
  * SMART in dollars. Options and futures have no pre-market session.
@@ -1181,361 +171,6 @@ function tradesUsExtendedHours(exchange: string | undefined, contract: BrokerCon
1181
171
  || (!!contract && canonicalExchange(venue) === "SMART" && (!contract.currency || contract.currency.toUpperCase() === "USD"));
1182
172
  }
1183
173
 
1184
- function baseSecuritySeries(
1185
- spec: ChartSeriesSpec,
1186
- financials: TickerFinancials,
1187
- index: number,
1188
- marketResolution?: ManualChartResolution | null,
1189
- quoteMetadata: QuoteMetadata | null | undefined = financials.quoteMetadata,
1190
- /** What a futures contract's price counts ("USD / gallon"), from its terms. */
1191
- contractUnit?: string | null,
1192
- ): ResolvedSeries | null {
1193
- if (spec.source.kind !== "security") return null;
1194
- const field = getTimeSeriesField(spec.source.fieldId);
1195
- if (!field) return null;
1196
- const points = extractSecuritySeries(financials, spec.source);
1197
- const unreportedPeriod = points.length === 0 ? unreportedStatementPeriod(financials, spec.source) : null;
1198
- const symbol = instrumentLabel(spec.source);
1199
- const statementCurrency = field.id.startsWith("fundamental.") && field.unit.startsWith("currency")
1200
- ? reportingCurrencySeries(points, financials.financialCurrency) : null;
1201
- // A broker contract names its currency even before (or without) a quote.
1202
- const currency = statementCurrency ? statementCurrency.currency : financials.quote?.currency || quoteMetadata?.currency || spec.source.instrument.instrument?.currency;
1203
- const assetKind = resolveAssetDisplayKind({ assetCategory: financials.quote?.instrumentType || quoteMetadata?.instrumentType });
1204
- const unknownBondBasis = hasUnknownBondHistoryBasis(financials.quote,
1205
- spec.source.instrument.instrument?.secType, quoteMetadata?.instrumentType);
1206
- const volumeUnit = unknownBondBasis ? undefined : assetKind === "equity" ? "shares" as const : assetKind === "contract" ? "contracts" as const : undefined;
1207
- // A price quote establishes its currency, but only instrument metadata can
1208
- // establish a per-share or per-crypto-unit basis. This does not establish the
1209
- // provider's volume denomination or a derivative's contract multiplier.
1210
- const unitTemplate = field.unitGroup === "price" && isMarketFieldId(field.id)
1211
- ? unknownBondBasis ? "unknown" : `currency${assetKind === "equity" ? "/share" : assetKind === "crypto" ? "/unit" : ""}`
1212
- : field.unit;
1213
- const generic = field.unitGroup === "price"
1214
- ? futuresGenericListing(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
1215
- // A generic future on an index, VIX or Treasuries reads in points or 32nds, and an index
1216
- // future's continuous symbol or contract in points, not dollars, as its quote does.
1217
- const genericBasis = field.unitGroup === "price" && isMarketFieldId(field.id)
1218
- ? futuresListingPriceBasis(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
1219
- // An index's level is points too, never the currency its members trade in.
1220
- const indexLevel = field.unitGroup === "price" && isMarketFieldId(field.id)
1221
- && isIndexLevel(financials.quote?.instrumentType || quoteMetadata?.instrumentType);
1222
- const genericUnit = genericBasis === "points" || indexLevel ? INDEX_POINTS_UNIT : genericBasis === "thirty-seconds" ? "32nds" : null;
1223
- // A future's price in a currency counts what its contract terms say: heating
1224
- // oil is USD / gallon and crude USD / barrel, which a bare USD does not tell apart.
1225
- const statedUnit = !genericUnit && unitTemplate === "currency" ? contractPriceUnit(contractUnit, currency) : null;
1226
- const unit = field.id === "market.volume" ? volumeUnit ?? ""
1227
- : statedUnit ?? genericUnit ?? (unitTemplate.startsWith("currency") && currency ? unitTemplate.replace("currency", currency) : unitTemplate);
1228
- const currencyUnitGroup = genericUnit ? `${field.unitGroup}:${genericUnit}`
1229
- : unknownBondBasis && field.unitGroup === "price" ? "price:unknown"
1230
- : field.unit.startsWith("currency") && currency
1231
- ? `${field.unitGroup}:${currency}`
1232
- : field.unitGroup;
1233
- const marketExchange = spec.source.instrument.exchange
1234
- || financials.quote?.listingExchangeName
1235
- || financials.quote?.exchangeName;
1236
- const marketField = isMarketFieldId(spec.source.fieldId);
1237
- // A routing code such as a broker's SMART names no venue. The quote's
1238
- // listing then supplies the session clock, so closed hours stay off the axis.
1239
- const sessionExchange = [marketExchange, financials.quote?.listingExchangeName, quoteMetadata?.listingExchangeName, financials.quote?.exchangeName]
1240
- .find((value) => resolveExchangeTimeZone(value));
1241
- // A dollar listing saved with IBKR's SMART route and no other venue trades on
1242
- // the US session, and so does a dollar option, which names no venue at all.
1243
- const dollars = !currency || currency === "USD";
1244
- const usOption = dollars && (spec.source.instrument.instrument?.secType?.toUpperCase() === "OPT"
1245
- || /^OPTION$/i.test(financials.quote?.instrumentType ?? quoteMetadata?.instrumentType ?? ""));
1246
- const routedUsListing = !sessionExchange && ((canonicalExchange(marketExchange) === "SMART" && dollars) || usOption);
1247
- const marketTimeZone = marketField
1248
- && canonicalExchange(marketExchange) !== "CCC"
1249
- ? resolveExchangeTimeZone(sessionExchange) ?? (routedUsListing ? "America/New_York" : null)
1250
- : null;
1251
- // A route has no hours of its own: its sessions, which the day view and
1252
- // VWAP follow, are the venue's, and NYSE's for an unnamed dollar listing.
1253
- const listingExchange = canonicalExchange(marketExchange) === "SMART" || usOption
1254
- ? canonicalExchange(sessionExchange) || (routedUsListing ? "NYSE" : marketExchange)
1255
- : marketExchange;
1256
- const latestChangePercent = marketField && field.unit.startsWith("currency")
1257
- && !unknownBondBasis
1258
- && financials.quote && hasValidQuoteObservationTime(financials.quote)
1259
- ? financials.quote.changePercent
1260
- : undefined;
1261
- const priceIssues = valuationPriceIssues(financials, spec.source);
1262
- // A source with closes only, such as a generic future, has flat bars (open,
1263
- // high and low filled from the close): nothing to draw as candles.
1264
- const closesOnly = marketField && points.length > 1
1265
- && points.every((point) => [point.open, point.high, point.low].every((value) => value == null || value === point.close));
1266
- return {
1267
- id: spec.id,
1268
- label: spec.label?.trim() || (generic ? futuresGenericCaption(generic) : `${symbol} ${field.seriesLabel ?? field.shortLabel}`),
1269
- color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
1270
- unit,
1271
- unitGroup: currencyUnitGroup,
1272
- ...(statedUnit ? { statedUnit } : {}),
1273
- priceAssetCategory: marketField && field.unitGroup === "price" && !unknownBondBasis
1274
- ? financials.quote?.instrumentType || quoteMetadata?.instrumentType : undefined,
1275
- volumeUnit,
1276
- ...(priceIssues.length ? { valuationPriceIssues: priceIssues } : {}),
1277
- warning: field.id === "market.volume" && !volumeUnit && points.length > 0
1278
- ? "Volume unit unknown." : statementCurrency?.warning ?? valuationCurrencyWarning(financials, spec.source),
1279
- nativeFrequency: marketField ? marketSeriesFrequency(spec.source, marketResolution)
1280
- : spec.source.period && spec.source.period !== "auto" ? spec.source.period : field.nativeFrequency,
1281
- ...(unreportedPeriod ? { unreportedPeriod } : {}),
1282
- ...(marketField ? { historyResolution: marketResolution } : {}),
1283
- timestampMode: spec.source.timestampMode,
1284
- dataShape: field.dataShape,
1285
- style: closesOnly && isOhlcSeriesStyle(spec.style ?? field.defaultStyle) ? "line" : spec.style,
1286
- transform: spec.transform,
1287
- axis: spec.axis === "right" ? "right" : "left",
1288
- panelId: spec.panelId,
1289
- interpolation: spec.interpolation,
1290
- observationKind: marketField ? "market" : undefined,
1291
- ...(marketField ? { listing: { symbol: spec.source.instrument.symbol, exchange: listingExchange ?? "" } } : {}),
1292
- timeBasis: marketTimeZone
1293
- ? {
1294
- kind: "market",
1295
- timeZone: marketTimeZone,
1296
- exchange: canonicalExchange(sessionExchange) || undefined,
1297
- cadenceMs: marketResolution
1298
- ? CHART_RESOLUTION_STEP_MS[marketResolution]
1299
- : undefined,
1300
- }
1301
- : undefined,
1302
- latestChangePercent: typeof latestChangePercent === "number" && Number.isFinite(latestChangePercent)
1303
- ? latestChangePercent
1304
- : undefined,
1305
- points,
1306
- };
1307
- }
1308
-
1309
- const FRED_CATALOG_UNITS: Readonly<Record<string, string>> = {
1310
- "percent": "%",
1311
- "millions of u.s. dollars": "million USD",
1312
- "millions of dollars": "million USD",
1313
- "billions of dollars": "billion USD",
1314
- "billions of u.s. dollars": "billion USD",
1315
- "billions of us dollars": "billion USD",
1316
- "dollars per barrel": "USD/barrel",
1317
- "thousands of persons": "thousand people",
1318
- };
1319
-
1320
- function fredCatalogUnit(units: string): string {
1321
- return FRED_CATALOG_UNITS[units.toLowerCase()] ?? units;
1322
- }
1323
-
1324
- function baseEconomicSeries(
1325
- spec: ChartSeriesSpec,
1326
- loaded: FredSeriesLoadResult,
1327
- index: number,
1328
- ): ResolvedSeries | null {
1329
- if (spec.source.kind !== "economic") return null;
1330
- const { data } = loaded;
1331
- const units = data.info?.units?.trim() || "value";
1332
- const catalogUnit = fredCatalogUnit(units);
1333
- // "Percent Change from Year Ago" and "Percent per Annum" are percent too.
1334
- const isPercent = units.toLowerCase().includes("percent");
1335
- return {
1336
- id: spec.id,
1337
- label: spec.label?.trim() || data.info?.title?.trim() || spec.source.seriesId,
1338
- color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
1339
- unit: isPercent ? "%" : catalogUnit,
1340
- unitGroup: isPercent ? "percent" : `economic:${catalogUnit.toLowerCase()}`,
1341
- nativeFrequency: "auto",
1342
- timestampMode: "period-end",
1343
- dataShape: "scalar",
1344
- style: spec.style,
1345
- transform: spec.transform,
1346
- axis: spec.axis === "right" ? "right" : "left",
1347
- panelId: spec.panelId,
1348
- interpolation: spec.interpolation,
1349
- points: extractFredSeries(data.observations, { providerId: "fred", timestampMode: "period-end" }),
1350
- warning: "FRED vintage dates are unavailable; observations use period dates.",
1351
- };
1352
- }
1353
-
1354
- function baseCapabilitySeries(
1355
- spec: ChartSeriesSpec,
1356
- loaded: ResolvedSeries,
1357
- index: number,
1358
- ): ResolvedSeries {
1359
- const points = loaded.points.flatMap((point) => {
1360
- const date = finiteDate(point.date as unknown as string | Date | undefined);
1361
- const observedAt = finiteDate(point.observedAt as unknown as string | Date | undefined) ?? date;
1362
- const availableAt = finiteDate(point.availableAt as unknown as string | Date | undefined) ?? undefined;
1363
- return date && observedAt ? [{ ...point, date, observedAt, ...(availableAt ? { availableAt } : {}) }] : [];
1364
- });
1365
- return {
1366
- ...loaded,
1367
- id: spec.id,
1368
- label: spec.label?.trim() || loaded.label || (spec.source.kind === "capability" ? spec.source.seriesId : spec.id),
1369
- color: spec.color ?? loaded.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
1370
- style: spec.style,
1371
- transform: spec.transform,
1372
- axis: spec.axis === "right" ? "right" : spec.axis === "left" ? "left" : loaded.axis,
1373
- panelId: spec.panelId,
1374
- interpolation: spec.interpolation,
1375
- observationKind: loaded.observationKind ?? "dataset",
1376
- points,
1377
- };
1378
- }
1379
-
1380
- function staleFredWarning(loaded: FredSeriesLoadResult): string | null {
1381
- if (!loaded.stale) return null;
1382
- return `FRED refresh failed${loaded.refreshError ? ` (${loaded.refreshError})` : ""}; showing cached data fetched ${new Date(loaded.fetchedAt).toISOString().slice(0, 10)}.`;
1383
- }
1384
-
1385
- function assignAxes(
1386
- series: ResolvedSeries[],
1387
- specs: readonly { id: string; axis: ChartSeriesSpec["axis"] }[],
1388
- warnings: string[],
1389
- ): ResolvedSeries[] {
1390
- const requested = new Map(specs.map((spec) => [spec.id, spec.axis] as const));
1391
- const groupsByPanel = new Map<string, Partial<Record<"left" | "right", string>>>();
1392
- return series.map((entry) => {
1393
- const groups = groupsByPanel.get(entry.panelId) ?? {};
1394
- groupsByPanel.set(entry.panelId, groups);
1395
- const preferred = requested.get(entry.id);
1396
- let axis: "left" | "right";
1397
- if (preferred === "left" || preferred === "right") {
1398
- axis = preferred;
1399
- } else if (groups.left === entry.unitGroup) {
1400
- axis = "left";
1401
- } else if (groups.right === entry.unitGroup) {
1402
- axis = "right";
1403
- } else if (!groups.left) {
1404
- axis = "left";
1405
- } else {
1406
- axis = "right";
1407
- }
1408
- if (groups[axis] && groups[axis] !== entry.unitGroup) {
1409
- warnings.push(`${entry.label} shares the ${axis} axis with a different unit; choose an explicit panel for independent scaling.`);
1410
- }
1411
- groups[axis] ??= entry.unitGroup;
1412
- return { ...entry, axis };
1413
- });
1414
- }
1415
-
1416
- /**
1417
- * A series drawn in a ticker's price (a moving average of it, a band around
1418
- * it) takes the units of that ticker's price series when the chart has it, so
1419
- * the two share an axis and the line sits on the price instead of on a scale
1420
- * of its own.
1421
- */
1422
- function adoptPriceUnits(series: ResolvedSeries[], specs: readonly ChartSeriesSpec[]): ResolvedSeries[] {
1423
- if (!series.some((entry) => entry.priceOf)) return series;
1424
- const specsById = new Map(specs.map((spec) => [spec.id, spec]));
1425
- const prices = series.filter((entry) => {
1426
- const source = specsById.get(entry.id)?.source;
1427
- return source?.kind === "security" && getTimeSeriesField(source.fieldId)?.unitGroup === "price"
1428
- && entry.transform === "raw";
1429
- });
1430
- if (prices.length === 0) return series;
1431
- return series.map((entry) => {
1432
- if (!entry.priceOf) return entry;
1433
- const wanted = parsePublicTickerKey(entry.priceOf.toUpperCase());
1434
- const key = publicTickerKey(wanted.symbol, wanted.exchange);
1435
- const named = (price: ResolvedSeries) => {
1436
- const source = specsById.get(price.id)?.source;
1437
- if (source?.kind !== "security") return false;
1438
- return publicTickerKey(source.instrument.symbol, source.instrument.exchange) === key
1439
- || (!wanted.exchange && source.instrument.symbol.toUpperCase() === wanted.symbol);
1440
- };
1441
- const price = prices.find((candidate) => named(candidate) && candidate.panelId === entry.panelId)
1442
- ?? prices.find(named);
1443
- return price && entry.transform === "raw"
1444
- ? { ...entry, unit: price.unit, unitGroup: price.unitGroup }
1445
- : entry;
1446
- });
1447
- }
1448
-
1449
- function prepareBaseSeriesForStudies(
1450
- series: ResolvedSeries,
1451
- bounds: DateBounds,
1452
- clipToBounds = false,
1453
- fallbackBaselineBounds?: DateBounds,
1454
- ): ResolvedSeries {
1455
- const baselineTransform = series.transform === "percent" || series.transform === "index100";
1456
- let source = series;
1457
- if (clipToBounds && bounds.start !== null && bounds.end !== null) {
1458
- source = clipSeriesToWindow(series, new Date(bounds.start), new Date(bounds.end));
1459
- } else if (clipToBounds) {
1460
- source = { ...series, points: filterPoints(series.points, bounds) };
1461
- }
1462
- const baseline = baselineTransform
1463
- ? scalarBaseline(series, bounds)
1464
- ?? (fallbackBaselineBounds ? scalarBaseline(series, fallbackBaselineBounds) : null)
1465
- : null;
1466
- return applyResolvedSeriesTransform(
1467
- source,
1468
- source.transform,
1469
- baselineTransform ? { baseline } : undefined,
1470
- );
1471
- }
1472
-
1473
- function scalarBaseline(series: ResolvedSeries, bounds: DateBounds): number | null {
1474
- const points = filterPoints(series.points, bounds);
1475
- for (const point of points) {
1476
- const value = typeof point.value === "number" && Number.isFinite(point.value)
1477
- ? point.value
1478
- : typeof point.close === "number" && Number.isFinite(point.close)
1479
- ? point.close
1480
- : null;
1481
- if (value !== null && value !== 0) return value;
1482
- }
1483
- return null;
1484
- }
1485
-
1486
- function studyForOutput(
1487
- outputId: string,
1488
- studies: readonly ChartSpec["studies"][number][],
1489
- ): ChartSpec["studies"][number] | undefined {
1490
- return studies
1491
- .filter((study) => outputId === study.id || outputId.startsWith(`${study.id}:`))
1492
- .sort((left, right) => right.id.length - left.id.length)[0];
1493
- }
1494
-
1495
- /** Price overlays read in the price's own terms, so they take its transform. */
1496
- function followsInputTransform(kind: ChartSpec["studies"][number]["kind"]): boolean {
1497
- return kind === "sma" || kind === "ema" || kind === "bollinger" || kind === "vwap" || kind === "anchored-vwap";
1498
- }
1499
-
1500
- function presentationBounds(
1501
- series: ResolvedSeries,
1502
- studies: readonly ChartSpec["studies"][number][],
1503
- comparison: PriceComparison | null,
1504
- fallback: DateBounds,
1505
- ): DateBounds {
1506
- const study = studyForOutput(series.id, studies);
1507
- const sourceId = study && followsInputTransform(study.kind)
1508
- ? study.inputSeriesIds[0] : series.id;
1509
- return (sourceId && comparison?.sourceBounds?.[sourceId]) || fallback;
1510
- }
1511
-
1512
- function applyStudyPresentationTransforms(
1513
- outputs: ResolvedSeries[],
1514
- studies: readonly ChartSpec["studies"][number][],
1515
- rawSeries: readonly ResolvedSeries[],
1516
- visibleBounds: DateBounds,
1517
- fallbackBaselineBounds?: DateBounds,
1518
- priceComparison?: PriceComparison | null,
1519
- view: DateBounds = visibleBounds,
1520
- ): ResolvedSeries[] {
1521
- const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
1522
- return outputs.map((output) => {
1523
- const study = studyForOutput(output.id, studies);
1524
- if (!study || !followsInputTransform(study.kind)) {
1525
- return output;
1526
- }
1527
- const input = rawById.get(study.inputSeriesIds[0] ?? "");
1528
- if (!input || input.transform === "raw") return output;
1529
- const baseline = input.transform === "percent" || input.transform === "index100"
1530
- ? scalarBaseline(input, priceComparisonBoundsForSeries(input, priceComparison ?? null) ?? visibleBounds)
1531
- ?? (fallbackBaselineBounds ? scalarBaseline(input, fallbackBaselineBounds) : null)
1532
- : undefined;
1533
- const transformed = applyResolvedSeriesTransform(output, input.transform, { baseline });
1534
- const inputBounds = priceComparison?.sourceBounds?.[input.id];
1535
- return inputBounds ? { ...transformed, points: comparedBufferPoints(transformed.points, inputBounds, view) } : transformed;
1536
- });
1537
- }
1538
-
1539
174
  /** Fewer daily or coarser bars than this make a line or two, not a chart. */
1540
175
  const SHORT_HISTORY_MAX_BARS = 20;
1541
176
 
@@ -1822,6 +457,8 @@ async function resolveChartSpecPass(
1822
457
  const served = supportBySource[index];
1823
458
  if (served?.length) rememberSeedListingFacts(source.instrument, { resolutionSupport: served });
1824
459
  });
460
+ // Futures source bars start on the clock: bars built from them must not straddle a session's open.
461
+ const openMinute = futuresOpenMinuteOf(activeMarketSources.map((source) => source.instrument));
1825
462
  const initialResolution = requestResolution(
1826
463
  spec,
1827
464
  initialVisibleBounds,
@@ -1829,20 +466,23 @@ async function resolveChartSpecPass(
1829
466
  options,
1830
467
  sharedSupport,
1831
468
  provisionalSupport,
469
+ openMinute,
1832
470
  );
1833
- if (
1834
- spec.viewport.resolution !== "auto"
1835
- && initialResolution !== spec.viewport.resolution
1836
- ) {
471
+ const requestedSource = spec.viewport.resolution === "auto"
472
+ ? null : chartBarSizeSource(spec.viewport.resolution, provisionalSupport ? [] : sharedSupport, openMinute);
473
+ if (requestedSource !== null && initialResolution !== requestedSource) {
1837
474
  warnings.push(
1838
475
  `${spec.viewport.resolution.toUpperCase()} data is unavailable for this range. Auto resolution was used instead.`,
1839
476
  );
1840
477
  }
478
+ // Bars of a size the source does not serve (4h, 2d) are built from the bars it does (1h, 1d).
479
+ const builtBarSize = isCustomChartResolution(spec.viewport.resolution) && initialResolution === requestedSource
480
+ ? spec.viewport.resolution : null;
1841
481
  const requestVisibleBounds = requestBounds ?? initialVisibleBounds;
1842
482
  // A view the user panned or zoomed to, as opposed to authored dates.
1843
483
  const movedWindow = requestBounds !== null && !sameBounds(requestBounds, initialVisibleBounds);
1844
484
  const hasExplicitWindow = explicitBounds(spec) !== null || movedWindow;
1845
- const initialCalculationBounds = calculationBounds(spec, requestVisibleBounds, initialResolution);
485
+ const initialCalculationBounds = calculationBounds(spec, requestVisibleBounds, builtBarSize ?? initialResolution);
1846
486
 
1847
487
  // Only US stocks and funds trade before and after the regular session; asking elsewhere costs a request for nothing.
1848
488
  const extendedHoursFor = (source: Extract<ChartSeriesSpec["source"], { kind: "security" }>, resolution: ManualChartResolution) => (
@@ -1993,7 +633,7 @@ async function resolveChartSpecPass(
1993
633
  }
1994
634
  // Read per request: one recovered window answers every visible window that shares its acquisition.
1995
635
  if (loaded.recovery && request.visibleBounds.start !== null && request.visibleBounds.start < loaded.recovery.start) {
1996
- priorityWarnings.push(historyRetentionNotice(loaded.recovery.retention, loaded.resolution ?? request.resolution,
636
+ priorityWarnings.push(historyRetentionNotice(loaded.recovery.retention, builtBarSize ?? loaded.resolution ?? request.resolution,
1997
637
  listingTimeZone(source.instrument.symbol, source.instrument.exchange)));
1998
638
  retainedHistoryOnly = true;
1999
639
  }
@@ -2036,7 +676,7 @@ async function resolveChartSpecPass(
2036
676
  // A source that answered a panned view short, and then had nothing older, starts there.
2037
677
  if (combined.historyStart !== undefined && !loaded.recovery && !combined.coverageStart && request.visibleBounds.start !== null
2038
678
  && request.visibleBounds.start < combined.historyStart && combined.resolution) {
2039
- priorityWarnings.push(historyStartNotice(combined.historyStart, combined.resolution,
679
+ priorityWarnings.push(historyStartNotice(combined.historyStart, builtBarSize ?? combined.resolution,
2040
680
  listingTimeZone(source.instrument.symbol, source.instrument.exchange), referenceNow.getTime()));
2041
681
  }
2042
682
  if (accumulationKey && loaded.resolution && isIntradayResolution(loaded.resolution)
@@ -2173,7 +813,7 @@ async function resolveChartSpecPass(
2173
813
  }
2174
814
  if (forwardPE && financials?.epsEstimates && !history) {
2175
815
  history = await loadHistory(resolvedSource, true).catch((error: unknown) => {
2176
- warnings.push(`${seriesSpec.label ?? seriesSpec.id}: ${error instanceof Error ? error.message : String(error)}`);
816
+ warnings.push(`${seriesMessageName(seriesSpec)}: ${error instanceof Error ? error.message : String(error)}`);
2177
817
  return null;
2178
818
  });
2179
819
  }
@@ -2225,6 +865,16 @@ async function resolveChartSpecPass(
2225
865
  priceHistoryResolution: history.resolution, priceHistoryRequestKey: history.requestKey,
2226
866
  priceHistorySession: history.session, priceHistorySourceKey: history.sourceKey };
2227
867
  }
868
+ // Built after the live quote joins the source bars, so the forming bar holds it too.
869
+ // Captures keep the source bars, which their resolution names.
870
+ const sourceBars = merged;
871
+ const builtBars = builtBarSize && marketField && history?.resolution === initialResolution ? builtBarSize : null;
872
+ if (builtBars) {
873
+ merged = { ...merged, priceHistory: aggregatePriceBars(merged.priceHistory, builtBars, {
874
+ symbol: resolvedSource.instrument.symbol, exchange: resolvedSource.instrument.exchange,
875
+ extendedHours: history?.extendedHours === true,
876
+ }) };
877
+ }
2228
878
  if ((source.fieldId === "fundamental.eps" || source.fieldId === "valuation.trailingPE")
2229
879
  && [...merged.annualStatements, ...merged.quarterlyStatements].some((row) => row.epsBasis)) {
2230
880
  warnings.push(SEC_EPS_BASIS_NOTICE);
@@ -2233,8 +883,8 @@ async function resolveChartSpecPass(
2233
883
  // Keep provider history separate from quote-extended display bars. A live
2234
884
  // mark is not a daily closing observation, including on snapshot replay.
2235
885
  const historical = history ? { ...merged, priceHistory: history.points } : merged;
2236
- sources.onSecurityData?.(resolvedSpec, history?.session || realizedVolInputs.has(seriesSpec.id) ? historical : merged, history !== null);
2237
- const result = baseSecuritySeries(
886
+ sources.onSecurityData?.(resolvedSpec, history?.session || realizedVolInputs.has(seriesSpec.id) ? historical : sourceBars, history !== null);
887
+ const base = baseSecuritySeries(
2238
888
  resolvedSpec,
2239
889
  merged,
2240
890
  index,
@@ -2242,7 +892,8 @@ async function resolveChartSpecPass(
2242
892
  quoteMetadata,
2243
893
  contractUnit,
2244
894
  );
2245
- if (!result) throw new Error(`Unknown field ${source.fieldId}.`);
895
+ if (!base) throw new Error(`Unknown field ${source.fieldId}.`);
896
+ const result = builtBars ? withBuiltBarSize(base, builtBars) : base;
2246
897
  if (marketField && history?.historyStart !== undefined && Number.isFinite(history.historyStart)) {
2247
898
  result.historyStart = new Date(history.historyStart);
2248
899
  }
@@ -2273,7 +924,7 @@ async function resolveChartSpecPass(
2273
924
  return result;
2274
925
  } catch (error) {
2275
926
  const message = error instanceof Error ? error.message : String(error);
2276
- errors.push(`${seriesSpec.label ?? seriesSpec.id}: ${message}`);
927
+ errors.push(`${seriesMessageName(seriesSpec)}: ${message}`);
2277
928
  return unloadableSeries(seriesSpec, index, message);
2278
929
  }
2279
930
  }));
@@ -2307,7 +958,8 @@ async function resolveChartSpecPass(
2307
958
  const commonCadence = servedCadences.size === 1 ? [...servedCadences][0] : undefined;
2308
959
  const resolution = commonCadence ?? undefined;
2309
960
  const comparisonResolution = resolution ?? "auto";
2310
- const priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow);
961
+ const priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow,
962
+ { preset: hasExplicitWindow ? null : spec.viewport.range, now: referenceNow.getTime() + Math.max(0, Date.now() - resolutionStartedAt) });
2311
963
  const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
2312
964
  const comparisonBounds = priceComparison && priceComparison.start !== null
2313
965
  ? priceComparison : bounds;
@@ -2323,7 +975,8 @@ async function resolveChartSpecPass(
2323
975
 
2324
976
  // Study outputs are appended by the pure engine before the final viewport clip.
2325
977
  if (spec.studies.length > 0) {
2326
- const studyResult = resolveStudies(calculationSeries, spec.studies, resolution, historicalPriceSeries);
978
+ const studyResult = resolveStudies(calculationSeries, spec.studies, resolution, historicalPriceSeries,
979
+ studyAnchors(calculationSeries, priceComparison, comparisonBounds));
2327
980
  resolved = [
2328
981
  ...resolved,
2329
982
  ...applyStudyPresentationTransforms(
@@ -2396,13 +1049,13 @@ async function resolveChartSpecPass(
2396
1049
  }
2397
1050
  for (const panel of spec.panels) {
2398
1051
  if (panel.scale !== "log") continue;
2399
- const hiddenCount = resolved
2400
- .filter((entry) => entry.panelId === panel.id)
2401
- .flatMap((entry) => entry.points)
2402
- .filter((point) => typeof point.value === "number" && Number.isFinite(point.value) && point.value <= 0)
2403
- .length;
1052
+ const isHidden = (point: TimeSeriesPoint) => typeof point.value === "number" && Number.isFinite(point.value) && point.value <= 0;
1053
+ const affected = resolved.filter((entry) => entry.panelId === panel.id && entry.points.some(isHidden));
1054
+ const hiddenCount = affected.reduce((total, entry) => total + entry.points.filter(isHidden).length, 0);
2404
1055
  if (hiddenCount > 0) {
2405
- warnings.push(`${panel.label ?? panel.id}: ${hiddenCount} non-positive observation${hiddenCount === 1 ? " is" : "s are"} hidden on the logarithmic scale.`);
1056
+ // A panel without a title is named by the series it holds, never by its id (`main`).
1057
+ const name = panel.label ?? [...new Set(affected.map((entry) => entry.label))].join(", ");
1058
+ warnings.push(`${name}: ${hiddenCount} non-positive observation${hiddenCount === 1 ? " is" : "s are"} hidden on the logarithmic scale.`);
2406
1059
  }
2407
1060
  }
2408
1061
 
@@ -2431,110 +1084,6 @@ async function resolveChartSpecPass(
2431
1084
  ...(delistings.size ? { delistings: [...delistings.values()] } : {}),
2432
1085
  viewport,
2433
1086
  resolution,
1087
+ ...(builtBarSize && rawSeries.some((entry) => entry.barSize === builtBarSize) ? { barSize: builtBarSize } : {}),
2434
1088
  };
2435
1089
  }
2436
-
2437
- /** Older tails are left to a full history refresh rather than a recent-window request. */
2438
- const MAX_TAIL_RECONCILE_LAG_MS = 3 * DAY_MS;
2439
-
2440
- function latestObservationTime(points: TickerFinancials["priceHistory"]): number {
2441
- for (let index = points.length - 1; index >= 0; index -= 1) {
2442
- const point = points[index]!;
2443
- const time = getPricePointTimestamp(point);
2444
- if (Number.isFinite(time) && Number.isFinite(point.close)) return time;
2445
- }
2446
- return Number.NaN;
2447
- }
2448
-
2449
- function samePricePoints(left: TickerFinancials["priceHistory"], right: TickerFinancials["priceHistory"]): boolean {
2450
- if (left.length !== right.length) return false;
2451
- for (let index = left.length - 1; index >= 0; index -= 1) {
2452
- const a = left[index]!;
2453
- const b = right[index]!;
2454
- if (a === b) continue;
2455
- if (getPricePointTimestamp(a) !== getPricePointTimestamp(b) || a.open !== b.open || a.high !== b.high
2456
- || a.low !== b.low || a.close !== b.close || a.volume !== b.volume) return false;
2457
- }
2458
- return true;
2459
- }
2460
-
2461
- /**
2462
- * Refreshes the newest bars of each intraday history the last resolve plotted
2463
- * up to the present, so bars formed from quotes settle to the provider's
2464
- * version shortly after they close. Only the recent window is requested, and a
2465
- * window from a different source or session contract is never mixed in.
2466
- * Resolves true when any plotted bar changed.
2467
- */
2468
- export function reconcileChartTail(
2469
- sources: Pick<ChartResolveSources, "dataProvider">,
2470
- cache: ChartResolveCache,
2471
- now = Date.now(),
2472
- ): Promise<boolean> {
2473
- if (cache.tailReconcile) return cache.tailReconcile;
2474
- const provider = sources.dataProvider;
2475
- if (!provider || cache.liveTails.size === 0
2476
- || !(provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
2477
- return Promise.resolve(false);
2478
- }
2479
- const run: Promise<boolean> = Promise.all([...cache.liveTails].map(async ([key, tail]) => {
2480
- const pending = cache.priceHistoryByRequest.get(key);
2481
- const loaded = pending ? await pending.catch(() => {
2482
- // Intentional ignore: resolveChartSpecPass already includes this history failure in the series errors.
2483
- return null;
2484
- }) : null;
2485
- if (!pending || !loaded || loaded.resolution !== tail.resolution) return false;
2486
- const accumulated = cache.accumulatedPriceHistory.get(tail.accumulationKey);
2487
- const latest = latestObservationTime(accumulated?.points ?? loaded.points);
2488
- if (!Number.isFinite(latest) || now - latest > MAX_TAIL_RECONCILE_LAG_MS) return false;
2489
- // The newest loaded bar may still have been forming when it was fetched.
2490
- // Whole-bar starts and whole-minute ends let every chart of this history
2491
- // that asks at the same boundary share one request and one cached window.
2492
- const step = CHART_RESOLUTION_STEP_MS[tail.resolution];
2493
- const start = Math.floor((latest - step) / step) * step;
2494
- const end = Math.ceil(now / 60_000) * 60_000;
2495
- const { symbol, exchange } = tail.source.instrument;
2496
- const context = { ...requestContext(tail.source), ...(tail.historySession ? { historySession: tail.historySession } : {}) };
2497
- const requestKey = JSON.stringify([symbol, exchange ?? "", context.brokerId ?? null,
2498
- context.brokerInstanceId ?? null, context.instrument ?? null, tail.resolution, start, end, tail.historySession ?? null]);
2499
- const shared = tailRequestsInFlight.get(provider) ?? new Map<string, Promise<PriceHistoryResult | null>>();
2500
- tailRequestsInFlight.set(provider, shared);
2501
- let request = shared.get(requestKey);
2502
- if (!request) {
2503
- const fetched = fetchHistoryResult(provider, symbol, exchange ?? "",
2504
- { kind: "detail", start: new Date(start), end: new Date(end), interval: tail.resolution }, context)
2505
- .catch(logFailure("time-series-resolve", "Could not reconcile chart history", null))
2506
- .finally(() => {
2507
- if (shared.get(requestKey) === fetched) shared.delete(requestKey);
2508
- });
2509
- shared.set(requestKey, fetched);
2510
- request = fetched;
2511
- }
2512
- const result = await request;
2513
- if (!result || result.resolution !== loaded.resolution || !!result.extendedHours !== !!loaded.extendedHours
2514
- || priceHistoryAcquisitionIdentity(result) !== priceHistoryAcquisitionIdentity(loaded)) return false;
2515
- const points = result.points.filter((point) => {
2516
- const time = getPricePointTimestamp(point);
2517
- return Number.isFinite(time) && time >= start && Number.isFinite(point.close);
2518
- });
2519
- // A full refetch that landed meanwhile supersedes this window.
2520
- if (points.length === 0 || cache.priceHistoryByRequest.get(key) !== pending) return false;
2521
- const refreshed = mergePriceHistoryWindows(loaded.points, points, tail.resolution);
2522
- if (samePricePoints(refreshed, loaded.points)) return false;
2523
- const acquisition = priceHistoryTailAcquisition(loaded, result);
2524
- // The merged bars are as current as the newer acquisition says.
2525
- cache.priceHistoryByRequest.set(key, Promise.resolve({ ...loaded, points: refreshed,
2526
- session: acquisition.session, sourceKey: acquisition.sourceKey, savedCopyBehind: acquisition.savedCopyBehind }));
2527
- if (accumulated) {
2528
- const accumulatedAcquisition = priceHistoryTailAcquisition(accumulated, result);
2529
- cache.accumulatedPriceHistory.set(tail.accumulationKey, { ...accumulated,
2530
- points: mergePriceHistoryWindows(accumulated.points, points, tail.resolution),
2531
- session: accumulatedAcquisition.session, sourceKey: accumulatedAcquisition.sourceKey });
2532
- }
2533
- cache.revision += 1;
2534
- return true;
2535
- })).then((changes) => changes.some(Boolean)).finally(() => {
2536
- if (cache.tailReconcile === run) cache.tailReconcile = null;
2537
- });
2538
- cache.tailReconcile = run;
2539
- return run;
2540
- }