gloomberb 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,4 +1,5 @@
1
1
  import { logFailure } from "../utils/log-failure";
2
+ import { servedFromSavedCopy } from "../data/saved-copies";
2
3
  import { hasValidQuoteObservationTime } from "../market-data/quotes/freshness";
3
4
  import { delistingLabel } from "../market-data/quotes/delisting";
4
5
  import { financialPeriodCoverage, financialPeriodCoverageWarnings, limitSeriesObservations } from "./financial-period-coverage";
@@ -42,7 +43,7 @@ import { FORWARD_PE_BASIS_NOTICE, REALIZED_NTM_PE_BASIS_NOTICE } from "./forward
42
43
  import { valuationPriceWarning } from "./valuation-price";
43
44
  import { VALUATION_CURRENCY_WARNING_PREFIX } from "./valuation-currency";
44
45
  import { chartPriceHistoryIntegrityNotices } from "./market";
45
- import { maxStudyWarmupPoints, resolveStudies } from "./studies";
46
+ import { maxStudyWarmupPoints, resolveStudies } from "./studies/resolve";
46
47
  import { clipSeriesToWindow } from "./alignment";
47
48
  import { totalReturnSeries } from "./total-return";
48
49
  import {
@@ -54,6 +55,7 @@ import {
54
55
  totalReturnLevelsNotice,
55
56
  } from "./price-comparison";
56
57
  import { chartQuoteOverrideKeyForSource } from "./live-quotes";
58
+ import { coinLegsAtSessionClose } from "./coin-session-close";
57
59
  import { chartSeriesSourceKey } from "../capabilities/chart-series";
58
60
  import { rememberSeedListingFacts } from "./parsed-history-cache";
59
61
  import {
@@ -827,7 +829,7 @@ async function resolveChartSpecPass(
827
829
  cache.capabilitySeriesByRequest.set(key, pending);
828
830
  cache.revision += 1;
829
831
  }
830
- return baseCapabilitySeries(seriesSpec, await pending, index);
832
+ return asSavedCopy(baseCapabilitySeries(seriesSpec, await pending, index));
831
833
  }
832
834
  if (seriesSpec.source.kind === "economic") {
833
835
  const request: FredSeriesRequest = {
@@ -845,6 +847,8 @@ async function resolveChartSpecPass(
845
847
  priorityWarnings.push(`${result.label}: ${coverageNotice}`);
846
848
  coverageNotices.push(`${result.label}: ${coverageNotice}`);
847
849
  }
850
+ // A saved copy that answered for a failed refresh is dated in the footer, as every pane's is.
851
+ if (result && fred.source === "stale-fallback") return asSavedCopy({ ...result, savedAt: fred.fetchedAt });
848
852
  const freshnessWarning = staleFredWarning(fred);
849
853
  if (result && freshnessWarning) priorityWarnings.push(`${result.label}: ${freshnessWarning}`);
850
854
  return result;
@@ -1054,8 +1058,16 @@ async function resolveChartSpecPass(
1054
1058
  const commonCadence = servedCadences.size === 1 ? [...servedCadences][0] : undefined;
1055
1059
  const resolution = commonCadence ?? undefined;
1056
1060
  const comparisonResolution = resolution ?? "auto";
1057
- const priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow,
1058
- { preset: hasExplicitWindow ? null : spec.viewport.range, now: referenceNow.getTime() + Math.max(0, Date.now() - resolutionStartedAt) });
1061
+ const comparisonOptions = { preset: hasExplicitWindow ? null : spec.viewport.range, now: referenceNow.getTime() + Math.max(0, Date.now() - resolutionStartedAt) };
1062
+ let priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow, comparisonOptions);
1063
+ // A coin compared in daily closes with a listing that closes each day is measured at that close when its hourly bars reach.
1064
+ const timed = sources.dataProvider && comparisonResolution === "1d"
1065
+ ? await coinLegsAtSessionClose(priceComparison, rawSeries, sources.dataProvider, comparisonOptions.now) : null;
1066
+ if (timed?.timing.atSessionClose.size) {
1067
+ rawSeries.splice(0, rawSeries.length, ...timed.series);
1068
+ priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow,
1069
+ { ...comparisonOptions, atSessionClose: timed.timing.atSessionClose, closedEnds: timed.timing.closedEnds });
1070
+ }
1059
1071
  const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
1060
1072
  const comparisonBounds = priceComparison && priceComparison.start !== null
1061
1073
  ? priceComparison : bounds;
@@ -1180,6 +1192,7 @@ async function resolveChartSpecPass(
1180
1192
  loading: false,
1181
1193
  errors,
1182
1194
  warnings: [...new Set([...priorityWarnings, ...warnings])],
1195
+ ...savedCopyOf(rawSeries.filter((entry) => visibleSeriesIds.has(entry.id))),
1183
1196
  ...(coverageNotices.length ? { coverageNotices: [...new Set(coverageNotices)] } : {}),
1184
1197
  ...(returnBasisNotice ? { returnBasisNotice } : {}),
1185
1198
  ...(delistings.size ? { delistings: [...delistings.values()] } : {}),
@@ -1188,3 +1201,18 @@ async function resolveChartSpecPass(
1188
1201
  ...(builtBarSize && rawSeries.some((entry) => entry.barSize === builtBarSize) ? { barSize: builtBarSize } : {}),
1189
1202
  };
1190
1203
  }
1204
+
1205
+ /**
1206
+ * A series drawn from a copy a failed refresh left answering (`savedAt`),
1207
+ * marked as one, so a command charting it ends with "couldn't refresh; saved
1208
+ * copy from …" as the app's footer says.
1209
+ */
1210
+ function asSavedCopy(series: ResolvedSeries): ResolvedSeries {
1211
+ return series.savedAt != null ? servedFromSavedCopy(series, series.savedAt) : series;
1212
+ }
1213
+
1214
+ /** The oldest saved copy among `series` that answered for a failed refresh, which the chart's footer dates. */
1215
+ function savedCopyOf(series: readonly ResolvedSeries[]): { savedAt?: number } {
1216
+ const saved = series.flatMap((entry) => entry.savedAt != null && Number.isFinite(entry.savedAt) ? [entry.savedAt] : []);
1217
+ return saved.length ? { savedAt: Math.min(...saved) } : {};
1218
+ }
@@ -2,7 +2,7 @@ import { limitSeriesObservations } from "./financial-period-coverage";
2
2
  import type { ManualChartResolution } from "./resolution";
3
3
  import { getTimeSeriesField } from "./field-catalog";
4
4
  import { collectPriceHistoryIntegrity } from "./market";
5
- import { activeStudyInputSeriesIds } from "./studies";
5
+ import { activeStudyInputSeriesIds } from "./studies/resolve";
6
6
  import { applyResolvedSeriesTransform } from "./transforms";
7
7
  import { clipSeriesToWindow } from "./alignment";
8
8
  import type { StudyAnchor } from "./leverage";
@@ -0,0 +1,205 @@
1
+ import { alignTimeSeries } from "../alignment";
2
+ import type { ChartStudySpec, ResolvedSeries, TimeSeriesPoint } from "../types";
3
+ import { isFiniteNumber } from "../../utils/guards";
4
+ import { GALLONS_PER_BARREL, isBarrelsPerTonne, perBarrelUnit, perTonneUnit } from "../price-units";
5
+ import { derivedPoint, outputSeries } from "./shared";
6
+ import {
7
+ combinedUnit,
8
+ derivedValueDecimals,
9
+ fxConversionIssue,
10
+ isPriceGroup,
11
+ knownUnitSignature,
12
+ legName,
13
+ unitMismatchMessage,
14
+ type SeriesUnit,
15
+ } from "./units";
16
+
17
+ interface FormulaLeg {
18
+ /** The input's place in the study's inputs. */
19
+ index: number;
20
+ /** Its multiplier, its sign included. */
21
+ weight: number;
22
+ /** The multiplier carries the 42 gallons that make a per-gallon price per barrel. */
23
+ perBarrel: boolean;
24
+ /** The barrels in a tonne the multiplier carries (7.45 for gasoil), making a per-barrel price per tonne. */
25
+ perTonne: number | null;
26
+ }
27
+
28
+ export interface FormulaTerms {
29
+ /** The inputs the formula adds up. */
30
+ legs: FormulaLeg[];
31
+ /** The inputs the whole sum is multiplied (1) or divided (-1) by: `/EURUSD=X`. */
32
+ factors: Array<{ index: number; power: 1 | -1 }>;
33
+ divisor: number;
34
+ }
35
+
36
+ /**
37
+ * A formula study's terms: `weight1`, `perBarrel1`, `perTonne1` ... per
38
+ * input it adds up, `power1` ... (1 or -1) per input it multiplies or divides
39
+ * the sum by, and an optional `divisor`. A saved or hand-edited spec can carry
40
+ * a multiplier or divisor no formula was written with (a value that is not a
41
+ * number is dropped when the spec loads, so it arrives missing); that is an
42
+ * error, not a silent 1.
43
+ */
44
+ export function formulaTerms(spec: ChartStudySpec, inputs: readonly ResolvedSeries[]): FormulaTerms | { error: string } {
45
+ const legs: FormulaLeg[] = [];
46
+ const factors: FormulaTerms["factors"] = [];
47
+ for (const [index, input] of inputs.entries()) {
48
+ const power = spec.parameters[`power${index + 1}`];
49
+ if (power !== undefined) {
50
+ if (power !== 1 && power !== -1) return { error: `The formula neither multiplies nor divides by ${legName(input)}; write the formula again.` };
51
+ factors.push({ index, power });
52
+ continue;
53
+ }
54
+ const weight = spec.parameters[`weight${index + 1}`];
55
+ if (!isFiniteNumber(weight)) return { error: `The multiplier of ${legName(input)} is missing or not a number; write the formula again.` };
56
+ if (weight === 0) return { error: `${legName(input)} has a multiplier of 0, which removes it; leave it out of the formula instead.` };
57
+ const perTonne = spec.parameters[`perTonne${index + 1}`];
58
+ legs.push({
59
+ index,
60
+ weight,
61
+ perBarrel: spec.parameters[`perBarrel${index + 1}`] === 1,
62
+ perTonne: isFiniteNumber(perTonne) && isBarrelsPerTonne(perTonne) ? perTonne : null,
63
+ });
64
+ }
65
+ if (legs.length === 0) return { error: "The formula has no series to add up, only series it divides by; write the formula again." };
66
+ const divisor = spec.parameters.divisor ?? 1;
67
+ if (!isFiniteNumber(divisor)) return { error: "The formula's divisor is not a number; write the formula again." };
68
+ if (divisor === 0) return { error: "A formula cannot be divided by 0." };
69
+ return { legs, factors, divisor };
70
+ }
71
+
72
+ /**
73
+ * A leg's unit once its multiplier is applied: 42 gallons of a per-gallon
74
+ * price are a barrel's worth, and 7.45 barrels of gasoil a tonne's.
75
+ */
76
+ function formulaLegUnit(leg: FormulaLeg, input: ResolvedSeries): string {
77
+ const unit = leg.perBarrel ? perBarrelUnit(input.unit) : input.unit;
78
+ return leg.perTonne ? perTonneUnit(unit) : unit;
79
+ }
80
+
81
+ /** Why a formula's legs cannot be summed, or null when every leg counts the same known unit. */
82
+ function formulaUnitMismatch(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): string | null {
83
+ if (terms.legs.length < 2) return null;
84
+ const legs = terms.legs.map((leg) => ({ series: inputs[leg.index]!, unit: formulaLegUnit(leg, inputs[leg.index]!) }));
85
+ const signatures = legs.map(({ series, unit }) => knownUnitSignature({ unit, unitGroup: series.unitGroup }));
86
+ const other = signatures.findIndex((signature, index) => index > 0 && (signature === null || signature !== signatures[0]));
87
+ if (signatures[0] !== null && other < 0) return null;
88
+ return unitMismatchMessage("formula", legs[0]!, legs[Math.max(1, other)]!);
89
+ }
90
+
91
+ function formatFormulaNumber(value: number): string {
92
+ return String(Number(value.toPrecision(10)));
93
+ }
94
+
95
+ /**
96
+ * A formula as the legend names it, from the terms it was written with:
97
+ * `42×HO=F - CL=F`, `(2×42×RB=F + 42×HO=F - 3×CL=F)/3`, `(42×HO=F - BZ=F)/EURUSD=X`.
98
+ */
99
+ function formulaLabel(inputs: readonly ResolvedSeries[], terms: FormulaTerms): string {
100
+ const sum = terms.legs.map((leg, position) => {
101
+ const magnitude = Math.abs(leg.weight);
102
+ // The conversions it carries are named as written: 7.45×42×HO=F.
103
+ const conversions = [...(leg.perTonne ? [leg.perTonne] : []), ...(leg.perBarrel ? [GALLONS_PER_BARREL] : [])];
104
+ const count = conversions.reduce((rest, factor) => rest / factor, magnitude);
105
+ const factors = [...(Math.abs(count - 1) < 1e-9 ? [] : [formatFormulaNumber(count)]), ...conversions.map(String)];
106
+ const term = [...factors, legName(inputs[leg.index]!)].join("×");
107
+ if (position === 0) return leg.weight < 0 ? `-${term}` : term;
108
+ return `${leg.weight < 0 ? "-" : "+"} ${term}`;
109
+ }).join(" ");
110
+ if (terms.divisor === 1 && terms.factors.length === 0) return sum;
111
+ return [
112
+ `(${sum})`,
113
+ ...(terms.divisor === 1 ? [] : [`/${formatFormulaNumber(terms.divisor)}`]),
114
+ ...terms.factors.map(({ index, power }) => `${power < 0 ? "/" : "×"}${legName(inputs[index]!)}`),
115
+ ].join("");
116
+ }
117
+
118
+ /** Every input's value on each date they share, each carried forward to the others' dates. */
119
+ function alignedSamples(inputs: readonly ResolvedSeries[]): Array<{ point: TimeSeriesPoint; values: number[] }> {
120
+ return alignTimeSeries(inputs, { mode: "intersection", carryForward: true }).flatMap((row) => {
121
+ const aligned = inputs.map((input) => row.values[input.id]);
122
+ if (aligned.some((entry) => !isFiniteNumber(entry?.value))) return [];
123
+ const availability = Math.max(...aligned.map((entry) => entry!.point.availableAt?.getTime() ?? entry!.point.date.getTime()));
124
+ return [{
125
+ values: aligned.map((entry) => entry!.value as number),
126
+ point: {
127
+ date: new Date(row.date),
128
+ observedAt: new Date(row.date),
129
+ availableAt: Number.isFinite(availability) ? new Date(availability) : undefined,
130
+ value: null,
131
+ provenance: { quality: "derived" as const },
132
+ },
133
+ }];
134
+ });
135
+ }
136
+
137
+ /**
138
+ * A weighted sum of the legs over a divisor, in the unit the legs share once
139
+ * converted, then times or over the series it is scaled by, in the unit
140
+ * that arithmetic leaves: a crack in USD / barrel over EURUSD=X is EUR / barrel.
141
+ */
142
+ /**
143
+ * A formula's unit: its legs' once converted, then times or over each series
144
+ * it is scaled by. Legs that count different units, or an FX rate the wrong
145
+ * way up, are an error that says why: a crack over USDJPY=X is no price, and
146
+ * times it JPY / barrel.
147
+ */
148
+ export function formulaUnit(terms: FormulaTerms, inputs: readonly ResolvedSeries[]): SeriesUnit | { error: string } {
149
+ const mismatch = formulaUnitMismatch(terms, inputs);
150
+ if (mismatch) return { error: mismatch };
151
+ const first = inputs[terms.legs[0]!.index]!;
152
+ let unit: SeriesUnit = { unit: formulaLegUnit(terms.legs[0]!, first), unitGroup: first.unitGroup };
153
+ for (const factor of terms.factors) {
154
+ const series = inputs[factor.index]!;
155
+ const next = combinedUnit(unit, series, factor.power);
156
+ const issue = fxConversionIssue(unit, series, factor.power, next);
157
+ if (issue) return { error: issue };
158
+ unit = next;
159
+ }
160
+ return unit;
161
+ }
162
+
163
+ export function resolveFormula(
164
+ spec: ChartStudySpec,
165
+ inputs: readonly ResolvedSeries[],
166
+ terms: FormulaTerms,
167
+ unit: SeriesUnit,
168
+ color: string,
169
+ ): ResolvedSeries[] {
170
+ const first = inputs[terms.legs[0]!.index]!;
171
+ // Rounded off the float's last digits: 42 × 4.6233 - 96.12 is 98.0586, not 98.05860000000001.
172
+ const points = alignedSamples(inputs).map(({ point, values }) => {
173
+ let value: number | null = terms.legs.reduce((sum, leg) => sum + values[leg.index]! * leg.weight, 0) / terms.divisor;
174
+ for (const factor of terms.factors) {
175
+ const scale = values[factor.index]!;
176
+ value = value === null || (factor.power < 0 && scale === 0) ? null : factor.power < 0 ? value / scale : value * scale;
177
+ }
178
+ return derivedPoint({ point, value: 0 }, value === null ? null : Number(value.toPrecision(12)));
179
+ });
180
+ const frequencies = new Set(inputs.map((input) => input.nativeFrequency));
181
+ const resolutions = new Set(inputs.map((input) => input.historyResolution));
182
+ const scaled = terms.factors.length > 0;
183
+ // A price per quantity reads with its unit beside it, as a future's does.
184
+ const statedUnit = scaled
185
+ ? isPriceGroup(unit.unitGroup) && unit.unit.includes("/") ? unit.unit : null
186
+ : inputs.some((input) => input.statedUnit) ? unit.unit : null;
187
+ const valueDecimals = !scaled || isPriceGroup(unit.unitGroup) ? derivedValueDecimals(inputs, points) : undefined;
188
+ return [outputSeries(spec, first, {
189
+ label: formulaLabel(inputs, terms),
190
+ points,
191
+ color,
192
+ unit: unit.unit,
193
+ unitGroup: unit.unitGroup,
194
+ statedUnit,
195
+ // A weighted sum is no traded price: its decimals are its legs', not a contract's tick.
196
+ priceAssetCategory: null,
197
+ ...(valueDecimals !== undefined ? { valueDecimals } : {}),
198
+ // As the pair formulas: as-of levels, constant until an input changes.
199
+ style: "step",
200
+ interpolation: "step-after",
201
+ axis: "left",
202
+ nativeFrequency: frequencies.size === 1 ? first.nativeFrequency : "auto",
203
+ historyResolution: resolutions.size === 1 ? first.historyResolution : null,
204
+ })];
205
+ }
@@ -0,0 +1,299 @@
1
+ import { mergePriceHistoryIntegrity } from "../../utils/price-history-integrity";
2
+ import { isRealizedVolatilityEstimator, rollingRealizedVolatility } from "../../market-data/realized-volatility";
3
+ import { averageTrueRange } from "../trader-studies";
4
+ import type { ChartStudySpec, ResolvedSeries, TimeSeriesPoint } from "../types";
5
+ import { isFiniteNumber } from "../../utils/guards";
6
+ import {
7
+ derivedPoint,
8
+ outputSeries,
9
+ positiveInteger,
10
+ samplesFor,
11
+ STUDY_COLORS,
12
+ studyBars,
13
+ studyPeriod,
14
+ type NumericSample,
15
+ } from "./shared";
16
+
17
+ export interface IndexedValue {
18
+ index: number;
19
+ value: number;
20
+ }
21
+
22
+ /** Exported so backtest rules and chart studies share identical math. */
23
+ export function sma(values: readonly number[], period: number): IndexedValue[] {
24
+ if (values.length < period) return [];
25
+ const result: IndexedValue[] = [];
26
+ let sum = 0;
27
+ for (let index = 0; index < values.length; index += 1) {
28
+ sum += values[index]!;
29
+ if (index >= period) sum -= values[index - period]!;
30
+ if (index >= period - 1) result.push({ index, value: sum / period });
31
+ }
32
+ return result;
33
+ }
34
+
35
+ export function ema(values: readonly number[], period: number): IndexedValue[] {
36
+ if (values.length < period) return [];
37
+ const result: IndexedValue[] = [];
38
+ let current = values.slice(0, period).reduce((sum, value) => sum + value, 0) / period;
39
+ result.push({ index: period - 1, value: current });
40
+ const multiplier = 2 / (period + 1);
41
+ for (let index = period; index < values.length; index += 1) {
42
+ current = values[index]! * multiplier + current * (1 - multiplier);
43
+ result.push({ index, value: current });
44
+ }
45
+ return result;
46
+ }
47
+
48
+ export function rsi(values: readonly number[], period: number): IndexedValue[] {
49
+ if (values.length < period + 1) return [];
50
+ let averageGain = 0;
51
+ let averageLoss = 0;
52
+ for (let index = 1; index <= period; index += 1) {
53
+ const change = values[index]! - values[index - 1]!;
54
+ if (change > 0) averageGain += change;
55
+ else averageLoss -= change;
56
+ }
57
+ averageGain /= period;
58
+ averageLoss /= period;
59
+ const result: IndexedValue[] = [{
60
+ index: period,
61
+ value: averageLoss === 0 ? 100 : 100 - 100 / (1 + averageGain / averageLoss),
62
+ }];
63
+ for (let index = period + 1; index < values.length; index += 1) {
64
+ const change = values[index]! - values[index - 1]!;
65
+ averageGain = (averageGain * (period - 1) + Math.max(change, 0)) / period;
66
+ averageLoss = (averageLoss * (period - 1) + Math.max(-change, 0)) / period;
67
+ result.push({
68
+ index,
69
+ value: averageLoss === 0 ? 100 : 100 - 100 / (1 + averageGain / averageLoss),
70
+ });
71
+ }
72
+ return result;
73
+ }
74
+
75
+ function indexedPoints(samples: readonly NumericSample[], values: readonly IndexedValue[]): TimeSeriesPoint[] {
76
+ return values.flatMap(({ index, value }) => {
77
+ const sample = samples[index];
78
+ return sample ? [derivedPoint(sample, value)] : [];
79
+ });
80
+ }
81
+
82
+ export function studyWarmupPoints(spec: ChartStudySpec): number {
83
+ if (spec.kind === "realized-vol") {
84
+ const window = positiveInteger(spec.parameters.window, 30);
85
+ return spec.parameters.estimator === undefined || spec.parameters.estimator === "close-to-close" || spec.parameters.estimator === "yang-zhang"
86
+ ? window : window - 1;
87
+ }
88
+ if (spec.kind === "sma" || spec.kind === "ema" || spec.kind === "bollinger") {
89
+ return studyPeriod(spec, 20) - 1;
90
+ }
91
+ if (spec.kind === "rsi") return studyPeriod(spec, 14);
92
+ if (spec.kind === "macd") {
93
+ const slow = positiveInteger(spec.parameters.slow, 26);
94
+ const signal = positiveInteger(spec.parameters.signal, 9);
95
+ return slow + signal - 2;
96
+ }
97
+ if (spec.kind === "correlation") return studyPeriod(spec, 20);
98
+ if (spec.kind === "atr") return studyPeriod(spec, 14) - 1;
99
+ return 0;
100
+ }
101
+
102
+ export function resolveRealizedVolatility(spec: ChartStudySpec, input: ResolvedSeries, color: string): ResolvedSeries[] {
103
+ const window = positiveInteger(spec.parameters.window, 30);
104
+ const estimator = isRealizedVolatilityEstimator(spec.parameters.estimator) ? spec.parameters.estimator : "close-to-close";
105
+ // Retain invalid rows and the last correction for each date. Dropping one
106
+ // would turn two separated returns into adjacent observations.
107
+ const byDate = new Map(input.points.map((point) => [point.date.getTime(), point]));
108
+ const source = [...byDate.values()].sort((a, b) => a.date.getTime() - b.date.getTime());
109
+ const calculated = rollingRealizedVolatility(source.map((point) => ({
110
+ date: point.date,
111
+ close: point.provenance?.priceHistoryIntegrity || point.provenance?.valuationPriceIssues?.length
112
+ ? Number.NaN : (point.close === undefined ? point.value : point.close) ?? Number.NaN,
113
+ open: point.open ?? undefined,
114
+ high: point.high ?? undefined,
115
+ low: point.low ?? undefined,
116
+ })), { windows: [window], estimator });
117
+ const warmup = studyWarmupPoints(spec);
118
+ const points = calculated.map((point, index): TimeSeriesPoint => {
119
+ const original = source[index]!;
120
+ const value = point.values[window];
121
+ const derived = derivedPoint({ point: original, value: 0 }, value == null ? null : value * 100);
122
+ if (value == null) {
123
+ const affected = source.slice(Math.max(0, index - warmup), index + 1);
124
+ const integrity = affected.flatMap((entry) => entry.provenance?.priceHistoryIntegrity ? [entry.provenance.priceHistoryIntegrity] : []);
125
+ if (integrity.length) derived.provenance!.priceHistoryIntegrity = mergePriceHistoryIntegrity(...integrity);
126
+ }
127
+ return derived;
128
+ });
129
+ return [outputSeries(spec, input, {
130
+ label: `RV(${window}, ${estimator}) ${input.label}`,
131
+ points,
132
+ color,
133
+ unit: "%",
134
+ unitGroup: "percent",
135
+ axis: "left",
136
+ })];
137
+ }
138
+
139
+ export function resolveMovingAverage(
140
+ spec: ChartStudySpec,
141
+ input: ResolvedSeries,
142
+ color: string,
143
+ exponential: boolean,
144
+ ): ResolvedSeries[] {
145
+ const period = studyPeriod(spec, 20);
146
+ const samples = samplesFor(input);
147
+ const calculated = exponential
148
+ ? ema(samples.map(({ value }) => value), period)
149
+ : sma(samples.map(({ value }) => value), period);
150
+ const name = exponential ? "EMA" : "SMA";
151
+ return [outputSeries(spec, input, {
152
+ label: `${name}(${period}) ${input.label}`,
153
+ points: indexedPoints(samples, calculated),
154
+ color,
155
+ })];
156
+ }
157
+
158
+ export function resolveBollinger(
159
+ spec: ChartStudySpec,
160
+ input: ResolvedSeries,
161
+ color: string,
162
+ ): ResolvedSeries[] {
163
+ const period = studyPeriod(spec, 20);
164
+ const deviations = isFiniteNumber(spec.parameters.stdDev) && spec.parameters.stdDev > 0
165
+ ? spec.parameters.stdDev
166
+ : 2;
167
+ const samples = samplesFor(input);
168
+ const values = samples.map(({ value }) => value);
169
+ const middle = sma(values, period);
170
+ const upper: IndexedValue[] = [];
171
+ const lower: IndexedValue[] = [];
172
+ for (const point of middle) {
173
+ const window = values.slice(point.index - period + 1, point.index + 1);
174
+ const variance = window.reduce((sum, value) => sum + (value - point.value) ** 2, 0) / period;
175
+ const deviation = Math.sqrt(variance) * deviations;
176
+ upper.push({ index: point.index, value: point.value + deviation });
177
+ lower.push({ index: point.index, value: point.value - deviation });
178
+ }
179
+ const label = `Bollinger(${period},${deviations}) ${input.label}`;
180
+ return [
181
+ outputSeries(spec, input, {
182
+ id: `${spec.id}:upper`,
183
+ label: `${label} Upper`,
184
+ points: indexedPoints(samples, upper),
185
+ color,
186
+ }),
187
+ outputSeries(spec, input, {
188
+ id: `${spec.id}:middle`,
189
+ label: `${label} Middle`,
190
+ points: indexedPoints(samples, middle),
191
+ color,
192
+ }),
193
+ outputSeries(spec, input, {
194
+ id: `${spec.id}:lower`,
195
+ label: `${label} Lower`,
196
+ points: indexedPoints(samples, lower),
197
+ color,
198
+ }),
199
+ ];
200
+ }
201
+
202
+ export function resolveRsi(spec: ChartStudySpec, input: ResolvedSeries, color: string): ResolvedSeries[] {
203
+ const period = studyPeriod(spec, 14);
204
+ const samples = samplesFor(input);
205
+ return [outputSeries(spec, input, {
206
+ label: `RSI(${period}) ${input.label}`,
207
+ points: indexedPoints(samples, rsi(samples.map(({ value }) => value), period)),
208
+ color,
209
+ unit: "index",
210
+ unitGroup: "oscillator-0-100",
211
+ axis: "left",
212
+ })];
213
+ }
214
+
215
+ export function resolveMacd(spec: ChartStudySpec, input: ResolvedSeries, color: string): ResolvedSeries[] {
216
+ const fastPeriod = positiveInteger(spec.parameters.fast, 12);
217
+ const slowPeriod = positiveInteger(spec.parameters.slow, 26);
218
+ const signalPeriod = positiveInteger(spec.parameters.signal, 9);
219
+ if (fastPeriod >= slowPeriod) return [];
220
+ const samples = samplesFor(input);
221
+ const values = samples.map(({ value }) => value);
222
+ const fast = new Map(ema(values, fastPeriod).map((point) => [point.index, point.value]));
223
+ const macd = ema(values, slowPeriod).flatMap(({ index, value }) => {
224
+ const fastValue = fast.get(index);
225
+ return fastValue === undefined ? [] : [{ index, value: fastValue - value }];
226
+ });
227
+ const signalOnMacd = ema(macd.map(({ value }) => value), signalPeriod);
228
+ const signal = signalOnMacd.flatMap(({ index, value }) => {
229
+ const source = macd[index];
230
+ return source ? [{ index: source.index, value }] : [];
231
+ });
232
+ const macdByIndex = new Map(macd.map((point) => [point.index, point.value]));
233
+ const histogram = signal.map(({ index, value }) => ({ index, value: macdByIndex.get(index)! - value }));
234
+ const label = `MACD(${fastPeriod},${slowPeriod},${signalPeriod}) ${input.label}`;
235
+ return [
236
+ outputSeries(spec, input, {
237
+ id: `${spec.id}:macd`,
238
+ label,
239
+ points: indexedPoints(samples, macd),
240
+ color,
241
+ axis: "left",
242
+ }),
243
+ outputSeries(spec, input, {
244
+ id: `${spec.id}:signal`,
245
+ label: `${label} Signal`,
246
+ points: indexedPoints(samples, signal),
247
+ color: STUDY_COLORS[1]!,
248
+ axis: "left",
249
+ }),
250
+ outputSeries(spec, input, {
251
+ id: `${spec.id}:histogram`,
252
+ label: `${label} Histogram`,
253
+ points: indexedPoints(samples, histogram),
254
+ color: STUDY_COLORS[4]!,
255
+ style: "columns",
256
+ axis: "left",
257
+ }),
258
+ ];
259
+ }
260
+
261
+ export function resolveVolume(spec: ChartStudySpec, input: ResolvedSeries, color: string, nameInput: boolean): ResolvedSeries[] {
262
+ // No bar has volume to plot (spot FX quotes 0 for every bar; a coin's window
263
+ // can miss every bar's), so the study adds no panel rather than an empty one.
264
+ if (input.points.length > 0 && !input.points.some((point) => isFiniteNumber(point.volume) && point.volume !== 0)) return [];
265
+ // Left to its own colour, volume follows the candles: the price's colour for
266
+ // a bar that rose, the falling colour for one that fell. A colour picked for it stays flat.
267
+ const directional = !spec.color;
268
+ const points = [...input.points]
269
+ .sort((left, right) => left.date.getTime() - right.date.getTime())
270
+ .flatMap((point) => isFiniteNumber(point.volume)
271
+ ? [{
272
+ ...derivedPoint({ point, value: point.volume }, point.volume),
273
+ ...(directional && isFiniteNumber(point.open) && isFiniteNumber(point.close) && point.close < point.open ? { fell: true } : {}),
274
+ }]
275
+ : []);
276
+ return [outputSeries(spec, input, {
277
+ label: nameInput ? `Volume ${input.label}` : "Volume",
278
+ points,
279
+ color: directional ? input.color : color,
280
+ unit: input.volumeUnit ?? "",
281
+ unitGroup: "volume",
282
+ style: "columns",
283
+ axis: "left",
284
+ })];
285
+ }
286
+
287
+ export function resolveAtr(spec: ChartStudySpec, input: ResolvedSeries, color: string, nameInput: boolean): ResolvedSeries[] {
288
+ const period = studyPeriod(spec, 14);
289
+ const bars = studyBars(input);
290
+ return [outputSeries(spec, input, {
291
+ label: nameInput ? `ATR(${period}) ${input.label}` : `ATR(${period})`,
292
+ points: averageTrueRange(bars, period).flatMap(({ index, value }) => {
293
+ const bar = bars[index];
294
+ return bar ? [derivedPoint(bar.sample, value)] : [];
295
+ }),
296
+ color,
297
+ axis: "left",
298
+ })];
299
+ }
@@ -0,0 +1,75 @@
1
+ import type { ManualChartResolution } from "../resolution";
2
+ import type { ChartStudySpec, ResolvedSeries } from "../types";
3
+ import { studyMessageName } from "../series-name";
4
+ import {
5
+ dailyLeverageGrowth,
6
+ leverageDecayPoints,
7
+ leverageFactorLabel,
8
+ leverageSymbol,
9
+ sessionCloses,
10
+ type StudyAnchor,
11
+ } from "../leverage";
12
+ import { DECAY_COLORS, derivedPoint, outputSeries } from "./shared";
13
+
14
+ /** The objective's line and its decay compound session returns: they need daily bars, whatever the chart's range. */
15
+ export function dailyBarsIssue(spec: ChartStudySpec, input: ResolvedSeries, marketResolution: ManualChartResolution | undefined): string | null {
16
+ const resolution = input.historyResolution === undefined ? marketResolution : input.historyResolution;
17
+ if (input.nativeFrequency === "daily" && (resolution === undefined || resolution === "1d")) return null;
18
+ return `${studyMessageName(spec)}: ${leverageFactorLabel(Number(spec.parameters.factor))} daily needs daily bars. Choose D bars.`;
19
+ }
20
+
21
+ /**
22
+ * The line a leveraged or inverse fund's daily objective draws from its index
23
+ * fund, in percent from where the chart compares (3× daily QQQ beside TQQQ),
24
+ * on the percent axis the compared legs share.
25
+ */
26
+ export function resolveLeverageLine(spec: ChartStudySpec, index: ResolvedSeries, color: string, anchor: StudyAnchor | undefined): ResolvedSeries[] {
27
+ const factor = spec.parameters.factor as number;
28
+ const closes = sessionCloses(index, anchor);
29
+ const growth = dailyLeverageGrowth(closes.map((entry) => entry.close), factor);
30
+ return [outputSeries(spec, index, {
31
+ label: `${leverageFactorLabel(factor)} daily ${leverageSymbol(index)}`,
32
+ points: closes.map((entry, position) => derivedPoint({ point: entry.point, value: entry.close }, (growth[position]! - 1) * 100)),
33
+ color,
34
+ unit: "%",
35
+ unitGroup: "percent",
36
+ statedUnit: null,
37
+ priceAssetCategory: null,
38
+ axis: "left",
39
+ })];
40
+ }
41
+
42
+ /**
43
+ * How far the fund falls short of its daily objective, in percentage points
44
+ * from where the chart compares: the fund against the daily line (fees,
45
+ * financing, tracking), and the daily line against the factor times the index
46
+ * fund's return over the period (compounding, which daily resets cost in a
47
+ * market that swings).
48
+ */
49
+ export function resolveLeverageDecay(
50
+ spec: ChartStudySpec,
51
+ fund: ResolvedSeries,
52
+ index: ResolvedSeries,
53
+ anchors: ReadonlyMap<string, StudyAnchor> | undefined,
54
+ ): ResolvedSeries[] {
55
+ const factor = spec.parameters.factor as number;
56
+ const decay = leverageDecayPoints(sessionCloses(fund, anchors?.get(fund.id)), sessionCloses(index, anchors?.get(index.id)), factor);
57
+ const daily = `${leverageFactorLabel(factor)} daily`;
58
+ const line = (id: string, label: string, color: string, value: (point: (typeof decay)[number]) => number) => outputSeries(spec, fund, {
59
+ id: `${spec.id}:${id}`,
60
+ label,
61
+ points: decay.map((entry) => derivedPoint({ point: entry.point, value: entry.fund }, value(entry))),
62
+ color: spec.color ?? color,
63
+ unit: "pp",
64
+ // Not a percent group: a difference of returns reads in points (-0.96 pp), not as a return.
65
+ unitGroup: "pp",
66
+ statedUnit: null,
67
+ priceAssetCategory: null,
68
+ axis: "left",
69
+ });
70
+ return [
71
+ line("fund", `${leverageSymbol(fund)} decay vs ${daily} ${leverageSymbol(index)}`, DECAY_COLORS[0], (entry) => entry.fund - entry.daily),
72
+ // `3× QQQ`: three times QQQ's return over the period, the multiple a daily reset does not deliver.
73
+ line("compounding", `${daily} ${leverageSymbol(index)} decay vs ${leverageFactorLabel(factor)} ${leverageSymbol(index)}`, DECAY_COLORS[1], (entry) => entry.daily - entry.period),
74
+ ];
75
+ }