@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,9 +1,10 @@
1
- import { METRIC_ALERTS_KEY, isDailyMetric, metricName } from "./metric-model";
1
+ import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, closeBasis, metricName, metricReadingText } from "./metric-model";
2
2
  import { metricAvailabilityError } from "./metric-data";
3
- import { readLevelAlertStores } from "./storage";
3
+ import { relativeAlertText } from "./metric-command";
4
+ import { levelAlertStoreKey, readLevelAlertStores } from "./storage";
4
5
  import { Box, type ScrollBoxRenderable } from "../../../ui";
5
6
  import { Button, DetailScrollBody, KeyValueRow, Notice, PaneStatusBody, PaneFooterScope, usePaneTabs } from "../../../components";
6
- import { usePluginPaneState } from "../../runtime";
7
+ import { usePluginPaneState, usePluginState } from "../../runtime";
7
8
  import { EventAlertsPane } from "./events-pane";
8
9
  import { AlertHistoryPane } from "./history-pane";
9
10
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from "react";
@@ -30,16 +31,21 @@ import {
30
31
  readAlertsStoreError,
31
32
  serializeAlerts,
32
33
  } from "./alert-engine";
33
- import { parseAlertCommandValues } from "./command";
34
+ import { parseAlertEdit } from "./command";
34
35
  import { ALERT_TABS, ALERTS_KEY } from "./constants";
35
36
  import {
37
+ alertListing,
36
38
  conditionLabel,
37
39
  formatAlertDistance,
38
40
  formatAlertTargetPrice,
39
41
  formatCurrentPrice,
40
42
  formatQuoteChecked,
43
+ isoUtc,
44
+ listingWithCurrency,
41
45
  relativeTime,
42
46
  } from "./format";
47
+ import { FIRED_LOG_KEY } from "./fired-log";
48
+ import { withAlertChange } from "./alert-log";
43
49
  import type { AlertRule } from "./types";
44
50
  import { useQuoteEntries } from "../../../market-data/hooks";
45
51
  import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
@@ -57,7 +63,8 @@ type AlertColumnId =
57
63
  | "away"
58
64
  | "condition"
59
65
  | "quote"
60
- | "triggered";
66
+ | "triggered"
67
+ | "created";
61
68
 
62
69
  type AlertColumn = DataTableColumn & { id: AlertColumnId };
63
70
 
@@ -69,8 +76,11 @@ const ALERT_COLUMNS: AlertColumn[] = [
69
76
  { id: "target", label: "Target", width: 11, align: "right" },
70
77
  { id: "away", label: "Away", width: 8, align: "right" },
71
78
  { id: "condition", label: "Trigger", width: 12, align: "left" },
72
- { id: "quote", label: "Quote", width: 14, align: "left" },
79
+ // Wide enough to say "not checked yet" for an alert no check has read.
80
+ { id: "quote", label: "Quote", width: 15, align: "left" },
73
81
  { id: "triggered", label: "Alerted", width: 8, align: "left" },
82
+ // The day the alert was made (UTC), as Events lists its rules; the detail gives the instant.
83
+ { id: "created", label: "Since", width: 10, align: "left" },
74
84
  ];
75
85
 
76
86
  const DETAIL_ROW = { labelWidth: 10, emphasis: false } as const;
@@ -89,20 +99,28 @@ function AlertDetail({ alert, scrollRef }: { alert: AlertRule; scrollRef: RefObj
89
99
  const reason = alert.lastCheckError
90
100
  ? alert.lastCheckError.includes(alert.symbol) ? alert.lastCheckError : `${alert.symbol}: ${alert.lastCheckError}`
91
101
  : null;
102
+ const basis = alert.metric ? closeBasis(alert.metric) : null;
92
103
  return (
93
104
  <DetailScrollBody ref={scrollRef} resetScrollKey={alert.id}>
94
105
  <Box flexDirection="column">
95
106
  {reason ? <Box paddingBottom={1}><Notice tone="warning">{reason}</Notice></Box> : null}
96
107
  <KeyValueRow {...DETAIL_ROW} label="State" value={alert.status === "triggered" ? "Triggered" : "Active"} tone={alert.status === "triggered" ? "positive" : "muted"} />
108
+ <KeyValueRow {...DETAIL_ROW} label="Listing" value={listingWithCurrency(alertListing(alert), alert.lastQuoteCurrency)} />
97
109
  {/* With no quote the reason above says why; the price and its distance wait for one. */}
98
- {alert.metric && isDailyMetric(alert.metric) ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? "Awaiting daily close"} /> : null}
110
+ {basis ? <KeyValueRow {...DETAIL_ROW} label="Close date" value={alert.lastDailyClose ?? `Awaiting ${basis}`} /> : null}
99
111
  {reason ? null : <KeyValueRow {...DETAIL_ROW} label="Current" value={formatCurrentPrice(alert, 24)} />}
112
+ {/* The two readings that place the quadrant, as RRG states them. */}
113
+ {!reason && alert.metric?.kind === "rrg" && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null ? <>
114
+ <KeyValueRow {...DETAIL_ROW} label="Strength" value={alert.lastCheckedPrice.toFixed(2)} />
115
+ <KeyValueRow {...DETAIL_ROW} label="Momentum" value={alert.lastCheckedTarget.toFixed(2)} />
116
+ </> : null}
100
117
  {reason ? null : <KeyValueRow {...DETAIL_ROW} label="Away" value={formatAlertDistance(alert)} />}
101
118
  {reason
102
119
  ? alert.lastCheckedAt ? <KeyValueRow {...DETAIL_ROW} label="Checked" value={relativeTime(alert.lastCheckedAt)} tone="muted" /> : null
103
120
  : <KeyValueRow {...DETAIL_ROW} label="Quote" value={formatQuoteChecked(alert)} tone="muted" />}
104
121
  {alert.triggeredAt ? <KeyValueRow {...DETAIL_ROW} label="Alerted" value={relativeTime(alert.triggeredAt)} /> : null}
105
122
  <KeyValueRow {...DETAIL_ROW} label="Armed" value={relativeTime(alert.armedAt ?? alert.createdAt)} tone="muted" />
123
+ <KeyValueRow {...DETAIL_ROW} label="Created" value={isoUtc(alert.createdAt)} tone="muted" />
106
124
  </Box>
107
125
  </DetailScrollBody>
108
126
  );
@@ -144,7 +162,10 @@ export function AlertsPane(props: PaneProps) {
144
162
  function PriceAlertsPane({ focused, width, height }: PaneProps) {
145
163
  const [alertsJson, setAlertsJson] = usePluginConfigState<unknown>(ALERTS_KEY, "[]");
146
164
  const [metricsJson, setMetricsJson] = usePluginConfigState<unknown>(METRIC_ALERTS_KEY, "[]");
165
+ const [relativeJson, setRelativeJson] = usePluginConfigState<unknown>(RELATIVE_ALERTS_KEY, "[]");
147
166
  const { openPluginCommandWorkflow, notify } = usePluginAppActions();
167
+ // Deleting or re-arming a rule goes in this device's alert log, beside the alerts that fired.
168
+ const [, setAlertLog] = usePluginState<unknown>(FIRED_LOG_KEY, []);
148
169
  const dialog = useDialog();
149
170
  const [selectedIdx, setSelectedIdx] = useState(0);
150
171
  const [detailOpen, setDetailOpen] = useState(false);
@@ -152,9 +173,11 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
152
173
  const detailAlertId = useRef<string | null>(null);
153
174
  const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
154
175
  // Keep the rule and daily-close basis visible in a narrow pane; the detail keeps all figures.
155
- const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered") : ALERT_COLUMNS;
176
+ const columns = width < 106 ? ALERT_COLUMNS.filter(column => column.id !== "away" && column.id !== "triggered" && column.id !== "created")
177
+ : width < 118 ? ALERT_COLUMNS.filter(column => column.id !== "created") : ALERT_COLUMNS;
156
178
  const showHorizontalScrollbar = columns.reduce((sum, column) => sum + column.width + 1, 2) > width;
157
- const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : metricsJson), [alertsJson, metricsJson]);
179
+ const stores = useMemo(() => readLevelAlertStores(key => key === ALERTS_KEY ? alertsJson : key === METRIC_ALERTS_KEY ? metricsJson : relativeJson),
180
+ [alertsJson, metricsJson, relativeJson]);
158
181
  const storeError = stores.error;
159
182
 
160
183
  const { alerts, rows } = useMemo(() => {
@@ -205,16 +228,17 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
205
228
  }), [liveEntries, rows]);
206
229
 
207
230
  const savePaneAlerts = useCallback((id: string, next: (current: AlertRule[]) => AlertRule[]): string | null => {
208
- const key = alerts.find(alert => alert.id === id)?.metric ? METRIC_ALERTS_KEY : ALERTS_KEY;
231
+ const found = alerts.find(alert => alert.id === id);
232
+ const key = found ? levelAlertStoreKey(found) : ALERTS_KEY;
209
233
  if (stores.errors[key]) return stores.errors[key];
210
234
  let error: string | null = null;
211
- const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : setAlertsJson;
235
+ const setJson = key === METRIC_ALERTS_KEY ? setMetricsJson : key === RELATIVE_ALERTS_KEY ? setRelativeJson : setAlertsJson;
212
236
  setJson((currentJson: unknown) => {
213
237
  error = readAlertsStoreError(currentJson);
214
238
  return error ? currentJson : serializeAlerts(next(deserializeAlerts(currentJson)));
215
239
  });
216
240
  return error;
217
- }, [alerts, setAlertsJson, setMetricsJson, stores]);
241
+ }, [alerts, setAlertsJson, setMetricsJson, setRelativeJson, stores]);
218
242
 
219
243
  const deleteAlert = useCallback(async (id: string) => {
220
244
  const alert = alerts.find((a) => a.id === id);
@@ -230,15 +254,24 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
230
254
  notify({ body: error, type: "error" });
231
255
  return;
232
256
  }
257
+ setAlertLog((saved: unknown) => withAlertChange(saved, "deleted", alert));
233
258
  setSelectedIdx((prev) => Math.max(0, Math.min(prev, rows.length - 2)));
234
- }, [alerts, dialog, notify, rows.length, savePaneAlerts]);
259
+ }, [alerts, dialog, notify, rows.length, savePaneAlerts, setAlertLog]);
235
260
 
236
261
  const rearmAlert = useCallback((id: string) => {
237
- const error = savePaneAlerts(id,
238
- (current) => current.map((a) => (a.id === id ? rebuildAlert(a) : a)),
239
- );
240
- if (error) notify({ body: error, type: "error" });
241
- }, [notify, savePaneAlerts]);
262
+ // Only an alert that went off is re-armed, as the store reads at the write: one still waiting is left, and not logged.
263
+ const rearmed: AlertRule[] = [];
264
+ const error = savePaneAlerts(id, (current) => current.map((a) => {
265
+ if (a.id !== id || a.status !== "triggered") return a;
266
+ rearmed.push(a);
267
+ return rebuildAlert(a);
268
+ }));
269
+ if (error) {
270
+ notify({ body: error, type: "error" });
271
+ return;
272
+ }
273
+ for (const alert of rearmed) setAlertLog((saved: unknown) => withAlertChange(saved, "rearmed", alert));
274
+ }, [notify, savePaneAlerts, setAlertLog]);
242
275
 
243
276
  const startAddAlert = useCallback(() => {
244
277
  openPluginCommandWorkflow("set-alert");
@@ -276,11 +309,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
276
309
  description: "SYMBOL above|below|crosses PRICE",
277
310
  placeholder: "AAPL above 200",
278
311
  }}
279
- currentValue={selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`}
312
+ currentValue={relativeAlertText(selected) ?? (selected.metric ? formatAlertDescription(selected).replace(" (TTM)", "").replace(" · on daily close", "") : `${selected.symbol} ${selected.condition} ${selected.targetPrice}`)}
280
313
  onApply={async (value) => {
281
- const parsed = parseAlertCommandValues({ shortcut: value });
282
- if (!parsed) throw new Error("Use SYMBOL above|below|crosses PRICE.");
283
- if (!!parsed.metric !== !!selected.metric) throw new Error("Add a new alert to change between price and a valuation or technical measure.");
314
+ const parsed = parseAlertEdit(selected, value);
284
315
  const error = savePaneAlerts(selected.id, (current) => current.map((alert) => (
285
316
  alert.id === selected.id
286
317
  ? { ...editAlert(alert, parsed.symbol, parsed.condition, parsed.price), ...(parsed.metric ? { metric: parsed.metric } : {}) }
@@ -382,6 +413,9 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
382
413
  case "current":
383
414
  return {
384
415
  text: formatCurrentPrice(alert, column.width),
416
+ // The quadrant word exports with the readings that place it.
417
+ ...(alert.metric?.kind === "rrg" && !alert.lastCheckError && alert.lastCheckedPrice != null && alert.lastCheckedTarget != null
418
+ ? { value: metricReadingText(alert.metric, { value: alert.lastCheckedPrice, target: alert.lastCheckedTarget }) } : {}),
385
419
  color: alert.lastCheckError ? colors.negative : colors.text,
386
420
  };
387
421
  case "target":
@@ -392,10 +426,12 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
392
426
  color: colors.textDim,
393
427
  };
394
428
  case "condition":
395
- return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : conditionLabel(alert.condition) };
429
+ return { text: alert.metric?.kind === "ma_cross" ? `Cross ${alert.metric.direction}` : alert.metric?.kind === "rrg" ? "Enters" : conditionLabel(alert.condition) };
396
430
  case "quote":
397
431
  return {
398
432
  text: formatQuoteChecked(alert),
433
+ // A settled close exports as its session date.
434
+ ...(alert.metric && closeBasis(alert.metric) && alert.lastDailyClose && !alert.lastCheckError ? { value: alert.lastDailyClose } : {}),
399
435
  color: alert.lastCheckError ? colors.negative : colors.textDim,
400
436
  };
401
437
  case "triggered":
@@ -403,6 +439,8 @@ function PriceAlertsPane({ focused, width, height }: PaneProps) {
403
439
  text: alert.triggeredAt ? relativeTime(alert.triggeredAt) : "-",
404
440
  color: colors.textDim,
405
441
  };
442
+ case "created":
443
+ return { text: isoUtc(alert.createdAt).slice(0, 10), color: colors.textDim };
406
444
  }
407
445
  }, []);
408
446
 
@@ -2,6 +2,7 @@ import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { Quote } from "../../../types/financials";
3
3
  import { getActiveQuoteDisplay } from "../../../market-data/market/status";
4
4
  import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
5
+ import { canonicalExchange } from "../../../utils/exchanges";
5
6
  import type { AlertRule } from "./types";
6
7
 
7
8
  export function normalizeAlertSymbol(value: string | null | undefined): string {
@@ -46,13 +47,18 @@ export function quoteAlertFields(
46
47
  | "lastQuoteUpdatedAt"
47
48
  | "lastQuoteSource"
48
49
  | "lastQuoteProviderId"
50
+ | "lastQuoteCurrency"
51
+ | "lastQuoteExchange"
49
52
  > & { lastCheckError?: undefined } {
53
+ const exchange = quote.listingExchangeName || quote.exchangeName;
50
54
  return {
51
55
  lastCheckedPrice: quote.price,
52
56
  lastCheckedAt: checkedAt,
53
57
  lastQuoteUpdatedAt: quote.lastUpdated,
54
58
  lastQuoteSource: quote.dataSource,
55
59
  lastQuoteProviderId: quote.providerId,
60
+ ...(quote.currency ? { lastQuoteCurrency: quote.currency } : {}),
61
+ ...(exchange ? { lastQuoteExchange: canonicalExchange(exchange) } : {}),
56
62
  lastCheckError: undefined,
57
63
  };
58
64
  }
@@ -1,4 +1,4 @@
1
- import { METRIC_ALERTS_KEY } from "./metric-model";
1
+ import { METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY, metricAlertsKey } from "./metric-model";
2
2
  import type { AppConfig } from "../../../types/config";
3
3
  import type { GloomPluginContext } from "../../../types/plugin";
4
4
  import {
@@ -21,8 +21,9 @@ export async function saveAlerts(
21
21
  scope: "all" | "price" | "metric" = "all",
22
22
  ): Promise<void> {
23
23
  const stores = readLevelAlertStores((key) => ctx.configState.get<unknown>(key));
24
- const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key]) => scope === "all"
25
- || key === (scope === "price" ? ALERTS_KEY : METRIC_ALERTS_KEY));
24
+ // An unreadable list is never written over; one that has nothing to write does not hold up the others.
25
+ const entries = Object.entries(writeLevelAlertStores(alerts)).filter(([key, value]) => (scope === "all"
26
+ || (scope === "price" ? key === ALERTS_KEY : key !== ALERTS_KEY)) && !(stores.errors[key] && value === "[]"));
26
27
  for (const [key] of entries) if (stores.errors[key]) throw new Error(stores.errors[key]!);
27
28
  for (const [key, value] of entries) {
28
29
  if (ctx.configState.get<unknown>(key) !== value) await ctx.configState.set(key, value);
@@ -46,17 +47,20 @@ export function readAlertStores(config: AppConfig) {
46
47
  export function readLevelAlertStores(get: (key: string) => unknown): { alerts: AlertRule[]; error: string | null; errors: Record<string, string | null> } {
47
48
  const alerts: AlertRule[] = [];
48
49
  const errors: Record<string, string | null> = {};
49
- for (const key of [ALERTS_KEY, METRIC_ALERTS_KEY]) {
50
+ for (const key of LEVEL_ALERT_KEYS) {
50
51
  const raw = get(key);
51
52
  let error = readAlertsStoreError(raw);
52
53
  const rows = error ? [] : deserializeAlerts(raw);
53
- if (rows.some(a => Boolean(a.metric) !== (key === METRIC_ALERTS_KEY))) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
54
- errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null : error;
54
+ if (rows.some(a => levelAlertStoreKey(a) !== key)) error = "Saved alerts contain a rule in the wrong list. Original data has been kept.";
55
+ errors[key] = key === METRIC_ALERTS_KEY ? error?.replaceAll("price alerts", "valuation and technical alerts") ?? null
56
+ : key === RELATIVE_ALERTS_KEY ? error?.replaceAll("price alerts", "relative strength alerts") ?? null : error;
55
57
  if (!error) alerts.push(...rows);
56
58
  }
57
59
  return { alerts, errors, error: Object.values(errors).filter(Boolean).join(" ") || null };
58
60
  }
59
61
  export function writeLevelAlertStores(alerts: AlertRule[]): Record<string, string> {
60
- return { [ALERTS_KEY]: serializeAlerts(alerts.filter(a => !a.metric)),
61
- [METRIC_ALERTS_KEY]: serializeAlerts(alerts.filter(a => a.metric)) };
62
+ return Object.fromEntries(LEVEL_ALERT_KEYS.map(key => [key, serializeAlerts(alerts.filter(a => levelAlertStoreKey(a) === key))]));
62
63
  }
64
+ /** Price, valuation/technical and relative strength rules each keep their own synced list. */
65
+ const LEVEL_ALERT_KEYS = [ALERTS_KEY, METRIC_ALERTS_KEY, RELATIVE_ALERTS_KEY] as const;
66
+ export const levelAlertStoreKey = (alert: Pick<AlertRule, "metric">): string => alert.metric ? metricAlertsKey(alert.metric) : ALERTS_KEY;
@@ -23,5 +23,9 @@ export interface AlertRule {
23
23
  lastQuoteUpdatedAt?: number;
24
24
  lastQuoteSource?: QuoteDataSource;
25
25
  lastQuoteProviderId?: string;
26
+ /** The currency of the quote the alert was last checked on (USD): its listing's, which lists show beside it. */
27
+ lastQuoteCurrency?: string;
28
+ /** The venue that quote was from (ARCA), which names the listing of a rule set by symbol alone. */
29
+ lastQuoteExchange?: string;
26
30
  message?: string;
27
31
  }
@@ -1,6 +1,7 @@
1
1
  import { usePaneSettingValue } from "../../../public/react";
2
2
  import { PortfolioRiskPane } from "./risk-pane";
3
3
  import { portfolioRiskHeadless } from "./risk-headless";
4
+ import { NO_HOLDINGS_HINT } from "./risk-model";
4
5
  import { portfolioRiskCache } from "./risk-client";
5
6
  import { Box, Text } from "../../../ui";
6
7
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -398,7 +399,7 @@ function LegacyPortfolioAnalyticsPane({ focused, width, height }: PaneProps) {
398
399
  message={accountsError ?? undefined}
399
400
  hint={accountsError
400
401
  ? "Reconnect the broker in the Brokers pane (BR), then refresh."
401
- : "Add holdings from the Portfolio pane (PF), or connect a broker in BR to sync them."}
402
+ : NO_HOLDINGS_HINT}
402
403
  />
403
404
  </Box>
404
405
  ) : historyOnly ? (
@@ -58,7 +58,12 @@ interface RiskMarketHistory {
58
58
  * Set when the listing quotes in another currency and its closes were
59
59
  * restated in USD at same-date daily FX closes; quote and closeMark are then in USD.
60
60
  */
61
- converted?: { currency: string; fxAsOf: string } | null;
61
+ converted?: {
62
+ currency: string;
63
+ fxAsOf: string;
64
+ /** US sessions converted at the FX close before them, the pair having none that day (bridgedFxClose). */
65
+ fxBridged?: string[];
66
+ } | null;
62
67
  error: string | null;
63
68
  }
64
69
  export interface RiskMarketSnapshot {
@@ -162,6 +167,8 @@ export function validateRiskHistory(
162
167
  closeMark: { price: number; date: string; currency: string } | null;
163
168
  /** Latest completed close in the returns' currency, whatever marks the holding. */
164
169
  latestClose: { price: number; date: string };
170
+ /** With a conversion, the US sessions converted at the FX close before them (bridgedFxClose). */
171
+ fxBridged?: string[];
165
172
  } {
166
173
  if (
167
174
  response.status !== "success" ||
@@ -244,13 +251,14 @@ export function validateRiskHistory(
244
251
  if (local.integrity)
245
252
  throw new Error("Daily history has inconsistent OHLC observations");
246
253
  // A converted series is on the NYSE calendar the basket and SPY share.
247
- const series = conversion
254
+ const usd = conversion
248
255
  ? convertClosesToUsd(
249
256
  points.map((point) => ({ date: new Date(point.date).toISOString().slice(0, 10), close: point.close })),
250
257
  conversion.fx,
251
258
  conversion.divisor,
252
259
  )
253
- : prices;
260
+ : null;
261
+ const series = usd ? usd.points : prices;
254
262
  const returns = conversion ? resolveDatedReturns(series).returns : local.returns;
255
263
  const cadence = qualifySharpeCadence(returns, [
256
264
  { symbol: instrument.symbol, exchange: conversion ? "NYSE" : exchange, history: series },
@@ -264,6 +272,7 @@ export function validateRiskHistory(
264
272
  };
265
273
  return {
266
274
  returns,
275
+ ...(usd?.bridged.length ? { fxBridged: usd.bridged } : {}),
267
276
  asOf: latestClose.date,
268
277
  // A missing or stale quote is not a current mark; the latest dated close is.
269
278
  closeMark:
@@ -591,7 +600,7 @@ export async function fetchPortfolioRiskMarket(
591
600
  historyParams,
592
601
  signal ? { signal } : undefined,
593
602
  );
594
- const { latestClose, ...resolved } = validateRiskHistory(
603
+ const { latestClose, fxBridged, ...resolved } = validateRiskHistory(
595
604
  response,
596
605
  instrument,
597
606
  quote,
@@ -610,7 +619,9 @@ export async function fetchPortfolioRiskMarket(
610
619
  quote: current,
611
620
  currency: "USD",
612
621
  closeMark: current ? null : { ...latestClose, currency: "USD" },
613
- converted: { currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf },
622
+ converted: {
623
+ currency: conversion.listingCurrency, fxAsOf: fxCloses.asOf, ...(fxBridged?.length ? { fxBridged } : {}),
624
+ },
614
625
  error: null,
615
626
  };
616
627
  continue;
@@ -38,10 +38,18 @@ export interface RiskConversion {
38
38
  export const FX_UNAVAILABLE = "Foreign holdings: daily FX closes unavailable";
39
39
  export const FX_NO_PAIR = "Foreign holdings: no daily FX pair for this currency";
40
40
  export const FX_STALE = "Foreign holdings: daily FX history is stale";
41
- export const FX_GAP = "Foreign holdings: daily FX history has gaps on the holding's dates";
41
+ export const FX_GAP = "Foreign holdings: daily FX history has gaps longer than three days on the holding's dates";
42
42
  export const FX_DISAGREES = "Foreign holdings: daily FX closes disagree with the current rate";
43
43
  export const LOCAL_GAP = "Foreign holdings: local daily history has a gap longer than a market holiday";
44
44
 
45
+ /**
46
+ * Most FX trading days in a row (weekdays) a pair may go without a daily
47
+ * close and still be bridged with the last close before them: Good Friday and
48
+ * Easter Monday, 25 December, 1 January, which the daily source skips while
49
+ * US sessions continue. A longer run is missing data and rejects the series.
50
+ */
51
+ const MAX_BRIDGED_FX_DAYS = 3;
52
+
45
53
  /**
46
54
  * Longest run of calendar days a listing may go without a close while US
47
55
  * sessions continue (Golden Week, Lunar New Year, national holidays). A longer
@@ -70,6 +78,42 @@ export function accountFxReason(currency: string, problem: string): string {
70
78
  return `${pair} ${tail ?? "daily FX closes unavailable"}`;
71
79
  }
72
80
 
81
+ /**
82
+ * The FX close a date converts at: the pair's own close that day, or the last
83
+ * close before it across at most MAX_BRIDGED_FX_DAYS weekdays without one
84
+ * (`bridged`). Null when the gap is longer, or no close comes before it.
85
+ */
86
+ export function bridgedFxClose(rateOn: (date: string) => number | undefined, date: string): { rate: number; bridged: boolean } | null {
87
+ let missing = 0;
88
+ for (let time = Date.parse(`${date}T00:00:00Z`), step = 0; step <= 7 + MAX_BRIDGED_FX_DAYS; step++, time -= DAY_MS) {
89
+ const day = new Date(time).toISOString().slice(0, 10);
90
+ const rate = rateOn(day);
91
+ if (rate != null && rate > 0) return { rate, bridged: day !== date };
92
+ const weekday = new Date(time).getUTCDay();
93
+ if (weekday !== 0 && weekday !== 6 && ++missing > MAX_BRIDGED_FX_DAYS) return null;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * "GBP/USD has no daily close on 2025-04-18 and 2025-04-21; the returns across
100
+ * them use the close before": what a conversion bridged (bridgedFxClose), said
101
+ * once per pair. Null when it bridged nothing.
102
+ */
103
+ export function fxBridgeNote(pair: string, dates: Iterable<string>): string | null {
104
+ const days = [...new Set(dates)].sort();
105
+ if (!days.length) return null;
106
+ const shown = days.length > 4 ? [...days.slice(0, 3), `${days.length - 3} more days`] : days;
107
+ const list = shown.length === 1 ? shown[0]! : `${shown.slice(0, -1).join(", ")} and ${shown.at(-1)!}`;
108
+ return `${pair} has no daily close on ${list}; the returns across ${days.length === 1 ? "it use the close before it" : "them use the close before each"}.`;
109
+ }
110
+
111
+ /** Account returns, and the dates whose FX close was the last one before them (bridgedFxClose). */
112
+ export interface AccountReturns {
113
+ returns: DatedReturn[];
114
+ bridged: string[];
115
+ }
116
+
73
117
  /**
74
118
  * Restates daily USD returns in the account currency at the FX closes of each
75
119
  * return's own start and end dates: the holding's USD value is divided by the
@@ -77,23 +121,29 @@ export function accountFxReason(currency: string, problem: string): string {
77
121
  * (1 + r) * fx(start) / fx(end) - 1. A listing already converted from its own
78
122
  * currency to USD therefore converts at the cross of the two closes of the
79
123
  * same date. Returns before the first FX close or after the last are dropped,
80
- * so the series starts and ends with the FX history; a return inside it with an
81
- * endpoint that has no FX close rejects the whole series instead of bridging it.
124
+ * so the series starts and ends with the FX history. An endpoint without an FX
125
+ * close converts at the last close before it across a gap of up to
126
+ * MAX_BRIDGED_FX_DAYS FX trading days (Good Friday and Easter Monday), and
127
+ * says so in `bridged`; a longer gap rejects the whole series.
82
128
  */
83
129
  export function convertReturnsToAccount(
84
130
  returns: readonly DatedReturn[],
85
131
  fx: Pick<AccountFx, "currency" | "closes" | "first" | "asOf">,
86
- ): DatedReturn[] {
132
+ ): AccountReturns {
87
133
  if (!fx.first || !fx.asOf) throw new Error(accountFxReason(fx.currency, FX_UNAVAILABLE));
88
134
  const converted: DatedReturn[] = [];
135
+ const bridged = new Set<string>();
136
+ const rateOn = (date: string) => fx.closes[date];
89
137
  for (const point of returns) {
90
138
  if (point.startDateKey < fx.first || point.dateKey > fx.asOf) continue;
91
- const start = fx.closes[point.startDateKey],
92
- end = fx.closes[point.dateKey];
93
- if (!(start! > 0) || !(end! > 0)) throw new Error(accountFxReason(fx.currency, FX_GAP));
94
- converted.push({ ...point, value: (1 + point.value) * (start! / end!) - 1 });
139
+ const start = bridgedFxClose(rateOn, point.startDateKey),
140
+ end = bridgedFxClose(rateOn, point.dateKey);
141
+ if (!start || !end) throw new Error(accountFxReason(fx.currency, FX_GAP));
142
+ if (start.bridged) bridged.add(point.startDateKey);
143
+ if (end.bridged) bridged.add(point.dateKey);
144
+ converted.push({ ...point, value: (1 + point.value) * (start.rate / end.rate) - 1 });
95
145
  }
96
- return converted;
146
+ return { returns: converted, bridged: [...bridged].sort() };
97
147
  }
98
148
 
99
149
  /** The pair that restates a listing currency in USD, or null when there is none. */
@@ -127,7 +177,9 @@ export function validateFxHistory(
127
177
  * On a US session the listing did not trade (its own holiday), the holding is
128
178
  * worth its last local close at that day's FX close: its USD value moves with
129
179
  * the currency alone, as it would in the account. A US session with no FX
130
- * close, or a local silence longer than any market holiday, rejects the whole
180
+ * close converts at the last close before it across a gap of up to
181
+ * MAX_BRIDGED_FX_DAYS FX trading days, listed in `bridged`; a longer FX gap,
182
+ * or a local silence longer than any market holiday, rejects the whole
131
183
  * series instead of bridging it. The series ends at the earlier of the last
132
184
  * local close and the last FX close, so no close is carried past the data.
133
185
  */
@@ -135,7 +187,7 @@ export function convertClosesToUsd(
135
187
  local: readonly { date: string; close: number }[],
136
188
  fx: FxCloses,
137
189
  divisor: number,
138
- ): PricePoint[] {
190
+ ): { points: PricePoint[]; bridged: string[] } {
139
191
  const points = [...local]
140
192
  .map((point) => ({ date: point.date.slice(0, 10), close: point.close }))
141
193
  .sort((left, right) => left.date.localeCompare(right.date));
@@ -143,6 +195,8 @@ export function convertClosesToUsd(
143
195
  const start = points[0]!.date > fx.first ? points[0]!.date : fx.first;
144
196
  const end = points.at(-1)!.date < fx.asOf ? points.at(-1)!.date : fx.asOf;
145
197
  const converted: PricePoint[] = [];
198
+ const bridged: string[] = [];
199
+ const rateOn = (date: string) => fx.closes.get(date);
146
200
  let cursor = 0;
147
201
  for (let time = Date.parse(`${start}T00:00:00Z`); time <= Date.parse(`${end}T00:00:00Z`); time += DAY_MS) {
148
202
  const date = new Date(time).toISOString().slice(0, 10);
@@ -153,11 +207,12 @@ export function convertClosesToUsd(
153
207
  const last = points[cursor]!;
154
208
  if (last.date > date) continue;
155
209
  if (time - Date.parse(`${last.date}T00:00:00Z`) > MAX_LOCAL_CLOSED_DAYS * DAY_MS) throw new Error(LOCAL_GAP);
156
- const rate = fx.closes.get(date);
157
- if (rate == null) throw new Error(FX_GAP);
158
- const close = (last.close * rate) / divisor;
210
+ const fxClose = bridgedFxClose(rateOn, date);
211
+ if (!fxClose) throw new Error(FX_GAP);
212
+ if (fxClose.bridged) bridged.push(date);
213
+ const close = (last.close * fxClose.rate) / divisor;
159
214
  converted.push({ date: new Date(time), open: close, high: close, low: close, close });
160
215
  }
161
216
  if (converted.length < 2) throw new Error("Daily history unavailable");
162
- return converted;
217
+ return { points: converted, bridged };
163
218
  }
@@ -3,6 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { fetchPortfolioRiskMarket, RISK_HISTORY_LIMIT } from "./risk-client";
4
4
  import { parsePortfolioRiskEvidence } from "./risk-evidence";
5
5
  import { describeCollections } from "../../../portfolio/collection-name";
6
+ import { shellWord } from "../../../utils/shell-word";
6
7
  import { formatShocks, parseAssetShifts, parseFxShifts, riskCashLines } from "./risk-book";
7
8
  import {
8
9
  BASKET_VIEWS,
@@ -48,6 +49,22 @@ function reportPortfolioName(metadata: Record<string, unknown> | undefined): str
48
49
  return typeof name === "string" ? name : undefined;
49
50
  }
50
51
 
52
+ /**
53
+ * What a portfolio with nothing to estimate says, as one line: the commands
54
+ * that fill it. Risk is read from lots, so a symbol listed without shares
55
+ * (`portfolio add`) is no holding yet; `portfolio position set` gives it
56
+ * shares, and adds the symbol too when it is not listed.
57
+ */
58
+ function emptyPortfolioHint(name: string, listed: readonly string[]): string {
59
+ const portfolio = shellWord(name);
60
+ if (listed.length > 0) {
61
+ return `${name} lists ${listed.slice(0, 3).join(", ")}${listed.length > 3 ? ` and ${listed.length - 3} more` : ""} but holds no shares: `
62
+ + `gloom portfolio position set ${portfolio} ${shellWord(listed[0]!)} 10 150.`;
63
+ }
64
+ return `No holdings in ${name}. Add symbols with gloom portfolio add ${portfolio} AAPL,MSFT, `
65
+ + `then their shares with gloom portfolio position set ${portfolio} AAPL 10 150.`;
66
+ }
67
+
51
68
  /** Rows of one view in the compact result; the pane and reports keep every row. */
52
69
  const COMPACT_ROW_LIMIT = 100;
53
70
  /** The correlation view is every pair of holdings; the compact result keeps the strongest. */
@@ -260,6 +277,16 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
260
277
  throw new Error(
261
278
  "Imported evidence belongs to a different portfolio or currency",
262
279
  );
280
+ // A portfolio with no holdings and no cash has nothing to estimate: say how to fill it, not a table of dashes.
281
+ if (tickers.length === 0 && cash.length === 0 && !evidence) {
282
+ const listed = local.tickers.filter((ticker) => ticker.metadata.portfolios.includes(id)).map((ticker) => ticker.metadata.ticker);
283
+ return {
284
+ sections: [],
285
+ complete: false,
286
+ empty: emptyPortfolioHint(local.portfolio.name, listed),
287
+ metadata: { portfolio: { id, name: local.portfolio.name, currency: local.portfolio.currency } },
288
+ };
289
+ }
263
290
  const requestedView = isRiskView(args.options.view) ? args.options.view : null;
264
291
  const view: string = requestedView ?? "risk";
265
292
  const localOnly =