gloomberb 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +31 -6
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -1,6 +1,11 @@
1
+ import { METRIC_CHOICES, benchmarkKey, metricWizardFields, parseMetricAlert } from "./metric-command";
2
+ import { firstDailyCheck, metricAvailabilityError, metricReadings } from "./metric-data";
3
+ import { alertLogRule } from "./alert-log";
4
+ import { closeBasis, isDailyMetric, isRelativeMetric, metricReadingText, metricTrigger, validMetricRule, type MetricReading } from "./metric-model";
5
+ import { canonicalExchange } from "../../../utils/exchanges";
1
6
  import { startCatalystAlerts } from "./catalysts";
2
7
  import { notTheCoinNote } from "../../../cli/commands/crypto-hints";
3
- import { createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
8
+ import { alertChangeEntry, createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
4
9
  import { checkIvLevelAlertsNow, startIvLevelAlerts } from "./iv-level";
5
10
  import { logFailure } from "../../../utils/log-failure";
6
11
  import { RESEARCH_FORM_KINDS, RESEARCH_LABELS, researchWizardFields, createResearchAlert } from "./research-builder";
@@ -19,12 +24,15 @@ import { isResearchAlertKind, MAX_LEAD_DAYS } from "./research-rules";
19
24
  import { EVENT_ALERT_TEMPLATES, MAX_EVENT_ALERTS, createEventAlert, eventAlertStoreKey, readEventAlertStores, updateEventAlerts } from "./events";
20
25
  import { formatMarketPrice } from "../../../market-data/market/format";
21
26
  import { settleTypedListingKey } from "../../../tickers/listing-venues";
27
+ import { parseListingArg } from "../../../cli/listing-arg";
28
+ import { notATickerSymbol } from "../../../sources/provider-errors";
22
29
  import type { Quote } from "../../../types/financials";
23
30
  import type { GloomPlugin } from "../../../types/plugin";
24
31
  import {
25
32
  createAlert,
26
33
  evaluateAlert,
27
34
  formatAlertDescription,
35
+ sameLevelRule,
28
36
  } from "./alert-engine";
29
37
  import {
30
38
  parseAlertCommandValues,
@@ -62,11 +70,21 @@ let alertStream: AlertQuoteStream | null = null;
62
70
  */
63
71
  const STREAM_PERSIST_MS = 60_000;
64
72
 
73
+ /** BTC/USD, BRK/B and EUR/USD are one listing written with a slash, as `alerts add` reads them; XLK/SPY is not. */
74
+ function slashSpellsListing(symbol: string): boolean {
75
+ try {
76
+ return parseListingArg(symbol).key !== symbol.toUpperCase();
77
+ } catch {
78
+ // Intentional ignore: text the listing reader refuses is no pair to divide, so the quote's own error stands.
79
+ return true;
80
+ }
81
+ }
82
+
65
83
  export const alertsPlugin: GloomPlugin = {
66
84
  id: "alerts",
67
85
  name: "Alerts",
68
86
  version: "1.0.0",
69
- description: "Price, earnings, filing, news and flow alerts",
87
+ description: "Price, valuation, daily technical and event alerts",
70
88
  toggleable: true,
71
89
  cliCommands: [alertsCliCommand],
72
90
 
@@ -114,7 +132,7 @@ export const alertsPlugin: GloomPlugin = {
114
132
  let persist = false;
115
133
  let touched = false;
116
134
  for (const alert of alerts) {
117
- if (alert.status !== "active") continue;
135
+ if (alert.metric || alert.status !== "active") continue;
118
136
  const quote = readStreamedAlertQuote(alert, undefined, now);
119
137
  if (!quote) continue;
120
138
  // A re-armed or new alert has no stored baseline: judge nothing on this
@@ -136,7 +154,7 @@ export const alertsPlugin: GloomPlugin = {
136
154
  }
137
155
  if (!persist && !(touched && now - lastStreamPersistAt >= STREAM_PERSIST_MS)) return;
138
156
  lastStreamPersistAt = now;
139
- void saveAlerts(ctx, alerts).catch(logFailure("alerts.stream", "Save streamed price alerts failed"));
157
+ void saveAlerts(ctx, alerts, "price").catch(logFailure("alerts.stream", "Save streamed price alerts failed"));
140
158
  if (persist) alertStream?.sync();
141
159
  };
142
160
  alertStream?.dispose();
@@ -144,8 +162,8 @@ export const alertsPlugin: GloomPlugin = {
144
162
  ctx.registerCommand({
145
163
  id: "set-alert",
146
164
  label: "Add Alert",
147
- description: "Create a price alert from a symbol, condition, and target price",
148
- keywords: ["add", "set", "alert", "price", "trigger", "notify", "alarm", "watch"],
165
+ description: "Create a price, valuation or daily technical alert",
166
+ keywords: ["add", "set", "alert", "pe", "ps", "rsi", "moving average", "valuation", "price", "trigger", "notify", "alarm", "watch"],
149
167
  category: "data",
150
168
  shortcut: "SA",
151
169
  shortcutArg: {
@@ -155,6 +173,7 @@ export const alertsPlugin: GloomPlugin = {
155
173
  },
156
174
  wizardLayout: "form",
157
175
  wizard: [
176
+ { key: "kind", label: "Kind", type: "select", defaultValue: "price", options: METRIC_CHOICES },
158
177
  {
159
178
  key: "symbol",
160
179
  label: "Symbol",
@@ -162,6 +181,7 @@ export const alertsPlugin: GloomPlugin = {
162
181
  type: "text",
163
182
  },
164
183
  {
184
+ dependsOn: { key: "kind", value: "price" },
165
185
  key: "condition",
166
186
  label: "Condition",
167
187
  type: "select",
@@ -172,34 +192,84 @@ export const alertsPlugin: GloomPlugin = {
172
192
  ],
173
193
  },
174
194
  {
195
+ dependsOn: { key: "kind", value: "price" },
175
196
  key: "price",
176
197
  label: "Target Price",
177
198
  placeholder: "200.00",
178
199
  type: "number",
179
200
  },
201
+ ...metricWizardFields,
180
202
  ],
181
203
  async execute(values) {
182
- const input = parseAlertCommandValues(values);
204
+ let input = parseAlertCommandValues(values);
183
205
  if (!input) throw new Error("Use a symbol, condition, and target price.");
184
206
  // 7203:TSE, typed or from a link, is the venue its symbol trades on, as `alerts add` reads it.
185
- const symbol = (await settleTypedListingKey(input.symbol, { store: ctx.tickerRepository, dataProvider: ctx.marketData })).toUpperCase();
186
-
187
- const quote = await resolveAlertQuote(ctx.marketData, symbol);
207
+ const quoteFor = async (typed: string) => {
208
+ const key = (await settleTypedListingKey(typed, { store: ctx.tickerRepository, dataProvider: ctx.marketData })).toUpperCase();
209
+ return { key, quote: await resolveAlertQuote(ctx.marketData, key) };
210
+ };
211
+ let resolved: Awaited<ReturnType<typeof quoteFor>>;
212
+ try {
213
+ resolved = await quoteFor(input.symbol);
214
+ } catch (error) {
215
+ // XLK/SPY is a listing first; only one the service says is no ticker is the ratio of the two, as `alerts add` reads it.
216
+ // A timeout stays a timeout, and BTC/USD, BRK/B or EUR/USD, one listing written with a slash, keeps its own answer.
217
+ const pair = !input.metric && /^[^/\s]+\/[^/\s]+$/.test(input.symbol) && notATickerSymbol(error instanceof Error ? error.message : String(error))
218
+ && !slashSpellsListing(input.symbol) ? parseMetricAlert(`${input.symbol} ratio ${input.condition} ${input.price}`) : null;
219
+ if (!pair) throw error;
220
+ input = pair;
221
+ resolved = await quoteFor(pair.symbol);
222
+ }
223
+ const { key: symbol, quote } = resolved;
224
+ let metric = input.metric;
225
+ if (metric && isRelativeMetric(metric)) {
226
+ const reference = (await quoteFor(benchmarkKey(metric))).quote;
227
+ const benchmarkExchange = canonicalExchange(reference.listingExchangeName || reference.exchangeName || "");
228
+ const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName || "");
229
+ const benchmark = reference.symbol || metric.benchmark;
230
+ if (benchmark === (quote.symbol || symbol) && benchmarkExchange === exchange) throw new Error(`Compare ${benchmark} with another listing.`);
231
+ metric = { ...metric, benchmark, ...(benchmarkExchange ? { benchmarkExchange } : {}) };
232
+ // Never save a rule its own list would refuse to read back.
233
+ if (!validMetricRule({ metric, condition: input.condition, targetPrice: input.price })) throw new Error(`${benchmark} cannot be a benchmark: relative strength alerts compare two US listings.`);
234
+ }
188
235
  // A target ten times away is a slip or another instrument's price, such as bitcoin's on the BTC fund.
189
236
  const far = farTargetMessage(symbol, input.price, quote.price, symbol.includes(":") ? null : notTheCoinNote(symbol, quote));
190
- if (far) throw new Error(far);
237
+ if (!input.metric && far) throw new Error(far);
191
238
 
192
- const alert = {
239
+ const alert: AlertRule = {
193
240
  ...createAlert(quote.symbol || symbol, input.condition, input.price),
194
- ...quoteAlertFields(quote),
241
+ ...(!metric ? quoteAlertFields(quote) : {
242
+ metric, exchange: quote.listingExchangeName || quote.exchangeName,
243
+ ...quote.currency ? { lastQuoteCurrency: quote.currency } : {},
244
+ }),
195
245
  };
246
+ // A close not yet final is read once it is: the rule is kept and says when it is first judged.
247
+ let firstCheck: string | null = null;
248
+ if (metric) {
249
+ const now = Date.now();
250
+ if (isDailyMetric(metric)) {
251
+ const check = firstDailyCheck(alert.exchange, now, metric.kind === "rrg");
252
+ if (check) firstCheck = `first checked on the ${check.day} ${closeBasis(metric) === "weekly close" ? "weekly close" : "close"}, from ${new Date(check.at).toISOString().slice(11, 16)} UTC`;
253
+ }
254
+ try {
255
+ const reading = await metricReadings(ctx.marketData, now)(alert);
256
+ Object.assign(alert, { lastCheckedPrice: reading.value, lastCheckedTarget: reading.target, lastQuoteUpdatedAt: reading.at,
257
+ lastCheckedAt: now, ...reading.day ? { lastDailyClose: reading.day } : {} });
258
+ } catch (error) {
259
+ if (!isDailyMetric(metric) || metricAvailabilityError(alert)) throw error;
260
+ }
261
+ }
196
262
  const existing = loadAlerts(ctx);
263
+ // The same rule on the same listing is one alert: adding it again names it and adds nothing.
264
+ const duplicate = existing.find((entry) => sameLevelRule(entry, alert));
265
+ if (duplicate) throw new Error(`${formatAlertDescription(alert)} already exists (alert ${duplicate.id}). Nothing was added.`);
197
266
  existing.push(alert);
198
- await saveAlerts(ctx, existing);
267
+ await saveAlerts(ctx, existing, input.metric ? "metric" : "price");
268
+ recordFired(alertChangeEntry("created", alertLogRule(alert)));
199
269
  alertStream?.sync();
200
270
 
201
271
  ctx.notify({
202
- body: `Alert set: ${formatAlertDescription(alert)} (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`,
272
+ body: `Alert set: ${formatAlertDescription(alert)}${input.metric ? firstCheck ? `, ${firstCheck}` : "" : ` (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`}`,
203
273
  type: "success",
204
274
  });
205
275
  },
@@ -270,6 +340,7 @@ export const alertsPlugin: GloomPlugin = {
270
340
  // An IV level goes in its own list, which released versions never read.
271
341
  const key = eventAlertStoreKey(rule);
272
342
  await ctx.configState.set(key, updateEventAlerts(ctx.configState.get<string>(key), (rules) => [...rules, rule]));
343
+ recordFired(alertChangeEntry("created", alertLogRule(rule)));
273
344
  ctx.notify({ body: "Event alert saved.", type: "success" });
274
345
  if (scheduled) checkScheduledEventAlertsNow();
275
346
  if (rule.kind === "iv_level") checkIvLevelAlertsNow();
@@ -278,16 +349,16 @@ export const alertsPlugin: GloomPlugin = {
278
349
 
279
350
  // The poll carries whatever the stream does not: symbols the feed has not
280
351
  // delivered lately, and every symbol while the app is hidden.
281
- const poll = async () => {
352
+ const pollLevels = async (metrics: boolean) => {
282
353
  alertStream?.sync();
283
- const pending = loadAlerts(ctx).filter((alert) => alert.status === "active" && !readStreamedAlertQuote(alert));
354
+ const pending = loadAlerts(ctx).filter((alert) => Boolean(alert.metric) === metrics && alert.status === "active" && !readStreamedAlertQuote(alert));
284
355
  if (pending.length === 0) return;
285
356
 
286
357
  ctx.log.info("poll", { active: pending.length });
287
358
 
288
359
  // One batched pass over the distinct symbols: the alerts pane reads the same
289
360
  // persisted store, so this is the only place that talks to the provider.
290
- const quoteKeys = [...new Set(pending.map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
361
+ const quoteKeys = [...new Set(pending.filter(alert => !alert.metric).map((alert) => `${alert.symbol}\0${alert.exchange ?? ""}`))];
291
362
  const results = await Promise.all(quoteKeys.map(async (key): Promise<[string, Quote | string]> => {
292
363
  const [symbol, exchange = ""] = key.split("\0");
293
364
  try {
@@ -297,15 +368,50 @@ export const alertsPlugin: GloomPlugin = {
297
368
  return [key, createQuoteErrorMessage(symbol ?? "", err)];
298
369
  }
299
370
  }));
371
+ const readMetric = metricReadings(ctx.marketData);
372
+ const readings = new Map<string, { armedAt: number; reading: MetricReading | string }>();
373
+ for (const alert of pending.filter(a => a.metric)) {
374
+ try { readings.set(alert.id, { armedAt: alert.armedAt ?? alert.createdAt, reading: await readMetric(alert) }); }
375
+ catch (error) { readings.set(alert.id, { armedAt: alert.armedAt ?? alert.createdAt, reading: error instanceof Error ? error.message : String(error) }); }
376
+ }
300
377
  if (generation !== pollGeneration) return;
301
378
  const quotes = new Map<string, Quote | string>(results);
302
379
 
303
380
  // Read the store again: the stream or the pane may have changed an alert
304
381
  // while the quotes were in flight, and an alert must trigger only once.
305
- const alerts = loadAlerts(ctx);
382
+ const alerts = loadAlerts(ctx).filter(alert => Boolean(alert.metric) === metrics);
306
383
  let changed = false;
307
384
  for (const alert of alerts) {
308
385
  if (alert.status !== "active") continue;
386
+ if (alert.metric) {
387
+ const result = readings.get(alert.id);
388
+ if (!result || result.armedAt !== (alert.armedAt ?? alert.createdAt)) continue;
389
+ const reading = result.reading;
390
+ if (typeof reading !== "string" && reading.day && reading.day === alert.lastDailyClose && !alert.lastCheckError) continue;
391
+ if (typeof reading === "string" && reading === alert.lastCheckError) continue;
392
+ if (typeof reading === "string") Object.assign(alert, quoteErrorAlertFields(reading));
393
+ else {
394
+ if (metricTrigger(alert, reading)) {
395
+ const firedAt = Date.now();
396
+ alert.status = "triggered"; alert.triggeredAt = firedAt;
397
+ const description = formatAlertDescription(alert);
398
+ const readingText = metricReadingText(alert.metric, reading);
399
+ recordFired({ kind: alert.metric.kind, ruleId: alert.id, rule: description, firedAt, value: reading.value,
400
+ unit: alert.metric.kind === "pe" || alert.metric.kind === "ps" ? "x" : undefined,
401
+ // An RRG quadrant is two readings; the figure is strength, the detail says both.
402
+ ...(alert.metric.kind === "rrg" ? { detail: readingText } : {}),
403
+ quoteAt: firedAlertTime(reading.at), source: closeBasis(alert.metric),
404
+ symbol: alert.symbol, exchange: alert.exchange });
405
+ ctx.notify({ body: alert.metric.kind === "rrg" ? `${description} triggered: ${readingText}` : `${description} triggered at ${readingText}`, type: "success",
406
+ refId: `price:${alert.id}`, desktop: "always", persistent: true, sound: "Glass",
407
+ action: { label: "Open", onClick: () => ctx.showPane("alerts") } });
408
+ }
409
+ Object.assign(alert, { lastCheckedPrice: reading.value, lastCheckedTarget: reading.target,
410
+ lastDailyClose: reading.day, lastQuoteUpdatedAt: reading.at, lastCheckedAt: Date.now(), lastCheckError: undefined });
411
+ }
412
+ changed = true;
413
+ continue;
414
+ }
309
415
  if (readStreamedAlertQuote(alert)) continue;
310
416
  const quote = quotes.get(`${alert.symbol}\0${alert.exchange ?? ""}`);
311
417
  if (quote === undefined) continue;
@@ -327,10 +433,17 @@ export const alertsPlugin: GloomPlugin = {
327
433
  }
328
434
 
329
435
  if (changed) {
330
- await saveAlerts(ctx, alerts);
436
+ await saveAlerts(ctx, alerts, metrics ? "metric" : "price");
331
437
  }
332
438
  };
333
439
 
440
+ // Metric work may span a poll, but cannot delay the next price check or overlap itself.
441
+ let metricPoll: Promise<void> | null = null;
442
+ const poll = () => {
443
+ metricPoll ??= pollLevels(true).catch(logFailure("alerts.metrics", "Check metric alerts failed")).finally(() => { metricPoll = null; });
444
+ return pollLevels(false);
445
+ };
446
+
334
447
  // Re-armed each cycle so a change to the pane's Check interval setting takes
335
448
  // effect on the next tick without a restart.
336
449
  const scheduleNextPoll = () => {
@@ -377,9 +490,9 @@ export const alertsPlugin: GloomPlugin = {
377
490
  id: "alerts-pane",
378
491
  paneId: "alerts",
379
492
  label: "Alerts",
380
- description: "Price, earnings, filing, news and flow alerts",
493
+ description: "Price, valuation, daily technical and event alerts",
381
494
  keywords: [
382
- "alerts", "alert", "price", "trigger", "alarm", "watch", "notify", "notification",
495
+ "alerts", "alert", "price", "valuation", "pe", "ps", "rsi", "moving average", "trigger", "alarm", "watch", "notify", "notification",
383
496
  "earnings", "cpi", "fomc", "filing", "filings", "sec", "8-k", "13f", "news", "keyword",
384
497
  "analyst", "upgrade", "downgrade", "52-week", "unusual volume", "volume", "short interest",
385
498
  "insider", "congress", "catalyst", "implied volatility", "iv spike", "options flow", "perps", "funding",
@@ -39,11 +39,11 @@ export function streamedAlertQuote(quote: Quote | null | undefined, now = Date.n
39
39
  }
40
40
 
41
41
  export function readStreamedAlertQuote(
42
- alert: Pick<AlertRule, "symbol" | "exchange">,
42
+ alert: Pick<AlertRule, "symbol" | "exchange" | "metric">,
43
43
  coordinator: MarketDataCoordinator | null = getSharedMarketDataCoordinator(),
44
44
  now = Date.now(),
45
45
  ): Quote | null {
46
- if (!coordinator) return null;
46
+ if (alert.metric || !coordinator) return null;
47
47
  return streamedAlertQuote(resolveEntryData(coordinator.getQuoteEntry(alertInstrument(alert))), now);
48
48
  }
49
49
 
@@ -93,7 +93,7 @@ export function createAlertQuoteStream({
93
93
  const next = getSharedMarketDataCoordinator();
94
94
  const instruments = new Map<string, InstrumentRef>();
95
95
  for (const alert of readAlerts()) {
96
- if (alert.status !== "active") continue;
96
+ if (alert.metric || alert.status !== "active") continue;
97
97
  const instrument = alertInstrument(alert);
98
98
  instruments.set(buildQuoteKey(instrument), instrument);
99
99
  }
@@ -0,0 +1,134 @@
1
+ import type { AlertMetric, RotationQuadrant } from "./metric-model";
2
+ import { ROTATION_QUADRANTS, validMetricRule } from "./metric-model";
3
+ import { readAlertPrice } from "./price";
4
+ import type { AlertCondition } from "./types";
5
+ import type { WizardStep } from "../../../types/plugin";
6
+ import { parsePublicTickerKey } from "../../../utils/exchanges";
7
+
8
+ export interface MetricAlertInput { symbol: string; condition: AlertCondition; price: number; metric: AlertMetric }
9
+ const direction = (s: string) => /^(above|over|>)$/i.test(s) ? "above" as const : /^(below|under|<)$/i.test(s) ? "below" as const : null;
10
+ const USAGE = "Use AAPL pe below 25, AAPL ps above 8, NVDA rsi(14) under 30, SPY ma(50) crosses above ma(200), or TSLA closes below ma(200).";
11
+ function checked(input: MetricAlertInput): MetricAlertInput {
12
+ if (!validMetricRule({ ...input, targetPrice: input.price })) throw new Error("Use a positive multiple, RSI from 0 to 100, and whole MA/RSI periods from 2 to 200 (fast MA shorter than slow MA).");
13
+ return input;
14
+ }
15
+ // RRG's own default benchmark (relative-rotation/model.ts), kept here so the CLI does not load the RRG pane's model.
16
+ const DEFAULT_ROTATION_BENCHMARK = "SPY:NYSEARCA";
17
+ const RELATIVE_USAGE = "Relative strength alerts take the ratio of two listings or an RRG quadrant: alerts add XLK/SPY above 0.25, or alerts add XLE rrg leading [--benchmark SPY].";
18
+ const QUADRANT_USAGE = "An RRG alert names the quadrant it enters: leading, weakening, lagging or improving, such as alerts add XLE rrg leading [--benchmark SPY].";
19
+ /** A benchmark typed as SPY or SPY:NYSEARCA; its listing is confirmed when the alert is added. */
20
+ function benchmarkListing(text: string): { benchmark: string; benchmarkExchange?: string } {
21
+ const parsed = parsePublicTickerKey(text.toUpperCase());
22
+ if (!/^[A-Z0-9.^=-]{1,20}$/.test(parsed.symbol)) throw new Error(`Not a benchmark listing: ${text}.`);
23
+ return { benchmark: parsed.symbol, ...(parsed.exchange ? { benchmarkExchange: parsed.exchange } : {}) };
24
+ }
25
+ const sameListing = (symbol: string, metric: { benchmark: string }) => parsePublicTickerKey(symbol).symbol === metric.benchmark;
26
+ /**
27
+ * `XLK/SPY ratio above 0.25` and `XLE rrg leading [vs SPY]`; undefined for anything else. `XLK/SPY above 0.25`
28
+ * without the word is first a listing (BRK/B, EUR/USD), so `alerts add` reads it as a ratio only once it quotes nothing.
29
+ */
30
+ function parseRelativeAlert(parts: readonly string[]): MetricAlertInput | undefined {
31
+ const [first = "", second = "", ...rest] = parts;
32
+ const kind = second.toLowerCase();
33
+ if (kind === "rrg") {
34
+ const [quadrant = "", vs, benchmark, ...extra] = rest;
35
+ if (first.includes("/")) throw new Error(`Name the benchmark of an RRG alert after it: alerts add ${first.split("/")[0]!.toUpperCase()} rrg ${quadrant.toLowerCase() || "leading"} --benchmark ${first.split("/")[1]!.toUpperCase()}.`);
36
+ if (!(ROTATION_QUADRANTS as readonly string[]).includes(quadrant.toLowerCase())) throw new Error(QUADRANT_USAGE);
37
+ if (vs !== undefined && (!/^(vs\.?|versus|against)$/i.test(vs) || !benchmark || extra.length > 0)) throw new Error(QUADRANT_USAGE);
38
+ const metric = { kind: "rrg" as const, quadrant: quadrant.toLowerCase() as RotationQuadrant, ...benchmarkListing(benchmark ?? DEFAULT_ROTATION_BENCHMARK) };
39
+ if (sameListing(first, metric)) throw new Error(`An RRG alert compares ${first.toUpperCase()} with another listing: alerts add ${first.toUpperCase()} rrg ${metric.quadrant} --benchmark SPY.`);
40
+ return checked({ symbol: first.toUpperCase(), condition: "crosses", price: 0, metric });
41
+ }
42
+ const pair = /^([^/]+)\/([^/]+)$/.exec(first);
43
+ if (pair && kind === "ratio") {
44
+ const [typed = "", level, ...extra] = rest;
45
+ const condition = direction(typed) ?? (/^(crosses|cross|x)$/i.test(typed) ? "crosses" as const : null);
46
+ const example = `alerts add ${first.toUpperCase()} above 0.25`;
47
+ if (!condition || level === undefined || extra.length > 0) throw new Error(`A ratio alert takes above, below or crosses and a ratio: ${example}.`);
48
+ const metric = { kind: "rs_ratio" as const, ...benchmarkListing(pair[2]!) };
49
+ if (sameListing(pair[1]!, metric)) throw new Error(`A ratio alert divides one listing by another: ${example}.`);
50
+ const price = readAlertPrice(level);
51
+ if (!(price > 0)) throw new Error(`A ratio alert takes a positive ratio: ${example}.`);
52
+ return checked({ symbol: pair[1]!.toUpperCase(), condition, price, metric });
53
+ }
54
+ // `XLE rs above 100`, `XLE vs SPY above 1`, `XLE relative strength leading`: a relative alert typed another way.
55
+ if (/^(rs|rsr|rs-?ratio|rs_ratio|rs-?momentum|rel|relative|rotation|quadrant|ratio|vs\.?|versus)$/i.test(kind)
56
+ && rest.some((word) => /^(above|below|over|under|crosses|cross|>|<|leading|weakening|lagging|improving|rrg)$/i.test(word))) {
57
+ throw new Error(RELATIVE_USAGE);
58
+ }
59
+ return undefined;
60
+ }
61
+ /** Parses both compact CLI notation and the words traders type, without swallowing malformed metrics as calendar events. */
62
+ export function parseMetricAlert(input: string): MetricAlertInput | null {
63
+ const words = input.trim().replace(/\s+/g, " ");
64
+ const parts = words.split(" ");
65
+ const relative = parseRelativeAlert(parts);
66
+ if (relative) return relative;
67
+ if (!/^(p\/?e|p\/?s|rsi(?:\(|$)|ma(?:\(|$)|\d+-day|closes?$)/i.test(parts[1] ?? "")) return null;
68
+ const ratio = /^(\S+) (p\/?e|p\/?s) (above|below|over|under|>|<) (\S+)$/i.exec(words);
69
+ if (ratio) return checked({ symbol: ratio[1]!.toUpperCase(), metric: { kind: ratio[2]!.toLowerCase().replace("/", "") as "pe" | "ps" }, condition: direction(ratio[3]!)!, price: readAlertPrice(ratio[4]!) });
70
+ const rsi = /^(\S+) rsi(?:\((\d+)\)| (\d+))? (above|below|over|under|>|<) (\S+)$/i.exec(words);
71
+ if (rsi) return checked({ symbol: rsi[1]!.toUpperCase(), metric: { kind: "rsi", period: Number(rsi[2] ?? rsi[3] ?? 14) }, condition: direction(rsi[4]!)!, price: readAlertPrice(rsi[5]!) });
72
+ const ma = words.replace(/(?:its|the) /gi, "").replace(/(\d+)-day(?: (?:ma|moving average))?/gi, "ma($1)").replace(/ma (\d+)/gi, "ma($1)");
73
+ const cross = /^(\S+) ma\((\d+)\) crosses (above|below) ma\((\d+)\)$/i.exec(ma);
74
+ if (cross) return checked({ symbol: cross[1]!.toUpperCase(), metric: { kind: "ma_cross", period: Number(cross[2]), slowPeriod: Number(cross[4]), direction: direction(cross[3]!)! }, condition: "crosses", price: 0 });
75
+ const close = /^(\S+) closes? (above|below|over|under) ma\((\d+)\)$/i.exec(ma);
76
+ if (close) return checked({ symbol: close[1]!.toUpperCase(), metric: { kind: "close_ma", period: Number(close[3]) }, condition: direction(close[2]!)!, price: 0 });
77
+ throw new Error(USAGE);
78
+ }
79
+ export const benchmarkKey = (metric: { benchmark: string; benchmarkExchange?: string }) => `${metric.benchmark}${metric.benchmarkExchange ? `:${metric.benchmarkExchange}` : ""}`;
80
+ /** A relative alert as `alerts add` takes it, for editing: `XLK/SPY ratio above 0.25`, `XLE rrg leading vs SPY`. */
81
+ export function relativeAlertText(input: { symbol: string; condition: AlertCondition; targetPrice: number; metric?: AlertMetric }): string | null {
82
+ const m = input.metric;
83
+ if (m?.kind === "rs_ratio") return `${input.symbol}/${m.benchmark} ratio ${input.condition} ${input.targetPrice}`;
84
+ return m?.kind === "rrg" ? `${input.symbol} rrg ${m.quadrant} vs ${m.benchmark}` : null;
85
+ }
86
+ export function metricShortcut(input: MetricAlertInput): Record<string, string> {
87
+ const m = input.metric;
88
+ if (m.kind === "rs_ratio") return { symbol: input.symbol, kind: m.kind, "rs_ratio:benchmark": benchmarkKey(m), "rs_ratio:condition": input.condition, "rs_ratio:target": String(input.price) };
89
+ if (m.kind === "rrg") return { symbol: input.symbol, kind: m.kind, "rrg:quadrant": m.quadrant, "rrg:benchmark": benchmarkKey(m) };
90
+ return { symbol: input.symbol, kind: m.kind, [`${m.kind}:condition`]: m.kind === "ma_cross" ? m.direction : input.condition,
91
+ [`${m.kind}:target`]: String(input.price), ...("period" in m ? { [`${m.kind}:period`]: String(m.period) } : {}),
92
+ ...(m.kind === "ma_cross" ? { "ma_cross:slowPeriod": String(m.slowPeriod) } : {}) };
93
+ }
94
+ export function metricFormInput(values: Record<string, string>): MetricAlertInput | null {
95
+ const kind = values.kind;
96
+ if (!kind || kind === "price") return null;
97
+ const symbol = values.symbol?.trim().toUpperCase();
98
+ if (!symbol) return null;
99
+ if (kind === "rs_ratio") {
100
+ const benchmark = values["rs_ratio:benchmark"]?.trim();
101
+ return benchmark ? parseMetricAlert(`${symbol}/${benchmark} ratio ${values["rs_ratio:condition"] ?? "above"} ${values["rs_ratio:target"] ?? ""}`) : null;
102
+ }
103
+ if (kind === "rrg") return parseMetricAlert(`${symbol} rrg ${values["rrg:quadrant"] ?? "leading"} vs ${values["rrg:benchmark"]?.trim() || DEFAULT_ROTATION_BENCHMARK}`);
104
+ const condition = values[`${kind}:condition`] ?? "below";
105
+ const window = values[`${kind}:period`] ?? (kind === "rsi" ? "14" : kind === "ma_cross" ? "50" : "200");
106
+ const text = kind === "ma_cross" ? `${symbol} ma(${window}) crosses ${condition} ma(${values["ma_cross:slowPeriod"] ?? "200"})`
107
+ : kind === "close_ma" ? `${symbol} closes ${condition} ma(${window})`
108
+ : `${symbol} ${kind === "rsi" ? `rsi(${window})` : kind} ${condition} ${values[`${kind}:target`] ?? ""}`;
109
+ return parseMetricAlert(text);
110
+ }
111
+ export const METRIC_CHOICES = [
112
+ { value: "price", label: "Price" }, { value: "pe", label: "P/E (TTM)" }, { value: "ps", label: "P/S (TTM)" },
113
+ { value: "rsi", label: "RSI" }, { value: "ma_cross", label: "MA crossover" }, { value: "close_ma", label: "Close vs MA" },
114
+ { value: "rs_ratio", label: "Ratio to benchmark" }, { value: "rrg", label: "RRG quadrant" },
115
+ ];
116
+ const RELATIVE_FIELDS: WizardStep[] = [
117
+ { key: "rs_ratio:benchmark", label: "Divided by", type: "text", placeholder: "SPY", dependsOn: { key: "kind", value: "rs_ratio" } },
118
+ { key: "rs_ratio:condition", label: "Condition", type: "select", defaultValue: "above", dependsOn: { key: "kind", value: "rs_ratio" },
119
+ options: [{ label: "Above", value: "above" }, { label: "Below", value: "below" }, { label: "Crosses", value: "crosses" }] },
120
+ { key: "rs_ratio:target", label: "Ratio", type: "number", placeholder: "0.25", dependsOn: { key: "kind", value: "rs_ratio" } },
121
+ { key: "rrg:quadrant", label: "Enters", type: "select", defaultValue: "leading", dependsOn: { key: "kind", value: "rrg" },
122
+ options: ROTATION_QUADRANTS.map((value) => ({ label: `${value[0]!.toUpperCase()}${value.slice(1)}`, value })) },
123
+ { key: "rrg:benchmark", label: "Benchmark", type: "text", defaultValue: "SPY", placeholder: "SPY", dependsOn: { key: "kind", value: "rrg" } },
124
+ ];
125
+ export const metricWizardFields: WizardStep[] = [...METRIC_CHOICES.filter(x => x.value !== "price" && x.value !== "rs_ratio" && x.value !== "rrg").flatMap(({ value: kind }) => {
126
+ const dependsOn = { key: "kind", value: kind };
127
+ return [
128
+ ...(["rsi", "ma_cross", "close_ma"].includes(kind) ? [{ key: `${kind}:period`, label: kind === "ma_cross" ? "Fast MA (days)" : kind === "rsi" ? "RSI period" : "MA (days)", type: "number" as const, defaultValue: kind === "rsi" ? "14" : kind === "ma_cross" ? "50" : "200", dependsOn }] : []),
129
+ { key: `${kind}:condition`, label: "Condition", type: "select" as const, defaultValue: "below", dependsOn,
130
+ options: [{ label: kind === "ma_cross" ? "Crosses above" : "Above", value: "above" }, { label: kind === "ma_cross" ? "Crosses below" : "Below", value: "below" }] },
131
+ ...(kind === "ma_cross" ? [{ key: "ma_cross:slowPeriod", label: "Slow MA (days)", type: "number" as const, defaultValue: "200", dependsOn }]
132
+ : kind === "close_ma" ? [] : [{ key: `${kind}:target`, label: kind === "rsi" ? "RSI level" : "Target multiple", type: "number" as const, placeholder: kind === "rsi" ? "30" : kind === "pe" ? "25" : "8", dependsOn }]),
133
+ ];
134
+ }), ...RELATIVE_FIELDS];
@@ -0,0 +1,111 @@
1
+ import type { AssetDataProvider } from "../../../types/data-provider";
2
+ import type { TickerFinancials } from "../../../types/financials";
3
+ import { liveMarketCapitalization, liveTrailingPE } from "../../../market-data/quotes/live-valuation";
4
+ import { comparablePriceEarnings } from "../../../utils/price-earnings";
5
+ import { canonicalExchange } from "../../../utils/exchanges";
6
+ import { getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
7
+ import { barSessionDay } from "../../../market-data/market/period-return";
8
+ import { finalDailyBars } from "../../../time-series/session-bar";
9
+ import { getPricePointTimestamp } from "../../../utils/price-history";
10
+ import { DAILY_CLOSE_SETTLE_MS, dailyMetricReading, latestSettledClose, DailyMetricCache, isDailyMetric, isRelativeMetric, type MetricReading } from "./metric-model";
11
+ import type { AlertRule } from "./types";
12
+
13
+ // Keep valuation fixtures in step with the delivery evaluator and the research panes.
14
+ /** Current trailing figures on the same regular-session and currency basis as the research panes. */
15
+ export function valuationAlertValue(kind: "pe" | "ps", data: TickerFinancials): number | null {
16
+ const { quote, fundamentals: f } = data;
17
+ if (!quote || quote.stale || !Number.isFinite(quote.price) || quote.price <= 0 || !f || f.stale) return null;
18
+ if (kind === "pe") return comparablePriceEarnings(liveTrailingPE(quote, f));
19
+ const cap = liveMarketCapitalization(quote, f);
20
+ if (!cap || !f.revenue || !Number.isFinite(f.revenue) || f.revenue <= 0
21
+ || !f.financialCurrency || cap.currency !== f.financialCurrency) return null;
22
+ const value = cap.value / f.revenue;
23
+ return Number.isFinite(value) && value > 0 ? value : null;
24
+ }
25
+
26
+ const US_CALENDAR = "Daily technical alerts need a published US listing calendar; delivery is unavailable for this listing.";
27
+ /** Why a rule's listings cannot be judged on settled closes: a benchmark is held to the same US calendar as the listing. */
28
+ export function metricAvailabilityError(rule: AlertRule, now = Date.now()): string | null {
29
+ if (!rule.metric || !isDailyMetric(rule.metric)) return null;
30
+ const today = new Date(now).toISOString().slice(0, 10);
31
+ if (!getPublishedUsEquitySession(canonicalExchange(rule.exchange ?? ""), today)) return US_CALENDAR;
32
+ return isRelativeMetric(rule.metric) && !getPublishedUsEquitySession(canonicalExchange(rule.metric.benchmarkExchange ?? ""), today)
33
+ ? `Relative strength alerts compare two US listings; ${rule.metric.benchmark} has no published US listing calendar here.` : null;
34
+ }
35
+
36
+ /**
37
+ * The first close a daily technical rule set at `now` is judged on, and when:
38
+ * the next session to close after it, read once the close has settled
39
+ * (DAILY_CLOSE_SETTLE_MS). `weekly` (an RRG quadrant) waits for the last
40
+ * session of that Monday-Friday week, its weekly close. Null when the listing
41
+ * has no published calendar.
42
+ */
43
+ export function firstDailyCheck(exchange: string | undefined, now = Date.now(), weekly = false): { day: string; at: number } | null {
44
+ const venue = canonicalExchange(exchange ?? "");
45
+ let first: { day: string; close: number } | null = null;
46
+ for (let offset = 0; offset < 15; offset++) {
47
+ const time = now + offset * 86_400_000;
48
+ const day = new Date(time).toISOString().slice(0, 10);
49
+ // The weekly close is the last session before the weekend after the first close.
50
+ if (first && new Date(time).getUTCDay() === 6) break;
51
+ const session = getPublishedUsEquitySession(venue, day);
52
+ if (!session) return null;
53
+ if (session.kind !== "session" || session.close <= now) continue;
54
+ first = { day, close: session.close };
55
+ if (!weekly) break;
56
+ }
57
+ return first ? { day: first.day, at: first.close + DAILY_CLOSE_SETTLE_MS } : null;
58
+ }
59
+
60
+ /** The bar size the daily rules read, the one `history` and the charts ask for. */
61
+ const DAILY_BARS = "1d";
62
+
63
+ const dailyCaches = new WeakMap<AssetDataProvider, DailyMetricCache>();
64
+
65
+ /** A pass shares each listing's reads; there is no second provider or quote subscription. */
66
+ export function metricReadings(provider: AssetDataProvider, now = Date.now()) {
67
+ const daily = dailyCaches.get(provider) ?? new DailyMetricCache();
68
+ dailyCaches.set(provider, daily);
69
+ const financials = new Map<string, Promise<TickerFinancials>>();
70
+ const history = new Map<string, Promise<Awaited<ReturnType<AssetDataProvider["getPriceHistory"]>> >>();
71
+ return async (rule: AlertRule): Promise<MetricReading> => {
72
+ const m = rule.metric!;
73
+ const key = `${rule.symbol}|${rule.exchange ?? ""}`;
74
+ if (!isDailyMetric(m)) {
75
+ const data = financials.get(key) ?? provider.getTickerFinancials(rule.symbol, rule.exchange);
76
+ financials.set(key, data);
77
+ const loaded = await data;
78
+ const value = valuationAlertValue(m.kind as "pe" | "ps", loaded);
79
+ if (value == null) throw new Error(m.kind === "pe" ? "Trailing P/E is unavailable or not meaningful." : "Trailing P/S is unavailable on a matching currency basis.");
80
+ return { value, target: rule.targetPrice, at: loaded.quote?.lastUpdated ?? now };
81
+ }
82
+ const exchange = canonicalExchange(rule.exchange ?? "");
83
+ const session = (day: string) => getPublishedUsEquitySession(exchange, day);
84
+ const unavailable = metricAvailabilityError(rule, now);
85
+ if (unavailable) throw new Error(unavailable);
86
+ const latest = latestSettledClose(now, session);
87
+ const ruleKey = JSON.stringify([rule.id, rule.armedAt ?? rule.createdAt, key, rule.condition, rule.targetPrice, m]);
88
+ return daily.read(ruleKey, latest.day, now, async () => {
89
+ const load = async (symbol: string, venue: string) => {
90
+ // An explicit end at the settled session avoids today's forming-bar quote read.
91
+ const end = new Date(`${latest.day}T23:59:59Z`);
92
+ const start = new Date(Date.parse(latest.day) - 366 * 86_400_000);
93
+ if (provider.getDetailedPriceHistoryWithMetadata) return (await provider.getDetailedPriceHistoryWithMetadata(symbol, venue, start, end, DAILY_BARS, { cacheMode: "current" })).points;
94
+ if (provider.getDetailedPriceHistory) return provider.getDetailedPriceHistory(symbol, venue, start, end, DAILY_BARS, { cacheMode: "current" });
95
+ throw new Error("Completed daily history is unavailable from this connection.");
96
+ };
97
+ const completed = async (symbol: string, venue: string) => {
98
+ const listing = `${symbol}|${venue}`;
99
+ const request = history.get(listing) ?? load(symbol, venue);
100
+ history.set(listing, request);
101
+ // Final as `history` shows it: a close it prints without `partial` is one the rule may judge.
102
+ // Bars read back from the saved cache carry their dates as text.
103
+ return finalDailyBars(await request, { now, venue }).map(point => ({ date: barSessionDay(getPricePointTimestamp(point), venue), close: point.close }));
104
+ };
105
+ const bars = await completed(rule.symbol, exchange);
106
+ // RS ratios and RRG quadrants read the benchmark's own completed bars, never a quote.
107
+ const benchmark = isRelativeMetric(m) ? await completed(m.benchmark, canonicalExchange(m.benchmarkExchange ?? "")) : undefined;
108
+ return dailyMetricReading(rule, bars, now, session, benchmark);
109
+ });
110
+ };
111
+ }