gloomberb 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +31 -6
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -50,6 +50,7 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
50
50
  quickAddItems,
51
51
  quickAddLoading,
52
52
  quickAddError,
53
+ retryQuickAdd,
53
54
  quickAddQuery,
54
55
  quickAddRef,
55
56
  quickAddSelection,
@@ -123,7 +124,7 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
123
124
  onBlur={deactivateQuickAdd}
124
125
  />
125
126
 
126
- {quickAddActive && quickAddQuery.trim() && (
127
+ {(quickAddActive || quickAddError) && quickAddQuery.trim() && (
127
128
  quickAddItems.length > 0 ? (
128
129
  <Box width={contentWidth} overflow="hidden">
129
130
  <ListView
@@ -138,13 +139,14 @@ export function SeriesEditorDialog({ dialogId, resolve, initialSpec }: SeriesEdi
138
139
  onActivate={(_, index) => { beginQuickAdd(); addCatalogSuggestion(quickAddSuggestions[index]); }}
139
140
  />
140
141
  </Box>
141
- ) : !quickAddLoading && quickAddError ? (
142
- <EmptyState title={quickAddError} status="error" />
143
- ) : (
142
+ ) : !quickAddError ? (
144
143
  <Text fg={colors.textMuted}>
145
144
  {quickAddLoading ? "Searching instruments…" : "No matching security or metric."}
146
145
  </Text>
147
- )
146
+ ) : null
147
+ )}
148
+ {quickAddQuery.trim() && quickAddError && (
149
+ <EmptyState title={quickAddError} status="error" retry={{ onRetry: () => { beginQuickAdd(); return retryQuickAdd(); } }} />
148
150
  )}
149
151
 
150
152
  {items.length > 0 ? (
@@ -1,23 +1,29 @@
1
1
  import { currencyPairSpelling } from "../../../utils/currency-pair";
2
2
  import { dashedCoinPair } from "../../../utils/crypto-pair";
3
- import { GALLONS_PER_BARREL } from "../../../time-series/price-units";
3
+ import { GALLONS_PER_BARREL, isBarrelsPerTonne } from "../../../time-series/price-units";
4
4
  import { MAX_CHART_COMPOSER_SERIES } from "./chart-spec";
5
5
  import { parseSeriesExpression, resolveChartFieldAlias, type ParsedSeriesExpression } from "./series-expression";
6
6
  import { getTimeSeriesField } from "../../../time-series/field-catalog";
7
7
 
8
8
  /**
9
9
  * Series formulas: a sum of series each times a number, the whole optionally
10
- * in brackets over a number.
10
+ * in brackets over or times numbers and series.
11
11
  *
12
12
  * 42*HO=F - CL=F heating-oil crack, USD per barrel
13
13
  * HO=F*42 - BZ=F a multiplier on either side
14
14
  * (2*42*RB=F + 42*HO=F - 3*CL=F)/3 3:2:1 crack, per barrel of crude
15
+ * (42*HO=F - BZ=F)/EURUSD=X the crack in EUR per barrel
16
+ * 7.45*42*HO=F - 7.45*BZ=F a gasoil-style crack per tonne
15
17
  *
16
18
  * A literal 42 on a series is the 42 gallons of a barrel: it makes a price per
17
- * gallon a price per barrel. Any other number is a count and keeps the unit.
19
+ * gallon a price per barrel. A literal 7.45 or 8.45 is the barrels in a tonne
20
+ * of gasoil or gasoline: it makes a price per barrel a price per tonne. Any
21
+ * other number is a count and keeps the unit. A series the whole formula is
22
+ * divided or multiplied by changes its unit as the arithmetic does: over
23
+ * EURUSD=X (USD per EUR), dollars are euros.
18
24
  *
19
25
  * Grammar, no other precedence to learn:
20
- * formula := sum | "(" sum ")" [ "/" number ]
26
+ * formula := sum | "(" sum ")" { ( "/" | "*" ) factor }
21
27
  * sum := [ "+" | "-" ] term { ( "+" | "-" ) term }
22
28
  * term := factor { ( "*" | "×" ) factor } with exactly one series
23
29
  * factor := number | series
@@ -29,17 +35,30 @@ interface FormulaLeg {
29
35
  weight: number;
30
36
  /** The leg was multiplied by a literal 42: the gallons in a barrel. */
31
37
  perBarrel: boolean;
38
+ /** The barrels in a tonne the leg was multiplied by (7.45 or 8.45), or null. */
39
+ perTonne: number | null;
32
40
  /** The leg as written: `HO=F`. */
33
41
  text: string;
34
42
  }
35
43
 
44
+ /** A series the whole formula is multiplied (power 1) or divided (-1) by. */
45
+ interface FormulaFactor {
46
+ series: ParsedSeriesExpression;
47
+ power: 1 | -1;
48
+ /** The series as written: `EURUSD=X`. */
49
+ text: string;
50
+ }
51
+
36
52
  export interface ParsedFormula {
37
53
  legs: FormulaLeg[];
38
54
  /** What the whole sum is divided by; 1 when it is not. */
39
55
  divisor: number;
56
+ /** The series the whole sum is then multiplied or divided by, in the order written. */
57
+ factors: FormulaFactor[];
40
58
  }
41
59
 
42
60
  const EXAMPLE = "(2*42*RB=F + 42*HO=F - 3*CL=F)/3";
61
+ const SCALED_EXAMPLE = "(42*HO=F - BZ=F)/EURUSD=X";
43
62
 
44
63
  type Token =
45
64
  | { kind: "number"; value: number; text: string }
@@ -75,9 +94,20 @@ function minusIsOperator(atom: string, previous: Token | undefined, rest: string
75
94
  )));
76
95
  }
77
96
 
97
+ /**
98
+ * After the formula's closing bracket every `/` divides, except inside a
99
+ * currency or coin pair written with one: `(...)/EUR/USD` is over EUR/USD.
100
+ */
101
+ function slashIsPairSpelling(atom: string, rest: string): boolean {
102
+ const quote = rest.match(/^[^\s*×+()/]*/)![0];
103
+ const symbol = `${atom}/${quote.split(":")[0]}`;
104
+ return /^[A-Za-z]{3,}$/.test(atom) && !!quote && (!!currencyPairSpelling(symbol) || !!dashedCoinPair(symbol));
105
+ }
106
+
78
107
  function tokenize(expression: string): Token[] {
79
108
  const tokens: Token[] = [];
80
109
  let atom = "";
110
+ let closed = false;
81
111
  const flush = () => {
82
112
  if (!atom) return;
83
113
  tokens.push(NUMBER.test(atom) ? { kind: "number", value: Number(atom), text: atom } : { kind: "atom", text: atom });
@@ -89,11 +119,14 @@ function tokenize(expression: string): Token[] {
89
119
  flush();
90
120
  } else if (char === "*" || char === "×" || char === "+" || char === "(" || char === ")") {
91
121
  flush();
122
+ if (char === ")") closed = true;
92
123
  tokens.push({ kind: char === "×" ? "*" : char, text: char });
93
124
  } else if (char === "-" && (minusIsOperator(atom, tokens.at(-1), expression.slice(index + 1)) || /\s/.test(expression[index - 1] ?? " "))) {
94
125
  flush();
95
126
  tokens.push({ kind: "-", text: char });
96
- } else if (char === "/" && !atom && tokens.at(-1)?.kind === ")") {
127
+ } else if (char === "/" && (!atom || (closed && !slashIsPairSpelling(atom, expression.slice(index + 1))))) {
128
+ // A slash on its own divides; inside a word it is part of a pair (EUR/USD), until the brackets close.
129
+ flush();
97
130
  tokens.push({ kind: "/", text: char });
98
131
  } else {
99
132
  atom += char;
@@ -111,7 +144,7 @@ function fail(message: string): never {
111
144
  function legSeries(text: string): ParsedSeriesExpression {
112
145
  const symbol = text.split(":")[0]!;
113
146
  if (symbol.includes("/") && !currencyPairSpelling(symbol) && !dashedCoinPair(symbol)) {
114
- fail(`"${text}" divides inside the formula. Divide the whole formula by a number in brackets, as in ${EXAMPLE}; chart a ratio of two series as A / B on its own.`);
147
+ fail(`"${text}" divides inside the formula. Divide the whole formula in brackets by a number or a series, as in ${SCALED_EXAMPLE}; chart a ratio of two series as A / B on its own.`);
115
148
  }
116
149
  return parseSeriesExpression(text)
117
150
  ?? fail(`"${text}" is not a series. Use SYMBOL, SYMBOL:field, FUT:code, UST:maturity, FRED:series, GEO:series or CAP:capability-id:series-id.`);
@@ -143,7 +176,7 @@ export function parseFormulaExpression(expression: string): ParsedFormula | null
143
176
  const closes = tokens.filter((token) => token.kind === ")").length;
144
177
  if (opens !== closes) fail(`The brackets in "${trimmed}" do not pair up.`);
145
178
  if (opens > 1 || (opens === 1 && !bracketed)) {
146
- fail(`Brackets go around the whole formula only, before an optional divisor: ${EXAMPLE}.`);
179
+ fail(`Brackets go around the whole formula only, then divide or multiply it: ${EXAMPLE}, ${SCALED_EXAMPLE}.`);
147
180
  }
148
181
 
149
182
  const legs: FormulaLeg[] = [];
@@ -158,6 +191,7 @@ export function parseFormulaExpression(expression: string): ParsedFormula | null
158
191
  // One term: numbers and exactly one series, multiplied.
159
192
  let weight = sign;
160
193
  let perBarrel = false;
194
+ let perTonne: number | null = null;
161
195
  const factors: Token[] = [];
162
196
  for (;;) {
163
197
  const factor = take();
@@ -171,6 +205,7 @@ export function parseFormulaExpression(expression: string): ParsedFormula | null
171
205
  if (factor.kind === "number") {
172
206
  if (factor.value === 0) fail("A multiplier of 0 removes its series; leave the term out instead.");
173
207
  if (factor.value === GALLONS_PER_BARREL && !perBarrel) perBarrel = true;
208
+ else if (isBarrelsPerTonne(factor.value) && perTonne === null) perTonne = factor.value;
174
209
  weight *= factor.value;
175
210
  }
176
211
  if (peek()?.kind !== "*") break;
@@ -189,14 +224,14 @@ export function parseFormulaExpression(expression: string): ParsedFormula | null
189
224
  if (legs.some((leg) => leg.text.toUpperCase() === text.toUpperCase())) {
190
225
  fail(`${text} appears twice. Write it once with its combined multiplier, such as 2*${text}.`);
191
226
  }
192
- legs.push({ series: parsed, weight, perBarrel, text });
227
+ legs.push({ series: parsed, weight, perBarrel, perTonne, text });
193
228
  expectTerm = false;
194
229
 
195
230
  const next = peek();
196
231
  if (!next || next.kind === ")") break;
197
232
  if (next.kind !== "+" && next.kind !== "-") {
198
233
  fail(next.kind === "/"
199
- ? `Divide the whole formula in brackets by a number, as in ${EXAMPLE}.`
234
+ ? `Divide the whole formula in brackets by a number or a series, as in ${SCALED_EXAMPLE}.`
200
235
  : `Expected + or - after ${written}, not "${next.text}".`);
201
236
  }
202
237
  take();
@@ -208,26 +243,44 @@ export function parseFormulaExpression(expression: string): ParsedFormula | null
208
243
  }
209
244
 
210
245
  let divisor = 1;
246
+ const scaleFactors: FormulaFactor[] = [];
211
247
  if (bracketed) {
212
248
  if (take()?.kind !== ")") fail(`The brackets in "${trimmed}" do not pair up.`);
213
- if (peek()?.kind === "/") {
214
- take();
249
+ while (peek()?.kind === "/" || peek()?.kind === "*") {
250
+ const divides = take()!.kind === "/";
215
251
  const value = take();
216
- if (!value || value.kind !== "number") {
217
- fail(`Divide the whole formula by a number, such as /3${value ? `, not ${value.text}` : ""}.`);
252
+ if (!value || (value.kind !== "number" && value.kind !== "atom")) {
253
+ fail(`${divides ? "Divide" : "Multiply"} the whole formula by a number or a series, such as ${divides ? "/3 or /EURUSD=X" : "*7.45 or *EURUSD=X"}${value ? `, not ${value.text}` : ""}.`);
254
+ }
255
+ if (value.kind === "number") {
256
+ if (value.value === 0) fail(divides ? "A formula cannot be divided by 0." : "A multiplier of 0 removes the formula; leave it out instead.");
257
+ if (divides) {
258
+ divisor *= value.value;
259
+ } else {
260
+ // A number times the whole sum multiplies each leg, converting its unit as it would there.
261
+ for (const leg of legs) {
262
+ if (value.value === GALLONS_PER_BARREL && !leg.perBarrel) leg.perBarrel = true;
263
+ else if (isBarrelsPerTonne(value.value) && leg.perTonne === null) leg.perTonne = value.value;
264
+ leg.weight *= value.value;
265
+ }
266
+ }
267
+ continue;
268
+ }
269
+ const text = value.text;
270
+ if ([...legs, ...scaleFactors].some((entry) => entry.text.toUpperCase() === text.toUpperCase())) {
271
+ fail(`${text} appears twice in the formula. Use each series once.`);
218
272
  }
219
- if (value.value === 0) fail("A formula cannot be divided by 0.");
220
- divisor = value.value;
273
+ scaleFactors.push({ series: legSeries(text), power: divides ? -1 : 1, text });
221
274
  }
222
275
  }
223
276
  const rest = tokens.slice(position);
224
277
  if (rest.length > 0) {
225
278
  fail(rest[0]!.kind === "/" || rest[0]!.kind === "*"
226
- ? `Divide the whole formula in brackets by a number, as in ${EXAMPLE}.`
279
+ ? `Divide or multiply the whole formula in brackets, as in ${EXAMPLE} or ${SCALED_EXAMPLE}.`
227
280
  : `Unexpected "${rest.map((token) => token.text).join(" ")}" after the formula.`);
228
281
  }
229
- if (legs.length > MAX_CHART_COMPOSER_SERIES) {
282
+ if (legs.length + scaleFactors.length > MAX_CHART_COMPOSER_SERIES) {
230
283
  fail(`A formula takes up to ${MAX_CHART_COMPOSER_SERIES} series.`);
231
284
  }
232
- return { legs, divisor };
285
+ return { legs, divisor, factors: scaleFactors };
233
286
  }
@@ -138,6 +138,12 @@ export const paneSchemas = {
138
138
  ],
139
139
  defaultValue: "percent",
140
140
  },
141
+ {
142
+ key: "totalReturn",
143
+ description: "Reinvest each cash distribution at its ex-date close, so the returns are total returns, not price returns.",
144
+ type: "boolean",
145
+ defaultValue: false,
146
+ },
141
147
  {
142
148
  key: "chartResolution",
143
149
  description: BAR_SIZE_DESCRIPTION,
@@ -151,7 +157,7 @@ export const paneSchemas = {
151
157
  discovery: {
152
158
  id: "price-comparison",
153
159
  aliases: ["stock comparison", "price performance comparison", "returns comparison", "compare stock prices"],
154
- limitations: ["Compares closing-price changes in each listing's currency over shared observation dates. Cash distributions and FX conversion are excluded; these are not reinvested total returns."],
160
+ limitations: ["Compares closing-price changes in each listing's currency over shared observation dates. Cash distributions and FX conversion are excluded unless totalReturn is set, which reinvests each distribution at its ex-date close; FX conversion is never applied."],
155
161
  screenshotReadiness: "ready",
156
162
  },
157
163
  },
@@ -203,6 +209,13 @@ export const paneSchemas = {
203
209
  "valuation-graph-pane": {
204
210
  argument: { kind: "tickers" },
205
211
  options: [
212
+ {
213
+ key: "rangePreset",
214
+ description: "Chart history window.",
215
+ type: "enum",
216
+ aliases: ["range"],
217
+ values: HISTORY_RANGE_VALUES,
218
+ },
206
219
  {
207
220
  key: "metric",
208
221
  description: "Valuation metric to compare.",
@@ -219,9 +232,13 @@ export const paneSchemas = {
219
232
  // The same metric the GE template opens with in the app.
220
233
  defaultValue: "trailingPE",
221
234
  },
222
- // Annual, as the GE template opens in the app.
223
- FINANCIAL_PERIOD_OPTION,
224
- FINANCIAL_PERIOD_COUNT_OPTION,
235
+ {
236
+ ...FINANCIAL_PERIOD_OPTION,
237
+ description: "Daily multiples, or one observation per reporting period.",
238
+ values: [{ value: "daily", aliases: ["d"] }, ...FINANCIAL_PERIOD_OPTION.values!, { value: "ttm" }],
239
+ defaultValue: "daily",
240
+ },
241
+ { ...FINANCIAL_PERIOD_COUNT_OPTION, description: "Keep only the latest N observations per ticker." },
225
242
  ],
226
243
  discovery: {
227
244
  id: "valuation-series",
@@ -37,7 +37,7 @@ import { futuresSessionOpenMinute } from "../../../market-data/market/trading-se
37
37
  import { cleanFloat32Price } from "../../../cli/history-rows";
38
38
  import { parseChartSpec } from "./chart-spec";
39
39
  import { paneSchemas } from "./headless-schema";
40
- import { barHistoryFreshness, REPORTED_DATA } from "../shared/report-freshness";
40
+ import { barHistoryFreshness, DAILY_CLOSES, REPORTED_DATA } from "../shared/report-freshness";
41
41
  import type { BarPeriodToDate } from "../../../market-data/market/bar-period";
42
42
  import { closedHistoryBarDay, formingHistoryBar } from "../ticker-detail/data-panes/history-bars";
43
43
  import { inferSeriesFrequency } from "../../../series/csv";
@@ -151,6 +151,7 @@ export async function loadChartPaneModel(
151
151
  },
152
152
  resolveCapabilitySeries: headlessCapabilityResolver(context),
153
153
  loadFuturesPriceUnit: (symbol) => loadFuturesPriceUnit(symbol, (key) => loadFuturesContractTerms(key, context.apiClient)),
154
+ loadEarningsHistory: (symbol) => context.apiClient.getCloudEarningsHistory(symbol, 100),
154
155
  loadFredSeries: async (request) => ({
155
156
  data: await context.apiClient.getCloudFredSeries(request.seriesId, {
156
157
  startDate: request.startDate,
@@ -221,9 +222,14 @@ export async function loadChartPaneModel(
221
222
  kind, source.fieldId.split(".")[1]!, source.period === "quarterly" ? "quarterly" : "annual",
222
223
  source.instrument.symbol,
223
224
  ).map((row) => [row.date, row.growth])) : null;
225
+ // A formula or a converted price prints at its legs' decimals in text; JSON keeps every digit.
226
+ const { valueDecimals, ...reported } = series;
224
227
  return {
225
- ...series,
228
+ ...reported,
226
229
  ...name ? { name } : {},
230
+ ...valueDecimals !== undefined ? { textDecimals: valueDecimals } : {},
231
+ // A company's reported total (revenue, market cap) reads as 63.33B in the report, as in the legend and the tables.
232
+ ...series.unitGroup.split(":")[0] === "currency-total" ? { textCompact: true } : {},
227
233
  derived: !ids.has(series.id),
228
234
  points: series.points.map((point) => ({
229
235
  ...point,
@@ -247,7 +253,7 @@ export async function loadChartPaneModel(
247
253
  ...(chart.priceHistoryIntegrity?.length ? { priceHistoryIntegrity: chart.priceHistoryIntegrity } : {}),
248
254
  ...(valuationPriceIssues.length ? { valuationPriceIssues } : {}),
249
255
  ...(periodCoverage.length ? { periodCoverage } : {}),
250
- notices: [...chart.warnings.filter((warning) => warning === FINANCIAL_VINTAGE_NOTICE || warning === SEC_EPS_BASIS_NOTICE || warning === chart.priceComparison?.notice
256
+ notices: [...chart.coverageNotices ?? [], ...chart.returnBasisNotice ? [chart.returnBasisNotice] : [], ...chart.warnings.filter((warning) => warning === FINANCIAL_VINTAGE_NOTICE || warning === SEC_EPS_BASIS_NOTICE || warning === chart.priceComparison?.notice
251
257
  || chart.delistings?.some((delisting) => delisting.notice === warning)), ...integrityNotices],
252
258
  priceComparison: chart.priceComparison ?? null,
253
259
  summaries: chart.series.filter((series) => !series.profile).map((series) => ({ id: series.id, ...summarizeResolvedSeries(series) })),
@@ -346,6 +352,13 @@ function chartFreshness(
346
352
  // A leg that failed to load has no bars to date; the ones that loaded say what the chart is.
347
353
  const loaded = allSeries.filter((entry) => entry.points.length > 0);
348
354
  const series = loaded.length ? loaded : allSeries;
355
+ const visible = spec.series.filter((entry) => entry.visible !== false);
356
+ if (visible.length && series.every(entry => (entry as HeadlessSeries & Pick<ResolvedSeries, "nativeFrequency">).nativeFrequency === "daily")
357
+ && visible.every((entry) => entry.source.kind === "security"
358
+ && entry.source.period === "daily" && entry.source.fieldId.startsWith("valuation."))) {
359
+ const ends = series.flatMap((entry) => entry.points.at(-1)?.date ?? []).map((date) => new Date(date).toISOString().slice(0, 10)).sort();
360
+ return { ...DAILY_CLOSES, ...(ends.length ? { asOf: ends.at(-1)!, oldest: ends[0]! } : {}) };
361
+ }
349
362
  const resolutions = series.map((entry) => (entry as HeadlessSeries & { historyResolution?: string }).historyResolution);
350
363
  const dataset = isDatasetChart(spec);
351
364
  if (!series.length || resolutions.some((resolution) => !resolution)) {
@@ -712,6 +725,9 @@ export function chartHeadless(template: keyof typeof paneSchemas): HeadlessPaneD
712
725
  }, context.marketData) };
713
726
  }
714
727
  const model = await loadChartPaneModel(spec, context);
728
+ // A bare coin ticker charted as another listing (ETH, the Grayscale fund) says where the coin is, as `quote` does.
729
+ const coinNotes = (await Promise.all(listings.values())).flatMap((read) => read?.coinNote ?? []);
730
+ if (coinNotes.length) model.warnings = [...model.warnings ?? [], ...coinNotes];
715
731
  if (template === "graph-intraday-price-pane" && !intradayHistories.length && model.chart.viewport) {
716
732
  const { start, end } = model.chart.viewport;
717
733
  for (const { target, data } of [
@@ -453,7 +453,7 @@ const chartComposerTemplates: PaneTemplateDef[] = [
453
453
  prefix: "CMP",
454
454
  aliases: ["COMP", "TRA"],
455
455
  label: "Comparison Chart",
456
- description: "Compare price returns over shared observation dates for two or more tickers. A leveraged or inverse fund gets its daily objective drawn from its index fund (3× daily QQQ beside TQQQ) and its decay against it.",
456
+ description: "Compare price returns over shared observation dates for two or more tickers, or total returns with distributions reinvested. A leveraged or inverse fund gets its daily objective drawn from its index fund (3× daily QQQ beside TQQQ) and its decay against it.",
457
457
  // Leveraged and inverse funds: the daily line and the decay come with the comparison.
458
458
  keywords: ["leveraged", "inverse", "decay", "daily-reset", "daily reset", "volatility decay"],
459
459
  argKind: "ticker-list",
@@ -54,8 +54,10 @@ import {
54
54
  } from "../../../state/app/context";
55
55
  import { colors } from "../../../theme/colors";
56
56
  import { isUsListingExchange, parsePublicTickerKey, publicTickerKey } from "../../../utils/exchanges";
57
- import { isMarketFieldId } from "../../../time-series/field-catalog";
58
- import { isDatasetChart } from "../../../time-series/spec";
57
+ import { getTimeSeriesField, isMarketFieldId } from "../../../time-series/field-catalog";
58
+ import { isDatasetChart, isOhlcSeriesStyle } from "../../../time-series/spec";
59
+ import { useThemeColors } from "../../../theme/theme-context";
60
+ import { priceComparisonSeriesIds } from "../../../time-series/price-comparison";
59
61
  import { seriesMessageName, studyMessageName } from "../../../time-series/series-name";
60
62
  import { CHART_COMPOSER_PANE_ID } from "../../../types/config";
61
63
  import { useRemoteUiNode } from "../../../remote/semantic-tree";
@@ -117,6 +119,8 @@ import {
117
119
  CHART_BAR_SIZE_PRESETS,
118
120
  CHART_RANGES as RANGES,
119
121
  CHART_RESOLUTIONS,
122
+ CHART_CANDLE_COLOURS_SETTING_KEY,
123
+ chartCandleColours,
120
124
  chartStudyOptionsFor,
121
125
  chartStudyPeriodTitle,
122
126
  chartStudySettingLabel,
@@ -342,6 +346,14 @@ function ChartComposerSurface({
342
346
  // plausible time is dropped without pinning. This handle only sets the
343
347
  // pin; the chart it opens on is what shows and keeps it.
344
348
  const [pinnedMoment, setPinnedMoment] = usePaneSettingValue<unknown>(CHART_PINNED_MOMENT_SETTING_KEY, null);
349
+ const [candleColoursSetting] = usePaneSettingValue<unknown>(CHART_CANDLE_COLOURS_SETTING_KEY, null);
350
+ const gainColour = useThemeColors().positive;
351
+ // Rising candles in the gain colour; falling ones already take the loss colour.
352
+ const candleColour = useCallback((list: ResolvedSeries[]) => (
353
+ chartCandleColours(candleColoursSetting) === "change"
354
+ ? list.map((entry) => isOhlcSeriesStyle(entry.style) ? { ...entry, color: gainColour } : entry)
355
+ : list
356
+ ), [candleColoursSetting, gainColour]);
345
357
  const momentLink = useTabCursorLink(paneId ? `${paneId}:moment` : null, { viewer: false });
346
358
  useEffect(() => {
347
359
  if (pinnedMoment == null || !momentLink?.pin) return;
@@ -446,6 +458,14 @@ function ChartComposerSurface({
446
458
  const selectedPairStudies = getSelectedPairStudies(spec);
447
459
  const viewport = resolution.viewport;
448
460
  const baseSeriesIds = useMemo(() => new Set(spec.series.map((series) => series.id)), [spec.series]);
461
+ const rangeStatisticsSeriesId = spec.series.length === 1
462
+ && spec.series[0]!.source.kind === "security"
463
+ && spec.series[0]!.source.fieldId.startsWith("valuation.")
464
+ && spec.series[0]!.source.period === "daily"
465
+ && resolution.series.find(series => series.id === spec.series[0]!.id)?.nativeFrequency === "daily"
466
+ && spec.series[0]!.transform === "raw"
467
+ && spec.series[0]!.visible !== false
468
+ ? spec.series[0]!.id : undefined;
449
469
  // Hidden series are never loaded, so the resolver has nothing to report for
450
470
  // them. Without a placeholder they vanish from the legend entirely and the
451
471
  // only way back is the series dialog.
@@ -453,6 +473,12 @@ function ChartComposerSurface({
453
473
  // Each security series names its company once, beside its label: "AAPL Price · Apple".
454
474
  const named = new Set<string>();
455
475
  const resolved = (resolution.legendSeries ?? []).map((entry) => {
476
+ const authored = spec.series.find((series) => series.id === entry.id);
477
+ if (authored?.source.kind === "security" && authored.source.fieldId.startsWith("valuation.")) {
478
+ const field = getTimeSeriesField(authored.source.fieldId);
479
+ return { ...entry, label: authored.label?.trim()
480
+ || `${authored.source.instrument.symbol} ${field?.shortLabel ?? "Multiple"}` };
481
+ }
456
482
  const name = nameSeries ? listings.names.get(entry.id) : undefined;
457
483
  const company = name ? shortCompanyName(name) : "";
458
484
  if (!company || named.has(company)) return entry;
@@ -461,9 +487,9 @@ function ChartComposerSurface({
461
487
  });
462
488
  const resolvedIds = new Set(resolved.map((entry) => entry.id));
463
489
  const missing = spec.series.filter((entry) => !resolvedIds.has(entry.id));
464
- if (missing.length === 0) return named.size > 0 ? resolved : resolution.legendSeries;
490
+ if (missing.length === 0) return candleColour(resolved);
465
491
  const bySpecOrder = new Map(spec.series.map((entry, index) => [entry.id, index] as const));
466
- return [
492
+ return candleColour([
467
493
  ...resolved,
468
494
  ...missing.map((entry): ResolvedSeries => ({
469
495
  id: entry.id,
@@ -481,15 +507,15 @@ function ChartComposerSurface({
481
507
  hidden: true,
482
508
  points: [],
483
509
  })),
484
- ].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
485
- }, [listings.names, nameSeries, resolution.legendSeries, spec.series]);
510
+ ].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER)));
511
+ }, [candleColour, listings.names, nameSeries, resolution.legendSeries, spec.series]);
486
512
  const plottedSeries = useMemo(
487
- () => projectVisibleChartSeries(
513
+ () => candleColour(projectVisibleChartSeries(
488
514
  spec,
489
515
  resolution.bufferedSeries ?? resolution.series,
490
516
  resolution.legendSeries,
491
- ),
492
- [resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
517
+ )),
518
+ [candleColour, resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
493
519
  );
494
520
  const { notify } = usePluginAppActions();
495
521
  const { runtime } = usePluginRenderContext();
@@ -1176,6 +1202,22 @@ function ChartComposerSurface({
1176
1202
  }]
1177
1203
  : [];
1178
1204
 
1205
+ // Returns of two or more prices compare as price returns; the toggle reinvests their cash distributions.
1206
+ const totalReturnOn = spec.viewport.totalReturn === true;
1207
+ const totalReturnFilters = totalReturnOn || priceComparisonSeriesIds(spec) !== null
1208
+ ? [{
1209
+ id: "total-return",
1210
+ kind: "toggle" as const,
1211
+ label: "Total return",
1212
+ short: "Total ret",
1213
+ value: totalReturnOn,
1214
+ onChange: (value: boolean) => {
1215
+ const { totalReturn: _previous, ...viewport } = specRef.current.viewport;
1216
+ updateSpec({ ...specRef.current, viewport: value ? { ...viewport, totalReturn: true } : viewport });
1217
+ },
1218
+ }]
1219
+ : [];
1220
+
1179
1221
  // A company that reports no quarterly figures says so, and where its annual ones are.
1180
1222
  const unreported = resolution.legendSeries?.find((entry) => entry.unreportedPeriod && entry.points.length === 0);
1181
1223
  const emptyMessage = spec.series.length === 0
@@ -1202,6 +1244,7 @@ function ChartComposerSurface({
1202
1244
  onChange: (value: string) => setRange(value as TimeRange) },
1203
1245
  ...genericFilters,
1204
1246
  ...extendedHoursFilters,
1247
+ ...totalReturnFilters,
1205
1248
  ]}
1206
1249
  view={resolutionOptions.length > 1 ? {
1207
1250
  value: spec.viewport.resolution,
@@ -1272,6 +1315,7 @@ function ChartComposerSurface({
1272
1315
  timePick={timePick}
1273
1316
  pricePick={pricePick}
1274
1317
  levels={chartLevels}
1318
+ rangeStatisticsSeriesId={rangeStatisticsSeriesId}
1275
1319
  emptyMessage={emptyMessage}
1276
1320
  emptyDetail={emptyDetail}
1277
1321
  legendAccessory={(
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../../time-series/types";
7
7
  import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
8
8
  import { MAX_CHART_COMPOSER_SERIES } from "./chart-spec";
9
- import { appendChartSeries, appendPlacedSeries, buildCustomSeries, buildSeriesSpec, panelsForSeries } from "./chart-spec-edit";
9
+ import { appendChartSeries, appendPlacedSeries, buildCustomSeries, buildSeriesSpec, nextGeneratedPanelId, panelsForSeries } from "./chart-spec-edit";
10
10
  import { currencyPairSpelling } from "../../../utils/currency-pair";
11
11
  import { dashedCoinPair } from "../../../utils/crypto-pair";
12
12
  import {
@@ -82,21 +82,51 @@ function placeFormulaLeg(formula: ParsedFormula, index: number, series: ChartSer
82
82
  return mixed && series.panelId === "main" && !formula.legs[index]!.perBarrel ? { ...series, axis: "right" } : series;
83
83
  }
84
84
 
85
- /** The study that weighs a formula's legs, charted as the given series, in the Formula panel. */
85
+ /**
86
+ * A series the formula is divided or multiplied by (an FX rate) counts
87
+ * something else again: it is drawn in a panel of its own, not on the legs' axes.
88
+ */
89
+ function placeFormulaFactor(spec: Pick<ChartSpec, "panels" | "series" | "studies">, series: ChartSeriesSpec): ChartSeriesSpec {
90
+ return { ...series, panelId: nextGeneratedPanelId(spec), axis: "auto" };
91
+ }
92
+
93
+ /** A factor's panel is shorter than a series panel, so the legs and the formula keep their room. */
94
+ const FACTOR_PANEL_HEIGHT = 0.2;
95
+
96
+ /** The panels with each new factor's last and short: the formula reads right under its legs. */
97
+ function withFactorPanels(panels: ChartSpec["panels"], factors: readonly ChartSeriesSpec[]): ChartSpec["panels"] {
98
+ const ids = new Set(factors.map((series) => series.panelId));
99
+ return [
100
+ ...panels.filter((panel) => !ids.has(panel.id)),
101
+ ...panels.filter((panel) => ids.has(panel.id)).map((panel) => ({ ...panel, height: FACTOR_PANEL_HEIGHT })),
102
+ ];
103
+ }
104
+
105
+ /** The study that weighs a formula's legs and scales the sum, charted as the given series, in the Formula panel. */
86
106
  function formulaStudy(formula: ParsedFormula, inputSeriesIds: string[]): ChartStudySpec {
87
107
  const parameters: ChartStudySpec["parameters"] = { divisor: formula.divisor };
88
108
  formula.legs.forEach((leg, index) => {
89
109
  parameters[`weight${index + 1}`] = leg.weight;
90
110
  if (leg.perBarrel) parameters[`perBarrel${index + 1}`] = 1;
111
+ if (leg.perTonne) parameters[`perTonne${index + 1}`] = leg.perTonne;
112
+ });
113
+ formula.factors.forEach((factor, index) => {
114
+ parameters[`power${formula.legs.length + index + 1}`] = factor.power;
91
115
  });
92
116
  return { id: FORMULA_STUDY_ID, kind: "formula", inputSeriesIds, parameters, panelId: "formula", axis: "auto" };
93
117
  }
94
118
 
95
- /** A formula's legs as series, with the study that sums them in the Formula panel. */
119
+ /** A formula's legs and factors as series, with the study that sums them in the Formula panel. */
96
120
  function formulaChartPreset(formula: ParsedFormula): ChartSpec {
97
- const series = buildCustomSeries(formula.legs.map((leg) => leg.series))
98
- .map((entry, index) => placeFormulaLeg(formula, index, entry));
99
- return chartSpec(series, { studies: [formulaStudy(formula, series.map((entry) => entry.id))] });
121
+ const legs = buildCustomSeries([...formula.legs, ...formula.factors].map((entry) => entry.series));
122
+ const series: ChartSeriesSpec[] = [];
123
+ legs.forEach((entry, index) => {
124
+ series.push(index < formula.legs.length
125
+ ? placeFormulaLeg(formula, index, entry)
126
+ : placeFormulaFactor({ panels: [], series, studies: [] }, entry));
127
+ });
128
+ const spec = chartSpec(series, { studies: [formulaStudy(formula, series.map((entry) => entry.id))] });
129
+ return { ...spec, panels: withFactorPanels(spec.panels, series.slice(formula.legs.length)) };
100
130
  }
101
131
 
102
132
  /**
@@ -107,17 +137,22 @@ function appendChartFormula(spec: ChartSpec, expression: string): { spec: ChartS
107
137
  const formula = parseFormulaExpression(expression);
108
138
  if (!formula) throw new Error(`"${expression}" is not a formula.`);
109
139
  const key = (series: ChartSeriesSpec) => formatSeriesExpression(series).toLowerCase();
110
- const legs = formula.legs.map((leg) => key(buildSeriesSpec(leg.series, 0)));
140
+ const terms = [...formula.legs, ...formula.factors];
141
+ const legs = terms.map((leg) => key(buildSeriesSpec(leg.series, 0)));
111
142
  const missing = legs.filter((leg) => !spec.series.some((series) => key(series) === leg)).length;
112
143
  if (spec.series.length + missing > MAX_CHART_COMPOSER_SERIES) {
113
144
  throw new Error(`Charts support up to ${MAX_CHART_COMPOSER_SERIES} base series.`);
114
145
  }
115
146
  let next: ChartSpec = { ...spec, studies: spec.studies.filter((study) => study.id !== FORMULA_STUDY_ID) };
116
- const inputs = formula.legs.map((leg, index) => {
147
+ const placedFactors: ChartSeriesSpec[] = [];
148
+ const inputs = terms.map((leg, index) => {
117
149
  const charted = next.series.find((series) => key(series) === legs[index]);
118
150
  if (charted) return charted;
119
151
  const appended = appendChartSeries(next, leg.series);
120
- const placed = placeFormulaLeg(formula, index, appended.series);
152
+ const placed = index < formula.legs.length
153
+ ? placeFormulaLeg(formula, index, appended.series)
154
+ : placeFormulaFactor(next, appended.series);
155
+ if (index >= formula.legs.length) placedFactors.push(placed);
121
156
  next = { ...appended.spec, series: appended.spec.series.map((series) => series.id === placed.id ? placed : series) };
122
157
  return placed;
123
158
  });
@@ -127,7 +162,10 @@ function appendChartFormula(spec: ChartSpec, expression: string): { spec: ChartS
127
162
  spec: {
128
163
  ...next,
129
164
  studies,
130
- panels: [...next.panels, ...panelsForSeries(next.series, studies).filter((panel) => !known.has(panel.id))],
165
+ panels: withFactorPanels(
166
+ [...next.panels, ...panelsForSeries(next.series, studies).filter((panel) => !known.has(panel.id))],
167
+ placedFactors,
168
+ ),
131
169
  },
132
170
  series: inputs.at(-1)!,
133
171
  };
@@ -275,11 +313,8 @@ export function buildValuationChartPreset(
275
313
  const series = buildSeriesSpec(
276
314
  { kind: "security", ...instrument, fieldId: resolvedField },
277
315
  index,
278
- { style: normalized.length === 1 ? "line" : "columns", axis: "left" },
316
+ { style: "line", axis: "left" },
279
317
  );
280
- // Fiscal years: most US filers report no Q4 EPS, which charts do not
281
- // reconstruct, so a quarterly trailing multiple has only its latest few
282
- // quarters. Quarterly is the series' Period control away.
283
- return series.source.kind === "security" ? { ...series, source: { ...series.source, period: "annual" } } : series;
318
+ return series.source.kind === "security" ? { ...series, source: { ...series.source, period: "daily" } } : series;
284
319
  }));
285
320
  }