@gloom-sh/terminal 0.18.5 → 0.19.1

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 (570) 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 +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  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 +19 -7
  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/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  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/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -1,11 +1,19 @@
1
1
  // Keep this model identical to the delivery model; the parity fixture checks both copies.
2
2
  /** Synced separately from price rules so older clients cannot judge a ratio as a price. */
3
3
  export const METRIC_ALERTS_KEY = "metricAlerts";
4
+ /** Relative strength has a list of its own too: a client that reads metricAlerts keeps none of it once it holds a measure it does not know. */
5
+ export const RELATIVE_ALERTS_KEY = "relativeAlerts";
6
+ export const ROTATION_QUADRANTS = ["leading", "weakening", "lagging", "improving"] as const;
7
+ export type RotationQuadrant = typeof ROTATION_QUADRANTS[number];
4
8
  export type AlertMetric =
5
9
  | { kind: "pe" | "ps" }
6
10
  | { kind: "rsi"; period: number }
7
11
  | { kind: "close_ma"; period: number }
8
- | { kind: "ma_cross"; period: number; slowPeriod: number; direction: "above" | "below" };
12
+ | { kind: "ma_cross"; period: number; slowPeriod: number; direction: "above" | "below" }
13
+ /** The listing's settled daily close divided by the benchmark's on the same session. */
14
+ | { kind: "rs_ratio"; benchmark: string; benchmarkExchange?: string }
15
+ /** The weekly RRG quadrant against the benchmark, entered on a published weekly close. */
16
+ | { kind: "rrg"; quadrant: RotationQuadrant; benchmark: string; benchmarkExchange?: string };
9
17
  export interface MetricReading {
10
18
  value: number;
11
19
  target: number;
@@ -25,6 +33,8 @@ export interface MetricRule {
25
33
  }
26
34
  const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
27
35
  const period = (value: unknown): value is number => finite(value) && Number.isInteger(value) && value >= 2 && value <= 200;
36
+ const listing = (value: unknown): value is string => typeof value === "string" && /^[A-Z0-9.^=-]{1,20}$/.test(value);
37
+ const venue = (value: unknown) => value === undefined || (typeof value === "string" && /^[A-Z0-9._-]{1,24}$/.test(value));
28
38
 
29
39
  export function readAlertMetric(raw: unknown): AlertMetric | null {
30
40
  if (!raw || typeof raw !== "object") return null;
@@ -35,13 +45,26 @@ export function readAlertMetric(raw: unknown): AlertMetric | null {
35
45
  && (m.direction === "above" || m.direction === "below")) {
36
46
  return { kind: m.kind, period: m.period, slowPeriod: m.slowPeriod, direction: m.direction };
37
47
  }
48
+ if ((m.kind === "rs_ratio" || m.kind === "rrg") && listing(m.benchmark) && venue(m.benchmarkExchange)) {
49
+ const benchmark = { benchmark: m.benchmark, ...(m.benchmarkExchange ? { benchmarkExchange: m.benchmarkExchange as string } : {}) };
50
+ if (m.kind === "rs_ratio") return { kind: m.kind, ...benchmark };
51
+ if (ROTATION_QUADRANTS.includes(m.quadrant as RotationQuadrant)) return { kind: m.kind, quadrant: m.quadrant as RotationQuadrant, ...benchmark };
52
+ }
38
53
  return null;
39
54
  }
55
+ export function isRelativeMetric(metric: AlertMetric): metric is Extract<AlertMetric, { benchmark: string }> {
56
+ return metric.kind === "rs_ratio" || metric.kind === "rrg";
57
+ }
58
+ /** The synced list a rule belongs in. */
59
+ export function metricAlertsKey(metric: AlertMetric): string {
60
+ return isRelativeMetric(metric) ? RELATIVE_ALERTS_KEY : METRIC_ALERTS_KEY;
61
+ }
40
62
 
41
63
  export function validMetricRule(rule: Pick<MetricRule, "metric" | "condition" | "targetPrice">): boolean {
42
64
  const m = readAlertMetric(rule.metric);
43
65
  if (!m || !finite(rule.targetPrice)) return false;
44
- if (m.kind === "ma_cross") return rule.condition === "crosses" && rule.targetPrice === 0;
66
+ if (m.kind === "ma_cross" || m.kind === "rrg") return rule.condition === "crosses" && rule.targetPrice === 0;
67
+ if (m.kind === "rs_ratio") return ["above", "below", "crosses"].includes(rule.condition) && rule.targetPrice > 0;
45
68
  if (rule.condition !== "above" && rule.condition !== "below") return false;
46
69
  return m.kind === "close_ma" ? rule.targetPrice === 0
47
70
  : m.kind === "rsi" ? rule.targetPrice >= 0 && rule.targetPrice <= 100 : rule.targetPrice > 0;
@@ -51,18 +74,62 @@ export function metricName(metric: AlertMetric): string {
51
74
  if (metric.kind === "pe") return "P/E (TTM)";
52
75
  if (metric.kind === "ps") return "P/S (TTM)";
53
76
  if (metric.kind === "rsi") return `RSI(${metric.period})`;
77
+ if (metric.kind === "rs_ratio") return `Ratio to ${metric.benchmark}`;
78
+ if (metric.kind === "rrg") return `RRG vs ${metric.benchmark}`;
54
79
  return metric.kind === "ma_cross" ? `${metric.period}-day MA` : "Close";
55
80
  }
81
+ const quadrantName = (quadrant: RotationQuadrant) => `${quadrant[0]!.toUpperCase()}${quadrant.slice(1)}`;
56
82
  export function metricDescription(rule: MetricRule & { symbol: string }): string {
57
83
  const m = rule.metric!;
84
+ const basis = closeBasis(m);
85
+ const suffix = basis ? ` · on ${basis}` : "";
86
+ if (m.kind === "rs_ratio") return `${rule.symbol} ratio to ${m.benchmark} ${rule.condition} ${rule.targetPrice}${suffix}`;
87
+ if (m.kind === "rrg") return `${rule.symbol} ${metricName(m)} enters ${quadrantName(m.quadrant)}${suffix}`;
58
88
  const target = m.kind === "ma_cross" ? `${m.slowPeriod}-day MA`
59
89
  : m.kind === "close_ma" ? `${m.period}-day MA` : String(rule.targetPrice);
60
90
  const condition = m.kind === "ma_cross" ? `crosses ${m.direction}` : rule.condition;
61
- return `${rule.symbol} ${metricName(m)} ${condition} ${target}${isDailyMetric(m) ? " · on daily close" : ""}`;
91
+ return `${rule.symbol} ${metricName(m)} ${condition} ${target}${suffix}`;
62
92
  }
93
+ /** Judged on settled closes only, never a forming session; RRG quadrants on weekly ones. */
63
94
  export function isDailyMetric(metric: AlertMetric): boolean {
64
95
  return metric.kind !== "pe" && metric.kind !== "ps";
65
96
  }
97
+ export function closeBasis(metric: AlertMetric): "daily close" | "weekly close" | null {
98
+ return metric.kind === "rrg" ? "weekly close" : isDailyMetric(metric) ? "daily close" : null;
99
+ }
100
+ /** The reading as alerts state it: 31.2x, 0.2512, or Leading (strength 101.23, momentum 100.40) as RRG words them. */
101
+ export function metricReadingText(metric: AlertMetric, reading: Pick<MetricReading, "value" | "target">): string {
102
+ if (metric.kind === "rrg") {
103
+ return `${rotationQuadrant(reading.value, reading.target) ?? "No quadrant"} (strength ${reading.value.toFixed(2)}, momentum ${reading.target.toFixed(2)})`;
104
+ }
105
+ return `${Number(reading.value.toFixed(4))}${metric.kind === "pe" || metric.kind === "ps" ? "x" : ""}`;
106
+ }
107
+
108
+ export type RotationQuadrantName = "Leading" | "Weakening" | "Lagging" | "Improving" | "Neutral";
109
+ export function rotationQuadrant(strength: number | null | undefined, momentum: number | null | undefined): RotationQuadrantName | null {
110
+ if (strength == null || momentum == null || !finite(strength) || !finite(momentum)) return null;
111
+ if (Math.abs(strength - 100) < 1e-9 || Math.abs(momentum - 100) < 1e-9) return "Neutral";
112
+ return strength > 100 ? momentum > 100 ? "Leading" : "Weakening" : momentum > 100 ? "Improving" : "Lagging";
113
+ }
114
+ /**
115
+ * Weekly strength (RS-Ratio: 100 x the price ratio over its 13-week mean) and momentum (RS-Momentum: 100 x
116
+ * strength over strength four weeks earlier), as RRG plots them. A missing week breaks both until 13 and 17 contiguous weeks.
117
+ */
118
+ export function rotationSeries(ratios: readonly (number | null)[]): { strength: number | null; momentum: number | null }[] {
119
+ const strengths = ratios.map((ratio, index) => {
120
+ const window = ratios.slice(Math.max(0, index - 12), index + 1);
121
+ if (ratio == null || window.length !== 13 || window.some((value) => value == null)) return null;
122
+ const average = (window as number[]).reduce((sum, value) => sum + value, 0) / 13;
123
+ return (100 * ratio) / average;
124
+ });
125
+ return strengths.map((strength, index) => {
126
+ const previous = index >= 4 ? (strengths[index - 4] ?? null) : null;
127
+ const contiguous = index >= 4 && strengths.slice(index - 4, index + 1).every((value) => value != null);
128
+ return { strength, momentum: strength != null && previous != null && contiguous ? (100 * strength) / previous : null };
129
+ });
130
+ }
131
+ /** Weeks a quadrant change needs: 17 for this week's momentum, and the week before it. */
132
+ export const ROTATION_ALERT_WEEKS = 18;
66
133
 
67
134
  /** Wilder's initial average of N changes, then (previous * (N - 1) + change) / N. */
68
135
  function rsi(closes: readonly number[], window: number): number[] {
@@ -110,24 +177,96 @@ export class DailyMetricCache {
110
177
  }
111
178
  }
112
179
 
113
- export function dailyMetricReading(
114
- rule: MetricRule,
115
- bars: readonly { date: string; close: number }[],
116
- now: number,
117
- session: (day: string) => DailySession | null,
118
- ): MetricReading {
119
- const m = rule.metric!;
120
- if (!isDailyMetric(m) || !("period" in m)) throw new Error("Expected a daily technical alert.");
121
- const latest = latestSettledClose(now, session);
180
+ type DailyBar = { date: string; close: number };
181
+ function closesThrough(bars: readonly DailyBar[], through: string): Map<string, number> {
122
182
  const byDay = new Map<string, number>();
123
183
  for (const bar of bars) {
124
184
  const day = bar.date.slice(0, 10);
125
- if (day > latest.day) continue;
185
+ if (day > through) continue;
126
186
  if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !finite(bar.close) || bar.close <= 0 || byDay.has(day)) {
127
187
  throw new Error("Daily history contains an invalid or duplicate bar.");
128
188
  }
129
189
  byDay.set(day, bar.close);
130
190
  }
191
+ return byDay;
192
+ }
193
+ const isoDay = (time: number) => new Date(time).toISOString().slice(0, 10);
194
+ function sessionBefore(day: string, session: (day: string) => DailySession | null): string {
195
+ for (let offset = 1; offset < 15; offset++) {
196
+ const previous = isoDay(Date.parse(day) - offset * DAY);
197
+ const s = session(previous);
198
+ if (!s) throw new Error("Daily close calendar unavailable for part of the history.");
199
+ if (s.kind === "session") return previous;
200
+ }
201
+ throw new Error(`No session before ${day}.`);
202
+ }
203
+ /** The last session of each Monday-Friday week, oldest first, ending with the newest week whose close has settled. */
204
+ export function settledWeekCloses(now: number, session: (day: string) => DailySession | null, count: number): { day: string; at: number }[] {
205
+ const latest = latestSettledClose(now, session);
206
+ const close = (friday: number) => {
207
+ for (let offset = 0; offset < 5; offset++) {
208
+ const day = isoDay(friday - offset * DAY);
209
+ const s = session(day);
210
+ if (!s) throw new Error("Daily close calendar unavailable for part of the history.");
211
+ if (s.kind === "session") return { day, at: s.close };
212
+ }
213
+ throw new Error(`No session in the week ending ${isoDay(friday)}.`);
214
+ };
215
+ const time = Date.parse(latest.day);
216
+ let friday = time + ((5 - new Date(time).getUTCDay() + 7) % 7) * DAY;
217
+ // A week whose last session is still ahead or forming has no weekly close yet.
218
+ if (close(friday).day !== latest.day) friday -= 7 * DAY;
219
+ return Array.from({ length: count }, (_, index) => close(friday - (count - 1 - index) * 7 * DAY));
220
+ }
221
+
222
+ /** Both listings' own closes on the same sessions; a session either lacks is never bridged from another day. */
223
+ function relativeMetricReading(
224
+ rule: MetricRule,
225
+ m: Extract<AlertMetric, { benchmark: string }>,
226
+ bars: readonly DailyBar[],
227
+ benchmarkBars: readonly DailyBar[],
228
+ now: number,
229
+ session: (day: string) => DailySession | null,
230
+ ): MetricReading {
231
+ const latest = latestSettledClose(now, session);
232
+ const own = closesThrough(bars, latest.day), reference = closesThrough(benchmarkBars, latest.day);
233
+ const ratio = (day: string) => {
234
+ const close = own.get(day), base = reference.get(day);
235
+ if (close == null || base == null) {
236
+ if (day === latest.day) throw new Error(`Waiting for the ${latest.day} ${m.kind === "rrg" ? "weekly" : "daily"} close${close == null ? "" : ` of ${m.benchmark}`}.`);
237
+ throw new Error(`Daily history is missing ${day}${close == null ? "" : ` for ${m.benchmark}`}.`);
238
+ }
239
+ return close / base;
240
+ };
241
+ if (m.kind === "rs_ratio") {
242
+ const value = ratio(latest.day), previousValue = ratio(sessionBefore(latest.day, session));
243
+ return { value, target: rule.targetPrice, previousValue, previousTarget: rule.targetPrice, day: latest.day, at: latest.at };
244
+ }
245
+ const weeks = settledWeekCloses(now, session, ROTATION_ALERT_WEEKS);
246
+ const [before, last] = rotationSeries(weeks.map((week) => ratio(week.day))).slice(-2);
247
+ if (before?.strength == null || before.momentum == null || last?.strength == null || last.momentum == null) {
248
+ throw new Error(`Need ${ROTATION_ALERT_WEEKS} contiguous weekly closes.`);
249
+ }
250
+ const week = weeks.at(-1)!;
251
+ return { value: last.strength, target: last.momentum, previousValue: before.strength, previousTarget: before.momentum, day: week.day, at: week.at };
252
+ }
253
+
254
+ /** RS ratios and RRG quadrants take the benchmark's completed daily bars as well. */
255
+ export function dailyMetricReading(
256
+ rule: MetricRule,
257
+ bars: readonly DailyBar[],
258
+ now: number,
259
+ session: (day: string) => DailySession | null,
260
+ benchmarkBars?: readonly DailyBar[],
261
+ ): MetricReading {
262
+ const m = rule.metric!;
263
+ if (m.kind === "rs_ratio" || m.kind === "rrg") {
264
+ if (!benchmarkBars) throw new Error(`Completed daily history for ${m.benchmark} is required.`);
265
+ return relativeMetricReading(rule, m, bars, benchmarkBars, now, session);
266
+ }
267
+ if (!isDailyMetric(m) || !("period" in m)) throw new Error("Expected a daily technical alert.");
268
+ const latest = latestSettledClose(now, session);
269
+ const byDay = closesThrough(bars, latest.day);
131
270
  const days = [...byDay.keys()].sort();
132
271
  if (days.at(-1) !== latest.day) throw new Error(`Waiting for the ${latest.day} daily close.`);
133
272
  const needed = m.kind === "close_ma" ? m.period : (m.kind === "ma_cross" ? m.slowPeriod : m.period) + 1;
@@ -165,9 +304,19 @@ export function metricTrigger(rule: MetricRule, reading: MetricReading): "above"
165
304
  if (isDailyMetric(rule.metric!)) {
166
305
  if (!reading.day || reading.day <= (rule.lastDailyClose ?? "") || reading.at <= (rule.armedAt ?? rule.createdAt ?? Infinity)) return null;
167
306
  }
307
+ const m = rule.metric!;
308
+ if (m.kind === "rrg") {
309
+ // Fires on entering the quadrant, not on a week already inside it.
310
+ const before = rotationQuadrant(reading.previousValue, reading.previousTarget)?.toLowerCase();
311
+ return before && before !== m.quadrant && rotationQuadrant(reading.value, reading.target)?.toLowerCase() === m.quadrant ? "up" : null;
312
+ }
168
313
  if (rule.condition === "above") return reading.value > reading.target ? "above" : null;
169
314
  if (rule.condition === "below") return reading.value < reading.target ? "below" : null;
170
- const m = rule.metric!;
315
+ if (m.kind === "rs_ratio") {
316
+ if (!finite(reading.previousValue)) return null;
317
+ if (reading.previousValue <= reading.target && reading.value > reading.target) return "up";
318
+ return reading.previousValue >= reading.target && reading.value < reading.target ? "down" : null;
319
+ }
171
320
  if (m.kind !== "ma_cross" || !finite(reading.previousValue) || !finite(reading.previousTarget)) return null;
172
321
  if (m.direction === "above" && reading.previousValue <= reading.previousTarget && reading.value > reading.target) return "up";
173
322
  if (m.direction === "below" && reading.previousValue >= reading.previousTarget && reading.value < reading.target) return "down";
@@ -1,5 +1,5 @@
1
1
  {
2
- "modelSha256": "2364694ef384760911a9190f6ebb12bb66442e63c98defd30e16fd0bb731afd4",
2
+ "modelSha256": "7672569b5c80bb737181cb6a85e72b07e659d706e1792e5a89e8d570ae0ec581",
3
3
  "valuations": [
4
4
  {
5
5
  "name": "regular",
@@ -1,9 +1,10 @@
1
- import { METRIC_ALERTS_KEY, isDailyMetric, metricName } from "./metric-model";
1
+ import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, closeBasis, metricName, metricReadingText } from "./metric-model";
2
2
  import { metricAvailabilityError } from "./metric-data";
3
- import { readLevelAlertStores } from "./storage";
3
+ import { relativeAlertText } from "./metric-command";
4
+ import { levelAlertStoreKey, readLevelAlertStores } from "./storage";
4
5
  import { Box, type ScrollBoxRenderable } from "../../../ui";
5
6
  import { Button, DetailScrollBody, KeyValueRow, Notice, PaneStatusBody, PaneFooterScope, usePaneTabs } from "../../../components";
6
- import { usePluginPaneState } from "../../runtime";
7
+ import { usePluginPaneState, usePluginState } from "../../runtime";
7
8
  import { EventAlertsPane } from "./events-pane";
8
9
  import { AlertHistoryPane } from "./history-pane";
9
10
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from "react";
@@ -30,16 +31,21 @@ import {
30
31
  readAlertsStoreError,
31
32
  serializeAlerts,
32
33
  } from "./alert-engine";
33
- import { parseAlertCommandValues } from "./command";
34
+ import { parseAlertEdit } from "./command";
34
35
  import { ALERT_TABS, ALERTS_KEY } from "./constants";
35
36
  import {
37
+ alertListing,
36
38
  conditionLabel,
37
39
  formatAlertDistance,
38
40
  formatAlertTargetPrice,
39
41
  formatCurrentPrice,
40
42
  formatQuoteChecked,
43
+ isoUtc,
44
+ listingWithCurrency,
41
45
  relativeTime,
42
46
  } from "./format";
47
+ import { FIRED_LOG_KEY } from "./fired-log";
48
+ import { withAlertChange } from "./alert-log";
43
49
  import type { AlertRule } from "./types";
44
50
  import { useQuoteEntries } from "../../../market-data/hooks";
45
51
  import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
@@ -57,7 +63,8 @@ type AlertColumnId =
57
63
  | "away"
58
64
  | "condition"
59
65
  | "quote"
60
- | "triggered";
66
+ | "triggered"
67
+ | "created";
61
68
 
62
69
  type AlertColumn = DataTableColumn & { id: AlertColumnId };
63
70
 
@@ -69,8 +76,11 @@ const ALERT_COLUMNS: AlertColumn[] = [
69
76
  { id: "target", label: "Target", width: 11, align: "right" },
70
77
  { id: "away", label: "Away", width: 8, align: "right" },
71
78
  { id: "condition", label: "Trigger", width: 12, align: "left" },
72
- { id: "quote", label: "Quote", width: 14, align: "left" },
79
+ // Wide enough to say "not checked yet" for an alert no check has read.
80
+ { id: "quote", label: "Quote", width: 15, align: "left" },
73
81
  { id: "triggered", label: "Alerted", width: 8, align: "left" },
82
+ // The day the alert was made (UTC), as Events lists its rules; the detail gives the instant.
83
+ { id: "created", label: "Since", width: 10, align: "left" },
74
84
  ];
75
85
 
76
86
  const DETAIL_ROW = { labelWidth: 10, emphasis: false } as const;
@@ -89,20 +99,28 @@ function AlertDetail({ alert, scrollRef }: { alert: AlertRule; scrollRef: RefObj
89
99
  const reason = alert.lastCheckError
90
100
  ? alert.lastCheckError.includes(alert.symbol) ? alert.lastCheckError : `${alert.symbol}: ${alert.lastCheckError}`
91
101
  : null;
102
+ const basis = alert.metric ? closeBasis(alert.metric) : null;
92
103
  return (
93
104
  <DetailScrollBody ref={scrollRef} resetScrollKey={alert.id}>
94
105
  <Box flexDirection="column">
95
106
  {reason ? <Box paddingBottom={1}><Notice tone="warning">{reason}</Notice></Box> : null}
96
107
  <KeyValueRow {...DETAIL_ROW} label="State" value={alert.status === "triggered" ? "Triggered" : "Active"} tone={alert.status === "triggered" ? "positive" : "muted"} />
108
+ <KeyValueRow {...DETAIL_ROW} label="Listing" value={listingWithCurrency(alertListing(alert), alert.lastQuoteCurrency)} />
97
109
  {/* With no quote the reason above says why; the price and its distance wait for one. */}
98
- {alert.metric && isDailyMetric(alert.metric) ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? "Awaiting daily close"} /> : null}
110
+ {basis ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? `Awaiting ${basis}`} /> : null}
99
111
  {reason ? null : <KeyValueRow {...DETAIL_ROW} label="Current" value={formatCurrentPrice(alert, 24)} />}
112
+ {/* The two readings that place the quadrant, as RRG states them. */}
113
+ {!reason && alert.metric?.kind === "rrg" && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null ? <>
114
+ <KeyValueRow {...DETAIL_ROW} label="Strength" value={alert.lastCheckedPrice.toFixed(2)} />
115
+ <KeyValueRow {...DETAIL_ROW} label="Momentum" value={alert.lastCheckedTarget.toFixed(2)} />
116
+ </> : null}
100
117
  {reason ? null : <KeyValueRow {...DETAIL_ROW} label="Away" value={formatAlertDistance(alert)} />}
101
118
  {reason
102
119
  ? alert.lastCheckedAt ? <KeyValueRow {...DETAIL_ROW} label="Checked" value={relativeTime(alert.lastCheckedAt)} tone="muted" /> : null
103
120
  : <KeyValueRow {...DETAIL_ROW} label="Quote" value={formatQuoteChecked(alert)} tone="muted" />}
104
121
  {alert.triggeredAt ? <KeyValueRow {...DETAIL_ROW} label="Alerted" value={relativeTime(alert.triggeredAt)} /> : null}
105
122
  <KeyValueRow {...DETAIL_ROW} label="Armed" value={relativeTime(alert.armedAt ?? alert.createdAt)} tone="muted" />
123
+ <KeyValueRow {...DETAIL_ROW} label="Created" value={isoUtc(alert.createdAt)} tone="muted" />
106
124
  </Box>
107
125
  </DetailScrollBody>
108
126
  );
@@ -144,7 +162,10 @@ export function AlertsPane(props: PaneProps) {
144
162
  function PriceAlertsPane({ focused, width, height }: PaneProps) {
145
163
  const [alertsJson, setAlertsJson] = usePluginConfigState<unknown>(ALERTS_KEY, "[]");
146
164
  const [metricsJson, setMetricsJson] = usePluginConfigState<unknown>(METRIC_ALERTS_KEY, "[]");
165
+ const [relativeJson, setRelativeJson] = usePluginConfigState<unknown>(RELATIVE_ALERTS_KEY, "[]");
147
166
  const { openPluginCommandWorkflow, notify } = usePluginAppActions();
167
+ // Deleting or re-arming a rule goes in this device's alert log, beside the alerts that fired.
168
+ const [, setAlertLog] = usePluginState<unknown>(FIRED_LOG_KEY, []);
148
169
  const dialog = useDialog();
149
170
  const [selectedIdx, setSelectedIdx] = useState(0);
150
171
  const [detailOpen, setDetailOpen] = useState(false);
@@ -152,9 +173,11 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
152
173
  const detailAlertId = useRef<string | null>(null);
153
174
  const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
154
175
  // Keep the rule and daily-close basis visible in a narrow pane; the detail keeps all figures.
155
- const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered") : ALERT_COLUMNS;
176
+ const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered" && column.id !== "created")
177
+ : width < 118 ? ALERT_COLUMNS.filter(column => column.id !== "created") : ALERT_COLUMNS;
156
178
  const showHorizontalScrollbar = columns.reduce((sum, column) => sum + column.width + 1, 2) > width;
157
- const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : metricsJson), [alertsJson, metricsJson]);
179
+ const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : key === METRIC_ALERTS_KEY ? metricsJson : relativeJson),
180
+ [alertsJson, metricsJson, relativeJson]);
158
181
  const storeError = stores.error;
159
182
 
160
183
  const { alerts, rows } = useMemo(() => {
@@ -205,16 +228,17 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
205
228
  }), [liveEntries, rows]);
206
229
 
207
230
  const savePaneAlerts = useCallback((id: string, next: (current: AlertRule[]) => AlertRule[]): string | null => {
208
- const key = alerts.find(alert => alert.id === id)?.metric ? METRIC_ALERTS_KEY : ALERTS_KEY;
231
+ const found = alerts.find(alert => alert.id === id);
232
+ const key = found ? levelAlertStoreKey(found) : ALERTS_KEY;
209
233
  if (stores.errors[key]) return stores.errors[key];
210
234
  let error: string | null = null;
211
- const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : setAlertsJson;
235
+ const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : key === RELATIVE_ALERTS_KEY ? setRelativeJson : setAlertsJson;
212
236
  setJson((currentJson: unknown) => {
213
237
  error = readAlertsStoreError(currentJson);
214
238
  return error ? currentJson : serializeAlerts(next(deserializeAlerts(currentJson)));
215
239
  });
216
240
  return error;
217
- }, [alerts, setAlertsJson, setMetricsJson, stores]);
241
+ }, [alerts, setAlertsJson, setMetricsJson, setRelativeJson, stores]);
218
242
 
219
243
  const deleteAlert = useCallback(async (id: string) => {
220
244
  const alert = alerts.find((a) => a.id === id);
@@ -230,15 +254,24 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
230
254
  notify({ body: error, type: "error" });
231
255
  return;
232
256
  }
257
+ setAlertLog((saved: unknown) => withAlertChange(saved, "deleted", alert));
233
258
  setSelectedIdx((prev) => Math.max(0, Math.min(prev, rows.length - 2)));
234
- }, [alerts, dialog, notify, rows.length, savePaneAlerts]);
259
+ }, [alerts, dialog, notify, rows.length, savePaneAlerts, setAlertLog]);
235
260
 
236
261
  const rearmAlert = useCallback((id: string) => {
237
- const error = savePaneAlerts(id,
238
- (current) => current.map((a) => (a.id === id ? rebuildAlert(a) : a)),
239
- );
240
- if (error) notify({ body: error, type: "error" });
241
- }, [notify, savePaneAlerts]);
262
+ // Only an alert that went off is re-armed, as the store reads at the write: one still waiting is left, and not logged.
263
+ const rearmed: AlertRule[] = [];
264
+ const error = savePaneAlerts(id, (current) => current.map((a) => {
265
+ if (a.id !== id || a.status !== "triggered") return a;
266
+ rearmed.push(a);
267
+ return rebuildAlert(a);
268
+ }));
269
+ if (error) {
270
+ notify({ body: error, type: "error" });
271
+ return;
272
+ }
273
+ for (const alert of rearmed) setAlertLog((saved: unknown) => withAlertChange(saved, "rearmed", alert));
274
+ }, [notify, savePaneAlerts, setAlertLog]);
242
275
 
243
276
  const startAddAlert = useCallback(() => {
244
277
  openPluginCommandWorkflow("set-alert");
@@ -276,11 +309,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
276
309
  description: "SYMBOL above|below|crosses PRICE",
277
310
  placeholder: "AAPL above 200",
278
311
  }}
279
- currentValue={selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
312
+ currentValue={relativeAlertText(selected) ?? (selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`)}
280
313
  onApply={async (value) => {
281
- const parsed = parseAlertCommandValues({ shortcut: value });
282
- if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
283
- if (!!parsed.metric !== !!selected.metric) throw new Error("Add a new alert to change between price and a valuation or technical measure.");
314
+ const parsed = parseAlertEdit(selected, value);
284
315
  const error = savePaneAlerts(selected.id, (current) => current.map((alert) => (
285
316
  alert.id === selected.id
286
317
  ? { ...editAlert(alert, parsed.symbol, parsed.condition, parsed.price), ...(parsed.metric ? { metric: parsed.metric } : {}) }
@@ -382,6 +413,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
382
413
  case "current":
383
414
  return {
384
415
  text: formatCurrentPrice(alert, column.width),
416
+ // The quadrant word exports with the readings that place it.
417
+ ...(alert.metric?.kind === "rrg" && !alert.lastCheckError && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null
418
+ ? { value: metricReadingText(alert.metric, { value: alert.lastCheckedPrice, target: alert.lastCheckedTarget }) } : {}),
385
419
  color: alert.lastCheckError ? colors.negative : colors.text,
386
420
  };
387
421
  case "target":
@@ -392,10 +426,12 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
392
426
  color: colors.textDim,
393
427
  };
394
428
  case "condition":
395
- return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : conditionLabel(alert.condition) };
429
+ return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : alert.metric?.kind === "rrg" ? "Enters" : conditionLabel(alert.condition) };
396
430
  case "quote":
397
431
  return {
398
432
  text: formatQuoteChecked(alert),
433
+ // A settled close exports as its session date.
434
+ ...(alert.metric && closeBasis(alert.metric) && alert.lastDailyClose && !alert.lastCheckError ? { value: alert.lastDailyClose } : {}),
399
435
  color: alert.lastCheckError ? colors.negative : colors.textDim,
400
436
  };
401
437
  case "triggered":
@@ -403,6 +439,8 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
403
439
  text: alert.triggeredAt ? relativeTime(alert.triggeredAt) : "-",
404
440
  color: colors.textDim,
405
441
  };
442
+ case "created":
443
+ return { text: isoUtc(alert.createdAt).slice(0, 10), color: colors.textDim };
406
444
  }
407
445
  }, []);
408
446
 
@@ -2,6 +2,7 @@ import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { Quote } from "../../../types/financials";
3
3
  import { getActiveQuoteDisplay } from "../../../market-data/market/status";
4
4
  import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
5
+ import { canonicalExchange } from "../../../utils/exchanges";
5
6
  import type { AlertRule } from "./types";
6
7
 
7
8
  export function normalizeAlertSymbol(value: string | null | undefined): string {
@@ -46,13 +47,18 @@ export function quoteAlertFields(
46
47
  | "lastQuoteUpdatedAt"
47
48
  | "lastQuoteSource"
48
49
  | "lastQuoteProviderId"
50
+ | "lastQuoteCurrency"
51
+ | "lastQuoteExchange"
49
52
  > & { lastCheckError?: undefined } {
53
+ const exchange = quote.listingExchangeName || quote.exchangeName;
50
54
  return {
51
55
  lastCheckedPrice: quote.price,
52
56
  lastCheckedAt: checkedAt,
53
57
  lastQuoteUpdatedAt: quote.lastUpdated,
54
58
  lastQuoteSource: quote.dataSource,
55
59
  lastQuoteProviderId: quote.providerId,
60
+ ...(quote.currency ? { lastQuoteCurrency: quote.currency } : {}),
61
+ ...(exchange ? { lastQuoteExchange: canonicalExchange(exchange) } : {}),
56
62
  lastCheckError: undefined,
57
63
  };
58
64
  }
@@ -1,4 +1,4 @@
1
- import { METRIC_ALERTS_KEY } from "./metric-model";
1
+ import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, metricAlertsKey } from "./metric-model";
2
2
  import type { AppConfig } from "../../../types/config";
3
3
  import type { GloomPluginContext } from "../../../types/plugin";
4
4
  import {
@@ -21,8 +21,9 @@ export async function saveAlerts(
21
21
  scope: "all" | "price" | "metric" = "all",
22
22
  ): Promise<void> {
23
23
  const stores = readLevelAlertStores((key) => ctx.configState.get<unknown>(key));
24
- const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key]) => scope === "all"
25
- || key === (scope === "price" ? ALERTS_KEY : METRIC_ALERTS_KEY));
24
+ // An unreadable list is never written over; one that has nothing to write does not hold up the others.
25
+ const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key, value]) => (scope === "all"
26
+ || (scope === "price" ? key === ALERTS_KEY : key !== ALERTS_KEY)) && !(stores.errors[key] && value === "[]"));
26
27
  for (const [key] of entries) if (stores.errors[key]) throw new Error(stores.errors[key]!);
27
28
  for (const [key, value] of entries) {
28
29
  if (ctx.configState.get<unknown>(key) !== value) await ctx.configState.set(key, value);
@@ -46,17 +47,20 @@ export function readAlertStores(config: AppConfig) {
46
47
  export function readLevelAlertStores(get: (key: string) => unknown): { alerts: AlertRule[]; error: string | null; errors: Record<string, string | null> } {
47
48
  const alerts: AlertRule[] = [];
48
49
  const errors: Record<string, string | null> = {};
49
- for (const key of [ALERTS_KEY, METRIC_ALERTS_KEY]) {
50
+ for (const key of LEVEL_ALERT_KEYS) {
50
51
  const raw = get(key);
51
52
  let error = readAlertsStoreError(raw);
52
53
  const rows = error ? [] : deserializeAlerts(raw);
53
- if (rows.some(a => Boolean(a.metric) !== (key === METRIC_ALERTS_KEY))) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
54
- errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null : error;
54
+ if (rows.some(a => levelAlertStoreKey(a) !== key)) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
55
+ errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
56
+ : key === RELATIVE_ALERTS_KEY ? error?.replaceAll("price alerts", "relative strength alerts") ?? null : error;
55
57
  if (!error) alerts.push(...rows);
56
58
  }
57
59
  return { alerts, errors, error: Object.values(errors).filter(Boolean).join(" ") || null };
58
60
  }
59
61
  export function writeLevelAlertStores(alerts: AlertRule[]): Record<string, string> {
60
- return { [ALERTS_KEY]: serializeAlerts(alerts.filter(a => !a.metric)),
61
- [METRIC_ALERTS_KEY]: serializeAlerts(alerts.filter(a => a.metric)) };
62
+ return Object.fromEntries(LEVEL_ALERT_KEYS.map(key => [key, serializeAlerts(alerts.filter(a => levelAlertStoreKey(a) === key))]));
62
63
  }
64
+ /** Price, valuation/technical and relative strength rules each keep their own synced list. */
65
+ const LEVEL_ALERT_KEYS = [ALERTS_KEY, METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY] as const;
66
+ export const levelAlertStoreKey = (alert: Pick<AlertRule, "metric">): string => alert.metric ? metricAlertsKey(alert.metric) : ALERTS_KEY;
@@ -23,5 +23,9 @@ export interface AlertRule {
23
23
  lastQuoteUpdatedAt?: number;
24
24
  lastQuoteSource?: QuoteDataSource;
25
25
  lastQuoteProviderId?: string;
26
+ /** The currency of the quote the alert was last checked on (USD): its listing's, which lists show beside it. */
27
+ lastQuoteCurrency?: string;
28
+ /** The venue that quote was from (ARCA), which names the listing of a rule set by symbol alone. */
29
+ lastQuoteExchange?: string;
26
30
  message?: string;
27
31
  }
@@ -1,6 +1,7 @@
1
1
  import { usePaneSettingValue } from "../../../public/react";
2
2
  import { PortfolioRiskPane } from "./risk-pane";
3
3
  import { portfolioRiskHeadless } from "./risk-headless";
4
+ import { NO_HOLDINGS_HINT } from "./risk-model";
4
5
  import { portfolioRiskCache } from "./risk-client";
5
6
  import { Box, Text } from "../../../ui";
6
7
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -398,7 +399,7 @@ function LegacyPortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
398
399
  message={accountsError ?? undefined}
399
400
  hint={accountsError
400
401
  ? "Reconnect the broker in the Brokers pane (BR), then refresh."
401
- : "Add holdings from the Portfolio pane (PF), or connect a broker in BR to sync them."}
402
+ : NO_HOLDINGS_HINT}
402
403
  />
403
404
  </Box>
404
405
  ) : historyOnly ? (
@@ -58,7 +58,12 @@ interface RiskMarketHistory {
58
58
  * Set when the listing quotes in another currency and its closes were
59
59
  * restated in USD at same-date daily FX closes; quote and closeMark are then in USD.
60
60
  */
61
- converted?: { currency: string; fxAsOf: string } | null;
61
+ converted?: {
62
+ currency: string;
63
+ fxAsOf: string;
64
+ /** US sessions converted at the FX close before them, the pair having none that day (bridgedFxClose). */
65
+ fxBridged?: string[];
66
+ } | null;
62
67
  error: string | null;
63
68
  }
64
69
  export interface RiskMarketSnapshot {
@@ -162,6 +167,8 @@ export function validateRiskHistory(
162
167
  closeMark: { price: number; date: string; currency: string } | null;
163
168
  /** Latest completed close in the returns' currency, whatever marks the holding. */
164
169
  latestClose: { price: number; date: string };
170
+ /** With a conversion, the US sessions converted at the FX close before them (bridgedFxClose). */
171
+ fxBridged?: string[];
165
172
  } {
166
173
  if (
167
174
  response.status !== "success" ||
@@ -244,13 +251,14 @@ export function validateRiskHistory(
244
251
  if (local.integrity)
245
252
  throw new Error("Daily history has inconsistent OHLC observations");
246
253
  // A converted series is on the NYSE calendar the basket and SPY share.
247
- const series = conversion
254
+ const usd = conversion
248
255
  ? convertClosesToUsd(
249
256
  points.map((point) => ({ date: new Date(point.date).toISOString().slice(0, 10), close: point.close })),
250
257
  conversion.fx,
251
258
  conversion.divisor,
252
259
  )
253
- : prices;
260
+ : null;
261
+ const series = usd ? usd.points : prices;
254
262
  const returns = conversion ? resolveDatedReturns(series).returns : local.returns;
255
263
  const cadence = qualifySharpeCadence(returns, [
256
264
  { symbol: instrument.symbol, exchange: conversion ? "NYSE" : exchange, history: series },
@@ -264,6 +272,7 @@ export function validateRiskHistory(
264
272
  };
265
273
  return {
266
274
  returns,
275
+ ...(usd?.bridged.length ? { fxBridged: usd.bridged } : {}),
267
276
  asOf: latestClose.date,
268
277
  // A missing or stale quote is not a current mark; the latest dated close is.
269
278
  closeMark:
@@ -591,7 +600,7 @@ export async function fetchPortfolioRiskMarket(
591
600
  historyParams,
592
601
  signal ? { signal } : undefined,
593
602
  );
594
- const { latestClose, ...resolved } = validateRiskHistory(
603
+ const { latestClose, fxBridged, ...resolved } = validateRiskHistory(
595
604
  response,
596
605
  instrument,
597
606
  quote,
@@ -610,7 +619,9 @@ export async function fetchPortfolioRiskMarket(
610
619
  quote: current,
611
620
  currency: "USD",
612
621
  closeMark: current ? null : { ...latestClose, currency: "USD" },
613
- converted: { currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf },
622
+ converted: {
623
+ currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf, ...(fxBridged?.length ? { fxBridged } : {}),
624
+ },
614
625
  error: null,
615
626
  };
616
627
  continue;