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
@@ -1,6 +1,8 @@
1
1
  import { alignTimeSeries, effectiveTimeSeriesPointTime, scalarPointValue } from "./alignment";
2
2
  import { mergePriceHistoryIntegrity } from "../utils/price-history-integrity";
3
- import { resolveCurrencyUnit } from "../utils/currency-units";
3
+ import { resolveCurrencyUnit, US_CENTS } from "../utils/currency-units";
4
+ import { isCurrencyCode } from "../utils/currency-pair";
5
+ import { stablePriceFractionDigits } from "../market-data/market/format";
4
6
  import { isRealizedVolatilityEstimator, realizedVolatilityCadenceIssue, rollingRealizedVolatility } from "../market-data/realized-volatility";
5
7
  import { latestTradingSessionOpen } from "../market-data/market/trading-sessions";
6
8
  import { CHART_RESOLUTION_STEP_MS, isIntradayResolution, type ManualChartResolution } from "./resolution";
@@ -19,7 +21,16 @@ import type {
19
21
  import { isFiniteNumber } from "../utils/guards";
20
22
  import { studyMessageName } from "./series-name";
21
23
  import { dailyLeverageGrowth, isLeverageFactor, leverageDecayPoints, leverageFactorLabel, leverageSymbol, sessionCloses, type StudyAnchor } from "./leverage";
22
- import { GALLONS_PER_BARREL, isPerBarrelUnit, isPerGallonUnit, perBarrelUnit, quantityIdentity, unitCurrencyToken } from "./price-units";
24
+ import {
25
+ GALLONS_PER_BARREL,
26
+ isBarrelsPerTonne,
27
+ isPerBarrelUnit,
28
+ isPerGallonUnit,
29
+ perBarrelUnit,
30
+ perTonneUnit,
31
+ quantityIdentity,
32
+ unitCurrencyToken,
33
+ } from "./price-units";
23
34
 
24
35
  export interface StudyResolutionResult {
25
36
  series: ResolvedSeries[];
@@ -104,6 +115,8 @@ function outputSeries(
104
115
  statedUnit?: string | null;
105
116
  /** The kind of price the output is, for its decimals; by default the input's, while the output keeps its unit. */
106
117
  priceAssetCategory?: string | null;
118
+ /** The decimals a value worked out from prices prints at. */
119
+ valueDecimals?: number;
107
120
  },
108
121
  ): ResolvedSeries {
109
122
  const historyResolution = options.historyResolution === undefined ? input.historyResolution : options.historyResolution;
@@ -116,6 +129,7 @@ function outputSeries(
116
129
  unit: options.unit ?? input.unit,
117
130
  unitGroup: options.unitGroup ?? input.unitGroup,
118
131
  ...(statedUnit ? { statedUnit } : {}),
132
+ ...(options.valueDecimals !== undefined ? { valueDecimals: options.valueDecimals } : {}),
119
133
  priceAssetCategory: options.priceAssetCategory === undefined
120
134
  ? sameUnit ? input.priceAssetCategory : undefined
121
135
  : options.priceAssetCategory ?? undefined,
@@ -386,15 +400,21 @@ function resolveVolume(spec: ChartStudySpec, input: ResolvedSeries, color: strin
386
400
  // No bar has volume to plot (spot FX quotes 0 for every bar; a coin's window
387
401
  // can miss every bar's), so the study adds no panel rather than an empty one.
388
402
  if (input.points.length > 0 && !input.points.some((point) => isFiniteNumber(point.volume) && point.volume !== 0)) return [];
403
+ // Left to its own colour, volume follows the candles: the price's colour for
404
+ // a bar that rose, the falling colour for one that fell. A colour picked for it stays flat.
405
+ const directional = !spec.color;
389
406
  const points = [...input.points]
390
407
  .sort((left, right) => left.date.getTime() - right.date.getTime())
391
408
  .flatMap((point) => isFiniteNumber(point.volume)
392
- ? [derivedPoint({ point, value: point.volume }, point.volume)]
409
+ ? [{
410
+ ...derivedPoint({ point, value: point.volume }, point.volume),
411
+ ...(directional && isFiniteNumber(point.open) && isFiniteNumber(point.close) && point.close < point.open ? { fell: true } : {}),
412
+ }]
393
413
  : []);
394
414
  return [outputSeries(spec, input, {
395
415
  label: nameInput ? `Volume ${input.label}` : "Volume",
396
416
  points,
397
- color,
417
+ color: directional ? input.color : color,
398
418
  unit: input.volumeUnit ?? "",
399
419
  unitGroup: "volume",
400
420
  style: "columns",
@@ -716,13 +736,20 @@ function seriesCurrency(series: ResolvedSeries): string | null {
716
736
  function unitFactorIdentity(value: string): string {
717
737
  // "US cents", as futures terms word a grain's quote, is the USX the quote names.
718
738
  const token = unitCurrencyToken(value) ?? value.trim();
719
- if (/^[A-Za-z]{3}$/.test(token)) {
739
+ // Three letters are a currency unless they spell a quantity: "bbl" is a barrel.
740
+ if (/^[A-Za-z]{3}$/.test(token) && quantityIdentity(token) === token.toLowerCase()) {
720
741
  const { currency, divisor } = resolveCurrencyUnit(token);
721
742
  return `${currency}:${divisor}`;
722
743
  }
723
744
  return quantityIdentity(token);
724
745
  }
725
746
 
747
+ /** Whether a unit's factor is money (USD, GBp, US cents) rather than a quantity it buys. */
748
+ function isCurrencyFactor(value: string): boolean {
749
+ const currency = /^([A-Z]{3}):\d+$/.exec(unitFactorIdentity(value))?.[1];
750
+ return !!currency && (currency === US_CENTS || isCurrencyCode(currency));
751
+ }
752
+
726
753
  function knownUnitSignature(series: Pick<ResolvedSeries, "unit" | "unitGroup">): string | null {
727
754
  const unit = series.unit.trim();
728
755
  if (!unit || /(?:^|\W)(?:currency|unknown|unspecified|n\/a)(?:$|\W)/i.test(unit)
@@ -794,28 +821,41 @@ function unitMismatchMessage(
794
821
 
795
822
  function unitFactors(unit: string): { numerator: string[]; denominator: string[] } {
796
823
  const [numerator = "", ...denominator] = unit.split("/");
797
- const factor = (value: string) => {
798
- const normalized = value.trim();
824
+ const factor = (value: string) => value.split("·").flatMap((part) => {
825
+ const normalized = part.trim();
799
826
  if (!normalized || normalized === "1") return [];
800
827
  return [normalized.toLowerCase() === "shares" ? "share" : normalized];
801
- };
828
+ });
802
829
  return {
803
830
  numerator: factor(numerator),
804
831
  denominator: denominator.flatMap(factor),
805
832
  };
806
833
  }
807
834
 
808
- function ratioUnit(left: ResolvedSeries, right: ResolvedSeries): {
835
+ interface SeriesUnit {
809
836
  unit: string;
810
837
  unitGroup: string;
811
- } {
812
- if (!knownUnitSignature(left) || !knownUnitSignature(right)) {
813
- return { unit: "unknown", unitGroup: "derived-unit:unknown" };
814
- }
838
+ }
839
+
840
+ const UNKNOWN_UNIT: SeriesUnit = { unit: "unknown", unitGroup: "derived-unit:unknown" };
841
+
842
+ function isPriceGroup(unitGroup: string): boolean {
843
+ return /^price(?::|$)/.test(unitGroup);
844
+ }
845
+
846
+ /**
847
+ * The unit of `left` times (`power` 1) or over (-1) `right`, the factors they
848
+ * share cancelling: USD / barrel over an FX rate's USD / EUR is EUR / barrel.
849
+ * Units are spaced as either side writes them ("USD / gallon"). A price over
850
+ * or times a price that leaves one currency per one quantity is a price in
851
+ * that currency; anything else is a derived unit, or a plain ratio.
852
+ */
853
+ function combinedUnit(left: SeriesUnit, right: SeriesUnit, power: 1 | -1): SeriesUnit {
854
+ if (!knownUnitSignature(left) || !knownUnitSignature(right)) return UNKNOWN_UNIT;
815
855
  const leftFactors = unitFactors(left.unit);
816
856
  const rightFactors = unitFactors(right.unit);
817
- const numerator = [...leftFactors.numerator, ...rightFactors.denominator];
818
- const denominator = [...leftFactors.denominator, ...rightFactors.numerator];
857
+ const numerator = [...leftFactors.numerator, ...(power < 0 ? rightFactors.denominator : rightFactors.numerator)];
858
+ const denominator = [...leftFactors.denominator, ...(power < 0 ? rightFactors.numerator : rightFactors.denominator)];
819
859
  for (let index = numerator.length - 1; index >= 0; index -= 1) {
820
860
  const match = denominator.findIndex((factor) => (
821
861
  unitFactorIdentity(factor) === unitFactorIdentity(numerator[index]!)
@@ -824,9 +864,14 @@ function ratioUnit(left: ResolvedSeries, right: ResolvedSeries): {
824
864
  numerator.splice(index, 1);
825
865
  denominator.splice(match, 1);
826
866
  }
867
+ const slash = [left.unit, right.unit].some((unit) => /\s\/\s/.test(unit)) ? " / " : "/";
827
868
  const unit = denominator.length === 0
828
869
  ? numerator.join("·") || "x"
829
- : `${numerator.join("·") || "1"}/${denominator.join("·")}`;
870
+ : `${numerator.join("·") || "1"}${slash}${denominator.join("·")}`;
871
+ if (isPriceGroup(left.unitGroup) && isPriceGroup(right.unitGroup) && numerator.length === 1 && isCurrencyFactor(numerator[0]!)
872
+ && (denominator.length === 0 || (denominator.length === 1 && !isCurrencyFactor(denominator[0]!)))) {
873
+ return { unit, unitGroup: `price:${unitCurrencyToken(numerator[0]!) ?? numerator[0]!}` };
874
+ }
830
875
  const groupFactor = (value: string) => {
831
876
  const identity = unitFactorIdentity(value);
832
877
  return (/^[A-Za-z]{3}$/.test(value.trim()) ? identity.replace(/:1$/, "") : identity).toLowerCase();
@@ -840,6 +885,78 @@ function ratioUnit(left: ResolvedSeries, right: ResolvedSeries): {
840
885
  };
841
886
  }
842
887
 
888
+ function ratioUnit(left: SeriesUnit, right: SeriesUnit): SeriesUnit {
889
+ return combinedUnit(left, right, -1);
890
+ }
891
+
892
+ /** An FX rate's unit: one currency per another, as USD / EUR. */
893
+ function isRateUnit(unit: string): boolean {
894
+ const { numerator, denominator } = unitFactors(unit);
895
+ return numerator.length === 1 && denominator.length === 1 && isCurrencyFactor(numerator[0]!) && isCurrencyFactor(denominator[0]!);
896
+ }
897
+
898
+ /**
899
+ * Why a price times (`power` 1) or over (-1) an FX rate is no price, with how
900
+ * to convert it instead: a crack in USD / barrel over USDJPY=X (JPY / USD) is
901
+ * USD·USD / barrel·JPY, and times it JPY / barrel. A ratio, which cannot
902
+ * multiply, names the formula to write (`priceName` its numerator). Null when
903
+ * the rate converts the price, or either is not what this asks about.
904
+ */
905
+ function fxConversionIssue(price: SeriesUnit, rate: ResolvedSeries, power: 1 | -1, result: SeriesUnit, priceName?: string): string | null {
906
+ if (!isPriceGroup(price.unitGroup) || !isPriceGroup(rate.unitGroup) || !isRateUnit(rate.unit) || isPriceGroup(result.unitGroup)) {
907
+ return null;
908
+ }
909
+ const name = legName(rate);
910
+ const done = `${power < 0 ? "Dividing" : "Multiplying"} by ${name} (${rate.unit}) leaves ${result.unit}`;
911
+ const other = combinedUnit(price, rate, power < 0 ? 1 : -1);
912
+ if (isPriceGroup(other.unitGroup)) {
913
+ const fix = priceName ? `write (${priceName})${power < 0 ? "*" : "/"}${name}` : `${power < 0 ? "multiply" : "divide"} by ${name}`;
914
+ return `${done}; ${fix} instead to get ${other.unit}.`;
915
+ }
916
+ const currency = price.unitGroup.slice("price:".length) || "its currency";
917
+ return `${done}; ${price.unit} converts only with a rate that has ${currency} on one side.`;
918
+ }
919
+
920
+ /** The distinct currencies a unit still counts, once its factors cancel. */
921
+ function unitCurrencies(unit: string): Set<string> {
922
+ const { numerator, denominator } = unitFactors(unit);
923
+ return new Set([...numerator, ...denominator].filter(isCurrencyFactor).map((factor) => unitFactorIdentity(factor).split(":")[0]!));
924
+ }
925
+
926
+ /**
927
+ * The decimals a value worked out from prices prints at: the most its legs
928
+ * print, from two to four, and no more than a value of its own size needs:
929
+ * four under 10, two from 10. A crack of 4.9842 USD / gallon heating oil and
930
+ * 116.72 USD / barrel Brent reads 92.62; a gasoline-heating oil spread reads
931
+ * 1.7726. Undefined when a leg is not a price or the value has none yet.
932
+ */
933
+ function derivedValueDecimals(inputs: readonly ResolvedSeries[], points: readonly TimeSeriesPoint[]): number | undefined {
934
+ let digits = 0;
935
+ for (const input of inputs) {
936
+ if (!isPriceGroup(input.unitGroup) || input.transform !== "raw") return undefined;
937
+ const reference = latestScalarValue(input.points);
938
+ if (reference === undefined) return undefined;
939
+ digits = Math.max(digits, stablePriceFractionDigits({
940
+ assetCategory: input.priceAssetCategory,
941
+ currency: unitCurrencyToken(input.unit.split("/")[0] ?? "") ?? undefined,
942
+ referencePrice: reference,
943
+ sessionPrices: [reference],
944
+ }));
945
+ }
946
+ const size = latestScalarValue(points);
947
+ if (size === undefined) return undefined;
948
+ return Math.min(size < 10 ? 4 : 2, Math.max(2, digits));
949
+ }
950
+
951
+ /** The size of a series' last nonzero value, which its decimals are read from. */
952
+ function latestScalarValue(points: readonly TimeSeriesPoint[]): number | undefined {
953
+ for (let index = points.length - 1; index >= 0; index -= 1) {
954
+ const value = scalarPointValue(points[index]!);
955
+ if (value !== null && Number.isFinite(value) && value !== 0) return Math.abs(value);
956
+ }
957
+ return undefined;
958
+ }
959
+
843
960
  function pairedSamples(left: ResolvedSeries, right: ResolvedSeries, carryForward: boolean): PairedSample[] {
844
961
  return alignTimeSeries([left, right], {
845
962
  mode: "intersection",
@@ -897,7 +1014,13 @@ function resolvePairStudy(
897
1014
  const outputUnit = ratioStudy
898
1015
  ? ratioUnit(left, right)
899
1016
  : { unit: left.unit, unitGroup: left.unitGroup };
1017
+ // A price over an FX rate is a price in the other currency (EUR / barrel),
1018
+ // stated as a future's unit is; a plain ratio keeps its own decimals.
1019
+ const convertedPrice = ratioStudy && isPriceGroup(outputUnit.unitGroup);
1020
+ const valueDecimals = !ratioStudy || convertedPrice ? derivedValueDecimals([left, right], points) : undefined;
900
1021
  return [outputSeries(spec, left, {
1022
+ ...(convertedPrice ? { statedUnit: outputUnit.unit, priceAssetCategory: null } : {}),
1023
+ ...(valueDecimals !== undefined ? { valueDecimals } : {}),
901
1024
  label: ratioStudy
902
1025
  ? `${left.label} / ${right.label}`
903
1026
  : `${left.label} - ${multiplier === 1 ? "" : `${multiplier}×`}${right.label}`,
@@ -961,57 +1084,91 @@ function resolvePairStudy(
961
1084
  }
962
1085
 
963
1086
  /** Formulas of levels, which carry each input's latest value to the other inputs' dates. */
1087
+ /** The study's own colour, or the next after it no series or formula on the chart has taken. */
1088
+ function freeStudyColor(index: number, used: ReadonlySet<string>): string {
1089
+ for (let offset = 0; offset < STUDY_COLORS.length; offset += 1) {
1090
+ const color = STUDY_COLORS[(index + offset) % STUDY_COLORS.length]!;
1091
+ if (!used.has(color.toLowerCase())) return color;
1092
+ }
1093
+ return STUDY_COLORS[index % STUDY_COLORS.length]!;
1094
+ }
1095
+
964
1096
  function isLevelFormula(kind: ChartStudyKind): boolean {
965
1097
  return kind === "ratio" || kind === "spread" || kind === "formula";
966
1098
  }
967
1099
 
1100
+ interface FormulaLeg {
1101
+ /** The input's place in the study's inputs. */
1102
+ index: number;
1103
+ /** Its multiplier, its sign included. */
1104
+ weight: number;
1105
+ /** The multiplier carries the 42 gallons that make a per-gallon price per barrel. */
1106
+ perBarrel: boolean;
1107
+ /** The barrels in a tonne the multiplier carries (7.45 for gasoil), making a per-barrel price per tonne. */
1108
+ perTonne: number | null;
1109
+ }
1110
+
968
1111
  interface FormulaTerms {
969
- /** Each input's multiplier, in input order. */
970
- weights: number[];
971
- /** Whether an input's multiplier carries the 42 gallons that make its per-gallon price per barrel. */
972
- perBarrel: boolean[];
1112
+ /** The inputs the formula adds up. */
1113
+ legs: FormulaLeg[];
1114
+ /** The inputs the whole sum is multiplied (1) or divided (-1) by: `/EURUSD=X`. */
1115
+ factors: Array<{ index: number; power: 1 | -1 }>;
973
1116
  divisor: number;
974
1117
  }
975
1118
 
976
1119
  /**
977
- * A formula study's terms: `weight1`, `perBarrel1`, ... per input, and an
978
- * optional `divisor`. A saved or hand-edited spec can carry a multiplier or
979
- * divisor no formula was written with (a value that is not a number is
980
- * dropped when the spec loads, so it arrives missing); that is an error, not
981
- * a silent 1.
1120
+ * A formula study's terms: `weight1`, `perBarrel1`, `perTonne1` ... per
1121
+ * input it adds up, `power1` ... (1 or -1) per input it multiplies or divides
1122
+ * the sum by, and an optional `divisor`. A saved or hand-edited spec can carry
1123
+ * a multiplier or divisor no formula was written with (a value that is not a
1124
+ * number is dropped when the spec loads, so it arrives missing); that is an
1125
+ * error, not a silent 1.
982
1126
  */
983
1127
  function formulaTerms(spec: ChartStudySpec, inputs: readonly ResolvedSeries[]): FormulaTerms | { error: string } {
984
- const weights: number[] = [];
1128
+ const legs: FormulaLeg[] = [];
1129
+ const factors: FormulaTerms["factors"] = [];
985
1130
  for (const [index, input] of inputs.entries()) {
1131
+ const power = spec.parameters[`power${index + 1}`];
1132
+ if (power !== undefined) {
1133
+ if (power !== 1 && power !== -1) return { error: `The formula neither multiplies nor divides by ${legName(input)}; write the formula again.` };
1134
+ factors.push({ index, power });
1135
+ continue;
1136
+ }
986
1137
  const weight = spec.parameters[`weight${index + 1}`];
987
1138
  if (!isFiniteNumber(weight)) return { error: `The multiplier of ${legName(input)} is missing or not a number; write the formula again.` };
988
1139
  if (weight === 0) return { error: `${legName(input)} has a multiplier of 0, which removes it; leave it out of the formula instead.` };
989
- weights.push(weight);
1140
+ const perTonne = spec.parameters[`perTonne${index + 1}`];
1141
+ legs.push({
1142
+ index,
1143
+ weight,
1144
+ perBarrel: spec.parameters[`perBarrel${index + 1}`] === 1,
1145
+ perTonne: isFiniteNumber(perTonne) && isBarrelsPerTonne(perTonne) ? perTonne : null,
1146
+ });
990
1147
  }
1148
+ if (legs.length === 0) return { error: "The formula has no series to add up, only series it divides by; write the formula again." };
991
1149
  const divisor = spec.parameters.divisor ?? 1;
992
1150
  if (!isFiniteNumber(divisor)) return { error: "The formula's divisor is not a number; write the formula again." };
993
1151
  if (divisor === 0) return { error: "A formula cannot be divided by 0." };
994
- return {
995
- weights,
996
- perBarrel: inputs.map((_, index) => spec.parameters[`perBarrel${index + 1}`] === 1),
997
- divisor,
998
- };
1152
+ return { legs, factors, divisor };
999
1153
  }
1000
1154
 
1001
- /** Each input's unit once its multiplier is applied: 42 gallons of a per-gallon price are a barrel's worth. */
1002
- function formulaLegUnits(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): string[] {
1003
- return inputs.map((input, index) => terms.perBarrel[index] ? perBarrelUnit(input.unit) : input.unit);
1155
+ /**
1156
+ * A leg's unit once its multiplier is applied: 42 gallons of a per-gallon
1157
+ * price are a barrel's worth, and 7.45 barrels of gasoil a tonne's.
1158
+ */
1159
+ function formulaLegUnit(leg: FormulaLeg, input: ResolvedSeries): string {
1160
+ const unit = leg.perBarrel ? perBarrelUnit(input.unit) : input.unit;
1161
+ return leg.perTonne ? perTonneUnit(unit) : unit;
1004
1162
  }
1005
1163
 
1006
1164
  /** Why a formula's legs cannot be summed, or null when every leg counts the same known unit. */
1007
1165
  function formulaUnitMismatch(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): string | null {
1008
- if (inputs.length < 2) return null;
1009
- const units = formulaLegUnits(terms, inputs);
1010
- const signatures = inputs.map((input, index) => knownUnitSignature({ unit: units[index]!, unitGroup: input.unitGroup }));
1166
+ if (terms.legs.length < 2) return null;
1167
+ const legs = terms.legs.map((leg) => ({ series: inputs[leg.index]!, unit: formulaLegUnit(leg, inputs[leg.index]!) }));
1168
+ const signatures = legs.map(({ series, unit }) => knownUnitSignature({ unit, unitGroup: series.unitGroup }));
1011
1169
  const other = signatures.findIndex((signature, index) => index > 0 && (signature === null || signature !== signatures[0]));
1012
1170
  if (signatures[0] !== null && other < 0) return null;
1013
- const right = Math.max(1, other);
1014
- return unitMismatchMessage("formula", { series: inputs[0]!, unit: units[0]! }, { series: inputs[right]!, unit: units[right]! });
1171
+ return unitMismatchMessage("formula", legs[0]!, legs[Math.max(1, other)]!);
1015
1172
  }
1016
1173
 
1017
1174
  function formatFormulaNumber(value: number): string {
@@ -1020,20 +1177,25 @@ function formatFormulaNumber(value: number): string {
1020
1177
 
1021
1178
  /**
1022
1179
  * A formula as the legend names it, from the terms it was written with:
1023
- * `42×HO=F - CL=F`, `(2×42×RB=F + 42×HO=F - 3×CL=F)/3`.
1180
+ * `42×HO=F - CL=F`, `(2×42×RB=F + 42×HO=F - 3×CL=F)/3`, `(42×HO=F - BZ=F)/EURUSD=X`.
1024
1181
  */
1025
1182
  function formulaLabel(inputs: readonly ResolvedSeries[], terms: FormulaTerms): string {
1026
- const sum = inputs.map((input, index) => {
1027
- const weight = terms.weights[index]!;
1028
- const magnitude = Math.abs(weight);
1029
- const factors = terms.perBarrel[index]
1030
- ? [...(magnitude === GALLONS_PER_BARREL ? [] : [formatFormulaNumber(magnitude / GALLONS_PER_BARREL)]), String(GALLONS_PER_BARREL)]
1031
- : magnitude === 1 ? [] : [formatFormulaNumber(magnitude)];
1032
- const term = [...factors, legName(input)].join("×");
1033
- if (index === 0) return weight < 0 ? `-${term}` : term;
1034
- return `${weight < 0 ? "-" : "+"} ${term}`;
1183
+ const sum = terms.legs.map((leg, position) => {
1184
+ const magnitude = Math.abs(leg.weight);
1185
+ // The conversions it carries are named as written: 7.45×42×HO=F.
1186
+ const conversions = [...(leg.perTonne ? [leg.perTonne] : []), ...(leg.perBarrel ? [GALLONS_PER_BARREL] : [])];
1187
+ const count = conversions.reduce((rest, factor) => rest / factor, magnitude);
1188
+ const factors = [...(Math.abs(count - 1) < 1e-9 ? [] : [formatFormulaNumber(count)]), ...conversions.map(String)];
1189
+ const term = [...factors, legName(inputs[leg.index]!)].join("×");
1190
+ if (position === 0) return leg.weight < 0 ? `-${term}` : term;
1191
+ return `${leg.weight < 0 ? "-" : "+"} ${term}`;
1035
1192
  }).join(" ");
1036
- return terms.divisor === 1 ? sum : `(${sum})/${formatFormulaNumber(terms.divisor)}`;
1193
+ if (terms.divisor === 1 && terms.factors.length === 0) return sum;
1194
+ return [
1195
+ `(${sum})`,
1196
+ ...(terms.divisor === 1 ? [] : [`/${formatFormulaNumber(terms.divisor)}`]),
1197
+ ...terms.factors.map(({ index, power }) => `${power < 0 ? "/" : "×"}${legName(inputs[index]!)}`),
1198
+ ].join("");
1037
1199
  }
1038
1200
 
1039
1201
  /** Every input's value on each date they share, each carried forward to the others' dates. */
@@ -1055,31 +1217,67 @@ function alignedSamples(inputs: readonly ResolvedSeries[]): Array<{ point: TimeS
1055
1217
  });
1056
1218
  }
1057
1219
 
1058
- /** A weighted sum of the inputs over a divisor, in the unit the legs share once converted. */
1220
+ /**
1221
+ * A weighted sum of the legs over a divisor, in the unit the legs share once
1222
+ * converted, then times or over the series it is scaled by, in the unit
1223
+ * that arithmetic leaves: a crack in USD / barrel over EURUSD=X is EUR / barrel.
1224
+ */
1225
+ /**
1226
+ * A formula's unit: its legs' once converted, then times or over each series
1227
+ * it is scaled by. Legs that count different units, or an FX rate the wrong
1228
+ * way up, are an error that says why: a crack over USDJPY=X is no price, and
1229
+ * times it JPY / barrel.
1230
+ */
1231
+ function formulaUnit(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): SeriesUnit | { error: string } {
1232
+ const mismatch = formulaUnitMismatch(terms, inputs);
1233
+ if (mismatch) return { error: mismatch };
1234
+ const first = inputs[terms.legs[0]!.index]!;
1235
+ let unit: SeriesUnit = { unit: formulaLegUnit(terms.legs[0]!, first), unitGroup: first.unitGroup };
1236
+ for (const factor of terms.factors) {
1237
+ const series = inputs[factor.index]!;
1238
+ const next = combinedUnit(unit, series, factor.power);
1239
+ const issue = fxConversionIssue(unit, series, factor.power, next);
1240
+ if (issue) return { error: issue };
1241
+ unit = next;
1242
+ }
1243
+ return unit;
1244
+ }
1245
+
1059
1246
  function resolveFormula(
1060
1247
  spec: ChartStudySpec,
1061
1248
  inputs: readonly ResolvedSeries[],
1062
1249
  terms: FormulaTerms,
1250
+ unit: SeriesUnit,
1063
1251
  color: string,
1064
1252
  ): ResolvedSeries[] {
1065
- const first = inputs[0]!;
1066
- const unit = formulaLegUnits(terms, inputs)[0]!;
1253
+ const first = inputs[terms.legs[0]!.index]!;
1067
1254
  // Rounded off the float's last digits: 42 × 4.6233 - 96.12 is 98.0586, not 98.05860000000001.
1068
- const points = alignedSamples(inputs).map(({ point, values }) => derivedPoint(
1069
- { point, value: 0 },
1070
- Number((values.reduce((sum, value, index) => sum + value * terms.weights[index]!, 0) / terms.divisor).toPrecision(12)),
1071
- ));
1255
+ const points = alignedSamples(inputs).map(({ point, values }) => {
1256
+ let value: number | null = terms.legs.reduce((sum, leg) => sum + values[leg.index]! * leg.weight, 0) / terms.divisor;
1257
+ for (const factor of terms.factors) {
1258
+ const scale = values[factor.index]!;
1259
+ value = value === null || (factor.power < 0 && scale === 0) ? null : factor.power < 0 ? value / scale : value * scale;
1260
+ }
1261
+ return derivedPoint({ point, value: 0 }, value === null ? null : Number(value.toPrecision(12)));
1262
+ });
1072
1263
  const frequencies = new Set(inputs.map((input) => input.nativeFrequency));
1073
1264
  const resolutions = new Set(inputs.map((input) => input.historyResolution));
1265
+ const scaled = terms.factors.length > 0;
1266
+ // A price per quantity reads with its unit beside it, as a future's does.
1267
+ const statedUnit = scaled
1268
+ ? isPriceGroup(unit.unitGroup) && unit.unit.includes("/") ? unit.unit : null
1269
+ : inputs.some((input) => input.statedUnit) ? unit.unit : null;
1270
+ const valueDecimals = !scaled || isPriceGroup(unit.unitGroup) ? derivedValueDecimals(inputs, points) : undefined;
1074
1271
  return [outputSeries(spec, first, {
1075
1272
  label: formulaLabel(inputs, terms),
1076
1273
  points,
1077
1274
  color,
1078
- unit,
1079
- unitGroup: first.unitGroup,
1080
- statedUnit: inputs.some((input) => input.statedUnit) ? unit : null,
1081
- // A weighted sum is no traded price: its decimals are a value's, not a contract's tick.
1275
+ unit: unit.unit,
1276
+ unitGroup: unit.unitGroup,
1277
+ statedUnit,
1278
+ // A weighted sum is no traded price: its decimals are its legs', not a contract's tick.
1082
1279
  priceAssetCategory: null,
1280
+ ...(valueDecimals !== undefined ? { valueDecimals } : {}),
1083
1281
  // As the pair formulas: as-of levels, constant until an input changes.
1084
1282
  style: "step",
1085
1283
  interpolation: "step-after",
@@ -1285,6 +1483,8 @@ function resolveStudySpecs(
1285
1483
  anchors?: ReadonlyMap<string, StudyAnchor>,
1286
1484
  ): StudyResolutionResult {
1287
1485
  const byId = new Map(baseSeries.map((series) => [series.id, series]));
1486
+ // A formula never takes a colour a series on the chart has: a crack in euros is not drawn in its EURUSD leg's yellow.
1487
+ const usedColors = new Set(baseSeries.map((series) => series.color.toLowerCase()));
1288
1488
  const resolved: ResolvedSeries[] = [];
1289
1489
  const warnings: string[] = [];
1290
1490
  const errors: string[] = [];
@@ -1307,17 +1507,19 @@ function resolveStudySpecs(
1307
1507
  return;
1308
1508
  }
1309
1509
  }
1310
- let terms: FormulaTerms | null = null;
1510
+ let formula: { terms: FormulaTerms; unit: SeriesUnit } | null = null;
1311
1511
  if (spec.kind === "formula") {
1312
- const read = formulaTerms(spec, inputs as ResolvedSeries[]);
1313
- const mismatch = "error" in read ? read.error : formulaUnitMismatch(read, inputs as ResolvedSeries[]);
1314
- if (mismatch) {
1315
- errors.push(`${studyMessageName(spec)}: ${mismatch}`);
1512
+ const terms = formulaTerms(spec, inputs as ResolvedSeries[]);
1513
+ const unit = "error" in terms ? terms : formulaUnit(terms, inputs as ResolvedSeries[]);
1514
+ if ("error" in unit) {
1515
+ errors.push(`${studyMessageName(spec)}: ${unit.error}`);
1316
1516
  return;
1317
1517
  }
1318
- terms = read as FormulaTerms;
1518
+ formula = { terms: terms as FormulaTerms, unit };
1319
1519
  }
1320
- const color = spec.color ?? KIND_COLORS[spec.kind] ?? STUDY_COLORS[index % STUDY_COLORS.length]!;
1520
+ const color = spec.color ?? KIND_COLORS[spec.kind]
1521
+ ?? (isLevelFormula(spec.kind) ? freeStudyColor(index, usedColors) : STUDY_COLORS[index % STUDY_COLORS.length]!);
1522
+ if (isLevelFormula(spec.kind)) usedColors.add(color.toLowerCase());
1321
1523
  if (spec.kind === "leverage" || spec.kind === "leverage-decay") {
1322
1524
  const index = inputs.at(-1)!;
1323
1525
  if (!isLeverageFactor(spec.parameters.factor)) {
@@ -1388,7 +1590,7 @@ function resolveStudySpecs(
1388
1590
  resolved.push(...outputs);
1389
1591
  return;
1390
1592
  }
1391
- else if (spec.kind === "formula" && terms) outputs = resolveFormula(spec, inputs as ResolvedSeries[], terms, color);
1593
+ else if (spec.kind === "formula" && formula) outputs = resolveFormula(spec, inputs as ResolvedSeries[], formula.terms, formula.unit, color);
1392
1594
  else if (spec.kind === "volume") {
1393
1595
  outputs = resolveVolume(spec, input, color, nameVolumeInput);
1394
1596
  if (!input.volumeUnit && outputs.some((output) => output.points.length > 0)) {
@@ -1402,10 +1604,12 @@ function resolveStudySpecs(
1402
1604
  const differentCurrencies = inputCurrency !== null
1403
1605
  && pairedCurrency !== null
1404
1606
  && inputCurrency !== pairedCurrency;
1405
- if (spec.kind === "ratio" && differentCurrencies) {
1406
- warnings.push(
1407
- `${studyMessageName(spec)}: ${spec.kind} inputs use different currencies (${inputCurrency} and ${pairedCurrency}); raw values are not FX-converted.`,
1408
- );
1607
+ // Over an FX rate a price is converted, leaving one currency: EUR / barrel.
1608
+ const ratio = spec.kind === "ratio" ? ratioUnit(input, pairedInput) : null;
1609
+ if (ratio && differentCurrencies && unitCurrencies(ratio.unit).size !== 1) {
1610
+ const issue = fxConversionIssue(input, pairedInput, -1, ratio, legName(input));
1611
+ warnings.push(`${studyMessageName(spec)}: ${issue
1612
+ ?? `${spec.kind} inputs use different currencies (${inputCurrency} and ${pairedCurrency}); raw values are not FX-converted.`}`);
1409
1613
  }
1410
1614
  if (spec.kind === "correlation" && input.nativeFrequency !== pairedInput.nativeFrequency) {
1411
1615
  warnings.push(`${studyMessageName(spec)}: correlation mixes ${input.nativeFrequency} and ${pairedInput.nativeFrequency} observations; only matching observation times contribute.`);