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
@@ -0,0 +1,40 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { getSharedMarketDataCoordinator } from "../../../../market-data/coordinator";
3
+ import { formatMarketPrice, quoteFormatOptions, withCurrencyMinorDigits } from "../../../../market-data/market/format";
4
+ import { buildQuoteKey, instrumentFromQuoteTarget, resolveEntryData } from "../../../../market-data/selectors";
5
+ import type { PortfolioFuturesTerms } from "../../../../portfolio/futures-positions";
6
+ import { useLiveQuoteEntries } from "../../../../state/hooks/quote-streaming";
7
+ import type { QuoteSubscriptionTarget } from "../../../../types/data-provider";
8
+ import { logFailure } from "../../../../utils/log-failure";
9
+
10
+ /** Front references share one snapshot and stream per contract, independent of each row's own price. */
11
+ export function useFuturesFrontNotes(
12
+ terms: ReadonlyMap<string, PortfolioFuturesTerms>,
13
+ { enabled, liveStreaming }: { enabled: boolean; liveStreaming: boolean },
14
+ ): ReadonlyMap<string, string> {
15
+ const symbolsKey = useMemo(() => enabled
16
+ ? [...new Set([...terms.values()].flatMap((value) => value.front?.contractSymbol ?? []))].sort().join("\u001f") : "", [enabled, terms]);
17
+ const targets = useMemo<QuoteSubscriptionTarget[]>(() => symbolsKey ? symbolsKey.split("\u001f").map((symbol) => ({
18
+ symbol, exchange: "", route: "provider", surface: "watchlist", visible: true,
19
+ })) : [], [symbolsKey]);
20
+ const instruments = useMemo(() => targets.map(instrumentFromQuoteTarget), [targets]);
21
+ const coordinator = getSharedMarketDataCoordinator();
22
+ const { entries } = useLiveQuoteEntries(targets, { liveStreaming });
23
+ useEffect(() => {
24
+ // Polling primes its own batch immediately; a stream needs one snapshot while connecting.
25
+ if (!liveStreaming || !coordinator || !instruments.length) return;
26
+ void coordinator.loadQuotesBatch(instruments).catch(logFailure("portfolio-futures", "Read front contract quotes failed"));
27
+ }, [coordinator, instruments, liveStreaming]);
28
+ return useMemo(() => {
29
+ const notes = new Map<string, string>();
30
+ if (!enabled) return notes;
31
+ for (const [symbol, value] of terms) {
32
+ if (!value.front) continue;
33
+ const quote = resolveEntryData(entries.get(buildQuoteKey({ symbol: value.front.contractSymbol, exchange: "" })));
34
+ if (!quote || !Number.isFinite(quote.price)) continue;
35
+ const options = withCurrencyMinorDigits(quoteFormatOptions(quote, "FUTURE"), quote.currency);
36
+ notes.set(symbol, `front ${value.front.contract} ${formatMarketPrice(quote.price, options)}`);
37
+ }
38
+ return notes;
39
+ }, [enabled, entries, terms]);
40
+ }
@@ -104,6 +104,7 @@ import { hasUnknownOptionMultiplier } from "../../../../portfolio/position-metri
104
104
  import { withFuturesContractSizes } from "../../../../portfolio/futures-positions";
105
105
  import { withPurchaseDateCostFx } from "../../../../portfolio/cost-fx";
106
106
  import { usePortfolioFuturesTerms } from "./futures-terms";
107
+ import { useFuturesFrontNotes } from "./futures-front";
107
108
  import { usePurchaseDateCostFx } from "./purchase-date-fx";
108
109
  import {
109
110
  allocationWeightBase,
@@ -185,7 +186,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
185
186
  [config.baseCurrency, currentPortfolio],
186
187
  );
187
188
  // A future counts contracts of the size its terms give, and at the close is valued at its settlement.
188
- const futuresTerms = usePortfolioFuturesTerms(collectionTickers, { enabled: isPortfolioTab, settlement: valuationBasis === "regular-close" });
189
+ const futuresTerms = usePortfolioFuturesTerms(collectionTickers, { enabled: true, settlement: isPortfolioTab && valuationBasis === "regular-close" });
190
+ const futuresFrontNotes = useFuturesFrontNotes(futuresTerms.terms, { enabled: appActive && viewMode === "table", liveStreaming });
189
191
  // A dated lot in another currency costs what its purchase date's FX close says in that currency, as portfolio show values it.
190
192
  const purchaseDateFx = usePurchaseDateCostFx(collectionTickers, { enabled: isPortfolioTab, portfolioId: activeCollectionId, into: totalsCurrency });
191
193
  const tickers = useMemo(
@@ -796,6 +798,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
796
798
  onCursorChange={handleCursorChange}
797
799
  financialsMap={financialsMap}
798
800
  columnContext={columnContext}
801
+ futuresFrontNotes={futuresFrontNotes}
799
802
  flashSymbols={flashSymbols}
800
803
  sinceLastSeen={sinceLastSeen}
801
804
  onRootKeyDown={handleTableKeyDown}
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { useEffect, useMemo, useRef } from "react";
2
3
  import { getSharedMarketDataCoordinator } from "../../../../market-data/coordinator";
3
4
  import { instrumentFromTicker, quoteSubscriptionTargetFromTicker } from "../../../../market-data/request-types";
@@ -251,8 +252,9 @@ export function usePortfolioPaneStreaming({
251
252
  ? sharedCoordinator.loadSnapshotsBatch(normalSnapshotEntries.map((entry) => entry.instrument))
252
253
  : Promise.resolve(),
253
254
  ]);
254
- } catch {
255
+ } catch (error) {
255
256
  // Best-effort warmup for visible rows only.
257
+ logFailure("portfolio-list.pane.streaming", "Warm visible portfolio rows failed")(error);
256
258
  } finally {
257
259
  for (const entry of [...quoteEntries, ...forcedSnapshotEntries, ...normalSnapshotEntries]) {
258
260
  warmupInFlightRef.current.delete(entry.key);
@@ -304,8 +306,9 @@ export function usePortfolioPaneStreaming({
304
306
 
305
307
  try {
306
308
  await sharedCoordinator.loadQuotesBatch(quoteEntries.map((entry) => entry.instrument), { forceRefresh: true });
307
- } catch {
309
+ } catch (error) {
308
310
  // Best-effort watchdog for visible rows only.
311
+ logFailure("portfolio-list.pane.streaming", "Refresh visible portfolio quotes failed")(error);
309
312
  } finally {
310
313
  for (const entry of quoteEntries) {
311
314
  warmupInFlightRef.current.delete(entry.key);
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
2
3
  import type { ColumnConfig } from "../../../../types/config";
3
4
  import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../../types/financials";
@@ -171,7 +172,7 @@ function useAnalystSupplemental(
171
172
  }
172
173
  })).then((entries) => {
173
174
  if (mountedRef.current) setState((current) => setAnalystValues(current, entries));
174
- }).catch(() => {});
175
+ }).catch(logFailure("portfolio-list.pane.supplemental", "Apply analyst research failed"));
175
176
  }, [enabled, mountedRef, provider, setState, state.analystResearch, targetsKey]);
176
177
  }
177
178
 
@@ -211,7 +212,7 @@ function useCorporateSupplemental(
211
212
  }
212
213
  })).then((entries) => {
213
214
  if (mountedRef.current) setState((current) => setCorporateValues(current, entries));
214
- }).catch(() => {});
215
+ }).catch(logFailure("portfolio-list.pane.supplemental", "Apply corporate actions failed"));
215
216
  }, [enabled, mountedRef, provider, setState, state.corporateActions, targetsKey]);
216
217
  }
217
218
 
@@ -1,3 +1,4 @@
1
+ import { logFailure } from "../../../../utils/log-failure";
1
2
  import { getSharedRegistry } from "../../../registry";
2
3
  import {
3
4
  AmbiguousContractError,
@@ -178,7 +179,8 @@ export async function resolveQuickAddValidation({
178
179
  exchangeFromResolved(resolved),
179
180
  quoteContextFromResolved(resolved),
180
181
  );
181
- } catch {
182
+ } catch (error) {
183
+ logFailure("portfolio-list.quick-add.resolution", "Read quick-add quote failed")(error);
182
184
  quote = null;
183
185
  }
184
186
  }
@@ -1,8 +1,9 @@
1
1
  import { Box, Text } from "../../../../ui";
2
2
  import { TextAttributes } from "../../../../ui";
3
- import { colors, priceColor } from "../../../../theme/colors";
3
+ import { colors, priceColor, shownPriceColor } from "../../../../theme/colors";
4
4
  import type { AppState } from "../../../../state/app/context";
5
- import type { PaneFooterSegment } from "../../../../components/layout/pane/footer/model";
5
+ import { savedCopyFooterSegment, type PaneFooterSegment } from "../../../../components/layout/pane/footer/model";
6
+ import { oldestSavedCopyAt } from "../../../../data/saved-copies";
6
7
  import type { BrokerConnectionStatus } from "../../../../types/broker";
7
8
  import type { TickerFinancials } from "../../../../types/financials";
8
9
  import type { Portfolio, TickerRecord } from "../../../../types/ticker";
@@ -266,7 +267,7 @@ export function buildPortfolioSummarySegments({
266
267
  return totals.watchlistCount > 0
267
268
  ? [createSummarySegment("avg-day", [
268
269
  { text: "Avg Day", tone: "label" },
269
- { text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: priceColor(totals.avgWatchlistChange), bold: true },
270
+ { text: formatPercentRaw(totals.avgWatchlistChange), tone: "value", color: shownPriceColor(totals.avgWatchlistChange, 2), bold: true },
270
271
  ...dayBasisPart(totals),
271
272
  ])]
272
273
  : [];
@@ -341,7 +342,7 @@ export function buildPortfolioSummarySegments({
341
342
  candidates.push(createSummarySegment("day", [
342
343
  { text: "Day", tone: "label" },
343
344
  { text: money(accountMetrics.dailyPnl, true), tone: "value", color: priceColor(accountMetrics.dailyPnl), bold: true },
344
- { text: `(${formatPercentRaw(accountMetrics.dailyPnlPct)})`, tone: "muted", color: priceColor(accountMetrics.dailyPnlPct) },
345
+ { text: `(${formatPercentRaw(accountMetrics.dailyPnlPct)})`, tone: "muted", color: shownPriceColor(accountMetrics.dailyPnlPct, 2) },
345
346
  // The broker's own day is from its account's close, not the quotes': it names none.
346
347
  ...(accountMetrics.brokerDay || !Number.isFinite(accountMetrics.dailyPnl) ? [] : dayBasisPart(totals)),
347
348
  ]));
@@ -351,7 +352,7 @@ export function buildPortfolioSummarySegments({
351
352
  : !Number.isFinite(account?.unrealizedPnl)
352
353
  && totals.unrealizedPnlBasis === "mixed" ? "Mixed P&L" : "P&L", tone: "label" },
353
354
  { text: money(accountMetrics.unrealizedPnl, true), tone: "value", color: priceColor(accountMetrics.unrealizedPnl), bold: true },
354
- { text: `(${formatPercentRaw(accountMetrics.unrealizedPnlPct)})`, tone: "muted", color: priceColor(accountMetrics.unrealizedPnlPct) },
355
+ { text: `(${formatPercentRaw(accountMetrics.unrealizedPnlPct)})`, tone: "muted", color: shownPriceColor(accountMetrics.unrealizedPnlPct, 2) },
355
356
  ]));
356
357
 
357
358
  // A book with shorts splits its gross into the two sides, after the headline numbers.
@@ -446,11 +447,13 @@ export function buildPortfolioFooterSegments({
446
447
  : [];
447
448
  // Cached account numbers stay on screen when a live refresh fails, so the failure sits beside their date.
448
449
  if (accountState) return [...source, ...accountStatus];
450
+ const quotes = sortedTickers.map((ticker) => financialsMap.get(ticker.metadata.ticker)?.quote);
451
+ // Offline, the quotes listed are copies saved when they last refreshed: the footer says when, in place of the quote time.
452
+ const savedAt = refreshingSize > 0 ? null : oldestSavedCopyAt(quotes);
453
+ if (savedAt != null) return [...accountStatus, savedCopyFooterSegment("refresh", savedAt)];
449
454
  if (isPortfolioTab ? !totals.hasPositions && !accountStatusText : totals.watchlistCount === 0) return [];
450
455
 
451
- const lastRefreshTimestamp = getMostRecentQuoteUpdate(
452
- sortedTickers.map((ticker) => financialsMap.get(ticker.metadata.ticker)?.quote),
453
- );
456
+ const lastRefreshTimestamp = getMostRecentQuoteUpdate(quotes);
454
457
  const refreshText = refreshingSize > 0
455
458
  ? "Refreshing..."
456
459
  : lastRefreshTimestamp != null
@@ -111,6 +111,7 @@ export function PortfolioTickerTable({
111
111
  onCursorChange,
112
112
  financialsMap,
113
113
  columnContext,
114
+ futuresFrontNotes,
114
115
  flashSymbols,
115
116
  sinceLastSeen,
116
117
  onRootKeyDown,
@@ -134,6 +135,7 @@ export function PortfolioTickerTable({
134
135
  onCursorChange?: (ticker: TickerRecord, index: number) => void;
135
136
  financialsMap: Map<string, TickerFinancials>;
136
137
  columnContext: ColumnContext;
138
+ futuresFrontNotes?: ReadonlyMap<string, string>;
137
139
  flashSymbols: Map<string, QuoteFlashDirection>;
138
140
  /** For a few seconds after coming back to the pane, LAST is tinted by each row's move since the user left. */
139
141
  sinceLastSeen?: SinceLastSeenRecap | null;
@@ -155,21 +157,26 @@ export function PortfolioTickerTable({
155
157
  openPerpetualRef.current = onOpenPerpetual;
156
158
  const sparklineVisible = columns.some((column) => column.id === PRICE_SPARKLINE_COLUMN_ID);
157
159
  const tableFinancialsMap = useLiveSparklineFinancials(financialsMap, sortedTickers, sparklineVisible);
160
+ const noteColumnId = columns.some((column) => column.id === "name") ? "name" : "ticker";
161
+ const hasFrontReference = useMemo(() => sortedTickers.some((ticker) => columnContext.futuresTerms?.get(ticker.metadata.ticker)?.front),
162
+ [columnContext.futuresTerms, sortedTickers]);
158
163
  const resolveCell = useCallback(
159
164
  (column: ColumnConfig, ticker: TickerRecord, financials: TickerFinancials | undefined) => {
160
165
  const key = `${ticker.metadata.ticker}:${column.id}`;
161
166
  const version = buildCellVersion(column, ticker, financials, columnContext);
162
- const cell = cellCacheRef.current.get(key, version, () => {
167
+ const cached = cellCacheRef.current.get(key, version, () => {
163
168
  const cell = getColumnValue(column, ticker, financials, columnContext);
164
169
  const marketId = PERPETUAL_COLUMN_IDS.has(column.id) ? columnContext.perpetuals?.get(ticker.metadata.ticker)?.marketId : undefined;
165
170
  return marketId ? { ...cell, onPress: () => openPerpetualRef.current?.(marketId) } : cell;
166
171
  });
172
+ const note = column.id === noteColumnId ? futuresFrontNotes?.get(ticker.metadata.ticker) : undefined;
173
+ const cell = note ? { ...cached, note } : cached;
167
174
  // LAST keeps its price: a move since the user last looked only tints the cell.
168
175
  if (column.id !== "price" || !sinceLastSeen) return cell;
169
176
  const move = sinceLastSeen.moves.get(ticker.metadata.ticker);
170
177
  return move == null ? cell : { ...cell, backgroundColor: sinceLastSeenTint(move, sinceLastSeen.fading) };
171
178
  },
172
- [columnContext, sinceLastSeen],
179
+ [columnContext, futuresFrontNotes, noteColumnId, sinceLastSeen],
173
180
  );
174
181
  const pnlColumn = columns.find((column) => column.id === "pnl" || column.id === "pnl_pct");
175
182
  // Reads the same cached P&L cells the rows show; only changed rows recompute.
@@ -202,7 +209,8 @@ export function PortfolioTickerTable({
202
209
  : pnlLabel !== "P&L" && (column.id === "pnl" || column.id === "pnl_pct") ? {
203
210
  ...column,
204
211
  label: column.id === "pnl_pct" ? pnlLabel.replace("P&L", "%") : pnlLabel,
205
- } : column), [columns, grossWeights, hasNonShareQuantity, pnlLabel, sharesWidth, tickerWidth]);
212
+ } : column).map((column) => hasFrontReference && column.id === noteColumnId ? { ...column, flexGrow: 1 } : column),
213
+ [columns, grossWeights, hasFrontReference, hasNonShareQuantity, noteColumnId, pnlLabel, sharesWidth, tickerWidth]);
206
214
  // The table offers "Sort by…" while it has a sort column. A watchlist starts
207
215
  // unsorted and a third header click turns sorting off, so the unsorted state
208
216
  // needs its own entry until TickerListTableView takes `sortable`.
@@ -1,3 +1,4 @@
1
+ import { createTtlCache } from "../../../data/ttl-cache";
1
2
  import { httpFetch } from "../../../utils/http-transport";
2
3
  import { isoDateOrNull, numericOrNull, recordOrNull, textOrNull } from "../../../utils/guards";
3
4
  import {
@@ -84,20 +85,14 @@ async function readJson(url: string): Promise<unknown> {
84
85
  }
85
86
 
86
87
  /** The series break changes about every two years; one read serves the session. */
87
- let seriesBreakRead: Promise<string> | null = null;
88
+ const seriesBreaks = createTtlCache<string>({ ttlMs: Infinity });
88
89
 
89
90
  function currentSeriesBreak(force: boolean): Promise<string> {
90
- if (!seriesBreakRead || force) {
91
- const read = readJson(`${API}/list/seriesbreaks.json`).then((payload) => {
92
- const seriesBreak = newestSeriesBreak(parseSeriesBreaks(payload));
93
- if (!seriesBreak) throw new Error("No series break was listed.");
94
- return seriesBreak;
95
- });
96
- seriesBreakRead = read;
97
- // A failed read is not kept, so the next one asks again.
98
- read.catch(() => { if (seriesBreakRead === read) seriesBreakRead = null; });
99
- }
100
- return seriesBreakRead;
91
+ return seriesBreaks.load("current", async () => {
92
+ const seriesBreak = newestSeriesBreak(parseSeriesBreaks(await readJson(`${API}/list/seriesbreaks.json`)));
93
+ if (!seriesBreak) throw new Error("No series break was listed.");
94
+ return seriesBreak;
95
+ }, { force });
101
96
  }
102
97
 
103
98
  /**
@@ -117,31 +112,11 @@ export async function fetchPrimaryDealers(): Promise<DealerBoard> {
117
112
  };
118
113
  }
119
114
 
120
- interface SeriesRead {
121
- at: number;
122
- read: Promise<SeriesPoint[]>;
123
- points: SeriesPoint[] | null;
124
- }
125
-
126
- const seriesReads = new Map<string, SeriesRead>();
115
+ const seriesReads = createTtlCache<SeriesPoint[]>({ ttlMs: HISTORY_TTL_MS });
127
116
 
128
117
  function seriesRead(seriesBreak: string, key: string, force: boolean): Promise<SeriesPoint[]> {
129
- const id = `${seriesBreak}:${key}`;
130
- const known = seriesReads.get(id);
131
- if (known && !force && Date.now() - known.at < HISTORY_TTL_MS) return known.read;
132
118
  const url = `${API}/get/${encodeURIComponent(seriesBreak)}/timeseries/${encodeURIComponent(key)}.json`;
133
- const entry: SeriesRead = {
134
- at: Date.now(),
135
- points: null,
136
- read: readJson(url).then((payload) => {
137
- entry.points = parseSeriesPoints(payload);
138
- return entry.points;
139
- }),
140
- };
141
- seriesReads.set(id, entry);
142
- // A failed read is not kept, so the next one asks again.
143
- entry.read.catch(() => { if (seriesReads.get(id) === entry) seriesReads.delete(id); });
144
- return entry.read;
119
+ return seriesReads.load(`${seriesBreak}:${key}`, async () => parseSeriesPoints(await readJson(url)), { force });
145
120
  }
146
121
 
147
122
  /** A row's history, one read per series behind it, each kept for the session. */
@@ -158,9 +133,9 @@ export async function loadDealerHistory(
158
133
  function heldSeries(seriesBreak: string, row: Pick<DealerRow, "keys">): SeriesPoint[][] | null {
159
134
  const series: SeriesPoint[][] = [];
160
135
  for (const key of row.keys) {
161
- const known = seriesReads.get(`${seriesBreak}:${key}`);
162
- if (!known?.points || Date.now() - known.at >= HISTORY_TTL_MS) return null;
163
- series.push(known.points);
136
+ const points = seriesReads.peek(`${seriesBreak}:${key}`);
137
+ if (!points) return null;
138
+ series.push(points);
164
139
  }
165
140
  return series;
166
141
  }
@@ -178,6 +153,6 @@ export function cachedDealerHistory(seriesBreak: string, row: Pick<DealerRow, "i
178
153
 
179
154
  /** Forgets what was read, for a test or a refresh that must see the release again. */
180
155
  export function resetPrimaryDealersCache(): void {
181
- seriesBreakRead = null;
156
+ seriesBreaks.clear();
182
157
  seriesReads.clear();
183
158
  }
@@ -4,7 +4,7 @@ import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve
4
4
  import { isAccessDenied } from "../../../api-client/errors";
5
5
  import type { RateContract, RateMeeting } from "../../../api-client/rates";
6
6
  import { useAsyncResource, usePluginPaneState } from "../../../public/react";
7
- import { blendHex, colors, priceColor } from "../../../theme/colors";
7
+ import { blendHex, colors, shownPriceColor } from "../../../theme/colors";
8
8
  import type { PaneProps } from "../../../types/plugin";
9
9
  import { Box } from "../../../ui";
10
10
  import { nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
@@ -54,7 +54,7 @@ function timestamp(value: string | null): string { return value?.replace("T", "
54
54
  function percentile(value: number | null): string { return value == null ? "--" : value.toFixed(0); }
55
55
  /** Hikes up, cuts down, by the sign the text shows, so a move that rounds to zero stays neutral. */
56
56
  function signColor(value: number | null, digits: number): string {
57
- return value == null || !Number.isFinite(value) ? colors.textDim : priceColor(Number(value.toFixed(digits)));
57
+ return value == null || !Number.isFinite(value) ? colors.textDim : shownPriceColor(value, digits);
58
58
  }
59
59
  function bpText(value: number | null): string { return value == null ? "--" : rateChangeText(value / 100); }
60
60
  /** Days to a meeting, counted in UTC dates like the schedule. */
@@ -21,13 +21,13 @@ export const realizedVolModule: PluginModule = {
21
21
  { ...createTickerSurfacePaneTemplate({
22
22
  id: "realized-vol-graph-pane", paneId: "realized-vol", label: "Realized Volatility Graph",
23
23
  description: "Rolling realized volatility, price and a dated current ATM IV reference.",
24
- keywords: ["hvg", "realized", "historical", "volatility", "gv"], shortcut: "HVG", publicShare: true,
24
+ keywords: ["hvg", "realized", "historical", "volatility", "vol", "gv"], shortcut: "HVG", publicShare: true,
25
25
  viewKey: "graph", settings: () => ({ initialView: "graph" }),
26
26
  }), headless: realizedVolHeadless("graph") },
27
27
  { ...createTickerSurfacePaneTemplate({
28
28
  id: "realized-vol-table-pane", paneId: "realized-vol", label: "Realized Volatility Cone",
29
29
  description: "Current realized volatility against its historical range and percentile.",
30
- keywords: ["hvt", "volatility", "cone", "percentile"], shortcut: "HVT", publicShare: true,
30
+ keywords: ["hvt", "volatility", "vol", "cone", "percentile"], shortcut: "HVT", publicShare: true,
31
31
  viewKey: "cone", settings: () => ({ initialView: "cone" }),
32
32
  }), headless: realizedVolHeadless("cone") },
33
33
  ],
@@ -109,7 +109,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
109
109
  const refreshedCurrent = refreshedIv?.owner === ivLoader && refreshedIv.at > (loadedIv.updatedAt ?? 0) ? refreshedIv.data : null;
110
110
  const iv = { ...loadedIv, data: refreshedCurrent ?? loadedIv.data };
111
111
  ivDataRef.current = iv.data;
112
- useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
112
+ const ivRefresh = useLiveSessionRefresh(refreshIv, ATM_IV_REFRESH_MS, showIv && spotAvailable && !!iv.data?.reference && !loadedIv.loading,
113
113
  loadedIv.updatedAt);
114
114
  const model = useMemo(() => history.data ? projectRealizedVolatility(history.data.history, {
115
115
  symbol: history.data.symbol, estimator, windows, lookbackYears: Number(lookback) === 2 ? 2 : 1,
@@ -117,7 +117,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
117
117
  const chartInput = useMemo(() => model ? { history: model.history, rolling: model.series, windows: model.windows,
118
118
  priceUnit: priceSeriesUnit(quote?.currency ?? "", quote?.instrumentType), iv: showIv ? iv.data?.reference ?? null : null } : null,
119
119
  [model, quote?.currency, quote?.instrumentType, showIv, iv.data]);
120
- const notices = [identityError, history.error, history.data?.error, iv.error, iv.data?.error,
120
+ const notices = [identityError, history.error, history.data?.error, iv.error, ivRefresh.error, iv.data?.error,
121
121
  ...(model?.warnings ?? []), ...(showIv ? iv.data?.warnings ?? [] : []),
122
122
  ...(showIv && symbol && !spotAvailable ? ["Current ATM IV unavailable: underlying quote is missing or stale"] : []),
123
123
  ].filter((value): value is string => !!value);
@@ -161,7 +161,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
161
161
  stale: history.data?.stale ?? false, source: history.data?.source ?? null,
162
162
  asOf: model?.asOf?.toISOString() ?? null,
163
163
  errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []),
164
- ...(showIv ? [iv.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
164
+ ...(showIv ? [iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
165
165
  ].filter((value): value is string => !!value))],
166
166
  currentIv: showIv && iv.data?.reference ? { value: iv.data.reference.value * 100,
167
167
  date: iv.data.reference.date.toISOString(), label: iv.data.reference.label,
@@ -7,6 +7,7 @@ import type {
7
7
  } from "../../../api-client/types";
8
8
  import { createPluginCache } from "../../../data/plugin-cache";
9
9
  import { isAccessDenied } from "../../../api-client/errors";
10
+ import { quoteTradingExchange } from "../../../market-data/quotes/metadata";
10
11
  import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
11
12
  import {
12
13
  buildRotation,
@@ -75,6 +76,10 @@ export function validateRotationHistory(
75
76
  throw new Error("Invalid daily history.");
76
77
  return response.data;
77
78
  }
79
+ /** The venue a quote is for, as `quote` names it; empty when it names none. */
80
+ function listingVenue(quote: CloudQuotePayload | undefined): string {
81
+ return quote ? quoteTradingExchange(quote) : "";
82
+ }
78
83
  export async function fetchRotation(
79
84
  benchmark: RotationInstrument,
80
85
  instruments: RotationInstrument[],
@@ -106,6 +111,16 @@ export async function fetchRotation(
106
111
  if (isAccessDenied(error)) throw error;
107
112
  quoteError = "Listing currencies unavailable.";
108
113
  }
114
+ // A benchmark typed without its exchange (ACWI) is the listing its quote is for (ACWI:NASDAQ), as `quote`
115
+ // reads it: that venue's calendar picks the weekly closes.
116
+ const venue = benchmark.exchange
117
+ ? ""
118
+ : listingVenue(quotes.get(rotationId(benchmark)));
119
+ const reference = venue ? { ...benchmark, exchange: venue } : benchmark;
120
+ const requested = unique.map((row) => ({
121
+ id: rotationId(row),
122
+ instrument: rotationId(row) === rotationId(benchmark) ? reference : row,
123
+ }));
109
124
  const start = new Date(now);
110
125
  start.setUTCFullYear(start.getUTCFullYear() - 3);
111
126
  start.setUTCDate(1);
@@ -115,10 +130,9 @@ export async function fetchRotation(
115
130
  let next = 0;
116
131
  // Bound Cloud history requests; shared server caches batch upstream demand.
117
132
  await Promise.all(
118
- Array.from({ length: Math.min(4, unique.length) }, async () => {
119
- while (next < unique.length) {
120
- const instrument = unique[next++]!,
121
- id = rotationId(instrument),
133
+ Array.from({ length: Math.min(4, requested.length) }, async () => {
134
+ while (next < requested.length) {
135
+ const { id, instrument } = requested[next++]!,
122
136
  currency = quotes.get(id)?.currency ?? null;
123
137
  try {
124
138
  const response = await client.getCloudHistory(
@@ -1,6 +1,6 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import { fetchRotation } from "./client";
3
- import { rotationInstruments, sectorRotationInstruments } from "./model";
3
+ import { DEFAULT_ROTATION_BENCHMARK, rotationInstruments, sectorRotationInstruments } from "./model";
4
4
 
5
5
  /** Same rounding as the pane: ratios to 2 decimals, percentiles whole. */
6
6
  const fixed = (value: unknown, digits: number) =>
@@ -21,7 +21,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
21
21
  key: "benchmark",
22
22
  type: "string",
23
23
  description: "Benchmark listing.",
24
- defaultValue: "SPY:NYSEARCA",
24
+ defaultValue: DEFAULT_ROTATION_BENCHMARK,
25
25
  },
26
26
  {
27
27
  key: "trail",
@@ -44,7 +44,7 @@ export const rotationHeadless: HeadlessPaneDefinition<"bundle"> = {
44
44
  },
45
45
  async load(args, ctx) {
46
46
  const references = rotationInstruments(
47
- String(args.options.benchmark ?? "SPY:NYSEARCA"),
47
+ String(args.options.benchmark ?? DEFAULT_ROTATION_BENCHMARK),
48
48
  );
49
49
  if (references.length !== 1) throw new Error("Choose one benchmark.");
50
50
  const instruments = args.symbols.length
@@ -3,6 +3,7 @@ import type { PluginModule } from "../plugin-module";
3
3
  import { rotationCache } from "./client";
4
4
  import { rotationHeadless } from "./headless";
5
5
  import { RelativeRotationPane } from "./pane";
6
+ import { DEFAULT_ROTATION_BENCHMARK } from "./model";
6
7
 
7
8
  export const relativeRotationModule: PluginModule = {
8
9
  panes: [
@@ -41,7 +42,7 @@ export const relativeRotationModule: PluginModule = {
41
42
  key: "benchmark",
42
43
  label: "Benchmark",
43
44
  type: "text",
44
- placeholder: "SPY:NYSEARCA",
45
+ placeholder: DEFAULT_ROTATION_BENCHMARK,
45
46
  },
46
47
  {
47
48
  key: "trail",
@@ -2,6 +2,7 @@ import { publishedWeekClose } from "../analytics/sharpe-cadence";
2
2
  import { historyStatistics } from "../../../components/chart/curve/model";
3
3
  import type { CloudPricePointPayload } from "../../../api-client/types";
4
4
  import { getSectorCollection } from "../sectors/sector-data";
5
+ import { rotationQuadrant, rotationSeries, type RotationQuadrantName } from "../alerts/metric-model";
5
6
  import {
6
7
  canonicalExchange,
7
8
  normalizeSymbol,
@@ -11,6 +12,8 @@ import {
11
12
  const DAY = 86_400_000;
12
13
  const WEEK = 7 * DAY;
13
14
  export const ROTATION_LIMIT = 24;
15
+ /** RRG's benchmark when none is chosen; RRG alerts default to it too. */
16
+ export const DEFAULT_ROTATION_BENCHMARK = "SPY:NYSEARCA";
14
17
  export interface RotationInstrument {
15
18
  symbol: string;
16
19
  exchange: string;
@@ -46,8 +49,7 @@ export interface RotationRow {
46
49
  asOf: string | null;
47
50
  strength: number | null;
48
51
  momentum: number | null;
49
- quadrant:
50
- "Leading" | "Weakening" | "Lagging" | "Improving" | "Neutral" | null;
52
+ quadrant: RotationQuadrantName | null;
51
53
  strengthRank: RotationRank;
52
54
  momentumRank: RotationRank;
53
55
  relativeReturn13w: number | null;
@@ -149,21 +151,8 @@ function rotationRank(
149
151
  lastDate: stats.endDate,
150
152
  };
151
153
  }
152
- export function rotationQuadrant(
153
- strength: number | null,
154
- momentum: number | null,
155
- ): RotationRow["quadrant"] {
156
- if (strength == null || momentum == null) return null;
157
- if (Math.abs(strength - 100) < 1e-9 || Math.abs(momentum - 100) < 1e-9)
158
- return "Neutral";
159
- return strength > 100
160
- ? momentum > 100
161
- ? "Leading"
162
- : "Weakening"
163
- : momentum > 100
164
- ? "Improving"
165
- : "Lagging";
166
- }
154
+ /** The weekly strength, momentum and quadrant math is the RRG alert's, shared with Gloom Cloud's delivery. */
155
+ export { rotationQuadrant };
167
156
  export function buildRotation(
168
157
  benchmark: RotationHistory,
169
158
  instruments: RotationHistory[],
@@ -219,10 +208,12 @@ export function buildRotation(
219
208
  const benchmarkUsable = !!benchmark.currency && !!asOf;
220
209
  const rows = instruments.map((source): RotationRow => {
221
210
  const closes = rotationCloses(source.points, through);
211
+ // A benchmark with no usable weeks says so once in `gaps`; its rows are not to blame for it.
222
212
  const gaps = [
223
213
  source.error,
224
214
  source.stale ? "History is stale." : null,
225
- source.currency !== benchmark.currency || !source.currency
215
+ benchmarkUsable &&
216
+ (source.currency !== benchmark.currency || !source.currency)
226
217
  ? "Price currency does not match the benchmark."
227
218
  : null,
228
219
  ].filter((x): x is string => !!x);
@@ -240,34 +231,16 @@ export function buildRotation(
240
231
  ? ratio
241
232
  : null;
242
233
  });
243
- const strengths = ratios.map((ratio, index) => {
244
- const window = ratios.slice(Math.max(0, index - 12), index + 1);
245
- if (
246
- ratio == null ||
247
- window.length !== 13 ||
248
- window.some((value) => value == null)
249
- )
250
- return null;
251
- const mean =
252
- (window as number[]).reduce((sum, value) => sum + value, 0) / 13;
253
- return (100 * ratio) / mean;
254
- });
234
+ const series = rotationSeries(ratios);
255
235
  const history = weeks.map((week, index): RotationPoint => {
256
- const strength = strengths[index] ?? null,
257
- previous = index >= 4 ? (strengths[index - 4] ?? null) : null;
258
- const contiguous =
259
- index >= 4 &&
260
- strengths.slice(index - 4, index + 1).every((value) => value != null);
236
+ const { strength, momentum } = series[index]!;
261
237
  const priorRatio = index >= 13 ? (ratios[index - 13] ?? null) : null,
262
238
  ratio = ratios[index] ?? null;
263
239
  return {
264
240
  date: weekly.get(week)?.date ?? week,
265
241
  week,
266
242
  strength,
267
- momentum:
268
- strength != null && previous != null && contiguous
269
- ? (100 * strength) / previous
270
- : null,
243
+ momentum,
271
244
  relativeReturn13w:
272
245
  ratio != null &&
273
246
  priorRatio != null &&
@@ -284,7 +257,10 @@ export function buildRotation(
284
257
  Number.isFinite(latest.strength) &&
285
258
  latest.momentum != null &&
286
259
  Number.isFinite(latest.momentum);
287
- if (!latest || latest.strength == null || latest.momentum == null)
260
+ if (
261
+ benchmarkUsable &&
262
+ (!latest || latest.strength == null || latest.momentum == null)
263
+ )
288
264
  gaps.push(
289
265
  "At least 17 contiguous matched weekly closes are required; a missing week breaks the trail.",
290
266
  );
@@ -36,6 +36,7 @@ import {
36
36
  import { useResearchCloudSession } from "../shared/research-cloud-session";
37
37
  import { cachedRotation, loadRotation } from "./client";
38
38
  import {
39
+ DEFAULT_ROTATION_BENCHMARK,
39
40
  ROTATION_LIMIT,
40
41
  rotationId,
41
42
  rotationTrailWeeks,
@@ -140,13 +141,13 @@ function RotationDetail({
140
141
  export function RelativeRotationPane(props: PaneProps) {
141
142
  const [scope] = usePaneSettingValue("scope", "sectors");
142
143
  const [symbols] = usePaneSettingValue("symbols", "");
143
- const [benchmarkText] = usePaneSettingValue("benchmark", "SPY:NYSEARCA");
144
+ const [benchmarkText] = usePaneSettingValue("benchmark", DEFAULT_ROTATION_BENCHMARK);
144
145
  const [trailText] = usePaneSettingValue("trail", "6");
145
146
  const { collectionId } = usePaneCollection();
146
147
  const tickers = useTickers();
147
148
  const context = useMemo(() => {
148
149
  try {
149
- const references = rotationInstruments(benchmarkText || "SPY:NYSEARCA");
150
+ const references = rotationInstruments(benchmarkText || DEFAULT_ROTATION_BENCHMARK);
150
151
  if (references.length !== 1)
151
152
  throw new Error("Choose one benchmark in pane settings.");
152
153
  const instruments =