@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,5 +1,5 @@
1
1
  import { metricFormInput, metricShortcut, parseMetricAlert } from "./metric-command";
2
- import type { AlertMetric } from "./metric-model";
2
+ import { isRelativeMetric, type AlertMetric } from "./metric-model";
3
3
  import type { AlertCondition } from "./types";
4
4
  import { readAlertPrice } from "./price";
5
5
  import { normalizeAlertSymbol } from "./quotes";
@@ -86,3 +86,27 @@ export function parseAlertCommandValues(
86
86
  if (!symbol || !condition || !priceStr) return null;
87
87
  return { symbol, condition, price: readAlertPrice(priceStr.replace(/^\$/, "")) };
88
88
  }
89
+
90
+ /**
91
+ * The edit dialog's text read against the alert it changes. An edit keeps the alert in its kind and its synced list:
92
+ * price stays price, a valuation or technical measure stays one, and a ratio or RRG alert keeps its listings.
93
+ */
94
+ export function parseAlertEdit(
95
+ selected: { symbol: string; metric?: AlertMetric },
96
+ value: string,
97
+ ): AlertCommandInput {
98
+ const parsed = parseAlertCommandValues({ shortcut: value });
99
+ if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
100
+ if (!!parsed.metric !== !!selected.metric) throw new Error("Add a new alert to change between price and a valuation or technical measure.");
101
+ if (!parsed.metric || !selected.metric) return parsed;
102
+ if (isRelativeMetric(parsed.metric) !== isRelativeMetric(selected.metric)) {
103
+ throw new Error("Add a new alert to change between a relative strength alert and a valuation or technical measure.");
104
+ }
105
+ if (!isRelativeMetric(selected.metric)) return parsed;
106
+ // The benchmark listing was confirmed when the alert was added; a different pair is a new alert.
107
+ if (parsed.metric.kind !== selected.metric.kind || parsed.symbol !== selected.symbol || !isRelativeMetric(parsed.metric)
108
+ || parsed.metric.benchmark !== selected.metric.benchmark) {
109
+ throw new Error("Add a new alert to compare other listings.");
110
+ }
111
+ return { ...parsed, metric: { ...parsed.metric, benchmarkExchange: selected.metric.benchmarkExchange } };
112
+ }
@@ -36,6 +36,8 @@ import { rearmScheduledAlert } from "./scheduled";
36
36
  import { checkScheduledEventAlertsNow } from "./scheduled-check";
37
37
  import { checkIvLevelAlertsNow, IV_LEVEL_CHECKS_KEY, ivLevelCheckText, readIvLevelChecks, rearmIvLevelAlert } from "./iv-level";
38
38
  import { alertTarget, alertViewHints, eventRuleSubject, useOpenAlertTarget } from "./targets";
39
+ import { FIRED_LOG_KEY } from "./fired-log";
40
+ import { withAlertChange } from "./alert-log";
39
41
 
40
42
  const columnsFor = (width: number, lastCheck: boolean): DataTableColumn[] => [
41
43
  { id: "kind", label: "Event", width: 15, align: "left" },
@@ -59,6 +61,8 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
59
61
  );
60
62
  // Their readings stay on this device.
61
63
  const [savedChecks] = usePluginState<unknown>(IV_LEVEL_CHECKS_KEY, {});
64
+ // Deleting or re-arming a rule goes in this device's alert log, beside the alerts that fired.
65
+ const [, setAlertLog] = usePluginState<unknown>(FIRED_LOG_KEY, []);
62
66
  const checks = useMemo(() => readIvLevelChecks(savedChecks), [savedChecks]);
63
67
  /** Changes one rule in the list it is kept in, leaving what that list holds of kinds this version does not know. */
64
68
  const updateRules = useCallback(
@@ -123,11 +127,20 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
123
127
  (rule: EventAlertRule | undefined) => {
124
128
  if (!rule) return;
125
129
  const ivLevel = rule.kind === "iv_level";
126
- updateRules(rule, (rules) => rules.map((entry) => (entry.id === rule.id ? ivLevel ? rearmIvLevelAlert(entry) : rearmScheduledAlert(entry) : entry)));
130
+ const now = Date.now();
131
+ // Only a rule that went off is re-armed, as the list reads at the write: one still waiting is left, and not logged.
132
+ const rearmed: EventAlertRule[] = [];
133
+ updateRules(rule, (rules) => rules.map((entry) => {
134
+ if (entry.id !== rule.id || ruleState(entry, now) !== "Alerted") return entry;
135
+ rearmed.push(entry);
136
+ return ivLevel ? rearmIvLevelAlert(entry) : rearmScheduledAlert(entry);
137
+ }));
138
+ if (!rearmed.length) return;
139
+ for (const entry of rearmed) setAlertLog((saved: unknown) => withAlertChange(saved, "rearmed", entry));
127
140
  // Once the write has landed, so it alerts now rather than at the next 15-minute check.
128
141
  setTimeout(ivLevel ? checkIvLevelAlertsNow : checkScheduledEventAlertsNow, 1_000);
129
142
  },
130
- [updateRules],
143
+ [setAlertLog, updateRules],
131
144
  );
132
145
  const remove = useCallback(async () => {
133
146
  if (!selectedRule) return;
@@ -137,8 +150,10 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
137
150
  body: [eventAlertTarget(rule)],
138
151
  confirmLabel: "Delete",
139
152
  });
140
- if (confirmed) updateRules(rule, (rules) => rules.filter((entry) => entry.id !== rule.id));
141
- }, [dialog, selectedRule, updateRules]);
153
+ if (!confirmed) return;
154
+ updateRules(rule, (rules) => rules.filter((entry) => entry.id !== rule.id));
155
+ setAlertLog((saved: unknown) => withAlertChange(saved, "deleted", rule));
156
+ }, [dialog, selectedRule, setAlertLog, updateRules]);
142
157
  const status =
143
158
  error ??
144
159
  (selectedRule?.kind === "catalyst" ? catalystStatus : null) ??
@@ -5,11 +5,19 @@ import { ALERTS_PLUGIN_ID } from "./constants";
5
5
  /**
6
6
  * The alerts this device's app fired, kept on the device (plugin resume
7
7
  * state, never synced): `fn ALRT --tab history` and the History tab read it
8
- * signed out, and merge Gloom Cloud's deliveries in when signed in.
8
+ * signed out, and merge Gloom Cloud's deliveries in when signed in. Beside
9
+ * them it keeps when a rule was created, re-armed or deleted here, from the
10
+ * app or `gloom alerts` (`action`).
9
11
  */
10
12
  export const FIRED_LOG_KEY = "firedLog";
11
- /** The newest entries kept; older ones fall off as new ones are added. */
13
+ /** The newest fired alerts kept; older ones fall off as new ones are added. */
12
14
  export const MAX_FIRED_ALERTS = 1000;
15
+ /**
16
+ * The newest rule changes (created, re-armed, deleted) kept, apart from the
17
+ * fired alerts: a change never counts against them, so no number of changes
18
+ * pushes an alert that went off out of the log.
19
+ */
20
+ export const MAX_ALERT_CHANGES = 200;
13
21
 
14
22
  export interface FiredAlert {
15
23
  id: string;
@@ -32,10 +40,30 @@ export interface FiredAlert {
32
40
  source: string | null;
33
41
  symbol?: string;
34
42
  exchange?: string;
43
+ /** A change to the rule rather than the alert going off: created, re-armed or deleted. */
44
+ action?: AlertLogAction;
35
45
  }
36
46
 
37
47
  export type FiredAlertInput = Omit<FiredAlert, "id" | "firedAt"> & { firedAt: number };
38
48
 
49
+ const ALERT_LOG_ACTIONS = ["created", "rearmed", "deleted"] as const;
50
+ export type AlertLogAction = (typeof ALERT_LOG_ACTIONS)[number];
51
+ /** What History says of a change, in its Value column. */
52
+ const ALERT_LOG_ACTION_TEXT: Record<AlertLogAction, string> = { created: "Created", rearmed: "Re-armed", deleted: "Deleted" };
53
+
54
+ /** A change to a rule as the log keeps it: when, what was done, and the rule as it read. */
55
+ export function alertChangeEntry(
56
+ action: AlertLogAction,
57
+ rule: { kind: string; ruleId: string; rule: string; symbol?: string; exchange?: string },
58
+ at = Date.now(),
59
+ ): FiredAlertInput {
60
+ return {
61
+ kind: rule.kind, ruleId: rule.ruleId, rule: rule.rule, firedAt: at, value: null, quoteAt: null, source: null, action,
62
+ detail: ALERT_LOG_ACTION_TEXT[action],
63
+ ...(rule.symbol ? { symbol: rule.symbol } : {}), ...(rule.exchange ? { exchange: rule.exchange } : {}),
64
+ };
65
+ }
66
+
39
67
  /** How current a quote was, in the words reports use. */
40
68
  export function quoteSourceWords(source: QuoteDataSource | undefined): string | null {
41
69
  return source === "live" ? "real-time" : source === "delayed" ? "delayed" : source === "snapshot" ? "snapshot" : null;
@@ -57,7 +85,8 @@ export function readFiredAlerts(value: unknown): FiredAlert[] {
57
85
  && isText(entry.firedAt) && Number.isFinite(Date.parse(entry.firedAt))
58
86
  && (entry.value === null || Number.isFinite(entry.value))
59
87
  && (entry.quoteAt === null || isText(entry.quoteAt)) && (entry.source === null || isText(entry.source))
60
- && optionalText(entry.unit) && optionalText(entry.detail) && optionalText(entry.symbol) && optionalText(entry.exchange));
88
+ && optionalText(entry.unit) && optionalText(entry.detail) && optionalText(entry.symbol) && optionalText(entry.exchange)
89
+ && (entry.action === undefined || ALERT_LOG_ACTIONS.includes(entry.action)));
61
90
  }
62
91
 
63
92
  function firedAlertEntry(input: FiredAlertInput, id: string = crypto.randomUUID()): FiredAlert {
@@ -65,19 +94,39 @@ function firedAlertEntry(input: FiredAlertInput, id: string = crypto.randomUUID(
65
94
  return { id, firedAt: firedAlertTime(firedAt) ?? new Date().toISOString(), ...rest };
66
95
  }
67
96
 
68
- /** `entries` with one more at the end, the oldest dropped past the cap. */
97
+ /**
98
+ * The log within its caps, oldest first: the newest MAX_FIRED_ALERTS fired
99
+ * alerts and, counted apart, the newest MAX_ALERT_CHANGES rule changes. Each
100
+ * kind of entry only ever drops its own oldest.
101
+ */
102
+ function capAlertLog(entries: readonly FiredAlert[]): FiredAlert[] {
103
+ let changes = entries.reduce((count, entry) => count + (entry.action ? 1 : 0), 0);
104
+ let fired = entries.length - changes;
105
+ changes -= MAX_ALERT_CHANGES;
106
+ fired -= MAX_FIRED_ALERTS;
107
+ if (changes <= 0 && fired <= 0) return [...entries];
108
+ return entries.filter((entry) => {
109
+ if (entry.action ? changes <= 0 : fired <= 0) return true;
110
+ if (entry.action) changes -= 1;
111
+ else fired -= 1;
112
+ return false;
113
+ });
114
+ }
115
+
116
+ /** `entries` with one more at the end, the oldest of its kind (fired alert or rule change) dropped past its cap. */
69
117
  export function appendFiredAlert(entries: readonly FiredAlert[], input: FiredAlertInput, id: string = crypto.randomUUID()): FiredAlert[] {
70
- return [...entries, firedAlertEntry(input, id)].slice(-MAX_FIRED_ALERTS);
118
+ return capAlertLog([...entries, firedAlertEntry(input, id)]);
71
119
  }
72
120
 
73
- /** `entries` keeping only the newest `max` of each kind in `kinds`; other kinds are untouched. */
121
+ /** `entries` keeping only the newest `max` fired alerts of each kind in `kinds`; other kinds and rule changes are untouched. */
74
122
  function capFiredAlertKinds(entries: readonly FiredAlert[], kinds: ReadonlySet<string>, max: number): FiredAlert[] {
123
+ const counted = (entry: FiredAlert) => !entry.action && kinds.has(entry.kind);
75
124
  const over = new Map<string, number>();
76
- for (const entry of entries) if (kinds.has(entry.kind)) over.set(entry.kind, (over.get(entry.kind) ?? 0) + 1);
125
+ for (const entry of entries) if (counted(entry)) over.set(entry.kind, (over.get(entry.kind) ?? 0) + 1);
77
126
  for (const [kind, count] of over) over.set(kind, count - max);
78
127
  // Oldest first: the first entries of a kind over its cap are the ones to drop.
79
128
  return entries.filter((entry) => {
80
- const extra = over.get(entry.kind) ?? 0;
129
+ const extra = counted(entry) ? over.get(entry.kind) ?? 0 : 0;
81
130
  if (extra <= 0) return true;
82
131
  over.set(entry.kind, extra - 1);
83
132
  return false;
@@ -101,12 +150,27 @@ export function recordFiredAlertFor(
101
150
  let entries = [...readFiredAlerts(runtime.getResumeState(ALERTS_PLUGIN_ID, FIRED_LOG_KEY)), ...inputs.map((input) => firedAlertEntry(input))];
102
151
  // The source's own oldest entries go first, before the log's cap reaches anyone else's.
103
152
  if (keepAtMost != null) entries = capFiredAlertKinds(entries, new Set(inputs.map((input) => input.kind)), keepAtMost);
104
- runtime.setResumeState(ALERTS_PLUGIN_ID, FIRED_LOG_KEY, entries.slice(-MAX_FIRED_ALERTS));
153
+ runtime.setResumeState(ALERTS_PLUGIN_ID, FIRED_LOG_KEY, capAlertLog(entries));
105
154
  } catch (error) {
106
155
  onError(error);
107
156
  }
108
157
  }
109
158
 
159
+ /**
160
+ * Adds entries to the device's log through the plugin's saved state as the
161
+ * shell reads and writes it (`gloom alerts`), where there is no running
162
+ * plugin to record them.
163
+ */
164
+ export function appendAlertLog(
165
+ state: { get(key: string): unknown; set(key: string, value: unknown): void },
166
+ inputs: readonly FiredAlertInput[],
167
+ ): void {
168
+ if (!inputs.length) return;
169
+ let entries = readFiredAlerts(state.get(FIRED_LOG_KEY));
170
+ for (const input of inputs) entries = appendFiredAlert(entries, input);
171
+ state.set(FIRED_LOG_KEY, entries);
172
+ }
173
+
110
174
  /** Records a fired alert in the device's log. A log that cannot be written never stops the alert itself. */
111
175
  export function createFiredAlertRecorder(resume: Pick<PluginResumeState, "getState" | "setState">, onError: (error: unknown) => void) {
112
176
  return (input: FiredAlertInput) => {
@@ -1,8 +1,12 @@
1
- import { isDailyMetric } from "./metric-model";
1
+ import { closeBasis, metricReadingText, rotationQuadrant } from "./metric-model";
2
2
  import { formatMarketPrice } from "../../../market-data/market/format";
3
+ import { formatShortDate } from "../../../utils/datetime-format";
3
4
  import { formatQuoteAgeWithSource } from "../../../market-data/quotes/time";
4
5
  import type { AlertCondition, AlertRule } from "./types";
5
6
 
7
+ /** What a list says of an alert no check has read yet. */
8
+ export const NOT_CHECKED_YET = "not checked yet";
9
+
6
10
  export function relativeTime(ts: number): string {
7
11
  const diff = Date.now() - ts;
8
12
  const seconds = Math.floor(diff / 1000);
@@ -15,10 +19,16 @@ export function relativeTime(ts: number): string {
15
19
  return `${days}d ago`;
16
20
  }
17
21
 
22
+ /** A price ratio to four decimals, as GR states the latest ratio. */
23
+ const formatRatio = (value: number) => value.toFixed(Math.abs(value) >= 100 ? 2 : 4);
24
+
18
25
  export function formatCurrentPrice(alert: AlertRule, maxWidth = 9): string {
19
26
  if (alert.lastCheckError) return !alert.metric ? "No quote"
20
27
  : /US listing|US equities|calendar/.test(alert.lastCheckError) ? "US only"
21
- : /missing|bars|history/i.test(alert.lastCheckError) ? "Bar gap" : "No data";
28
+ : /missing|bars|history|weekly closes/i.test(alert.lastCheckError) ? "Bar gap" : "No data";
29
+ // The quadrant the latest weekly strength and momentum place it in.
30
+ if (alert.metric?.kind === "rrg") return rotationQuadrant(alert.lastCheckedPrice, alert.lastCheckedTarget) ?? "-";
31
+ if (alert.metric?.kind === "rs_ratio" && alert.lastCheckedPrice != null) return formatRatio(alert.lastCheckedPrice);
22
32
  return alert.lastCheckedPrice == null
23
33
  ? "-"
24
34
  : formatMarketPrice(alert.lastCheckedPrice, {
@@ -30,6 +40,8 @@ export function formatCurrentPrice(alert: AlertRule, maxWidth = 9): string {
30
40
  export function formatAlertTargetPrice(alert: AlertRule, maxWidth = 9): string {
31
41
  if (alert.metric?.kind === "ma_cross") return `${alert.metric.slowPeriod}-day MA`;
32
42
  if (alert.metric?.kind === "close_ma") return `${alert.metric.period}-day MA`;
43
+ if (alert.metric?.kind === "rrg") return `${alert.metric.quadrant[0]!.toUpperCase()}${alert.metric.quadrant.slice(1)}`;
44
+ if (alert.metric?.kind === "rs_ratio") return formatRatio(alert.targetPrice);
33
45
  return formatMarketPrice(alert.targetPrice, {
34
46
  maxWidth,
35
47
  minimumFractionDigits: 2,
@@ -38,7 +50,7 @@ export function formatAlertTargetPrice(alert: AlertRule, maxWidth = 9): string {
38
50
 
39
51
  export function formatAlertDistance(alert: AlertRule): string {
40
52
  const currentPrice = alert.lastCheckedPrice;
41
- if (alert.lastCheckError) return "-";
53
+ if (alert.lastCheckError || alert.metric?.kind === "rrg") return "-";
42
54
  if (currentPrice == null || !Number.isFinite(currentPrice) || currentPrice === 0) {
43
55
  return "-";
44
56
  }
@@ -52,7 +64,12 @@ export function formatAlertDistance(alert: AlertRule): string {
52
64
 
53
65
  /** The Quote cell: the quote's age and feed, or, on the row of an alert that got none, why (a stale quote, or none). */
54
66
  export function formatQuoteChecked(alert: AlertRule): string {
55
- if (alert.metric && isDailyMetric(alert.metric)) return "on daily close";
67
+ const basis = alert.metric ? closeBasis(alert.metric) : null;
68
+ // A settled close reads as the session it is from: Oct 26 close, Oct 23 weekly.
69
+ if (basis && alert.lastDailyClose && !alert.lastCheckError) {
70
+ return `${formatShortDate(alert.lastDailyClose, { year: false, utc: true })} ${basis === "weekly close" ? "weekly" : "close"}`;
71
+ }
72
+ if (basis) return `on ${basis}`;
56
73
  if (alert.lastCheckError) return /\bstale\b/i.test(alert.lastCheckError) ? "Stale" : "No quote";
57
74
  if (alert.lastQuoteUpdatedAt) {
58
75
  return formatQuoteAgeWithSource({
@@ -61,7 +78,60 @@ export function formatQuoteChecked(alert: AlertRule): string {
61
78
  });
62
79
  }
63
80
  if (alert.lastCheckedAt) return relativeTime(alert.lastCheckedAt);
64
- return "-";
81
+ return NOT_CHECKED_YET;
82
+ }
83
+
84
+ /** A time as lists and exports give it: ISO 8601 in UTC, to the second (2026-10-27T06:35:12Z); empty for none. */
85
+ export function isoUtc(value: number | null | undefined): string {
86
+ return value != null && Number.isFinite(value) && value > 0 ? new Date(value).toISOString().replace(/\.\d{3}Z$/, "Z") : "";
87
+ }
88
+
89
+ /**
90
+ * The listing an alert is checked on: `EWG:ARCA`, its own exchange, or the
91
+ * venue its last quote came from for a rule set by symbol alone; the symbol
92
+ * until it has been checked.
93
+ */
94
+ export function alertListing(alert: Pick<AlertRule, "symbol" | "exchange" | "lastQuoteExchange">): string {
95
+ const venue = alert.exchange || alert.lastQuoteExchange;
96
+ return venue && !alert.symbol.includes(":") ? `${alert.symbol}:${venue}` : alert.symbol;
97
+ }
98
+
99
+ /** A listing with the currency its prices are in: `EWG:ARCA (USD)`; the listing alone until a quote has said it. */
100
+ export function listingWithCurrency(listing: string, currency: string | null | undefined): string {
101
+ return currency ? `${listing} (${currency})` : listing;
102
+ }
103
+
104
+ /** The unit of an alert's reading: the listing's currency for a price or an average, `x` for a multiple, none for RSI. */
105
+ function readingUnit(alert: AlertRule): string {
106
+ const kind = alert.metric?.kind;
107
+ if (kind === "pe" || kind === "ps") return "x";
108
+ if (kind === "rsi" || kind === "rs_ratio" || kind === "rrg") return "";
109
+ return alert.lastQuoteCurrency ?? "";
110
+ }
111
+
112
+ /** When the last reading is from: the quote's own time, or the daily close's; null with none. */
113
+ export function alertReadingAt(alert: AlertRule): number | null {
114
+ if (alert.lastCheckError || alert.lastCheckedPrice == null) return null;
115
+ return alert.lastQuoteUpdatedAt ?? alert.lastCheckedAt ?? null;
116
+ }
117
+
118
+ /**
119
+ * The last reading an alert was checked on, with its unit and when it is
120
+ * from: `37.12 USD as of 2026-10-27T14:31Z`, `61.23 on the 2026-10-26 close`
121
+ * for a daily rule, why the last check read nothing, or `not checked yet`.
122
+ */
123
+ export function alertReadingText(alert: AlertRule): string {
124
+ if (alert.lastCheckError) return `no reading: ${alert.lastCheckError.replace(/\.$/, "")}`;
125
+ const value = alert.lastCheckedPrice;
126
+ if (value == null || !Number.isFinite(value)) return NOT_CHECKED_YET;
127
+ const unit = readingUnit(alert);
128
+ const figure = alert.metric?.kind === "rrg" ? metricReadingText(alert.metric, { value, target: alert.lastCheckedTarget ?? Number.NaN })
129
+ : alert.metric?.kind === "rs_ratio" ? formatRatio(value)
130
+ : `${alert.metric ? value.toFixed(2) : String(value)}${unit === "x" ? "x" : unit ? ` ${unit}` : ""}`;
131
+ const basis = alert.metric ? closeBasis(alert.metric) : null;
132
+ if (basis && alert.lastDailyClose) return `${figure} on the ${alert.lastDailyClose} ${basis === "weekly close" ? "weekly close" : "close"}`;
133
+ const at = alertReadingAt(alert);
134
+ return at ? `${figure} as of ${isoUtc(at).replace(/:\d{2}Z$/, "Z")}` : figure;
65
135
  }
66
136
 
67
137
  export function conditionLabel(condition: AlertCondition): string {
@@ -1,4 +1,4 @@
1
- import { isDailyMetric, metricName } from "./metric-model";
1
+ import { closeBasis, metricName, metricReadingText } from "./metric-model";
2
2
  /**
3
3
  * `gloom fn ALRT`: the Alerts pane's three tabs as reports. Prices lists
4
4
  * the price alerts as the pane's table does, Events the event alerts (earnings,
@@ -13,6 +13,8 @@ import { ALERT_TABS, ALERTS_PLUGIN_ID } from "./constants";
13
13
  import { ruleState } from "./event-rows";
14
14
  import { eventAlertTarget, type EventAlertRule } from "./events";
15
15
  import {
16
+ alertListing,
17
+ isoUtc,
16
18
  conditionLabel,
17
19
  formatAlertDistance,
18
20
  formatAlertTargetPrice,
@@ -54,15 +56,26 @@ const PRICE_COLUMNS: HeadlessPaneColumn[] = [
54
56
  { key: "awayPercent", header: "Away", align: "right", format: percent, textFormat: (_value, row) => formatAlertDistance(asAlert(row)) },
55
57
  { key: "condition", header: "Trigger", format: (value, row) => {
56
58
  const metric = asAlert(row).metric;
57
- return metric?.kind === "ma_cross" ? `Cross ${metric.direction}` : conditionLabel(value as AlertRule["condition"]);
59
+ return metric?.kind === "ma_cross" ? `Cross ${metric.direction}` : metric?.kind === "rrg" ? "Enters" : conditionLabel(value as AlertRule["condition"]);
58
60
  } },
59
61
  { key: "quoteAt", header: "Quote", textFormat: (_value, row) => formatQuoteChecked(asAlert(row)) },
60
62
  { key: "quoteSource", header: "Quote source" },
61
63
  { key: "lastCheckedAt", header: "Checked", format: instant },
62
64
  { key: "triggeredAt", header: "Alerted", format: instant, textFormat: (value) => (typeof value === "number" ? relativeTime(value) : "-") },
65
+ // After the columns exports have always had: when the rule was made, and the currency of its listing.
66
+ // To the second, as `alerts list` and the pane's detail write it: exports draw a stored millisecond time to the millisecond.
67
+ { key: "created", header: "Created" },
68
+ { key: "listing", header: "Listing" },
69
+ { key: "currency", header: "Currency" },
70
+ // A measure's last reading in words, last so the columns before it keep their places: an RRG quadrant with the strength and momentum that place it.
71
+ { key: "reading", header: "Reading" },
63
72
  ];
64
73
  /** The terminal table keeps the pane's columns: the Quote cell already says the quote's age and feed. */
65
- const PRICE_TEXT_COLUMNS = PRICE_COLUMNS.filter((column) => column.key !== "quoteSource" && column.key !== "lastCheckedAt");
74
+ const PRICE_TEXT_COLUMNS: HeadlessPaneColumn[] = [
75
+ ...PRICE_COLUMNS.filter((column) => !["quoteSource", "lastCheckedAt", "created", "listing", "currency", "reading"].includes(column.key)),
76
+ // The day it was made, as the Events table lists its rules.
77
+ { key: "createdAt", header: "Since", textFormat: (value) => (typeof value === "number" ? isoUtc(value).slice(0, 10) : "") },
78
+ ];
66
79
 
67
80
  const EVENT_COLUMNS: HeadlessPaneColumn[] = [
68
81
  { key: "event", header: "Event" },
@@ -98,13 +111,18 @@ const HISTORY_TEXT_COLUMNS = HISTORY_COLUMNS.filter((column) => column.key !== "
98
111
  function priceRow(alert: AlertRule): HeadlessPaneRow {
99
112
  const { lastQuoteProviderId: _provider, ...stored } = alert;
100
113
  const price = alert.lastCheckError ? null : alert.lastCheckedPrice;
101
- const away = price == null || !Number.isFinite(price) || price === 0 ? null : (((alert.lastCheckedTarget ?? alert.targetPrice) - price) / price) * 100;
114
+ const away = price == null || !Number.isFinite(price) || price === 0 || alert.metric?.kind === "rrg" ? null : (((alert.lastCheckedTarget ?? alert.targetPrice) - price) / price) * 100;
102
115
  const quoted = !alert.lastCheckError;
116
+ const target = alert.lastCheckedTarget ?? alert.targetPrice;
103
117
  return {
104
118
  ...stored,
105
119
  awayPercent: away != null && Number.isFinite(away) ? Number(away.toFixed(2)) : null,
106
120
  quoteAt: quoted ? firedAlertTime(alert.lastQuoteUpdatedAt) : null,
107
- quoteSource: quoted ? alert.metric && isDailyMetric(alert.metric) ? "daily close" : quoteSourceWords(alert.lastQuoteSource) : null,
121
+ quoteSource: quoted ? alert.metric && closeBasis(alert.metric) || quoteSourceWords(alert.lastQuoteSource) : null,
122
+ created: isoUtc(alert.createdAt) || null,
123
+ listing: alertListing(alert),
124
+ currency: alert.lastQuoteCurrency ?? null,
125
+ reading: alert.metric && price != null && Number.isFinite(price) && Number.isFinite(target) ? metricReadingText(alert.metric, { value: price, target }) : null,
108
126
  };
109
127
  }
110
128
 
@@ -234,7 +252,7 @@ export const alertsHeadless: HeadlessPaneDefinition<"rows"> = {
234
252
  discovery: {
235
253
  screenshotReadiness: "live-dom",
236
254
  dataRequirements: ["Your saved alerts and this device's log of fired alerts; History adds Gloom Cloud's deliveries when signed in"],
237
- limitations: ["Current price and quote time are what the app saved at its last check, blank until it has checked the alert."],
255
+ limitations: ["Current price and quote time are what the app or `gloom alerts add` saved at the last check; an alert neither has read says not checked yet."],
238
256
  },
239
257
  describe: (args) => `Alerts | ${String(args.options.tab ?? "prices")}`,
240
258
  async load(args, ctx) {
@@ -44,10 +44,11 @@ const upper = (value: unknown) => (typeof value === "string" && value ? value.to
44
44
  * symbol, within the time the two take to send it.
45
45
  */
46
46
  function sameAlert(entry: FiredAlert, item: AlertHistoryItem): boolean {
47
- if (item.kind !== entry.kind) return false;
47
+ // A rule created, re-armed or deleted here is no delivery.
48
+ if (entry.action || item.kind !== entry.kind) return false;
48
49
  const apart = Math.abs(Date.parse(item.deliveredAt) - Date.parse(entry.firedAt));
49
50
  if (!Number.isFinite(apart)) return false;
50
- if (entry.kind === "price" || ["pe", "ps", "rsi", "ma_cross", "close_ma"].includes(entry.kind)) return item.data?.alertId === entry.ruleId && apart <= PRICE_MATCH_MS;
51
+ if (entry.kind === "price" || ["pe", "ps", "rsi", "ma_cross", "close_ma", "rs_ratio", "rrg"].includes(entry.kind)) return item.data?.alertId === entry.ruleId && apart <= PRICE_MATCH_MS;
51
52
  const symbol = upper(item.data?.symbol);
52
53
  return !!symbol && symbol === upper(entry.symbol) && apart <= EVENT_MATCH_MS;
53
54
  }
@@ -38,7 +38,7 @@ export interface AlertHistory {
38
38
  const KIND_LABELS: Record<string, string> = {
39
39
  catalyst: "Catalyst",
40
40
  price: "Price",
41
- pe: "P/E", ps: "P/S", rsi: "RSI", ma_cross: "MA crossover", close_ma: "Close vs MA",
41
+ pe: "P/E", ps: "P/S", rsi: "RSI", ma_cross: "MA crossover", close_ma: "Close vs MA", rs_ratio: "Ratio", rrg: "RRG quadrant",
42
42
  breaking_news: "Breaking news",
43
43
  earnings: "Earnings",
44
44
  chat: "Chat",
@@ -1,9 +1,11 @@
1
- import { METRIC_CHOICES, metricWizardFields } from "./metric-command";
2
- import { metricReadings } from "./metric-data";
3
- import { metricTrigger, isDailyMetric, type MetricReading } from "./metric-model";
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";
4
6
  import { startCatalystAlerts } from "./catalysts";
5
7
  import { notTheCoinNote } from "../../../cli/commands/crypto-hints";
6
- import { createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
8
+ import { alertChangeEntry, createFiredAlertRecorder, firedAlertTime, quoteSourceWords } from "./fired-log";
7
9
  import { checkIvLevelAlertsNow, startIvLevelAlerts } from "./iv-level";
8
10
  import { logFailure } from "../../../utils/log-failure";
9
11
  import { RESEARCH_FORM_KINDS, RESEARCH_LABELS, researchWizardFields, createResearchAlert } from "./research-builder";
@@ -22,12 +24,15 @@ import { isResearchAlertKind, MAX_LEAD_DAYS } from "./research-rules";
22
24
  import { EVENT_ALERT_TEMPLATES, MAX_EVENT_ALERTS, createEventAlert, eventAlertStoreKey, readEventAlertStores, updateEventAlerts } from "./events";
23
25
  import { formatMarketPrice } from "../../../market-data/market/format";
24
26
  import { settleTypedListingKey } from "../../../tickers/listing-venues";
27
+ import { parseListingArg } from "../../../cli/listing-arg";
28
+ import { notATickerSymbol } from "../../../sources/provider-errors";
25
29
  import type { Quote } from "../../../types/financials";
26
30
  import type { GloomPlugin } from "../../../types/plugin";
27
31
  import {
28
32
  createAlert,
29
33
  evaluateAlert,
30
34
  formatAlertDescription,
35
+ sameLevelRule,
31
36
  } from "./alert-engine";
32
37
  import {
33
38
  parseAlertCommandValues,
@@ -65,6 +70,16 @@ let alertStream: AlertQuoteStream | null = null;
65
70
  */
66
71
  const STREAM_PERSIST_MS = 60_000;
67
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
+
68
83
  export const alertsPlugin: GloomPlugin = {
69
84
  id: "alerts",
70
85
  name: "Alerts",
@@ -186,31 +201,75 @@ export const alertsPlugin: GloomPlugin = {
186
201
  ...metricWizardFields,
187
202
  ],
188
203
  async execute(values) {
189
- const input = parseAlertCommandValues(values);
204
+ let input = parseAlertCommandValues(values);
190
205
  if (!input) throw new Error("Use a symbol, condition, and target price.");
191
206
  // 7203:TSE, typed or from a link, is the venue its symbol trades on, as `alerts add` reads it.
192
- const symbol = (await settleTypedListingKey(input.symbol, { store: ctx.tickerRepository, dataProvider: ctx.marketData })).toUpperCase();
193
-
194
- 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
+ }
195
235
  // A target ten times away is a slip or another instrument's price, such as bitcoin's on the BTC fund.
196
236
  const far = farTargetMessage(symbol, input.price, quote.price, symbol.includes(":") ? null : notTheCoinNote(symbol, quote));
197
237
  if (!input.metric && far) throw new Error(far);
198
238
 
199
- const alert = {
239
+ const alert: AlertRule = {
200
240
  ...createAlert(quote.symbol || symbol, input.condition, input.price),
201
- ...(!input.metric ? quoteAlertFields(quote) : { metric: input.metric, exchange: quote.listingExchangeName || quote.exchangeName }),
241
+ ...(!metric ? quoteAlertFields(quote) : {
242
+ metric, exchange: quote.listingExchangeName || quote.exchangeName,
243
+ ...quote.currency ? { lastQuoteCurrency: quote.currency } : {},
244
+ }),
202
245
  };
203
- if (input.metric) {
204
- const reading = await metricReadings(ctx.marketData)(alert);
205
- Object.assign(alert, { lastCheckedPrice: reading.value, lastCheckedTarget: reading.target, lastQuoteUpdatedAt: reading.at });
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
+ }
206
261
  }
207
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.`);
208
266
  existing.push(alert);
209
267
  await saveAlerts(ctx, existing, input.metric ? "metric" : "price");
268
+ recordFired(alertChangeEntry("created", alertLogRule(alert)));
210
269
  alertStream?.sync();
211
270
 
212
271
  ctx.notify({
213
- body: `Alert set: ${formatAlertDescription(alert)}${input.metric ? "" : ` (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`}`,
272
+ body: `Alert set: ${formatAlertDescription(alert)}${input.metric ? firstCheck ? `, ${firstCheck}` : "" : ` (current ${formatMarketPrice(quote.price, { minimumFractionDigits: 2 })})`}`,
214
273
  type: "success",
215
274
  });
216
275
  },
@@ -281,6 +340,7 @@ export const alertsPlugin: GloomPlugin = {
281
340
  // An IV level goes in its own list, which released versions never read.
282
341
  const key = eventAlertStoreKey(rule);
283
342
  await ctx.configState.set(key, updateEventAlerts(ctx.configState.get<string>(key), (rules) => [...rules, rule]));
343
+ recordFired(alertChangeEntry("created", alertLogRule(rule)));
284
344
  ctx.notify({ body: "Event alert saved.", type: "success" });
285
345
  if (scheduled) checkScheduledEventAlertsNow();
286
346
  if (rule.kind === "iv_level") checkIvLevelAlertsNow();
@@ -335,11 +395,14 @@ export const alertsPlugin: GloomPlugin = {
335
395
  const firedAt = Date.now();
336
396
  alert.status = "triggered"; alert.triggeredAt = firedAt;
337
397
  const description = formatAlertDescription(alert);
398
+ const readingText = metricReadingText(alert.metric, reading);
338
399
  recordFired({ kind: alert.metric.kind, ruleId: alert.id, rule: description, firedAt, value: reading.value,
339
400
  unit: alert.metric.kind === "pe" || alert.metric.kind === "ps" ? "x" : undefined,
340
- quoteAt: firedAlertTime(reading.at), source: isDailyMetric(alert.metric) ? "daily close" : null,
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),
341
404
  symbol: alert.symbol, exchange: alert.exchange });
342
- ctx.notify({ body: `${description} triggered at ${Number(reading.value.toFixed(4))}`, type: "success",
405
+ ctx.notify({ body: alert.metric.kind === "rrg" ? `${description} triggered: ${readingText}` : `${description} triggered at ${readingText}`, type: "success",
343
406
  refId: `price:${alert.id}`, desktop: "always", persistent: true, sound: "Glass",
344
407
  action: { label: "Open", onClick: () => ctx.showPane("alerts") } });
345
408
  }