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
@@ -13,7 +13,7 @@ import {
13
13
  seriesPriceReference,
14
14
  } from "./format";
15
15
  import type { CompositeChartProps, CompositeChartScene } from "./types";
16
- import { seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
16
+ import { DIRECTION_MARKER, seriesColorsDirection, seriesCueCssBackground, type SeriesLineCue } from "./series-cues";
17
17
  import { DisclosureMarker } from "../../ui/disclosure-marker";
18
18
 
19
19
  const LEGEND_WHEEL_DELTA_PER_CELL = 8;
@@ -120,12 +120,35 @@ function packLegendLines(widths: readonly number[], available: number, maxLines
120
120
  return lines;
121
121
  }
122
122
 
123
- /** The legend's marker for a series on the desktop: its line as the plot strokes it, or a dot. */
124
- function CompositeSeriesSwatch({ series, cue, filled = true }: {
123
+ /** A triangle pointing up or down, as the desktop legend keys a bar's direction. */
124
+ const DIRECTION_TRIANGLES = {
125
+ up: "polygon(50% 0, 100% 100%, 0 100%)",
126
+ down: "polygon(0 0, 100% 0, 50% 100%)",
127
+ } as const;
128
+
129
+ /** The legend's marker for a series on the desktop: its line as the plot strokes it, its up and down colors, or a dot. */
130
+ function CompositeSeriesSwatch({ series, cue, downColor = null, filled = true }: {
125
131
  series: ResolvedSeries;
126
132
  cue: SeriesLineCue | null;
133
+ /** The down color, for a series keyed by direction: an up triangle in its color, then a down one in this. */
134
+ downColor?: string | null;
127
135
  filled?: boolean;
128
136
  }) {
137
+ if (downColor) {
138
+ return (
139
+ <Box
140
+ flexShrink={0}
141
+ flexDirection="row"
142
+ style={{ gap: 2, marginInlineEnd: 6, opacity: filled ? 1 : 0.35 }}
143
+ data-gloom-role="composite-chart-legend-marker"
144
+ data-gloom-cue="direction"
145
+ >
146
+ {([["up", series.color], ["down", downColor]] as const).map(([direction, color]) => (
147
+ <Box key={direction} flexShrink={0} style={{ width: 7, height: 7, backgroundColor: color, clipPath: DIRECTION_TRIANGLES[direction] }} />
148
+ ))}
149
+ </Box>
150
+ );
151
+ }
129
152
  return cue ? (
130
153
  // The line as the plot strokes it: solid, dashed, dotted or heavy.
131
154
  <Box
@@ -162,6 +185,7 @@ export function CompositeLegend({
162
185
  visibleSeriesIds,
163
186
  lineCues = NO_LINE_CUES,
164
187
  cellPlot = false,
188
+ directionDownColor = null,
165
189
  width,
166
190
  accessory,
167
191
  accessoryWidth,
@@ -183,6 +207,12 @@ export function CompositeLegend({
183
207
  lineCues?: ReadonlyMap<string, SeriesLineCue>;
184
208
  /** The plot is cell text, so a line's cue is its mark rather than its dash. */
185
209
  cellPlot?: boolean;
210
+ /**
211
+ * The color bars that closed down are drawn in, where the theme's up and
212
+ * down are not the familiar green and red: a candle series' marker then keys
213
+ * both (`▲▼`). Null keeps its dot.
214
+ */
215
+ directionDownColor?: string | null;
186
216
  width: number;
187
217
  accessory: CompositeChartProps["legendAccessory"];
188
218
  accessoryWidth: CompositeChartProps["legendAccessoryWidth"];
@@ -212,8 +242,9 @@ export function CompositeLegend({
212
242
  const valueDates = legendValueDates(series, scene, cursorValueById);
213
243
  const buildEntries = (withFiscalPeriods: boolean) => series.map((entry) => {
214
244
  const cue = lineCues.get(entry.id) ?? null;
215
- const marker = cue ? `${cellPlot ? cue.glyph : cue.sample} ` : DOT_MARKER;
216
- const markerWidth = isDesktopWeb ? cue ? DOM_LINE_CELLS : DOM_DOT_CELLS : displayWidth(marker);
245
+ const downColor = !cue && directionDownColor && seriesColorsDirection(entry) ? directionDownColor : null;
246
+ const marker = cue ? `${cellPlot ? cue.glyph : cue.sample} ` : downColor ? DIRECTION_MARKER.text : DOT_MARKER;
247
+ const markerWidth = isDesktopWeb ? cue || downColor ? DOM_LINE_CELLS : DOM_DOT_CELLS : displayWidth(marker);
217
248
  const toggleable = !!onToggleSeries && (isSeriesToggleable?.(entry) ?? true);
218
249
  const cursorValue = cursorValueById.get(entry.id);
219
250
  // A stacked layer reads its own amount, not the top of the stack it is drawn to.
@@ -253,6 +284,7 @@ export function CompositeLegend({
253
284
  return {
254
285
  entry,
255
286
  cue,
287
+ downColor,
256
288
  marker,
257
289
  markerWidth,
258
290
  label,
@@ -414,7 +446,7 @@ export function CompositeLegend({
414
446
  scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
415
447
  };
416
448
  const renderEntry = (
417
- { entry, cue, marker, text, statedUnit, valueTail, toggleable, tooltip, carried, width: entryWidth }: LegendEntry,
449
+ { entry, cue, downColor, marker, text, statedUnit, valueTail, toggleable, tooltip, carried, width: entryWidth }: LegendEntry,
418
450
  index: number,
419
451
  ) => {
420
452
  const entryVisible = visibleSeriesIds.has(entry.id);
@@ -446,7 +478,13 @@ export function CompositeLegend({
446
478
  title={isDesktopWeb ? tooltip : undefined}
447
479
  >
448
480
  {isDesktopWeb ? (
449
- <CompositeSeriesSwatch series={entry} cue={cue} filled={entryVisible} />
481
+ <CompositeSeriesSwatch series={entry} cue={cue} downColor={downColor} filled={entryVisible} />
482
+ ) : downColor && entryVisible ? (
483
+ <Text>
484
+ <Span fg={entry.color}>{DIRECTION_MARKER.up}</Span>
485
+ <Span fg={downColor}>{DIRECTION_MARKER.down}</Span>
486
+ {" "}
487
+ </Text>
450
488
  ) : (
451
489
  <Text fg={entryVisible ? entry.color : themeColors.textMuted}>{marker}</Text>
452
490
  )}
@@ -23,6 +23,8 @@ export interface CompositeChartLevel {
23
23
  label?: string;
24
24
  /** Exact instrument price, independent of the grid's compact tick formatter. */
25
25
  axisLabel?: string;
26
+ /** A named reference guide keeps its axis label ahead of ordinary grid ticks. */
27
+ prioritizeAxisLabel?: boolean;
26
28
  dashed?: boolean;
27
29
  }
28
30
 
@@ -64,6 +64,39 @@ export function startPanGesture(
64
64
  };
65
65
  }
66
66
 
67
+ /** Where the pointer was during a pan, for the speed it lets go at. */
68
+ export interface PanSample {
69
+ at: number;
70
+ x: number;
71
+ }
72
+
73
+ /** Only the last moments of a drag count: a pointer that stopped before letting go throws nothing. */
74
+ const PAN_RELEASE_WINDOW_MS = 100;
75
+ /** How long a throw takes to lose about two thirds of its speed. */
76
+ export const PAN_COAST_DECAY_MS = 325;
77
+ /**
78
+ * Plot widths per second a release must reach to coast, and below which a
79
+ * coast stops. Only a flick coasts: a drag placing a bar under the pointer
80
+ * moves at about one plot width a second and must leave it there.
81
+ */
82
+ export const PAN_COAST_START_SPEED = 2.5;
83
+ export const PAN_COAST_STOP_SPEED = 0.02;
84
+
85
+ /** Cells per millisecond the pointer moved over its last samples, 0 when it had come to rest. */
86
+ export function panReleaseVelocity(samples: readonly PanSample[], now: number): number {
87
+ const recent = samples.filter((sample) => now - sample.at <= PAN_RELEASE_WINDOW_MS);
88
+ const first = recent[0];
89
+ const last = recent.at(-1);
90
+ if (!first || !last || last.at <= first.at) return 0;
91
+ return (last.x - first.x) / (last.at - first.at);
92
+ }
93
+
94
+ /** One frame of a coast: the cells it moves (the decaying speed integrated over the frame) and the speed left. */
95
+ export function panCoastStep(velocity: number, elapsedMs: number): { cells: number; velocity: number } {
96
+ const decay = Math.exp(-Math.max(0, elapsedMs) / PAN_COAST_DECAY_MS);
97
+ return { cells: velocity * PAN_COAST_DECAY_MS * (1 - decay), velocity: velocity * decay };
98
+ }
99
+
67
100
  export interface PendingWheel {
68
101
  /** Fraction of the visible span, positive toward older observations. */
69
102
  panRatio: number;
@@ -30,6 +30,7 @@ function sameSeriesMeta(left: ResolvedSeries, right: ResolvedSeries): boolean {
30
30
  && left.unitGroup === right.unitGroup
31
31
  && left.statedUnit === right.statedUnit
32
32
  && left.priceAssetCategory === right.priceAssetCategory
33
+ && left.valueDecimals === right.valueDecimals
33
34
  && left.nativeFrequency === right.nativeFrequency
34
35
  && left.timestampMode === right.timestampMode
35
36
  && left.dataShape === right.dataShape
@@ -69,9 +69,14 @@ import {
69
69
  COMPOSITE_PANEL_ROLE,
70
70
  isVerticalWheelDirection,
71
71
  manualValueAxes,
72
+ PAN_COAST_START_SPEED,
73
+ PAN_COAST_STOP_SPEED,
74
+ panCoastStep,
75
+ panReleaseVelocity,
72
76
  startPanGesture,
73
77
  releaseEditableFocus,
74
78
  type PanGesture,
79
+ type PanSample,
75
80
  type PendingAxisWheel,
76
81
  type PendingWheel,
77
82
  type ValueAxisStart,
@@ -542,6 +547,7 @@ export function CompositePanelSurface({
542
547
  yRatio,
543
548
  label: level.axisLabel ?? (formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain)),
544
549
  color: level.color,
550
+ prioritizeAxisLabel: level.prioritizeAxisLabel,
545
551
  })) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
546
552
  const buildAxisMarkers = (side: "left" | "right") => {
547
553
  if (!panel.axes[side]) return undefined;
@@ -557,7 +563,8 @@ export function CompositePanelSurface({
557
563
  );
558
564
  const fixed = [lastPriceMarker, axisMarkers].filter(onSide).map(toAxisMarker);
559
565
  // The prior close's label yields to every level's.
560
- const levelLabels = [...levelMarkers, priorCloseMarker].filter(onSide).map(toAxisMarker);
566
+ const referenceLabels = levelMarkers.filter((marker) => marker.prioritizeAxisLabel && onSide(marker)).map(toAxisMarker);
567
+ const levelLabels = [...levelMarkers.filter((marker) => !marker.prioritizeAxisLabel), priorCloseMarker].filter(onSide).map(toAxisMarker);
561
568
  // A label is a row tall, so close levels would stack into an unreadable
562
569
  // pile and hide the last price: each keeps its line, and only the labels
563
570
  // with a row to themselves show. Desktop badges sit between rows.
@@ -567,8 +574,13 @@ export function CompositePanelSurface({
567
574
  pixelY: tick.topPercent / 100 * Math.max(panel.height * cellHeightPx - 1, 1),
568
575
  }))
569
576
  : [...(side === "left" ? leftAxisLabels : rightAxisLabels)].map(([row, label]) => ({ id: undefined, row, pixelY: row * cellHeightPx, label, color: colors.textDim }));
577
+ const rowOf = (marker: ReturnType<typeof toAxisMarker>) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row;
578
+ // Named guides are part of the chart's readout: ordinary ticks yield to
579
+ // them, while a live price or cursor still keeps its place.
580
+ const references = fitAxisLabels(referenceLabels, fixed, rowOf);
570
581
  const markers = [
571
- ...fitAxisLabels(levelLabels, [...fixed, ...grid], (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
582
+ ...references,
583
+ ...fitAxisLabels(levelLabels, [...references, ...fixed, ...grid], rowOf),
572
584
  ...fixed,
573
585
  ];
574
586
  return markers.length > 0 ? markers : undefined;
@@ -726,6 +738,7 @@ export function CompositePanelSurface({
726
738
  }
727
739
  if (!updateCursor(event)) return;
728
740
  const values = manualValueAxes(panel);
741
+ panSamplesRef.current = [{ at: performance.now(), x: getGlobalMouseX(event, renderer) }];
729
742
  dragRef.current = {
730
743
  ...startPanGesture(
731
744
  frame,
@@ -808,6 +821,8 @@ export function CompositePanelSurface({
808
821
  return;
809
822
  }
810
823
  const globalX = getGlobalMouseX(event, renderer);
824
+ panSamplesRef.current.push({ at: performance.now(), x: globalX });
825
+ if (panSamplesRef.current.length > 12) panSamplesRef.current.shift();
811
826
  if (drag.frame !== frame) {
812
827
  // Data refreshed mid-drag. The new frame maps positions differently, so
813
828
  // continue from where the plot is now instead of replaying the drag.
@@ -840,6 +855,43 @@ export function CompositePanelSurface({
840
855
  updateCursor,
841
856
  viewport,
842
857
  ]);
858
+ // A pan let go while still moving keeps going and slows down, on the
859
+ // desktop and web only: in a terminal every frame would upload the plot again.
860
+ const panSamplesRef = useRef<PanSample[]>([]);
861
+ const coastFrameRef = useRef<number | null>(null);
862
+ const coastPanRef = useRef(onPanViewport);
863
+ coastPanRef.current = onPanViewport;
864
+ const stopCoast = useCallback(() => {
865
+ if (coastFrameRef.current === null) return;
866
+ webFrame.cancelAnimationFrame?.(coastFrameRef.current);
867
+ coastFrameRef.current = null;
868
+ globalThis.removeEventListener?.("pointerdown", stopCoast, true);
869
+ globalThis.removeEventListener?.("keydown", stopCoast, true);
870
+ }, []);
871
+ const startCoast = useCallback((velocity: number, positionsPerCell: number) => {
872
+ stopCoast();
873
+ const requestFrame = webFrame.requestAnimationFrame;
874
+ const widthCells = Math.max(plotSize.width, 1);
875
+ const plotWidthsPerSecond = (speed: number) => Math.abs(speed) * 1000 / widthCells;
876
+ if (!isDesktopWeb || !requestFrame || plotWidthsPerSecond(velocity) < PAN_COAST_START_SPEED) return;
877
+ if (globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
878
+ let speed = velocity;
879
+ let last = performance.now();
880
+ const step = () => {
881
+ const now = performance.now();
882
+ const next = panCoastStep(speed, now - last);
883
+ last = now;
884
+ speed = next.velocity;
885
+ coastPanRef.current(next.cells * positionsPerCell);
886
+ if (plotWidthsPerSecond(speed) < PAN_COAST_STOP_SPEED) stopCoast();
887
+ else coastFrameRef.current = requestFrame(step);
888
+ };
889
+ coastFrameRef.current = requestFrame(step);
890
+ // Any press or key anywhere, a range button or a reset, takes the view back from the coast.
891
+ globalThis.addEventListener?.("pointerdown", stopCoast, true);
892
+ globalThis.addEventListener?.("keydown", stopCoast, true);
893
+ }, [isDesktopWeb, plotSize.width, stopCoast]);
894
+ useEffect(() => stopCoast, [stopCoast]);
843
895
  const finishToolDrag = useCallback(() => {
844
896
  const drag = dragRef.current;
845
897
  if (!drag || drag.kind === "pan") return;
@@ -862,12 +914,14 @@ export function CompositePanelSurface({
862
914
  if (range) onSetViewport(range);
863
915
  }, [drawColor, frame.minimumSpanMs, levels, onDraw, onSetViewport, panel, scene]);
864
916
  const resetDrag = useCallback(() => {
865
- if (dragRef.current?.kind === "pan") {
917
+ const drag = dragRef.current;
918
+ if (drag?.kind === "pan") {
866
919
  dragRef.current = null;
920
+ startCoast(panReleaseVelocity(panSamplesRef.current, performance.now()), drag.positionsPerCell);
867
921
  return;
868
922
  }
869
923
  finishToolDrag();
870
- }, [finishToolDrag]);
924
+ }, [finishToolDrag, startCoast]);
871
925
  // Said once per change, so a pointer move costs the chart nothing.
872
926
  const reportedDragRef = useRef(false);
873
927
  const reportDrag = useCallback((dragging: boolean) => {
@@ -876,10 +930,11 @@ export function CompositePanelSurface({
876
930
  onDragActiveChange?.(dragging);
877
931
  }, [onDragActiveChange]);
878
932
  const pressPlot = useCallback((event: ChartMouseEvent) => {
933
+ stopCoast();
879
934
  const before = dragRef.current;
880
935
  startDrag(event);
881
936
  if (dragRef.current !== null && dragRef.current !== before) reportDrag(true);
882
- }, [reportDrag, startDrag]);
937
+ }, [reportDrag, startDrag, stopCoast]);
883
938
  const releasePlot = useCallback(() => {
884
939
  resetDrag();
885
940
  reportDrag(false);
@@ -922,6 +977,7 @@ export function CompositePanelSurface({
922
977
  const panFromWheel = useCallback((event: ChartMouseEvent) => {
923
978
  const scroll = event.scroll;
924
979
  if (!scroll) return;
980
+ stopCoast();
925
981
  onActivate?.();
926
982
  consumeChartMouseEvent(event);
927
983
  const pointer = plotPointer(event);
@@ -0,0 +1,62 @@
1
+ import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
2
+ import { formatCompositeSeriesValue } from "./format";
3
+ import type { CompositeViewportRange } from "./interactions";
4
+ import type { CompositeChartLevel, CompositeChartLevels } from "./levels";
5
+
6
+ /** Population statistics of the finite observations actually in the window. */
7
+ export function compositeRangeStatistics(points: readonly TimeSeriesPoint[], viewport: CompositeViewportRange) {
8
+ const start = viewport.start.getTime();
9
+ const end = viewport.end.getTime();
10
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return null;
11
+ let count = 0;
12
+ let mean = 0;
13
+ let squaredDeviations = 0;
14
+ for (const point of points) {
15
+ const time = point.date.getTime();
16
+ const value = Number.isFinite(point.value) ? point.value : point.close;
17
+ if (!Number.isFinite(time) || time < start || time > end
18
+ || typeof value !== "number" || !Number.isFinite(value)) continue;
19
+ count += 1;
20
+ // Welford's update avoids subtracting nearly equal squared sums when
21
+ // valuations are tightly clustered around a large level.
22
+ const delta = value - mean;
23
+ mean += delta / count;
24
+ squaredDeviations += delta * (value - mean);
25
+ }
26
+ if (!count || !Number.isFinite(mean) || !Number.isFinite(squaredDeviations)) return null;
27
+ return { count, mean, standardDeviation: Math.sqrt(Math.max(0, squaredDeviations / count)) };
28
+ }
29
+
30
+ /** Read-only levels share the chart's axis labels, vector, Kitty and text paths. */
31
+ export function compositeRangeStatisticsLevels(
32
+ series: ResolvedSeries | undefined,
33
+ viewport: CompositeViewportRange | null,
34
+ color: string,
35
+ formatValue: (value: number, series: ResolvedSeries) => string = formatCompositeSeriesValue,
36
+ ): CompositeChartLevels | null {
37
+ if (!series || series.hidden || !viewport) return null;
38
+ const statistics = compositeRangeStatistics(series.points, viewport);
39
+ if (!statistics || statistics.count < 20) return null;
40
+ const { mean, standardDeviation } = statistics;
41
+ const guides = [
42
+ { id: "mean", label: "Avg", value: mean, dashed: false },
43
+ { id: "upper", label: "+1σ", value: mean + standardDeviation, dashed: true },
44
+ { id: "lower", label: "−1σ", value: mean - standardDeviation, dashed: true },
45
+ ];
46
+ const items: CompositeChartLevel[] = [];
47
+ for (const guide of guides) {
48
+ // A constant window (including one observation) has only its mean.
49
+ if (!Number.isFinite(guide.value) || items.some((item) => item.value === guide.value)) continue;
50
+ items.push({
51
+ id: `range-statistics:${series.id}:${guide.id}`,
52
+ value: guide.value,
53
+ color,
54
+ dashed: guide.dashed,
55
+ axisLabel: `${guide.label} ${formatValue(guide.value, series)}`,
56
+ prioritizeAxisLabel: true,
57
+ editable: false,
58
+ actionable: false,
59
+ });
60
+ }
61
+ return { seriesId: series.id, items };
62
+ }
@@ -436,13 +436,14 @@ function drawColumns(
436
436
  domain: CompositeAxisDomain,
437
437
  color: RgbaColor,
438
438
  negativeColor: RgbaColor,
439
+ fallColor: RgbaColor,
439
440
  layout: CompositeColumnLayout,
440
441
  widthByFamily: ReadonlyMap<string, number>,
441
442
  opacity: number,
442
443
  ): void {
443
444
  const baseline = pixelY(0, domain, height) ?? height - 1;
444
445
  for (const projected of series.points) {
445
- const fill = projected.value < 0 ? negativeColor : color;
446
+ const fill = projected.value < 0 ? negativeColor : projected.point.fell ? fallColor : color;
446
447
  const point = pixelPoint(projected, width, height, domain.manual);
447
448
  const geometry = columnPixelGeometry(projected, width, layout, widthByFamily);
448
449
  fillRect(
@@ -487,20 +488,27 @@ function drawOhlc(
487
488
  const closeY = pixelY(close, domain, height);
488
489
  if (highY === null || lowY === null || closeY === null) continue;
489
490
  const candleColor = close >= open ? color : negative;
490
- drawLine(data, width, height, x, highY, x, lowY, candleColor, 1.1);
491
- if (series.source.style === "candles" && openY !== null) {
492
- fillRect(
493
- data,
494
- width,
495
- height,
496
- x - candleWidth / 2,
497
- Math.min(openY, closeY),
498
- x + candleWidth / 2,
499
- Math.max(openY, closeY) + 1,
500
- candleColor,
501
- );
491
+ const style = series.source.style;
492
+ if (style !== "ohlc" && style !== "hlc" && openY !== null) {
493
+ const left = x - candleWidth / 2;
494
+ const right = x + candleWidth / 2;
495
+ const top = Math.min(openY, closeY);
496
+ const bottom = Math.max(openY, closeY) + 1;
497
+ // A hollow rising candle is an outline the wick stops at, as on other terminals.
498
+ if (style === "hollow" && close >= open) {
499
+ drawLine(data, width, height, x, highY, x, top, candleColor, 1.1);
500
+ drawLine(data, width, height, x, bottom, x, lowY, candleColor, 1.1);
501
+ drawLine(data, width, height, left, top, right, top, candleColor, 1.1);
502
+ drawLine(data, width, height, left, bottom, right, bottom, candleColor, 1.1);
503
+ drawLine(data, width, height, left, top, left, bottom, candleColor, 1.1);
504
+ drawLine(data, width, height, right, top, right, bottom, candleColor, 1.1);
505
+ continue;
506
+ }
507
+ drawLine(data, width, height, x, highY, x, lowY, candleColor, 1.1);
508
+ fillRect(data, width, height, left, top, right, bottom, candleColor);
502
509
  continue;
503
510
  }
511
+ drawLine(data, width, height, x, highY, x, lowY, candleColor, 1.1);
504
512
  if (series.source.style === "ohlc" && openY !== null) {
505
513
  drawLine(data, width, height, x - candleWidth / 2, openY, x, openY, candleColor, 1.2);
506
514
  }
@@ -585,6 +593,7 @@ export function renderCompositePanelBitmap(
585
593
  domain,
586
594
  color,
587
595
  negativeColor,
596
+ negative,
588
597
  columnLayout,
589
598
  columnWidthByFamily,
590
599
  mixesColumnsWithOtherMarks ? 0.48 : 0.72,
@@ -615,6 +624,8 @@ export function renderCompositePanelBitmap(
615
624
  break;
616
625
  }
617
626
  case "candles":
627
+ case "hollow":
628
+ case "heikin-ashi":
618
629
  case "ohlc":
619
630
  case "hlc":
620
631
  drawOhlc(data, width, height, series, domain, color, negative);
@@ -4,7 +4,7 @@ import type {
4
4
  ResolvedSeries,
5
5
  TimeSeriesPoint,
6
6
  } from "../../../time-series/types";
7
- import { effectiveTimeSeriesPointTime, timeSeriesObservationKey } from "../../../time-series/alignment";
7
+ import { effectiveTimeSeriesPointTime, sortedUniquePoints, timeSeriesObservationKey } from "../../../time-series/alignment";
8
8
  import type {
9
9
  BuildCompositeChartSceneOptions,
10
10
  CompositeAxisDomain,
@@ -36,6 +36,37 @@ function resolveTimeSeriesPointValue(point: TimeSeriesPoint): number | null {
36
36
  return null;
37
37
  }
38
38
 
39
+ /**
40
+ * Heikin Ashi bars in place of a series' own: each close averages the bar's
41
+ * four prices and each open sits halfway along the previous Heikin Ashi body,
42
+ * so the chain runs from the first loaded bar, before any viewport cut. The
43
+ * real close stays in `rawValue` for the last price marker.
44
+ */
45
+ export function heikinAshiPoints(points: readonly TimeSeriesPoint[]): TimeSeriesPoint[] {
46
+ let previous: { open: number; close: number } | null = null;
47
+ // Chain over the bars the chart draws: one per observation, in time order,
48
+ // so a superseded copy of a bar never feeds the next open.
49
+ return sortedUniquePoints(points).map((point) => {
50
+ const close = point.close ?? point.value;
51
+ if (!isFiniteNumber(close)) return point;
52
+ const open = isFiniteNumber(point.open) ? point.open : close;
53
+ const high = isFiniteNumber(point.high) ? point.high : Math.max(open, close);
54
+ const low = isFiniteNumber(point.low) ? point.low : Math.min(open, close);
55
+ const haClose = (open + high + low + close) / 4;
56
+ const haOpen = previous ? (previous.open + previous.close) / 2 : (open + close) / 2;
57
+ previous = { open: haOpen, close: haClose };
58
+ return {
59
+ ...point,
60
+ value: haClose,
61
+ rawValue: close,
62
+ open: haOpen,
63
+ high: Math.max(high, haOpen, haClose),
64
+ low: Math.min(low, haOpen, haClose),
65
+ close: haClose,
66
+ };
67
+ });
68
+ }
69
+
39
70
  function pointTime(point: TimeSeriesPoint): number | null {
40
71
  const time = point.date instanceof Date ? point.date.getTime() : new Date(point.date).getTime();
41
72
  return Number.isFinite(time) ? time : null;
@@ -330,11 +361,15 @@ function buildAxisDomain(
330
361
  scale: PanelScale,
331
362
  rows: number,
332
363
  manualScale?: CompositeManualValueScale,
364
+ referenceLevels?: BuildCompositeChartSceneOptions["referenceLevels"],
333
365
  ): CompositeAxisDomain | undefined {
334
366
  const axisSeries = series.filter((entry) => entry.axis === side);
335
367
  if (axisSeries.length === 0) return undefined;
336
368
  const values = axisSeries.flatMap(seriesDomainValues);
337
- const fitted = paddedDomain([...values, ...priorCloseInReach(axisSeries, values)], scale);
369
+ const references = referenceLevels && axisSeries.some((entry) => entry.id === referenceLevels.seriesId)
370
+ ? referenceLevels.items.map((level) => level.value).filter(Number.isFinite)
371
+ : [];
372
+ const fitted = paddedDomain([...values, ...priorCloseInReach(axisSeries, values), ...references], scale);
338
373
  const scaleKey = valueScaleKey(scale, axisSeries);
339
374
  // A range set by hand holds while the axis plots the same series and some
340
375
  // of their data is in it; otherwise the axis fits the data again.
@@ -498,9 +533,11 @@ function primaryPriceSeries(series: readonly ResolvedSeries[]): ResolvedSeries |
498
533
  )) ?? null;
499
534
  }
500
535
 
501
- function lastCloseOf(points: readonly CompositeProjectedPoint[]): number | null {
536
+ function lastCloseOf(points: readonly CompositeProjectedPoint[], heikinAshi = false): number | null {
502
537
  const last = points.at(-1);
503
538
  if (!last) return null;
539
+ // A Heikin Ashi close is an average; the marker names the price that traded.
540
+ if (heikinAshi && isFiniteNumber(last.point.rawValue)) return last.point.rawValue;
504
541
  return isFiniteNumber(last.point.close) ? last.point.close : last.value;
505
542
  }
506
543
 
@@ -518,7 +555,7 @@ function attachLastPriceMarker(
518
555
  // A rejected or missing latest bar cannot make an older close current.
519
556
  const latest = normalizedSourcePoints(primary).at(-1);
520
557
  if (latest?.point.provenance?.priceHistoryIntegrity || latest?.value === null) return;
521
- const value = lastCloseOf(projected.points);
558
+ const value = lastCloseOf(projected.points, primary.style === "heikin-ashi");
522
559
  if (value === null) return;
523
560
  const yRatio = projectCompositeValue(value, domain);
524
561
  // An axis scaled by hand can leave the last close off the panel.
@@ -760,7 +797,9 @@ export function buildCompositeChartScene(
760
797
  panels: ChartPanelSpec[],
761
798
  options: BuildCompositeChartSceneOptions,
762
799
  ): CompositeChartScene | null {
763
- const dataSeries = series.filter((entry) => normalizedPoints(entry).length > 0);
800
+ const dataSeries = series
801
+ .map((entry) => entry.style === "heikin-ashi" ? { ...entry, points: heikinAshiPoints(entry.points) } : entry)
802
+ .filter((entry) => normalizedPoints(entry).length > 0);
764
803
  if (dataSeries.length === 0) return null;
765
804
 
766
805
  // A known missing observation owns a date even though it has no drawable
@@ -849,8 +888,8 @@ export function buildCompositeChartScene(
849
888
  ? dataSeries.filter((entry) => entry.panelId === panel.id)
850
889
  : panelSeries;
851
890
  const rows = panelHeights.get(panel.id) ?? 1;
852
- const left = buildAxisDomain("left", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "left")]);
853
- const right = buildAxisDomain("right", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "right")]);
891
+ const left = buildAxisDomain("left", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "left")], options.referenceLevels);
892
+ const right = buildAxisDomain("right", domainSeries, scale, rows, options.valueScales?.[valueScaleSlot(panel.id, "right")], options.referenceLevels);
854
893
  const axes: Partial<Record<CompositeAxisSide, CompositeAxisDomain>> = { left, right };
855
894
  return {
856
895
  id: panel.id,
@@ -7,20 +7,23 @@ import type { SeriesAxisTag } from "./panel-labels";
7
7
  * ties. A forward pass separates neighbours; a backward pass fits the bottom
8
8
  * edge. Panels shorter than the readout use parallel lanes, not dropped tags
9
9
  * or smaller type. The gutter reserves those lanes before the cursor moves.
10
+ * Only a tag pushed off its value's height gets a leader: a tick beside a tag
11
+ * still on its value reads as a minus sign (`-8k` for a positive 8k).
10
12
  */
11
13
  export function placeSeriesAxisTags(tags: readonly SeriesAxisTag[], pixelHeight: number, labelHeight: number) {
12
14
  const height = Math.max(pixelHeight, labelHeight);
13
15
  const capacity = Math.max(1, Math.floor(height / labelHeight));
14
16
  const sorted = tags.map((tag, order) => ({ ...tag, order, anchorY: clamp(tag.yRatio, 0, 1) * Math.max(pixelHeight - 1, 0) }))
15
17
  .sort((a, b) => a.anchorY - b.anchorY || a.order - b.order);
16
- const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number }> = [];
18
+ const placed: Array<SeriesAxisTag & { anchorY: number; centerY: number; lane: number; leader: boolean }> = [];
17
19
  for (let start = 0; start < sorted.length; start += capacity) {
18
20
  const lane = sorted.slice(start, start + capacity);
19
21
  const centers = lane.map((tag) => clamp(tag.anchorY, labelHeight / 2, height - labelHeight / 2));
20
22
  for (let i = 1; i < centers.length; i++) centers[i] = Math.max(centers[i]!, centers[i - 1]! + labelHeight);
21
23
  if (centers.length) centers[centers.length - 1] = Math.min(centers.at(-1)!, height - labelHeight / 2);
22
24
  for (let i = centers.length - 2; i >= 0; i--) centers[i] = Math.min(centers[i]!, centers[i + 1]! - labelHeight);
23
- lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity) }));
25
+ lane.forEach((tag, index) => placed.push({ ...tag, centerY: centers[index]!, lane: Math.floor(start / capacity),
26
+ leader: Math.abs(centers[index]! - tag.anchorY) > labelHeight / 2 }));
24
27
  }
25
28
  return placed;
26
29
  }
@@ -37,13 +40,14 @@ export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
37
40
  const edge = left ? "right" : "left";
38
41
  const inset = `${tag.lane * 100 / lanes}%`;
39
42
  return <Box key={tag.seriesId} style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
40
- {/* The tick stays at the observation, even when the badge must move. */}
43
+ {/* The anchor stays at the observation; it draws a tick when the badge had to move off it. */}
41
44
  <Box
42
45
  data-gloom-role="chart-series-axis-anchor"
43
46
  data-series-id={tag.seriesId}
44
- style={{ position: "absolute", [edge]: 0, top: percent(tag.anchorY), width: `calc(${inset} + .5em)`, height: 0, borderTop: `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}` }}
47
+ data-leader={tag.leader ? "true" : undefined}
48
+ style={{ position: "absolute", [edge]: 0, top: percent(tag.anchorY), width: `calc(${inset} + .5em)`, height: 0, borderTop: tag.leader ? `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}` : undefined }}
45
49
  />
46
- <Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} />
50
+ {tag.leader ? <Box style={{ position: "absolute", [edge]: `calc(${inset} + .25em)`, top: percent(Math.min(tag.anchorY, tag.centerY)), height: percent(Math.abs(tag.centerY - tag.anchorY)), width: 0, borderLeft: `1px solid ${tag.color}` }} /> : null}
47
51
  <Box
48
52
  data-gloom-role="chart-series-axis-tag"
49
53
  data-series-id={tag.seriesId}
@@ -52,12 +56,15 @@ export function SeriesAxisTags({ placed, pixelHeight, lanes, background }: {
52
56
  data-carried={tag.carried ? "true" : undefined}
53
57
  aria-label={`${tag.seriesLabel}: ${tag.label}${tag.carried ? " (carried)" : ""}`}
54
58
  style={{
55
- position: "absolute", [edge]: `calc(${inset} + .5em)`, top: percent(tag.centerY), transform: "translateY(-50%)",
56
- width: `calc(${100 / lanes}% - .5em)`, height: "1.5em", lineHeight: "1.5em",
57
- alignItems: left ? "flex-end" : "flex-start", justifyContent: "center",
59
+ position: "absolute", [edge]: inset, top: percent(tag.centerY), transform: "translateY(-50%)",
60
+ // The value sits in a box in its series colour, as a chart level's label does, so nothing
61
+ // beside it can read as the value's sign. A box wider than its lane is clipped, not wrapped.
62
+ width: "max-content", maxWidth: `calc(${100 / lanes}% - ${lanes > 1 ? ".25em" : "0px"})`,
63
+ boxSizing: "border-box", height: "1.5em", lineHeight: "1.5em", paddingInline: ".15em",
64
+ alignItems: left ? "flex-end" : "flex-start", justifyContent: "center", overflow: "hidden",
58
65
  // Every badge paints above every leader, including another lane's.
59
66
  zIndex: 2, color: tag.color, backgroundColor: background, whiteSpace: "nowrap",
60
- borderBlockEnd: tag.carried ? `1px dashed ${tag.color}` : undefined,
67
+ border: `1px ${tag.carried ? "dashed" : "solid"} ${tag.color}`,
61
68
  }}
62
69
  >
63
70
  <Text fg={tag.color} selectable={false}>{tag.label}</Text>
@@ -69,3 +69,17 @@ export function seriesCueCssBackground(cue: SeriesLineCue, color: string): strin
69
69
  });
70
70
  return `repeating-linear-gradient(to right, ${stops.join(", ")})`;
71
71
  }
72
+
73
+ /** The OHLC family: one series drawn in the up color for a bar that closed up and the down color for one that closed down. */
74
+ const DIRECTION_STYLES = new Set<SeriesStyle>(["candles", "ohlc", "hlc"]);
75
+
76
+ /**
77
+ * Whether a series colors each bar by its direction, so its legend entry can
78
+ * key the two colors (`▲▼`) where the theme's are not the familiar green and red.
79
+ */
80
+ export function seriesColorsDirection(series: Pick<ResolvedSeries, "style">): boolean {
81
+ return DIRECTION_STYLES.has(series.style);
82
+ }
83
+
84
+ /** The legend marker that keys up and down: the up color's triangle, then the down color's. */
85
+ export const DIRECTION_MARKER = { up: "▲", down: "▼", text: "▲▼ " } as const;