@gloom-sh/terminal 0.18.5 → 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 (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -1,20 +1,29 @@
1
- import { metricDescription } from "./metric-model";
2
- import { parseMetricAlert, type MetricAlertInput } from "./metric-command";
3
- import { metricReadings } from "./metric-data";
1
+ import { closeBasis, isDailyMetric, isRelativeMetric, metricDescription, metricName, metricReadingText, validMetricRule } from "./metric-model";
2
+ import { logFailure } from "../../../utils/log-failure";
3
+ import { benchmarkKey, parseMetricAlert, type MetricAlertInput } from "./metric-command";
4
+ import { firstDailyCheck, metricAvailabilityError, metricReadings } from "./metric-data";
5
+ import { ALERT_ADD_FORMS, ALERT_KINDS_TEXT, parseRuleAddArgs, RULE_US_DATA, ruleDelivery, type RuleAddRequest } from "./cli-forms";
6
+ import { alertChangeEntry, appendAlertLog, type AlertLogAction } from "./fired-log";
7
+ import { alertLogRule } from "./alert-log";
8
+ import { alertListing, alertReadingAt, alertReadingText, isoUtc, listingWithCurrency, NOT_CHECKED_YET } from "./format";
9
+ import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
10
+ import { quoteAlertFields } from "./quotes";
11
+ import { createEventAlert } from "./events";
12
+ import { isResearchAlertKind, readResearchRule } from "./research-rules";
4
13
  import { withConfigData, withMarketData } from "../../../cli/scoped-context";
5
14
  import { CLI_COMMAND_GROUPS } from "../../../cli/help";
6
15
  import { dryRunNote } from "../../../cli/helpers";
7
16
  import { requireArg, takeOption } from "../../../cli/commands/command-utils";
8
17
  import { requireQuotedListing } from "../../../cli/quote-answer";
9
- import { EXCHANGE_OPTION, listingReadNotes, requireListingArg, usListingsOnlyMessage } from "../../../cli/listing-arg";
10
- import { notTheCoinNote } from "../../../cli/commands/crypto-hints";
18
+ import { coinListingNotes, EXCHANGE_OPTION, listingReadNotes, requireListingArg, usListingsOnlyMessage } from "../../../cli/listing-arg";
11
19
  import { saveConfig } from "../../../data/config/store";
12
20
  import { resumeStateKey } from "../../registry/plugin-state";
13
21
  import type { CliCommandDef } from "../../../types/plugin";
14
22
  import type { Quote } from "../../../types/financials";
15
23
  import { getActiveQuoteDisplay } from "../../../market-data/market/status";
16
24
  import { cliStyles } from "../../../utils/cli-output";
17
- import { canonicalExchange, isUsListingExchange } from "../../../utils/exchanges";
25
+ import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../../../utils/exchanges";
26
+ import { notATickerSymbol } from "../../../sources/provider-errors";
18
27
  import { meantThousandsDot, parsePastedNumber, thousandsDotReadings, type ThousandsDotReadings } from "../../../utils/decimal-comma";
19
28
  import { newYorkToday } from "../earnings/board-model";
20
29
  import { fetchEconCalendarFrom } from "../econ/calendar-source";
@@ -32,14 +41,15 @@ import {
32
41
  type CalendarEventChoice,
33
42
  type ReleaseRename,
34
43
  } from "./calendar-events";
35
- import { createAlert, rearmAlert } from "./alert-engine";
44
+ import { createAlert, rearmAlert, sameLevelRule } from "./alert-engine";
36
45
  import { ALERTS_PLUGIN_ID } from "./constants";
37
46
  import { MAX_EVENT_ALERTS, eventAlertTarget, toggleEventAlert, writeEventAlertStores, type EventAlertRule } from "./events";
38
- import { alertKindLabel } from "./history";
39
- import { IV_LEVEL_CHECKS_KEY, readIvLevelChecks, rearmIvLevelAlert, type IvLevelCheck } from "./iv-level";
47
+ import { alertKindLabel, fetchAlertHistory, ruleStateText, type AlertRuleState } from "./history";
48
+ import { apiClient } from "../../../api-client";
49
+ import { IV_LEVEL_CHECKS_KEY, ivLevelCheckText, readIvLevelChecks, rearmIvLevelAlert, type IvLevelCheck } from "./iv-level";
40
50
  import { parseAlertPrice, readAlertPrice } from "./price";
41
51
  import { farTargetMessage } from "./quotes";
42
- import { MACRO_EVENT_NAMES, createResearchAlert, formatPremium, ivLevelUnitError, parseAmount, parseIvLevel, researchAlertDescription } from "./research-builder";
52
+ import { MACRO_EVENT_NAMES, createResearchAlert, formatPremium, ivLevelUnitError, parseAmount, parseIvLevel } from "./research-builder";
43
53
  import { IV_LEVEL_RANGES, MAX_LEAD_DAYS, type IvLevelMetric, type MacroAlertEvent } from "./research-rules";
44
54
  import {
45
55
  createScheduledAlert,
@@ -75,11 +85,19 @@ import type { AlertCondition, AlertRule } from "./types";
75
85
  */
76
86
  const PRICE_DELIVERY = "the app alerts while it is running; sign in with Cloud sync for email or phone alerts when it is closed";
77
87
 
78
- const ADD_USAGE = "Usage: gloom alerts add <symbol> <above|below|crosses> <price>, alerts add <symbol> earnings [--days <n> | results], alerts add <calendar event> [--country <code>] [--days <n>], alerts add [symbol] news <keyword>, alerts add [symbol] flow [premium] [calls|puts] [sweeps|blocks], alerts add <symbol> <iv|rr> <above|below> <level>, or alerts add <OCC contract> iv spike <points>";
88
+ const ADD_USAGE = `Usage: gloom alerts add <symbol> <above|below|crosses> <price>, or another kind. ${ALERT_KINDS_TEXT}`;
79
89
  const DAYS_RULE = `--days takes a whole number of days from 1 to ${MAX_LEAD_DAYS}; without it the alert goes off 1 day before.`;
80
- /** The event form every refusal of `alerts add` names: any release in the economic calendar, CPI and FOMC among them. */
81
- const EVENT_FORM = `a release in the economic calendar by name (alerts add "NBS Manufacturing PMI" --country CN, alerts add CN PMI, alerts add CPI --days 1)`;
90
+ /** How a release in the economic calendar is named, CPI and FOMC among them. */
91
+ const CALENDAR_EXAMPLES = `alerts add "NBS Manufacturing PMI" --country CN, alerts add CN PMI, alerts add CPI --days 1`;
82
92
  const COUNTRY_RULE = "--country is for an alert on a calendar event, such as alerts add PMI --country CN.";
93
+ const BENCHMARK_RULE = "--benchmark takes the listing an RRG alert is measured against (SPY by default, as in RRG),";
94
+ /** `XLK/SPY`, `XLK:NYSEARCA/SPY`: two listings, or one written with a slash (BRK/B, EUR/USD). */
95
+ const PAIR_SHAPE = /^[^/\s]+\/[^/\s]+$/;
96
+ /** A listing check that hands back its refusal instead of ending the command, so a pair can be read as a ratio. */
97
+ class ListingRefusal extends Error {
98
+ constructor(message: string, readonly details?: string) { super(message); }
99
+ }
100
+ const PROBE = { fail: (message: string, details?: string): never => { throw new ListingRefusal(message, details); } };
83
101
 
84
102
  /** What `alerts add` was asked for, before any listing or date is looked up. */
85
103
  export type AlertAddRequest =
@@ -100,7 +118,9 @@ export type AlertAddRequest =
100
118
  /** The symbol's 30-day ATM IV (`atm`, in %) or 25-delta risk reversal (`rr`, in vol points) above or below a level, checked by the running app. */
101
119
  | { kind: "iv_level"; symbol: string; metric: IvLevelMetric; direction: "above" | "below"; level: number }
102
120
  /** One OCC contract's IV rising by `points`. */
103
- | { kind: "iv_spike"; contract: string; points: number };
121
+ | { kind: "iv_spike"; contract: string; points: number }
122
+ /** The rest of what Add Event Alert offers: 52-week range, unusual volume, filings, insiders, short interest and more. */
123
+ | RuleAddRequest;
104
124
 
105
125
  /** The `alerts add` forms Gloom Cloud's research rules take: they follow US listings. */
106
126
  type ResearchAddRequest = Extract<AlertAddRequest, { kind: "news" | "flow" | "iv_level" | "iv_spike" }>;
@@ -153,18 +173,30 @@ export function parseAlertAddArgs(
153
173
  days: string | null | undefined,
154
174
  exchange?: string | null,
155
175
  country?: string | null,
176
+ benchmark?: string | null,
156
177
  ): AlertAddRequest {
157
178
  const [first, second, third, ...rest] = args;
158
179
  if (!first) throw new Error(ADD_USAGE);
159
180
  const countryFilter = readCountryOption(country);
160
- const metric = parseMetricAlert(args.join(" "));
181
+ if (benchmark === null) throw new Error(`${BENCHMARK_RULE} such as alerts add XLE rrg leading --benchmark QQQ.`);
182
+ const rrgFlag = benchmark !== undefined && second?.toLowerCase() === "rrg";
183
+ const typedVs = /^(vs\.?|versus|against)$/i.test(args[3] ?? "") ? args[4] : undefined;
184
+ if (rrgFlag && typedVs !== undefined) {
185
+ // `vs QQQ --benchmark SMH` names two benchmarks; the same one twice (SPY and SPY:NYSEARCA) is one.
186
+ const [typed, flag] = [typedVs, benchmark].map((text) => parsePublicTickerKey(text.toUpperCase()));
187
+ if (typed!.symbol !== flag!.symbol || (typed!.exchange && flag!.exchange && typed!.exchange !== flag!.exchange)) {
188
+ throw new Error(`An RRG alert names its benchmark once: vs ${typedVs.toUpperCase()} or --benchmark ${benchmark.toUpperCase()}, not both.`);
189
+ }
190
+ }
191
+ const metric = parseMetricAlert(rrgFlag && typedVs === undefined ? [...args, "vs", benchmark].join(" ") : args.join(" "));
192
+ if (benchmark !== undefined && metric?.metric.kind !== "rrg") throw new Error(`${BENCHMARK_RULE} such as alerts add XLE rrg leading --benchmark QQQ; a ratio names both listings, as in alerts add XLK/SPY above 0.25.`);
161
193
  if (metric) {
162
- if (days !== undefined || country !== undefined) throw new Error("Valuation and daily technical alerts take neither --days nor --country.");
194
+ if (days !== undefined || country !== undefined) throw new Error(`${isRelativeMetric(metric.metric) ? "Relative strength" : "Valuation and daily technical"} alerts take neither --days nor --country.`);
163
195
  return { kind: "metric", ...metric };
164
196
  }
165
- const research = parseResearchAddArgs(args);
197
+ const research = parseResearchAddArgs(args) ?? parseRuleAddArgs(args);
166
198
  if (research) {
167
- if (days !== undefined) throw new Error("--days is for earnings and calendar event alerts, not news, flow or IV alerts.");
199
+ if (days !== undefined) throw new Error("--days is for earnings and calendar event alerts, not this kind of alert.");
168
200
  if (country !== undefined) throw new Error(COUNTRY_RULE);
169
201
  return research;
170
202
  }
@@ -192,7 +224,7 @@ export function parseAlertAddArgs(
192
224
  if (!CONDITIONS.includes(condition)) {
193
225
  // Words with no price after them name a release (CN PMI, Industrial Profits); a price ends a mistyped price alert.
194
226
  if (parseAlertPrice(args.at(-1)!).kind === "invalid") return releaseRequest(args, countryFilter, leadDays());
195
- throw new Error(`Unknown alert "${second}". Use above, below or crosses with a price, earnings with --days, news with a keyword, flow, iv or rr with above or below and a level, iv spike on an OCC contract, or ${EVENT_FORM}.`);
227
+ throw new Error(`Unknown alert "${second}". ${ALERT_KINDS_TEXT}`);
196
228
  }
197
229
  if (country !== undefined) throw new Error(COUNTRY_RULE);
198
230
  if (days !== undefined) throw new Error("--days is for earnings and calendar event alerts, not price alerts.");
@@ -312,6 +344,25 @@ function researchDelivery(kind: ResearchAddRequest["kind"]): string {
312
344
  return `Gloom Cloud checks it for a signed-in account with Cloud sync${plan} and sends it to your phone`;
313
345
  }
314
346
 
347
+ /**
348
+ * The rule Add Event Alert saves for a form `alerts add` named by its words,
349
+ * on the listing `quote` read the symbol as: a US research rule keeps its
350
+ * symbol and venue, hiring and a catalyst's company their listing as typed
351
+ * (ADYEN:AMS).
352
+ */
353
+ function createFormRule(request: RuleAddRequest, listing: { symbol: string; key: string; exchange: string } | null): EventAlertRule {
354
+ const { event } = request;
355
+ if (!isResearchAlertKind(event)) return createEventAlert(event, listing?.key ?? request.values.value ?? "");
356
+ const values = Object.fromEntries(Object.entries(request.values).map(([key, value]) => [`${event}:${key}`, value]));
357
+ if (listing && request.listing === "any") values[`${event}:symbol`] = listing.key;
358
+ else if (listing) {
359
+ const typed = listing.exchange ? canonicalExchange(listing.exchange) : "";
360
+ values[`${event}:symbol`] = listing.symbol;
361
+ values[`${event}:exchange`] = RESEARCH_EXCHANGES.has(typed) ? typed : "US";
362
+ }
363
+ return createResearchAlert(event, values);
364
+ }
365
+
315
366
  const isResearchRequest = (request: AlertAddRequest): request is ResearchAddRequest =>
316
367
  request.kind === "news" || request.kind === "flow" || request.kind === "iv_level" || request.kind === "iv_spike";
317
368
 
@@ -349,6 +400,33 @@ export function thousandsDotMessage(
349
400
  return `${dot.text} could be ${dot.thousands} (thousands) or ${dot.decimal} (decimal point), and ${listing.toUpperCase()} last traded at ${Number(last!.toFixed(2))}; type ${retype(dot.thousands)} or ${retype(dot.decimal)}`;
350
401
  }
351
402
 
403
+ /**
404
+ * What a valuation or daily technical alert read when it was added:
405
+ * "RSI(14) was 61.23 on the 2026-10-26 close", "50-day MA was 186.32 against
406
+ * the 200-day MA at 180.11 on the 2026-10-26 close", "P/E (TTM) is 24.10".
407
+ */
408
+ function metricReadingNote(alert: AlertRule): string {
409
+ const metric = alert.metric!;
410
+ // "Ratio to SPY was 0.2434 on the 2026-10-26 close", "RRG vs SPY was Leading (strength 110.06, momentum 108.88) on the 2026-10-23 weekly close".
411
+ if (isRelativeMetric(metric)) {
412
+ const reading = metricReadingText(metric, { value: alert.lastCheckedPrice ?? Number.NaN, target: alert.lastCheckedTarget ?? Number.NaN });
413
+ return `${metricName(metric)} was ${reading} on the ${alert.lastDailyClose} ${closeBasis(metric) === "weekly close" ? "weekly close" : "close"}`;
414
+ }
415
+ const value = (alert.lastCheckedPrice ?? Number.NaN).toFixed(2);
416
+ if (!isDailyMetric(metric)) return `${metricName(metric)} is ${value}`;
417
+ const against = metric.kind === "ma_cross" ? metric.slowPeriod : metric.kind === "close_ma" ? metric.period : null;
418
+ const target = against != null && alert.lastCheckedTarget != null ? ` against the ${against}-day MA at ${alert.lastCheckedTarget.toFixed(2)}` : "";
419
+ return `${metricName(metric)} was ${value}${target} on the ${alert.lastDailyClose} close`;
420
+ }
421
+
422
+ /** An event alert still waiting to go off, as `alerts list` says `active`: not paused, and not alerted for its coming event or level. */
423
+ function eventRuleArmed(rule: EventAlertRule, now: number): boolean {
424
+ if (rule.status !== "active") return false;
425
+ const scheduled = readScheduledRule(rule);
426
+ if (!scheduled) return !rule.triggeredAt;
427
+ return scheduledAlertStatus(rule, scheduled.leadDays, scheduledToday(scheduled.target, now), scheduled.trigger) === "active";
428
+ }
429
+
352
430
  /** One row of `alerts list`: a price alert as stored, or an event alert with its next date. */
353
431
  type AlertListRow = Record<string, unknown> & {
354
432
  id: string;
@@ -357,14 +435,32 @@ type AlertListRow = Record<string, unknown> & {
357
435
  };
358
436
 
359
437
  function priceRow(alert: AlertRule): AlertListRow {
360
- return { ...alert, kind: alert.metric?.kind ?? "price", rule: alert.metric ? metricDescription(alert) : undefined, target: alert.exchange ? `${alert.symbol}:${alert.exchange}` : alert.symbol };
438
+ return {
439
+ ...alert, kind: alert.metric?.kind ?? "price", rule: alert.metric ? metricDescription(alert) : undefined,
440
+ target: alert.exchange ? `${alert.symbol}:${alert.exchange}` : alert.symbol,
441
+ // The listing it is checked on, in its currency, and its last reading in words with when it is from.
442
+ listing: alertListing(alert), currency: alert.lastQuoteCurrency ?? null,
443
+ lastReading: alertReadingText(alert), lastReadingAt: isoUtc(alertReadingAt(alert)) || null,
444
+ };
361
445
  }
362
446
 
363
- function eventRow(rule: EventAlertRule, now: number, eventDate: string | null | undefined, check?: IvLevelCheck): AlertListRow {
447
+ function eventRow(
448
+ rule: EventAlertRule, now: number, eventDate: string | null | undefined, check?: IvLevelCheck, cloud?: CloudRuleStates,
449
+ ): AlertListRow {
364
450
  const scheduled = readScheduledRule(rule);
365
451
  if (!scheduled) {
366
452
  const status = rule.status === "active" && rule.triggeredAt ? "triggered" : rule.status;
453
+ // An IV level is read by this app, so its last reading is this device's; Gloom Cloud reads the rest.
454
+ const state = cloud?.states.get(rule.id);
455
+ const reading = rule.kind === "iv_level" ? {
456
+ lastReading: ivLevelCheckText(rule, check),
457
+ lastReadingAt: check?.value != null ? isoUtc(check.quoteAt ? Date.parse(check.quoteAt) : check.checkedAt) || null : null,
458
+ } : state ? {
459
+ lastReading: ruleStateText(state),
460
+ lastReadingAt: state.asOf ? isoUtc(Date.parse(state.asOf)) || null : null,
461
+ } : cloud?.failed ? {} : { lastReading: NOT_CHECKED_YET };
367
462
  return { id: rule.id, kind: rule.kind, target: eventAlertTarget(rule), status, createdAt: rule.createdAt,
463
+ listing: eventRuleListing(rule), ...reading,
368
464
  ...check ? { lastCheck: check } : {}, ...rule.triggeredAt ? { triggeredAt: rule.triggeredAt } : {} };
369
465
  }
370
466
  const date = eventDate ?? null;
@@ -380,11 +476,48 @@ function eventRow(rule: EventAlertRule, now: number, eventDate: string | null |
380
476
  eventDate: date,
381
477
  status: scheduledAlertStatus({ ...rule, eventDate: date ?? undefined }, scheduled.leadDays, scheduledToday(scheduled.target, now), scheduled.trigger),
382
478
  createdAt: rule.createdAt,
479
+ listing: scheduled.target.kind === "earnings" ? scheduled.target.exchange ? `${scheduled.target.symbol}:${scheduled.target.exchange}` : scheduled.target.symbol : "",
383
480
  };
384
481
  }
385
482
 
483
+ /** Gloom Cloud's last reading of each rule it checks, as ALRT's Events tab shows them; `failed` when it could not be asked. */
484
+ interface CloudRuleStates {
485
+ states: Map<string, AlertRuleState>;
486
+ failed: boolean;
487
+ warning?: string;
488
+ }
489
+
490
+ /**
491
+ * Gloom Cloud checks the event rules this app does not, for a signed-in account. With no session nothing has
492
+ * read them, so they are not checked yet; with one, their readings are Cloud's.
493
+ */
494
+ async function cloudRuleStates(rules: readonly EventAlertRule[]): Promise<CloudRuleStates> {
495
+ const empty = { states: new Map<string, AlertRuleState>(), failed: false };
496
+ if (!apiClient.getSessionToken() || !rules.some((rule) => rule.kind !== "iv_level" && !readScheduledRule(rule))) return empty;
497
+ try {
498
+ const history = await fetchAlertHistory(0);
499
+ return { states: new Map(history.states.map((state) => [state.ruleId, state])), failed: false };
500
+ } catch (error) {
501
+ return { ...empty, failed: true, warning: `Gloom Cloud's readings of event alerts could not be loaded: ${error instanceof Error ? error.message : String(error)}` };
502
+ }
503
+ }
504
+
505
+ /** The listing a research rule follows (AAPL, ADYEN:AMS for a hiring rule); empty for one on no symbol. */
506
+ function eventRuleListing(rule: EventAlertRule): string {
507
+ if (rule.target === "ticker") return rule.value;
508
+ if (!isResearchAlertKind(rule.kind)) return "";
509
+ const config = readResearchRule(rule.kind, rule.value);
510
+ if (!config?.symbol) return "";
511
+ return config.symbol.includes(":") || !config.exchange || config.exchange === "US" ? config.symbol : `${config.symbol}:${config.exchange}`;
512
+ }
513
+
386
514
  const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? "" : "s"}`;
387
515
 
516
+ /**
517
+ * One row as text and CSV give it. CSV keeps the columns it has always had in
518
+ * their order, then Created (ISO UTC), the listing, its currency and the last
519
+ * reading, in words and dated.
520
+ */
388
521
  function textRow(row: AlertListRow): Record<string, unknown> {
389
522
  const price = row.kind === "price";
390
523
  return {
@@ -400,6 +533,11 @@ function textRow(row: AlertListRow): Record<string, unknown> {
400
533
  status: row.status,
401
534
  lastCheckedPrice: row.lastCheckedPrice,
402
535
  triggeredAt: row.triggeredAt,
536
+ createdAt: isoUtc(typeof row.createdAt === "number" ? row.createdAt : null),
537
+ listing: row.listing ?? "",
538
+ currency: row.currency ?? "",
539
+ lastReading: row.lastReading ?? "",
540
+ lastReadingAt: row.lastReadingAt ?? "",
403
541
  };
404
542
  }
405
543
 
@@ -485,8 +623,8 @@ function releaseReadNotes(
485
623
  return notes;
486
624
  }
487
625
 
488
- /** Every form `alerts add` takes but a price alert's details, as a refusal ends. */
489
- const ADD_FORMS = `Event alerts take ${EVENT_FORM} or a symbol with earnings (alerts add CRWV earnings --days 3); price alerts take a condition and a price (alerts add AAPL above 250).`;
626
+ /** Every kind `alerts add` takes, as a refusal of words none of them reads ends. */
627
+ const ADD_FORMS = ALERT_KINDS_TEXT;
490
628
 
491
629
  /**
492
630
  * Why a name could not be looked up. A single word (alerts add AAPL) may be
@@ -506,9 +644,14 @@ function noReleaseMessage(
506
644
  renamed?: ReleaseRename,
507
645
  ): string {
508
646
  const near = suggestions.length > 0 ? ` Did you mean ${choiceList(suggestions).join(", ")}?` : "";
509
- return `No release in the economic calendar matches "${query}"${country ? ` in ${country.split(",").join(", ")}` : ""}.`
647
+ // Words with a country are a release named; anything else may as well be a kind of alert mistyped.
648
+ if (country) {
649
+ return `No release in the economic calendar matches "${query}" in ${country.split(",").join(", ")}.`
650
+ + `${renamed ? ` ${releaseRenameNote(renamed)}` : ""}${near} gloom econ --country <code> lists the calendar.`;
651
+ }
652
+ return `No kind of alert and no release in the economic calendar matches "${query}".`
510
653
  + `${renamed ? ` ${releaseRenameNote(renamed)}` : ""}${near} `
511
- + `${ADD_FORMS} gloom econ --country <code> lists the calendar.`;
654
+ + `${ADD_FORMS} Calendar releases go by name (${CALENDAR_EXAMPLES}); gloom econ --country <code> lists the calendar.`;
512
655
  }
513
656
 
514
657
  /** The refusal of a name listed in several countries, with the best of each. */
@@ -534,21 +677,13 @@ async function requireEventDate(
534
677
  export const alertsCliCommand: CliCommandDef = {
535
678
  name: "alerts",
536
679
  aliases: ["alert"],
537
- description: "List, add, and remove price, valuation and daily technical alerts, alerts before earnings and any economic calendar release (CPI, FOMC, NBS PMI), and news, options flow and IV alerts",
680
+ description: "List, add, re-arm and remove price, valuation, daily technical and relative strength alerts, alerts before earnings and any economic calendar release (CPI, FOMC, NBS PMI), and every event alert ALRT offers: news, options flow, IV, 52-week range, unusual volume, filings, insiders, short interest, analysts, hiring, perpetuals, catalysts, Congress and 13F",
538
681
  help: {
539
682
  group: CLI_COMMAND_GROUPS.portfolios,
683
+ // Every form `alerts add` takes, as the refusal of an unknown phrase names them and the CLI docs page lists them.
540
684
  usage: [
541
685
  "alerts list",
542
- "alerts add <symbol> <above|below|crosses> <price>",
543
- "alerts add <symbol> <pe|ps|rsi(14)> <above|below> <level>",
544
- "alerts add <symbol> ma(50) crosses <above|below> ma(200)",
545
- "alerts add <symbol> closes <above|below> ma(200)",
546
- "alerts add <symbol> earnings [--days <n> | results]",
547
- "alerts add <calendar event> [--country <code>] [--days <n>]",
548
- "alerts add [symbol] news <keyword>",
549
- "alerts add [symbol] flow [premium] [calls|puts] [sweeps|blocks]",
550
- "alerts add <symbol> <iv|rr> <above|below> <level>",
551
- "alerts add <OCC contract> iv spike <points>",
686
+ ...ALERT_ADD_FORMS.map((form) => `alerts add ${form.usage}`),
552
687
  "alerts delete <id>",
553
688
  "alerts rearm <id>",
554
689
  ],
@@ -556,6 +691,7 @@ export const alertsCliCommand: CliCommandDef = {
556
691
  EXCHANGE_OPTION,
557
692
  { flags: "--days <n>", description: `Days before the event the alert goes off, 1 to ${MAX_LEAD_DAYS} (default 1); earnings and calendar event alerts` },
558
693
  { flags: "--country <code>", description: "The country of a calendar event, as econ --country takes it (CN, US, DE); a code before the name does the same (alerts add CN PMI)" },
694
+ { flags: "--benchmark <symbol>", description: "The listing an RRG alert is measured against; SPY by default, as in RRG" },
559
695
  ],
560
696
  examples: [
561
697
  "alerts",
@@ -565,6 +701,9 @@ export const alertsCliCommand: CliCommandDef = {
565
701
  'alerts add NVDA "rsi(14)" under 30',
566
702
  'alerts add SPY "ma(50)" crosses above "ma(200)"',
567
703
  'alerts add TSLA closes below "ma(200)"',
704
+ "alerts add XLK/SPY above 0.25",
705
+ "alerts add XLE rrg leading",
706
+ "alerts add SMH rrg weakening --benchmark QQQ",
568
707
  "alerts add SAN:EPA below 70",
569
708
  "alerts add CRWV earnings --days 3",
570
709
  "alerts add CRWV earnings results",
@@ -575,6 +714,17 @@ export const alertsCliCommand: CliCommandDef = {
575
714
  "alerts add NVDA flow 5m calls sweeps",
576
715
  "alerts add IBIT iv above 50",
577
716
  "alerts add SPY rr below -6",
717
+ "alerts add XLE 52-week high",
718
+ "alerts add NVDA unusual volume 3x",
719
+ "alerts add AAPL filing 8-K",
720
+ "alerts add TSLA insider sell",
721
+ "alerts add GME short interest increase 20%",
722
+ "alerts add AAPL analyst downgrade",
723
+ "alerts add RIVN hiring slowing",
724
+ "alerts add perps hyperliquid:default:BTC funding above 0.05%",
725
+ "alerts add catalyst type clinical",
726
+ "alerts add congress \"Nancy Pelosi\"",
727
+ "alerts add 13f 1067983",
578
728
  ],
579
729
  },
580
730
  execute: async (rawArgs, ctx) => {
@@ -586,31 +736,55 @@ export const alertsCliCommand: CliCommandDef = {
586
736
  const countryFlag = rawArgs.some((arg) => arg === "--country" || arg.startsWith("--country="));
587
737
  const countryValue = takeOption(args, "--country");
588
738
  const country = countryFlag && countryValue === undefined ? null : countryValue;
739
+ const benchmarkFlag = rawArgs.some((arg) => arg === "--benchmark" || arg.startsWith("--benchmark="));
740
+ const benchmarkValue = takeOption(args, "--benchmark");
741
+ const benchmark = benchmarkFlag && benchmarkValue === undefined ? null : benchmarkValue;
589
742
  const action = args[0] ?? "list";
590
743
  const now = Date.now();
591
744
  const today = newYorkToday(new Date(now));
745
+ const failWith = (error: unknown): never => ctx.fail(error instanceof Error ? error.message : String(error));
592
746
 
593
747
  const parseAdd = (): AlertAddRequest => {
594
748
  try {
595
- return parseAlertAddArgs(args.slice(1), days, exchangeOption, country);
749
+ return parseAlertAddArgs(args.slice(1), days, exchangeOption, country, benchmark);
596
750
  } catch (error) {
597
- return ctx.fail(error instanceof Error ? error.message : String(error));
751
+ return failWith(error);
598
752
  }
599
753
  };
600
- const request = action === "add" ? parseAdd() : null;
754
+ const typedRequest = action === "add" ? parseAdd() : null;
755
+ // XLK/SPY is first a listing written with a slash (BRK/B, EUR/USD); one that quotes nothing is the ratio of the two.
756
+ const pairListing = typedRequest?.kind === "price" && PAIR_SHAPE.test(typedRequest.symbol)
757
+ ? await withMarketData(ctx, (market) => requireQuotedListing(typedRequest.symbol, exchangeOption, market, PROBE, "alerts add")).catch((error: unknown) => {
758
+ if (!(error instanceof ListingRefusal)) throw error;
759
+ if (!notATickerSymbol(error.message)) return ctx.fail(error.message, error.details);
760
+ return null;
761
+ })
762
+ : undefined;
763
+ let request: AlertAddRequest | null = typedRequest;
764
+ if (typedRequest?.kind === "price" && pairListing === null) {
765
+ if (exchangeOption) ctx.fail("A ratio alert names each listing, such as alerts add XLK:NYSEARCA/SPY above 0.25, and takes no --exchange.");
766
+ try {
767
+ request = { kind: "metric", ...parseMetricAlert(`${typedRequest.symbol} ratio ${typedRequest.condition} ${typedRequest.price}`)! };
768
+ } catch (error) {
769
+ failWith(error);
770
+ }
771
+ }
601
772
  const research = request && isResearchRequest(request) ? request : null;
773
+ const formRule = request?.kind === "rule" ? request : null;
602
774
  const listedSymbol = (request?.kind === "price" || request?.kind === "metric") ? request.symbol
603
- : research && research.kind !== "iv_spike" ? research.symbol : null;
775
+ : research && research.kind !== "iv_spike" ? research.symbol : formRule?.symbol ?? null;
776
+ // Gloom Cloud's research rules follow US listings; hiring and a catalyst's company may list anywhere.
777
+ const usOnly = research ? research.kind === "news" ? "News alerting" : research.kind === "flow" ? "Options flow" : "Options data"
778
+ : formRule?.listing === "us" ? RULE_US_DATA[formRule.event] ?? "This alert" : null;
604
779
  // A price alert is kept for the listing `quote` prints for the symbol, and for no symbol that quotes nothing;
605
780
  // a news, flow or IV rule on a symbol follows that listing too, and only in the US, where their data is.
606
- const priced = listedSymbol
781
+ const priced = pairListing ? pairListing : listedSymbol
607
782
  ? await withMarketData(ctx, async (market) => {
608
783
  const quoted = await requireQuotedListing(listedSymbol, exchangeOption, market, ctx, "alerts add");
609
- if (research) {
784
+ if (usOnly) {
610
785
  const venue = quoted.listing.exchange || canonicalExchange(quoted.quote.listingExchangeName || quoted.quote.exchangeName);
611
786
  if (!isUsListingExchange(venue)) {
612
- const what = research.kind === "news" ? "News alerting" : research.kind === "flow" ? "Options flow" : "Options data";
613
- const { message, details } = await usListingsOnlyMessage(what, quoted.listing, market,
787
+ const { message, details } = await usListingsOnlyMessage(usOnly, quoted.listing, market,
614
788
  (key) => `gloom alerts add ${key} ${args.slice(2).join(" ")}`);
615
789
  ctx.fail(message, details);
616
790
  }
@@ -619,18 +793,56 @@ export const alertsCliCommand: CliCommandDef = {
619
793
  })
620
794
  : null;
621
795
  const priceListing = priced?.listing ?? null;
796
+ // A bare coin ticker read as another listing (SOL, a stale company; BTC, the Grayscale trust) gets `quote`'s note
797
+ // naming the coin's pair, before a metric alert names the venue it was read on.
798
+ const coinNote = priced ? coinListingNotes(priced.listing, priced.quote)[0] ?? null : null;
799
+ // What the alert reads now, which it is saved with, so lists and ALRT show it before the app has run.
800
+ let reading: Partial<AlertRule> = {};
801
+ // A daily rule whose close could not be read yet: why, which the add says beside when it is first checked.
802
+ let unread: string | null = null;
803
+ // A ratio or RRG alert's benchmark, as its listing was read.
804
+ const benchmarkNotes: string[] = [];
622
805
  if (request?.kind === "metric" && priced) {
623
806
  const exchange = canonicalExchange(priced.listing.exchange || priced.quote.listingExchangeName || priced.quote.exchangeName);
807
+ const typedMetric = request.metric;
808
+ if (isRelativeMetric(typedMetric)) {
809
+ // The benchmark is held to a listing that quotes, as the symbol is.
810
+ const reference = await withMarketData(ctx, (market) => requireQuotedListing(benchmarkKey(typedMetric), null, market, ctx, "alerts add"));
811
+ const benchmarkExchange = canonicalExchange(reference.listing.exchange || reference.quote.listingExchangeName || reference.quote.exchangeName);
812
+ if (reference.listing.symbol === priced.listing.symbol && benchmarkExchange === exchange) {
813
+ ctx.fail(`${typedMetric.kind === "rrg" ? "An RRG alert" : "A ratio alert"} compares two listings; ${priced.listing.symbol} is on both sides.`);
814
+ }
815
+ request.metric = { ...typedMetric, benchmark: reference.listing.symbol, ...(benchmarkExchange ? { benchmarkExchange } : {}) };
816
+ // Never save a rule its own list would refuse to read back.
817
+ if (!validMetricRule({ ...request, targetPrice: request.price })) ctx.fail(`${reference.listing.key} cannot be a benchmark: relative strength alerts compare two US listings.`);
818
+ benchmarkNotes.push(...reference.notes);
819
+ }
624
820
  const preview = { ...createAlert(priced.listing.symbol, request.condition, request.price, exchange), metric: request.metric };
625
- await withMarketData(ctx, market => metricReadings(market.dataProvider)(preview)).catch(error => ctx.fail(error instanceof Error ? error.message : String(error)));
821
+ // A listing without a published close calendar can never be checked; anything else is saved and checked as it can be.
822
+ const unavailable = metricAvailabilityError(preview);
823
+ if (unavailable) ctx.fail(unavailable);
824
+ try {
825
+ const value = await withMarketData(ctx, market => metricReadings(market.dataProvider, now)(preview));
826
+ reading = { lastCheckedPrice: value.value, lastCheckedTarget: value.target, lastQuoteUpdatedAt: value.at, lastCheckedAt: now,
827
+ ...value.day ? { lastDailyClose: value.day } : {} };
828
+ } catch (error) {
829
+ const message = error instanceof Error ? error.message : String(error);
830
+ // A valuation the inputs cannot give is refused; a daily close not yet final, or history not there yet, is read once it is.
831
+ if (!isDailyMetric(request.metric)) ctx.fail(message);
832
+ unread = message;
833
+ }
834
+ if (priced.quote.currency) reading.lastQuoteCurrency = priced.quote.currency;
626
835
  priced.listing.exchange = exchange;
627
836
  }
837
+ if (request?.kind === "price" && priced && !isQuoteStaleForCurrentSession(priced.quote)) {
838
+ // The price it is added at is its first reading and the baseline a crossing is measured from, as Add Alert keeps it.
839
+ const price = lastPrice(priced.quote);
840
+ if (price != null) reading = quoteAlertFields({ ...priced.quote, price }, now);
841
+ }
628
842
  if (request?.kind === "price" && request.thousandsDot && priced) {
629
843
  const question = thousandsDotMessage(request, priced.listing.key, lastPrice(priced.quote), exchangeOption);
630
844
  if (question) ctx.fail(question);
631
845
  }
632
- // A bare coin ticker read as a fund (BTC, the Grayscale trust) gets `quote`'s note naming the coin's pair.
633
- const coinNote = priced && !priced.listing.exchange ? notTheCoinNote(priced.listing.symbol, priced.quote) : null;
634
846
  if (request?.kind === "price" && priced) {
635
847
  // A target ten times away is a slip or another instrument's price, such as bitcoin's on the BTC fund.
636
848
  // Against the price the alert is checked on, as Add Alert measures it.
@@ -638,7 +850,7 @@ export const alertsCliCommand: CliCommandDef = {
638
850
  if (far) ctx.fail(far);
639
851
  }
640
852
  // What a typed listing was read as (7203:TSE as 7203:JPX, MXN as MXN:FWB2), said beside what was added.
641
- let read = [...priced?.notes ?? [], ...coinNote ? [coinNote] : []];
853
+ let read = [...priced?.notes ?? [], ...coinNote ? [coinNote] : [], ...benchmarkNotes];
642
854
  let newEventRule: EventAlertRule | null = null;
643
855
  let newResearchRule: EventAlertRule | null = null;
644
856
  if (research) {
@@ -653,6 +865,13 @@ export const alertsCliCommand: CliCommandDef = {
653
865
  ctx.fail(error instanceof Error ? error.message : String(error));
654
866
  }
655
867
  }
868
+ if (formRule) {
869
+ try {
870
+ newResearchRule = createFormRule(formRule, priceListing);
871
+ } catch (error) {
872
+ ctx.fail(error instanceof Error ? error.message : String(error));
873
+ }
874
+ }
656
875
  // A release named from the calendar, with what the words were read as and what else they name.
657
876
  let newRelease: CalendarEventChoice | null = null;
658
877
  // CPI and FOMC, typed so or named by one of their calendar rows (CPI y/y, Fed Interest Rate Decision).
@@ -727,22 +946,25 @@ export const alertsCliCommand: CliCommandDef = {
727
946
  const lookup = (targets: ScheduledEventTarget[]) => targets.some((target) => target.kind === "earnings")
728
947
  ? withMarketData(ctx, (market) => lookupScheduledDates(targets, scheduledEventSources(market.dataProvider), today, now))
729
948
  : lookupScheduledDates(targets, scheduledEventSources(null), today, now);
730
- const { dates, warnings } = await listEventDates(events.rules, now, lookup);
949
+ const [{ dates, warnings }, cloud] = await Promise.all([listEventDates(events.rules, now, lookup), cloudRuleStates(events.rules)]);
731
950
  const rows = [
732
951
  ...alerts.map(priceRow),
733
- ...events.rules.map((rule) => eventRow(rule, now, dates.has(rule.id) ? dates.get(rule.id) : rule.eventDate, checks[rule.id])),
952
+ ...events.rules.map((rule) => eventRow(rule, now, dates.has(rule.id) ? dates.get(rule.id) : rule.eventDate, checks[rule.id], cloud)),
734
953
  ];
735
- const allWarnings = [...(alertsError ? [alertsError] : []), ...(events.error ? [events.error] : []), ...warnings];
954
+ const allWarnings = [...(alertsError ? [alertsError] : []), ...(events.error ? [events.error] : []), ...warnings, ...cloud.warning ? [cloud.warning] : []];
736
955
  ctx.printResult({ data: rows, ...(allWarnings.length ? { warnings: allWarnings } : {}) }, {
737
956
  rows: (data) => data.map(textRow),
738
957
  textColumns: [
739
958
  { key: "id", header: "ID", shrink: false },
740
959
  { key: "kind", header: "Kind" },
741
- { key: "target", header: "Target" },
960
+ // A price or technical alert names the listing it is checked on, in its currency.
961
+ { key: "target", header: "Target", format: (value, row) => row.currency ? listingWithCurrency(String(row.listing), String(row.currency)) : String(value ?? "") },
742
962
  { key: "trigger", header: "Trigger" },
743
963
  { key: "eventDate", header: "Event Date" },
744
964
  { key: "status", header: "Status" },
745
- { key: "lastCheckedPrice", header: "Last Price", align: "right", optional: true },
965
+ { key: "lastReading", header: "Last Reading" },
966
+ // ISO 8601 in UTC, as CSV writes it.
967
+ { key: "createdAt", header: "Created", format: (value) => String(value ?? "") },
746
968
  { key: "triggeredAt", header: "Triggered", optional: true },
747
969
  ],
748
970
  empty: "No alerts. Add one with gloom alerts add AAPL above 250, alerts add CRWV earnings --days 3, alerts add CPI --days 1, or alerts add CN PMI.",
@@ -775,15 +997,45 @@ export const alertsCliCommand: CliCommandDef = {
775
997
  if (events.error) ctx.fail(events.error);
776
998
  return events.rules;
777
999
  };
1000
+ // Each rule created, re-armed or deleted here goes in this device's alert log, beside the alerts that fired.
1001
+ const record = (action: AlertLogAction, rule: AlertRule | EventAlertRule) => {
1002
+ const state = config.persistence?.pluginState;
1003
+ if (ctx.cliOptions.dryRun || !state) return;
1004
+ try {
1005
+ appendAlertLog({
1006
+ get: (key) => state.get(ALERTS_PLUGIN_ID, resumeStateKey(key))?.value,
1007
+ set: (key, value) => state.set(ALERTS_PLUGIN_ID, resumeStateKey(key), value),
1008
+ }, [alertChangeEntry(action, alertLogRule(rule), Date.now())]);
1009
+ } catch (error) {
1010
+ logFailure("alerts.cli", "Record alert change failed")(error);
1011
+ }
1012
+ };
778
1013
 
779
1014
  if (request?.kind === "price" || request?.kind === "metric") {
780
1015
  const { condition, price } = request;
781
1016
  const symbol = requireArg(priceListing?.key, ADD_USAGE, ctx);
782
- const alert = { ...createAlert(symbol, condition, price, priceListing?.exchange), ...(request.kind === "metric" ? { metric: request.metric } : {}) };
1017
+ const alert: AlertRule = { ...createAlert(symbol, condition, price, priceListing?.exchange), ...(request.kind === "metric" ? { metric: request.metric } : {}), ...reading };
1018
+ // The same rule on the same listing is one alert: a second add names it and adds nothing.
1019
+ const existing = alerts.find((entry) => sameLevelRule(entry, alert));
1020
+ if (existing) {
1021
+ const rearm = existing.status === "triggered" ? `; it went off, so re-arm it with gloom alerts rearm ${existing.id}` : "";
1022
+ ctx.fail(`${alertLogRule(alert).rule} already exists as alert ${existing.id}${rearm}. Nothing was added.`);
1023
+ }
783
1024
  await save({ alerts: [...alerts, alert] });
784
- ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert }, ...(read.length > 0 ? { warnings: read } : {}) }, {
1025
+ record("created", alert);
1026
+ // A daily rule is first judged on the first close after it was set, once that close has settled.
1027
+ // An RRG quadrant, on the last session of that week.
1028
+ const weekly = alert.metric?.kind === "rrg";
1029
+ const firstCheck = alert.metric && isDailyMetric(alert.metric) ? firstDailyCheck(alert.exchange, now, weekly) : null;
1030
+ const notes = [
1031
+ alert.metric ? unread ? `Not read yet: ${unread.replace(/\.$/, "")}.` : `${metricReadingNote(alert)}.` : null,
1032
+ firstCheck ? `First checked on the ${firstCheck.day} ${weekly ? "weekly close" : "close"}, from ${isoUtc(firstCheck.at).slice(11, 16)} UTC.` : null,
1033
+ ].filter(Boolean).join(" ");
1034
+ ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert,
1035
+ ...firstCheck ? { firstCheck: { close: firstCheck.day, at: isoUtc(firstCheck.at) } } : {},
1036
+ ...unread ? { unread } : {} }, ...(read.length > 0 ? { warnings: read } : {}) }, {
785
1037
  // The delivery, as docs/usage.md states it, before any full stop, which after the price would read as part of it (below 81.).
786
- text: (data) => `Added alert ${data.alert.id}: ${data.alert.metric ? metricDescription(data.alert) : `${symbol} ${condition} ${price}`} (${PRICE_DELIVERY}).${dryRunNote(data.dryRun)}`,
1038
+ text: (data) => `Added alert ${data.alert.id}: ${data.alert.metric ? metricDescription(data.alert) : `${symbol} ${condition} ${price}`} (${PRICE_DELIVERY}).${notes ? ` ${notes}` : ""}${dryRunNote(data.dryRun)}`,
787
1039
  });
788
1040
  return;
789
1041
  }
@@ -791,12 +1043,13 @@ export const alertsCliCommand: CliCommandDef = {
791
1043
  const rule = newResearchRule;
792
1044
  const rules = requireEvents();
793
1045
  const existing = rules.find((entry) => entry.kind === rule.kind && entry.target === rule.target && entry.value.toLowerCase() === rule.value.toLowerCase());
794
- if (existing) ctx.fail(`This alert already exists (ID ${existing.id}).`);
1046
+ if (existing) ctx.fail(`This alert already exists (ID ${existing.id}). Nothing was added.`);
795
1047
  if (rules.length >= MAX_EVENT_ALERTS) ctx.fail(`Keep at most ${MAX_EVENT_ALERTS} event alerts. Delete one with gloom alerts delete <id>.`);
796
1048
  await save({ events: [...rules, rule] });
797
- const kind = research!.kind;
1049
+ record("created", rule);
1050
+ const delivery = research ? researchDelivery(research.kind) : ruleDelivery(formRule!.event);
798
1051
  ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert: rule }, ...(read.length > 0 ? { warnings: read } : {}) }, {
799
- text: (data) => `Added alert ${data.alert.id}: ${alertKindLabel(rule.kind)} ${researchAlertDescription(rule)} (${researchDelivery(kind)}).${dryRunNote(data.dryRun)}`,
1052
+ text: (data) => `Added alert ${data.alert.id}: ${alertKindLabel(rule.kind)} ${eventAlertTarget(rule)} (${delivery}).${dryRunNote(data.dryRun)}`,
800
1053
  });
801
1054
  return;
802
1055
  }
@@ -807,10 +1060,11 @@ export const alertsCliCommand: CliCommandDef = {
807
1060
  const name = scheduledEventName(scheduled.target);
808
1061
  const existing = rules.find((entry) => entry.kind === rule.kind && entry.value.toLowerCase() === rule.value.toLowerCase());
809
1062
  if (existing) ctx.fail(scheduled.trigger === "results"
810
- ? `An alert for ${name} results already exists (ID ${existing.id}).`
811
- : `An alert ${plural(scheduled.leadDays, "day")} before ${name} already exists (ID ${existing.id}).`);
1063
+ ? `An alert for ${name} results already exists (ID ${existing.id}). Nothing was added.`
1064
+ : `An alert ${plural(scheduled.leadDays, "day")} before ${name} already exists (ID ${existing.id}). Nothing was added.`);
812
1065
  if (rules.length >= MAX_EVENT_ALERTS) ctx.fail(`Keep at most ${MAX_EVENT_ALERTS} event alerts. Delete one with gloom alerts delete <id>.`);
813
1066
  await save({ events: [...rules, rule] });
1067
+ record("created", rule);
814
1068
  const eventDate = rule.eventDate ?? null;
815
1069
  const due = !!eventDate && scheduledPhase(eventDate, scheduled.leadDays, undefined, scheduledToday(scheduled.target, now)) === "due";
816
1070
  const release = followsRelease(scheduled.target);
@@ -835,12 +1089,14 @@ export const alertsCliCommand: CliCommandDef = {
835
1089
  }
836
1090
  if (action === "delete" || action === "rm") {
837
1091
  const id = requireArg(args[1], "Usage: gloom alerts delete <id>", ctx);
838
- const isPrice = alerts.some((alert) => alert.id === id);
839
- const isEvent = !isPrice && events.rules.some((rule) => rule.id === id);
840
- if (!isPrice && !isEvent && alertsError) ctx.fail(alertsError);
841
- if (isPrice) await save({ alerts: alerts.filter((alert) => alert.id !== id) });
842
- if (isEvent) await save({ events: requireEvents().filter((rule) => rule.id !== id) });
843
- const changed = isPrice || isEvent;
1092
+ const level = alerts.find((alert) => alert.id === id) ?? null;
1093
+ const rule = level ? null : events.rules.find((entry) => entry.id === id) ?? null;
1094
+ if (!level && !rule && alertsError) ctx.fail(alertsError);
1095
+ if (level) await save({ alerts: alerts.filter((alert) => alert.id !== id) });
1096
+ if (rule) await save({ events: requireEvents().filter((entry) => entry.id !== id) });
1097
+ const gone = level ?? rule;
1098
+ if (gone) record("deleted", gone);
1099
+ const changed = !!gone;
844
1100
  ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun && changed, dryRun: ctx.cliOptions.dryRun, id } }, {
845
1101
  text: (data) => changed
846
1102
  ? `Deleted alert ${id}.${dryRunNote(data.dryRun)}`
@@ -850,22 +1106,25 @@ export const alertsCliCommand: CliCommandDef = {
850
1106
  }
851
1107
  if (action === "rearm") {
852
1108
  const id = requireArg(args[1], "Usage: gloom alerts rearm <id>", ctx);
853
- const isPrice = alerts.some((alert) => alert.id === id);
854
- const rule = isPrice ? null : events.rules.find((entry) => entry.id === id) ?? null;
855
- if (!isPrice && !rule && alertsError) ctx.fail(alertsError);
856
- if (isPrice) await save({ alerts: alerts.map((alert) => alert.id === id ? rearmAlert(alert) : alert) });
857
- if (rule) {
1109
+ const level = alerts.find((alert) => alert.id === id) ?? null;
1110
+ const rule = level ? null : events.rules.find((entry) => entry.id === id) ?? null;
1111
+ if (!level && !rule && alertsError) ctx.fail(alertsError);
1112
+ // An alert still waiting to go off has nothing to re-arm, and is left as it is.
1113
+ const active = level ? level.status === "active" : rule ? eventRuleArmed(rule, now) : false;
1114
+ if (level && !active) await save({ alerts: alerts.map((alert) => alert.id === id ? rearmAlert(alert) : alert) });
1115
+ if (rule && !active) {
858
1116
  // A scheduled alert alerts again for its coming event; any other event alert resumes.
859
1117
  const next = readScheduledRule(rule) ? rearmScheduledAlert(rule)
860
1118
  : rule.kind === "iv_level" && rule.triggeredAt ? rearmIvLevelAlert(rule)
861
1119
  : rule.status === "paused" ? toggleEventAlert(rule) : rule;
862
1120
  await save({ events: requireEvents().map((entry) => entry.id === id ? next : entry) });
863
1121
  }
864
- const found = isPrice || !!rule;
865
- ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun && found, dryRun: ctx.cliOptions.dryRun, id } }, {
866
- text: (data) => found
867
- ? `Re-armed alert ${id}.${dryRunNote(data.dryRun)}`
868
- : cliStyles.muted(`No alert with ID ${id}.`),
1122
+ const found = level ?? rule;
1123
+ if (found && !active) record("rearmed", found);
1124
+ ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun && !!found && !active, dryRun: ctx.cliOptions.dryRun, id, ...active ? { active: true } : {} } }, {
1125
+ text: (data) => !found ? cliStyles.muted(`No alert with ID ${id}.`)
1126
+ : active ? cliStyles.muted(`Alert ${id} is already active; nothing to re-arm.`)
1127
+ : `Re-armed alert ${id}.${dryRunNote(data.dryRun)}`,
869
1128
  });
870
1129
  return;
871
1130
  }