@gloom-sh/terminal 0.18.5 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (570) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +163 -8
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +19 -7
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/hiring.ts +3 -0
  14. package/src/api-client/index.ts +15 -0
  15. package/src/api-client/ipo.ts +24 -1
  16. package/src/api-client/market-discovery.ts +10 -0
  17. package/src/api-client/market-movers.ts +7 -0
  18. package/src/api-client/members.ts +1 -1
  19. package/src/api-client/mna.ts +3 -0
  20. package/src/api-client/paths.ts +9 -1
  21. package/src/api-client/perps.ts +43 -0
  22. package/src/api-client/research-activity.ts +3 -2
  23. package/src/api-client/roadmap.ts +85 -0
  24. package/src/api-client/types.ts +82 -2
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +0 -2
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +2 -2
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/market.ts +107 -38
  35. package/src/cli/commands/news-search.ts +22 -0
  36. package/src/cli/commands/options-chain.ts +48 -7
  37. package/src/cli/commands/options-leaps.ts +9 -3
  38. package/src/cli/commands/overview.ts +147 -32
  39. package/src/cli/commands/search.ts +21 -1
  40. package/src/cli/commands/themes.ts +11 -2
  41. package/src/cli/commands/ticker.ts +87 -36
  42. package/src/cli/company-name-hint.ts +41 -0
  43. package/src/cli/earnings-calendar.ts +1 -1
  44. package/src/cli/errors.ts +25 -2
  45. package/src/cli/glossary.ts +31 -2
  46. package/src/cli/history-rows.ts +57 -9
  47. package/src/cli/index-roots.ts +0 -7
  48. package/src/cli/index.ts +2 -0
  49. package/src/cli/listing-arg.ts +235 -60
  50. package/src/cli/output-file.ts +68 -0
  51. package/src/cli/pane-functions/capabilities.ts +4 -0
  52. package/src/cli/pane-functions/catalog.ts +80 -7
  53. package/src/cli/pane-functions/discovery.ts +21 -1
  54. package/src/cli/pane-functions/dom.ts +13 -3
  55. package/src/cli/pane-functions/freshness.ts +93 -4
  56. package/src/cli/pane-functions/function-help.ts +20 -10
  57. package/src/cli/pane-functions/headless.ts +17 -7
  58. package/src/cli/pane-functions/index.ts +54 -16
  59. package/src/cli/pane-functions/market-session.ts +66 -4
  60. package/src/cli/pane-functions/options.ts +19 -1
  61. package/src/cli/pane-functions/resolver.ts +19 -8
  62. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  63. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  64. package/src/cli/quote-answer.ts +22 -11
  65. package/src/cli/saved-copy-persistence.ts +88 -0
  66. package/src/cli/ticker-resolution.ts +3 -2
  67. package/src/components/chart/composite/composite-chart.tsx +4 -0
  68. package/src/components/chart/composite/interactions.ts +6 -0
  69. package/src/components/chart/composite/legend.tsx +45 -7
  70. package/src/components/chart/composite/panel-gestures.ts +33 -0
  71. package/src/components/chart/composite/panel-surface.tsx +52 -3
  72. package/src/components/chart/composite/rasterizer.ts +24 -13
  73. package/src/components/chart/composite/scene.ts +39 -4
  74. package/src/components/chart/composite/series-cues.ts +14 -0
  75. package/src/components/chart/composite/text-renderer.ts +9 -2
  76. package/src/components/chart/composite/tool-catalog.ts +36 -0
  77. package/src/components/chart/composite/tools.ts +203 -44
  78. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  79. package/src/components/command-bar/assist/inventory.ts +2 -1
  80. package/src/components/command-bar/commands/direct/index.ts +28 -13
  81. package/src/components/command-bar/list/model.ts +5 -0
  82. package/src/components/command-bar/pane-templates/items.ts +3 -2
  83. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  84. package/src/components/command-bar/selection-runtime.ts +52 -3
  85. package/src/components/command-bar/surface/index.tsx +2 -0
  86. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  87. package/src/components/layout/pane/capture-stamp.ts +2 -2
  88. package/src/components/layout/pane/footer/index.tsx +22 -6
  89. package/src/components/layout/pane/footer/model.ts +83 -15
  90. package/src/components/layout/pane/status-footer.ts +35 -2
  91. package/src/components/layout/status-widget-space.ts +9 -0
  92. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  93. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  94. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  95. package/src/components/onboarding/wizard-positions.ts +3 -3
  96. package/src/components/ticker/list-table-view.tsx +8 -21
  97. package/src/components/ui/choice-dialog.tsx +23 -18
  98. package/src/components/ui/choice-groups.ts +18 -0
  99. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  100. package/src/components/ui/data-table/opentui/model.ts +26 -1
  101. package/src/components/ui/data-table/types.ts +2 -0
  102. package/src/components/ui/display.tsx +18 -3
  103. package/src/components/ui/icon.tsx +4 -1
  104. package/src/components/ui/list-view.tsx +8 -4
  105. package/src/components/ui/query-bar-keys.tsx +2 -0
  106. package/src/components/ui/query-bar.tsx +1 -0
  107. package/src/components/ui/select-button.tsx +5 -1
  108. package/src/components/ui/select-field.tsx +1 -0
  109. package/src/components/ui/table-layout.ts +8 -5
  110. package/src/core/app-runtime.ts +2 -0
  111. package/src/core/app-services.ts +2 -2
  112. package/src/data/config/chart-settings.ts +1 -1
  113. package/src/data/json-ticker-repository.ts +17 -8
  114. package/src/data/plugin-cache.ts +5 -3
  115. package/src/data/saved-copies.ts +8 -0
  116. package/src/data/ticker-repository.ts +16 -9
  117. package/src/data/ticker-writes.ts +81 -0
  118. package/src/data/ttl-cache.ts +165 -0
  119. package/src/docs/snapshot.json +103 -142
  120. package/src/i18n/es.ts +20 -6
  121. package/src/i18n/ja.ts +20 -6
  122. package/src/i18n/ko.ts +20 -6
  123. package/src/i18n/zh-cn.ts +20 -6
  124. package/src/i18n/zh-tw.ts +20 -6
  125. package/src/market-data/depositary-pair.ts +27 -3
  126. package/src/market-data/fund-facts.ts +50 -0
  127. package/src/market-data/market/change-basis.ts +16 -3
  128. package/src/market-data/market/format.ts +25 -0
  129. package/src/market-data/market/freshness.ts +2 -1
  130. package/src/market-data/options-alternatives.ts +81 -4
  131. package/src/market-data/quotes/metadata.ts +7 -0
  132. package/src/news/aggregator.ts +119 -9
  133. package/src/news/cloud-session.ts +21 -0
  134. package/src/news/hooks.ts +1 -0
  135. package/src/news/news-model.ts +1 -0
  136. package/src/news/types.ts +17 -0
  137. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  138. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  139. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  140. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  141. package/src/plugins/builtin/alerts/cli.ts +362 -80
  142. package/src/plugins/builtin/alerts/command.ts +25 -1
  143. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  144. package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
  145. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  146. package/src/plugins/builtin/alerts/format.ts +75 -5
  147. package/src/plugins/builtin/alerts/headless.ts +34 -12
  148. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  149. package/src/plugins/builtin/alerts/history.ts +1 -1
  150. package/src/plugins/builtin/alerts/index.tsx +79 -16
  151. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  152. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  153. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  154. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  155. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  156. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  157. package/src/plugins/builtin/alerts/storage.ts +12 -8
  158. package/src/plugins/builtin/alerts/types.ts +4 -0
  159. package/src/plugins/builtin/analytics/index.tsx +2 -1
  160. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  161. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  162. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  163. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  164. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  165. package/src/plugins/builtin/attention/index.tsx +2 -1
  166. package/src/plugins/builtin/attention/listing.ts +14 -0
  167. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  168. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  169. package/src/plugins/builtin/buildout/format.ts +38 -0
  170. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  171. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  172. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  173. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  174. package/src/plugins/builtin/cds/client.ts +142 -30
  175. package/src/plugins/builtin/cds/headless.ts +35 -8
  176. package/src/plugins/builtin/cds/model.ts +145 -18
  177. package/src/plugins/builtin/cds/pane.tsx +16 -5
  178. package/src/plugins/builtin/changelog/index.tsx +9 -6
  179. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  180. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  181. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  182. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  183. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  184. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  185. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  186. package/src/plugins/builtin/chart-composer/index.tsx +5 -6
  187. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  188. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  189. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  190. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  191. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  192. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  193. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  194. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  195. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  196. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  197. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  198. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  199. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  200. package/src/plugins/builtin/clinical-trials/model.ts +86 -31
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
  202. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  203. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
  204. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  205. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  206. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  207. package/src/plugins/builtin/company-attention/client.ts +8 -9
  208. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  209. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  210. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  211. package/src/plugins/builtin/company-attention/pane.tsx +12 -2
  212. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  213. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  214. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  215. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  216. package/src/plugins/builtin/composite-plugins.ts +2 -1
  217. package/src/plugins/builtin/correlation/headless.ts +13 -5
  218. package/src/plugins/builtin/correlation/history.ts +22 -0
  219. package/src/plugins/builtin/cot/client.ts +22 -18
  220. package/src/plugins/builtin/cot/headless.ts +15 -10
  221. package/src/plugins/builtin/cot/model.ts +14 -1
  222. package/src/plugins/builtin/cot/pane.tsx +22 -14
  223. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  224. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  225. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  226. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  227. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  228. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  229. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  230. package/src/plugins/builtin/credit-documents/headless.ts +10 -10
  231. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  232. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  233. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  234. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  235. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  236. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  237. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  238. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  239. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  240. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  241. package/src/plugins/builtin/docs/targets.ts +5 -1
  242. package/src/plugins/builtin/doe/headless.ts +3 -2
  243. package/src/plugins/builtin/doe/pane.tsx +13 -3
  244. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  245. package/src/plugins/builtin/earnings/board.tsx +62 -71
  246. package/src/plugins/builtin/earnings/headless.ts +40 -4
  247. package/src/plugins/builtin/earnings/scope.ts +31 -43
  248. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  249. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  250. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  251. package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
  252. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  253. package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
  254. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  255. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  256. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  257. package/src/plugins/builtin/econ/figures.ts +48 -0
  258. package/src/plugins/builtin/econ/index.tsx +14 -5
  259. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  260. package/src/plugins/builtin/econ/types.ts +2 -0
  261. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  262. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  263. package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
  264. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  265. package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
  266. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  267. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  268. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  269. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  270. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  271. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  272. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  273. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  274. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  275. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  276. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  277. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  278. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  279. package/src/plugins/builtin/gpu/headless.ts +8 -2
  280. package/src/plugins/builtin/help/function-index.ts +4 -1
  281. package/src/plugins/builtin/help/index.tsx +62 -59
  282. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  283. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  284. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  285. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  286. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  287. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  288. package/src/plugins/builtin/holders/headless.ts +22 -3
  289. package/src/plugins/builtin/holders/pane.tsx +3 -0
  290. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  291. package/src/plugins/builtin/insider/headless.ts +2 -2
  292. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  293. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  294. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  295. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  296. package/src/plugins/builtin/iv-history/client.ts +52 -11
  297. package/src/plugins/builtin/iv-history/format.ts +36 -23
  298. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  299. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  300. package/src/plugins/builtin/iv-history/model.ts +163 -27
  301. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  302. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  303. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  304. package/src/plugins/builtin/jobs/client.ts +25 -29
  305. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  306. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  307. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  308. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  309. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  310. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  311. package/src/plugins/builtin/macro-day/model.ts +39 -5
  312. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  313. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  314. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  315. package/src/plugins/builtin/market-movers/client.ts +27 -7
  316. package/src/plugins/builtin/market-movers/headless.ts +34 -12
  317. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  318. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  319. package/src/plugins/builtin/market-movers/model.ts +4 -1
  320. package/src/plugins/builtin/market-movers/screener.ts +22 -4
  321. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  322. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  323. package/src/plugins/builtin/market-movers/table.ts +17 -5
  324. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  325. package/src/plugins/builtin/mna/headless.ts +2 -0
  326. package/src/plugins/builtin/mna/pane.tsx +3 -1
  327. package/src/plugins/builtin/news/headless.ts +3 -1
  328. package/src/plugins/builtin/news/index.tsx +4 -0
  329. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  330. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  331. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  332. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  333. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  334. package/src/plugins/builtin/openfda/client.ts +14 -2
  335. package/src/plugins/builtin/openfda/index.tsx +3 -2
  336. package/src/plugins/builtin/openfda/model.ts +9 -0
  337. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  338. package/src/plugins/builtin/options/analytics.ts +29 -5
  339. package/src/plugins/builtin/options/enrichment.ts +17 -5
  340. package/src/plugins/builtin/options/table.ts +5 -2
  341. package/src/plugins/builtin/options/types.ts +2 -0
  342. package/src/plugins/builtin/options/view.tsx +17 -4
  343. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  344. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  345. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  346. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  347. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  348. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  349. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  350. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  351. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  352. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  353. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  354. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  355. package/src/plugins/builtin/pe-band/model.ts +12 -49
  356. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  357. package/src/plugins/builtin/perps/cascade.ts +236 -0
  358. package/src/plugins/builtin/perps/client.ts +56 -2
  359. package/src/plugins/builtin/perps/headless.ts +65 -9
  360. package/src/plugins/builtin/perps/index.tsx +7 -5
  361. package/src/plugins/builtin/perps/model.ts +18 -4
  362. package/src/plugins/builtin/perps/pane.tsx +19 -22
  363. package/src/plugins/builtin/perps/status-line.ts +25 -0
  364. package/src/plugins/builtin/plugin-module.ts +5 -1
  365. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  366. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  367. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  368. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  369. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  370. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
  371. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  372. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  373. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  374. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
  375. package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
  376. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  377. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  378. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  379. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  380. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  381. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  382. package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
  383. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  384. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  385. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  386. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  387. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  388. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  389. package/src/plugins/builtin/research/event-model.ts +40 -12
  390. package/src/plugins/builtin/research/headless.ts +1 -1
  391. package/src/plugins/builtin/research/index.tsx +7 -3
  392. package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
  393. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  394. package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
  395. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  396. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  397. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  398. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  399. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  400. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  401. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  402. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  403. package/src/plugins/builtin/roadmap/model.ts +67 -0
  404. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  405. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  406. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  407. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  408. package/src/plugins/builtin/scanner/headless.ts +204 -35
  409. package/src/plugins/builtin/scanner/index.ts +16 -5
  410. package/src/plugins/builtin/scanner/live-headless.ts +48 -5
  411. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  412. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  413. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  414. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  415. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  416. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  417. package/src/plugins/builtin/sec/headless.ts +4 -2
  418. package/src/plugins/builtin/sec/index.tsx +3 -0
  419. package/src/plugins/builtin/series-inventory.ts +2 -0
  420. package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
  421. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  422. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  423. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  424. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  425. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  426. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  427. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  428. package/src/plugins/builtin/shared/report-freshness.ts +11 -2
  429. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  430. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  431. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  432. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  433. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  434. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  435. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  436. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  437. package/src/plugins/builtin/themes/model.ts +16 -7
  438. package/src/plugins/builtin/themes/pane.tsx +5 -5
  439. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  440. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  441. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  442. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  443. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  444. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  445. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  446. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  447. package/src/plugins/builtin/ticker-detail/overview/model.ts +11 -3
  448. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  449. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
  450. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  451. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  452. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  453. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  454. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  455. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  456. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  457. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  458. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  459. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  460. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  461. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  462. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  463. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  464. package/src/plugins/builtin/volatility/index.tsx +2 -2
  465. package/src/plugins/builtin/volatility/pane.tsx +7 -4
  466. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  467. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  468. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  469. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  470. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  471. package/src/plugins/catalog-browser.ts +2 -1
  472. package/src/plugins/catalog-manifest.ts +2 -1
  473. package/src/plugins/host-link.ts +1 -1
  474. package/src/plugins/marketplace/model.ts +18 -5
  475. package/src/portfolio/column-values.ts +11 -7
  476. package/src/portfolio/futures-positions.ts +6 -3
  477. package/src/portfolio/position-quantity.ts +15 -9
  478. package/src/remote/resources.ts +2 -1
  479. package/src/renderers/dom/data-table/row.tsx +26 -0
  480. package/src/renderers/dom/desktop/icons.tsx +7 -0
  481. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  482. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  483. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  484. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  485. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  486. package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
  487. package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
  488. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  489. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  490. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  491. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  492. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  493. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  494. package/src/renderers/electrobun/bun/index.ts +26 -2
  495. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  496. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  497. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  498. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  499. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  500. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  501. package/src/renderers/electrobun/shared/protocol.ts +12 -0
  502. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  503. package/src/renderers/electrobun/view/main.tsx +24 -1
  504. package/src/renderers/electrobun/view/news-source.ts +5 -1
  505. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  506. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  507. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  508. package/src/renderers/opentui/start.tsx +5 -0
  509. package/src/series/registry.ts +1 -0
  510. package/src/series/types.ts +6 -0
  511. package/src/series/validate.ts +1 -0
  512. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  513. package/src/sources/gloomberb-cloud/index.ts +43 -8
  514. package/src/sources/gloomberb-cloud/news.ts +47 -7
  515. package/src/sources/provider-router/financials.ts +15 -3
  516. package/src/sources/provider-router/index.ts +8 -5
  517. package/src/state/ticker-cleanup.ts +50 -0
  518. package/src/sync/controller.ts +4 -3
  519. package/src/sync/core-contributors.ts +90 -31
  520. package/src/telemetry/stall-reports-dom.ts +38 -0
  521. package/src/telemetry/stall-reports-node.ts +75 -0
  522. package/src/telemetry/stall-reports.ts +214 -0
  523. package/src/test-support/preload.ts +14 -0
  524. package/src/theme/themes.ts +10 -0
  525. package/src/tickers/company-name-listing.ts +53 -13
  526. package/src/tickers/company-name.ts +50 -4
  527. package/src/tickers/index-roots.ts +7 -0
  528. package/src/tickers/listing-fate.ts +2 -2
  529. package/src/tickers/listing-venues.ts +15 -3
  530. package/src/tickers/search/index.ts +6 -2
  531. package/src/tickers/search/ranking.ts +5 -1
  532. package/src/tickers/search/upsert.ts +13 -0
  533. package/src/tickers/terminal-codes.ts +19 -0
  534. package/src/tickers/ticker-changes.ts +8 -4
  535. package/src/time-series/alignment.ts +1 -1
  536. package/src/time-series/economic.ts +18 -4
  537. package/src/time-series/field-catalog.ts +1 -1
  538. package/src/time-series/filed-trailing-eps.ts +197 -0
  539. package/src/time-series/fundamentals.ts +44 -1
  540. package/src/time-series/ice-credit.ts +3 -0
  541. package/src/time-series/price-comparison.ts +30 -2
  542. package/src/time-series/resolve-context.ts +3 -1
  543. package/src/time-series/resolve.ts +56 -4
  544. package/src/time-series/session-bar.ts +11 -0
  545. package/src/time-series/spec.ts +3 -2
  546. package/src/time-series/studies.ts +8 -2
  547. package/src/time-series/total-return.ts +160 -0
  548. package/src/time-series/transforms.ts +2 -3
  549. package/src/time-series/types.ts +22 -1
  550. package/src/types/company-from.ts +40 -0
  551. package/src/types/data-provider.ts +7 -0
  552. package/src/types/financials.ts +59 -1
  553. package/src/types/headless.ts +27 -3
  554. package/src/types/news-source.ts +1 -0
  555. package/src/types/plugin.ts +9 -0
  556. package/src/ui/host.tsx +3 -0
  557. package/src/updater/failure.ts +4 -1
  558. package/src/utils/cli-output.ts +42 -1
  559. package/src/utils/crypto-pair.ts +165 -7
  560. package/src/utils/declared-dividends.ts +1 -1
  561. package/src/utils/forward-dividend.ts +2 -0
  562. package/src/utils/fundamentals.ts +60 -8
  563. package/src/utils/fx-market-hours.ts +26 -3
  564. package/src/utils/main-thread-stall.ts +35 -2
  565. package/src/utils/market-capitalization.ts +24 -1
  566. package/src/utils/price-history.ts +7 -1
  567. package/src/utils/shell-word.ts +8 -0
  568. package/src/utils/trailing-dividend.ts +53 -4
  569. package/src/version.ts +2 -2
  570. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -4,7 +4,7 @@ import type { DataProvider } from "../../types/data-provider";
4
4
  import type { CliListing } from "../listing-arg";
5
5
  import { cliStyles, cliTerminalWidth, wrapText } from "../../utils/cli-output";
6
6
  import { isFiniteNumber } from "../../utils/guards";
7
- import { calculateOptionGreeks, solveChainVolatilities } from "../../plugins/builtin/options/analytics";
7
+ import { calculateOptionGreeks, ivSolvedNote, solveChainVolatilities } from "../../plugins/builtin/options/analytics";
8
8
  import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
9
9
  import {
10
10
  formatModelSpotPrice, modelSpotLabel, optionModelSpot, type OptionModelSpot,
@@ -16,9 +16,30 @@ import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calcula
16
16
  import { expiryIsoDate, OPTION_EXPIRATION_PLACEHOLDER, parseOptionExpiration, settlementStampTime } from "../../utils/option-expiry";
17
17
  import { loadModelDividendYield } from "../../market-data/model-dividend-yield";
18
18
  import { formatUtcTime } from "../../utils/utc-time";
19
+ import { takeOption } from "./command-utils";
19
20
 
20
21
  export const OPTIONS_USAGE = `options <symbol> [--expiration <${OPTION_EXPIRATION_PLACEHOLDER}>]`;
21
22
 
23
+ /** `--expiration` as `options` names it, and `--expiry` as `fn GEX` and `fn OPX` do. */
24
+ const OPTIONS_EXPIRATION_FLAGS = ["--expiration", "--expiry"] as const;
25
+ type OptionsExpirationFlag = (typeof OPTIONS_EXPIRATION_FLAGS)[number];
26
+
27
+ /**
28
+ * Takes the expiry option out of `args`, under either spelling: its value,
29
+ * the flag as typed (for messages), whether it was given at all (a flag with
30
+ * no value is given), and whether both spellings were.
31
+ */
32
+ export function takeExpirationOption(args: string[]): {
33
+ value: string | undefined;
34
+ flag: OptionsExpirationFlag;
35
+ given: boolean;
36
+ both: boolean;
37
+ } {
38
+ const typed = OPTIONS_EXPIRATION_FLAGS.filter((flag) => args.some((arg) => arg === flag || arg.startsWith(`${flag}=`)));
39
+ const [expiration, expiry] = OPTIONS_EXPIRATION_FLAGS.map((flag) => takeOption(args, flag));
40
+ return { value: expiration ?? expiry, flag: typed.length === 1 ? typed[0]! : "--expiration", given: typed.length > 0, both: typed.length > 1 };
41
+ }
42
+
22
43
  export { parseOptionExpiration };
23
44
 
24
45
  /** Whether the chain holds contracts of the requested expiry, matched by UTC calendar date as the chain stamps them. */
@@ -63,6 +84,8 @@ export interface OptionModelInputs {
63
84
  interface OptionModelFigures {
64
85
  /** Annualized volatility as a fraction, solved from the chain's quotes. */
65
86
  iv: number | null;
87
+ /** `iv` was read off the neighbouring strikes: this strike has no two-sided out-of-the-money quote to solve from. */
88
+ ivFilled?: boolean;
66
89
  delta: number | null;
67
90
  }
68
91
 
@@ -73,6 +96,7 @@ export const OPTION_MODEL_RATE = DEFAULT_OPTION_CALC_DRAFT.rate;
73
96
  export const MARKED_OPTION_FIELDS = {
74
97
  iv: "The venue's mark implied volatility (decimal), one per contract. Delta and the other greeks under mark are the venue's too: "
75
98
  + "Black-76 at the expiry's forward with no discounting, to its settlement.",
99
+ ivSource: "mark: iv is the venue's own, never solved or filled here.",
76
100
  bid: "Prices in USD are the coin prices (coin) at the expiry's forward (mark.forward), the venue's own model price.",
77
101
  mark: "price in USD; iv; delta and gamma per 1.00 move in the underlying; vega per volatility point and theta per calendar day, in USD.",
78
102
  } as const;
@@ -91,6 +115,8 @@ export function chainCoinCurrency(chain: OptionsChain): string | null {
91
115
  export const OPTION_VOLATILITY_FIELDS = {
92
116
  iv: "Annualized volatility (decimal) solved from the quote midpoint, one per strike from its out-of-the-money side, at the "
93
117
  + "spot, valuation time and rate in metadata.model. Delta here and dealer gamma (GEX) are valued with it.",
118
+ ivSource: "solved: iv comes from the strike's out-of-the-money quote midpoint. filled: that side has no two-sided quote, so iv is read off the "
119
+ + "nearest solved strikes (flat beyond the last one); it is no market's volatility, and delta is valued with it all the same.",
94
120
  impliedVolatility: "The data provider's own reported implied volatility, passed through unchanged. Delta does not use it; "
95
121
  + "GEX falls back to it only at a strike with no two-sided quote on either side.",
96
122
  } as const;
@@ -116,8 +142,9 @@ function optionModelFigures(chain: OptionsChain, inputs: OptionModelInputs): Map
116
142
  for (const contract of contracts) {
117
143
  const volatility = volatilities.byStrike.get(contract.strike);
118
144
  const delta = calculateOptionGreeks(contract, side, inputs.spot, inputs.dividendYield, volatilities)?.delta;
145
+ const iv = volatility != null && Number.isFinite(volatility) && volatility > 0 ? volatility : null;
119
146
  figures.set(contract, {
120
- iv: volatility != null && Number.isFinite(volatility) && volatility > 0 ? volatility : null,
147
+ iv, ivFilled: iv != null && volatilities.filled.has(contract.strike),
121
148
  delta: delta != null && Number.isFinite(delta) ? delta : null,
122
149
  });
123
150
  }
@@ -167,7 +194,8 @@ export async function loadOptionModelInputs(
167
194
  /** The chain with each contract's modeled `iv` and `delta` added; every provider field is kept. */
168
195
  export function chainWithModelFigures(chain: OptionsChain, inputs: OptionModelInputs): OptionsChain {
169
196
  const figures = optionModelFigures(chain, inputs);
170
- const add = (contract: OptionContract) => ({ ...contract, iv: figures.get(contract)?.iv ?? null, delta: figures.get(contract)?.delta ?? null });
197
+ const add = (contract: OptionContract) => ({ ...contract, iv: figures.get(contract)?.iv ?? null, ivFilled: figures.get(contract)?.ivFilled ?? false,
198
+ delta: figures.get(contract)?.delta ?? null });
171
199
  return { ...chain, calls: chain.calls.map(add), puts: chain.puts.map(add) };
172
200
  }
173
201
 
@@ -180,6 +208,13 @@ export interface OptionsChainBasis {
180
208
  spot: OptionModelSpot | null;
181
209
  dividendYield: number | null;
182
210
  openInterest: OpenInterestAsOf | null;
211
+ /** A strike's IV is filled from its neighbours (`chainHasFilledIv`), which the closing line says. */
212
+ ivFilled?: boolean;
213
+ }
214
+
215
+ /** Whether any contract's IV was read off the neighbouring strikes (see `ChainVolatilities.filled`). */
216
+ export function chainHasFilledIv(chain: OptionsChain): boolean {
217
+ return [...chain.calls, ...chain.puts].some((contract) => (contract as OptionContract & OptionModelFigures).ivFilled === true);
183
218
  }
184
219
 
185
220
  /** The chain with `iv` and `delta` valued at the spot `optionModelSpot` picks for it, and that spot. */
@@ -207,7 +242,8 @@ export async function loadOptionsChainOpenInterest(symbol: string, chain: Option
207
242
  }
208
243
 
209
244
  /**
210
- * The line under the chain: `Greeks at spot 89.79 (close Mon 12 Oct 20:00 UTC), dividend yield 2.34%, rate 4% · OI as of Fri 9 Oct`.
245
+ * The line under the chain: `Greeks at spot 89.79 (close Mon 12 Oct 20:00 UTC), dividend yield 2.34%, rate 4% · IV solved from quote mids ·
246
+ * OI as of Fri 9 Oct`, with `, ~ filled from neighbouring strikes` after the IV when a strike has no quote to solve from.
211
247
  * An open interest whose date cannot be established says why. A chain its
212
248
  * venue marks says whose marks they are: `Deribit marks: IV and greeks at
213
249
  * each expiry's forward, rate 0%; $ is the BTC price at the forward · OI live, Thu 22 Oct 22:42 UTC`.
@@ -230,6 +266,7 @@ export function optionsBasisFootnote(basis: OptionsChainBasis, chain?: OptionsCh
230
266
  // An unknown yield is valued as none, and says so rather than read as a stated 0%.
231
267
  + `dividend yield ${basis.dividendYield == null ? "unknown (valued at 0%)" : `${Number((basis.dividendYield * 100).toFixed(2))}%`}, `
232
268
  + `rate ${Number((OPTION_MODEL_RATE * 100).toFixed(2))}%`);
269
+ parts.push(ivSolvedNote(basis.ivFilled === true));
233
270
  }
234
271
  const oi = basis.openInterest;
235
272
  parts.push(oi?.date ? openInterestAsOfLabel(oi)
@@ -266,6 +303,8 @@ export function optionRows(chain: OptionsChain) {
266
303
  volume: contract.volume,
267
304
  openInterest: contract.openInterest,
268
305
  iv: modeled.iv ?? null,
306
+ // Solved from the strike's quote, or filled from its neighbours when it has none to solve from; a venue's own mark is neither.
307
+ ivSource: modeled.iv == null ? null : contract.mark ? "mark" : modeled.ivFilled ? "filled" : "solved",
269
308
  delta: modeled.delta ?? null,
270
309
  expiration: expiryIsoDate(contract.expiration),
271
310
  // A venue's marks and coin prices (Deribit), beside the dollar ones.
@@ -301,9 +340,11 @@ export function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid"
301
340
  return quote == null ? "—" : String(quote);
302
341
  }
303
342
 
304
- /** Implied volatility as a percent; a dash when it could not be solved, never 0.0%. */
305
- export function formatOptionIvCell(value: unknown): string {
306
- return isFiniteNumber(value) && value > 0 ? `${(value * 100).toFixed(1)}%` : "—";
343
+ /** Implied volatility as a percent; a dash when it could not be solved, never 0.0%, and a leading "~" when it was filled from neighbouring strikes. */
344
+ export function formatOptionIvCell(value: unknown, row?: Record<string, unknown>): string {
345
+ if (!isFiniteNumber(value) || !(value > 0)) return "—";
346
+ const text = `${(value * 100).toFixed(1)}%`;
347
+ return row?.ivSource === "filled" ? `~${text}` : text;
307
348
  }
308
349
 
309
350
  /** Delta to two places, signed for puts; a dash when it could not be valued, and no "-0.00" for a far wing. */
@@ -281,6 +281,8 @@ export type LeapsRow = {
281
281
  // The fields below are on a row only when `--columns` asked for them.
282
282
  /** `iv`: the implied volatility the contract's delta is valued at, percent a year. */
283
283
  ivPercent?: number | null;
284
+ /** `iv`: `filled` when that strike has no two-sided out-of-the-money quote to solve from and its IV is read off the neighbouring strikes. */
285
+ ivSource?: "solved" | "filled" | null;
284
286
  /** `ivr`: the underlying's 30-day IV rank against its 52 weeks, 0 to 100, and the close it ranks. */
285
287
  ivRank?: number | null;
286
288
  ivRankAsOf?: string | null;
@@ -369,7 +371,10 @@ function screenLeapsChain(
369
371
  dataSource: chain.dataSource ?? null,
370
372
  delayMinutes: chain.delayMinutes ?? null,
371
373
  };
372
- if (wants.has("iv")) row.ivPercent = contract.iv != null && Number.isFinite(contract.iv) ? round(contract.iv * 100, 2) : null;
374
+ if (wants.has("iv")) {
375
+ row.ivPercent = contract.iv != null && Number.isFinite(contract.iv) ? round(contract.iv * 100, 2) : null;
376
+ row.ivSource = row.ivPercent == null ? null : (contract as { ivFilled?: boolean }).ivFilled ? "filled" : "solved";
377
+ }
373
378
  // A two-sided quote passed the spread cap, so the midpoint is there.
374
379
  const mid = optionMid(contract)!;
375
380
  if (wants.has("cost")) row.costPerContract = round(mid * SHARES_PER_CONTRACT, 2);
@@ -694,7 +699,7 @@ const LEAPS_COLUMNS: CliResultColumn<LeapsRow>[] = [
694
699
 
695
700
  /** What each `--columns` name adds to the exports, after Extrinsic/yr %. */
696
701
  const EXTRA_EXPORT_COLUMNS: Record<LeapsExtraColumn, CliResultColumn<LeapsRow>[]> = {
697
- iv: [{ key: "ivPercent", header: "IV %", align: "right", format: formatPercent }],
702
+ iv: [{ key: "ivPercent", header: "IV %", align: "right", format: formatPercent }, { key: "ivSource", header: "IV source" }],
698
703
  ivr: [{ key: "ivRank", header: "IV Rank", align: "right" }, { key: "ivRankAsOf", header: "IV Rank As Of" }],
699
704
  cost: [{ key: "costPerContract", header: "Cost/Contract (mid x 100)", align: "right" }],
700
705
  breakeven: [
@@ -717,7 +722,8 @@ const LEAPS_TEXT_COLUMNS: CliResultColumn<LeapsRow>[] = TEXT_COLUMN_KEYS.map((ke
717
722
 
718
723
  /** The text table's columns for a contract's own `--columns` figures; the underlying's go in a table of their own above. */
719
724
  const EXTRA_TEXT_COLUMNS: Partial<Record<LeapsExtraColumn, CliResultColumn<LeapsRow>[]>> = {
720
- iv: [{ key: "ivPercent", header: "IV", align: "right", format: formatPercent }],
725
+ iv: [{ key: "ivPercent", header: "IV", align: "right", format: (value, row) => row.ivSource === "filled" && typeof value === "number" && Number.isFinite(value)
726
+ ? `~${value.toFixed(1)}%` : formatPercent(value) }],
721
727
  "carry-net": [{ key: "carryNetPerYearPercent", header: "Net/yr", align: "right", format: formatPercent }],
722
728
  cost: [{ key: "costPerContract", header: "Cost", align: "right", format: formatWhole }],
723
729
  breakeven: [
@@ -1,8 +1,8 @@
1
1
  import { logFailure } from "../../utils/log-failure";
2
2
  import { apiClient } from "../../api-client";
3
- import { ApiRequestError } from "../../api-client/errors";
3
+ import { ApiRequestError, isPermanentClientError } from "../../api-client/errors";
4
4
  import type { CliCommandDef } from "../../types/plugin";
5
- import { withCliServices, withMarketData } from "../context";
5
+ import { withCliServices, withConfigData, withMarketData } from "../context";
6
6
  import { CLI_ERROR_CODE, failWithFields } from "../errors";
7
7
  import type { CliResultColumn } from "../result";
8
8
  import { formatCompact } from "../../utils/format";
@@ -23,25 +23,36 @@ import {
23
23
  type MoverRankBasis,
24
24
  } from "../../plugins/builtin/market-movers/model";
25
25
  import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
26
+ import { hasPartialFloat, meetsMinFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "../../plugins/builtin/market-movers/min-float";
26
27
  import { fetchEconCalendarFrom } from "../../plugins/builtin/econ/calendar-source";
27
- import { computedFredSeriesNote, seriesNotOnFred } from "../../sources/gloomberb-cloud/fred-series";
28
+ import {
29
+ attachFredSeriesPersistence, computedFredSeriesNote, loadCachedFredSeries, resetFredSeriesPersistence, seriesNotOnFred,
30
+ } from "../../sources/gloomberb-cloud/fred-series";
31
+ import type { CloudFredSeriesPayload } from "../../api-client";
32
+ import { savedCopyMark } from "../../data/saved-copies";
33
+ import { ratesMacroPluginMeta } from "../../plugins/builtin/builtin-plugin-meta";
34
+ import { createCommandPluginPersistence } from "../saved-copy-persistence";
28
35
  import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
29
36
  import {
30
37
  COUNTRY_CYCLE,
31
38
  COUNTRY_FILTER_HINT,
39
+ awaitsConsensus,
32
40
  countriesWithoutEvents,
41
+ econEventName,
33
42
  FILTER_CYCLE,
34
43
  loadCalendar,
35
44
  eventCountry,
36
45
  matchesCountry,
46
+ matchesEventSearch,
37
47
  matchesImpact,
48
+ NO_CONSENSUS_NOTE,
38
49
  noScheduledReleasesMessage,
39
50
  parseCountryFilter,
40
51
  shortCalendarEnd,
41
52
  type CountryFilter,
42
53
  type ImpactFilter,
43
54
  } from "../../plugins/builtin/econ/calendar-model";
44
- import { hasNoReleaseTime, hasNoStatedTime } from "../../plugins/builtin/econ/types";
55
+ import { hasNoReleaseTime, hasNoStatedTime, type EconEvent } from "../../plugins/builtin/econ/types";
45
56
  import { econRowFigures } from "../../plugins/builtin/econ/figures";
46
57
  import { isoDate, rejectExtraArgs, requireArg, requireOneArg, takeFlag, takeOption } from "./command-utils";
47
58
  import { buildCorrelationSeries } from "../../plugins/builtin/correlation/matrix/model";
@@ -141,7 +152,7 @@ const MOVER_LISTS: Record<string, ScreenerCategory | "trending"> = {
141
152
  "most-active": "most_actives",
142
153
  trending: "trending",
143
154
  };
144
- const MOVERS_USAGE = "movers [gainers|losers|active|trending] [--session regular|extended]";
155
+ const MOVERS_USAGE = "movers [gainers|losers|active|trending] [--session regular|extended] [--min-float <shares>]";
145
156
  /** The most rows the movers list serves in one request. */
146
157
  const MOVER_REQUEST_CAP = 100;
147
158
  /** A one-shot list waits this long for its rows' average volumes and sizes, as `fn MOST` does. */
@@ -154,6 +165,16 @@ function moverRankBasis(value: string | undefined, ctx: Parameters<CliCommandDef
154
165
  return basis;
155
166
  }
156
167
 
168
+ /** `--min-float` in shares, null for none; fails on a value that is not a share count. */
169
+ function moverMinFloat(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): number | null {
170
+ if (value == null) return null;
171
+ try {
172
+ return minFloatShares(normalizeMinFloat(value));
173
+ } catch (error) {
174
+ ctx.fail(error instanceof Error ? error.message : String(error));
175
+ }
176
+ }
177
+
157
178
  function screenerCategory(args: readonly string[], ctx: Parameters<CliCommandDef["execute"]>[1]): ScreenerCategory | "trending" {
158
179
  rejectExtraArgs(args, 1, { usage: MOVERS_USAGE, takes: "one list" }, ctx);
159
180
  const list = args[0]?.toLowerCase() ?? "gainers";
@@ -199,12 +220,14 @@ async function runMoverCommand(rawArgs: string[], ctx: Parameters<CliCommandDef[
199
220
  const args = [...rawArgs];
200
221
  const sessionOption = takeOption(args, "--session");
201
222
  const basis = moverRankBasis(sessionOption, ctx);
223
+ const minFloat = moverMinFloat(takeOption(args, "--min-float"), ctx);
202
224
  const category = screenerCategory(args, ctx);
203
225
  const limit = ctx.cliOptions.limit ?? 25;
204
226
  if (sessionOption != null && (category === "trending" || category === "most_actives")) {
205
227
  ctx.fail("--session ranks Gainers and Losers.", category === "trending"
206
228
  ? "Trending keeps its source's order." : "Most active ranks by volume.");
207
229
  }
230
+ if (minFloat != null && category === "trending") ctx.fail("--min-float filters Gainers, Losers and Most active.", "Trending carries no float.");
208
231
  if (category === "trending") {
209
232
  await withCliServices(ctx, async (services) => {
210
233
  const trending = await fetchTrending(limit, undefined, { forceRefresh: ctx.cliOptions.refresh });
@@ -220,8 +243,8 @@ async function runMoverCommand(rawArgs: string[], ctx: Parameters<CliCommandDef[
220
243
  }
221
244
 
222
245
  // After the close a price list keeps only the rows moving its way on the basis
223
- // asked for, so it reads more candidates than it shows.
224
- const count = category === "most_actives" ? limit : Math.min(MOVER_REQUEST_CAP, Math.max(limit, MOVER_CANDIDATES));
246
+ // asked for, and a minimum float drops rows, so they read more candidates than they show.
247
+ const count = category === "most_actives" && minFloat == null ? limit : Math.min(MOVER_REQUEST_CAP, Math.max(limit, MOVER_CANDIDATES));
225
248
  // With the saved session, as MOST reads it: a Pro account gets the real-time lists, anyone else the delayed ones.
226
249
  const movers = await withMarketData(ctx, () =>
227
250
  fetchPreferredMarketMovers(category, count, { forceRefresh: ctx.cliOptions.refresh, session: basis, metadataWaitMs: MOVER_METADATA_WAIT_MS }));
@@ -230,7 +253,7 @@ async function runMoverCommand(rawArgs: string[], ctx: Parameters<CliCommandDef[
230
253
  // the regular session's move (its close's after the close), or with
231
254
  // --session extended by the extended print's move from that close.
232
255
  const { quotes, rankedBy } = rankMoverQuotes(category, movers.quotes.map(moverRegularSession), basis);
233
- const rows = quotes.slice(0, limit).map((quote) => {
256
+ const rows = quotes.filter((quote) => meetsMinFloat(quote.floatShares, minFloat)).slice(0, limit).map((quote) => {
234
257
  const { extended, ...row } = quote;
235
258
  return {
236
259
  ...row,
@@ -244,16 +267,18 @@ async function runMoverCommand(rawArgs: string[], ctx: Parameters<CliCommandDef[
244
267
  // As the MOST report reads it: real-time for a Pro account, delayed otherwise, each row dated by its own last price.
245
268
  ctx.printResult({
246
269
  data: rows,
247
- metadata: { category, rankedBy },
270
+ metadata: { category, rankedBy, ...(minFloat != null ? { minFloat } : {}) },
248
271
  ...(fellBack ? { warnings: ["No pre-market or after-hours prints now, so the list is ranked by the regular-session move."] } : {}),
249
272
  freshness: rowsFreshness(rows, { status: movers.source === "cloud" ? "live" : "delayed" }, { stale: movers.stale }),
250
273
  }, {
251
274
  heading: `Ranked by ${MOVER_RANKING_LABELS[rankedBy]}`,
275
+ ...(minFloat != null && hasPartialFloat(rows) ? { footnote: PARTIAL_FLOAT_NOTE } : {}),
252
276
  columns: [
253
277
  ...MOVER_COLUMNS.slice(0, 4),
254
278
  ...EXTENDED_COLUMNS,
255
279
  { key: "volume", header: "Volume", align: "right", value: (row) => formatCompact(Number(row.volume)) },
256
280
  { ...MARKET_CAP_CELL, value: (row) => row.marketCap == null ? "" : formatCompact(Number(row.marketCap)) },
281
+ ...(minFloat != null ? [{ key: "floatShares", header: "Float", align: "right" as const, value: (row: Record<string, unknown>) => formatCompact(Number(row.floatShares)) }] : []),
257
282
  ],
258
283
  });
259
284
  }
@@ -288,7 +313,7 @@ function utcDateTimePart(value: unknown, part: "date" | "time", readerClock = tr
288
313
  return part === "date" ? iso.slice(0, 10) : `${iso.slice(11, 16)} UTC${readerClock ? localTimeSuffix(date.getTime()) : ""}`;
289
314
  }
290
315
 
291
- const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--from <date>] [--tz <zone>]";
316
+ const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--search <words>] [--from <date>] [--tz <zone>]";
292
317
  const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
293
318
  /** "Sat" for 2026-10-31. */
294
319
  const weekdayOf = (date: string) => WEEKDAYS[new Date(`${date}T00:00:00Z`).getUTCDay()]!;
@@ -325,6 +350,19 @@ function econHeadColumns(viewerZone: string | null): CliResultColumn[] {
325
350
  ];
326
351
  }
327
352
 
353
+ /**
354
+ * The text table's columns up to the event. Plain output keeps every column a
355
+ * query has, so a column that is empty by construction has to be left out
356
+ * here, not hidden by the table: the viewer's clock only exists with --tz, and
357
+ * the publisher's own clock only on rows a bank or statistics office times
358
+ * itself. Exports keep both, so a script's header never moves.
359
+ */
360
+ function econTextHeadColumns(viewerZone: string | null, publisherTimed: boolean): CliResultColumn[] {
361
+ return econHeadColumns(viewerZone).filter((column) => (
362
+ column.key === "viewerTime" ? !!viewerZone : column.key === "localTime" ? publisherTimed : true
363
+ ));
364
+ }
365
+
328
366
  /**
329
367
  * Actual, Forecast and Prior: as the text draws them (`30.3K`, `-4.1M bbl`) in the table, as
330
368
  * numbers in the exports (30300, -4100000), where Unit and Scale say what they count and how they were drawn.
@@ -349,6 +387,9 @@ const econExportColumns: CliResultColumn[] = [
349
387
  { key: "actualText", header: "actual_text" },
350
388
  { key: "forecastText", header: "forecast_text" },
351
389
  { key: "priorText", header: "prior_text" },
390
+ // Which feed the forecast was read from and when, so a consensus can be told from another read of it.
391
+ { key: "consensusFeed", header: "consensus_feed" },
392
+ { key: "consensusReadAt", header: "consensus_read_at" },
352
393
  ];
353
394
 
354
395
  /**
@@ -409,6 +450,16 @@ function blankTimeNotes(rows: ReadonlyArray<{ event: string; country: string; ti
409
450
  return [...byNote].map(([note, events]) => `No time for ${events.join(", ")}: ${note.charAt(0).toLowerCase()}${note.slice(1)}.`);
410
451
  }
411
452
 
453
+ /**
454
+ * Whether a row has come out: it has an actual, or it has no figure at all (a
455
+ * speech, a meeting, a holiday) and its time has passed. A row with a forecast
456
+ * or a prior and no actual is still to come, whatever its time.
457
+ */
458
+ function cameOut(event: Pick<EconEvent, "actual" | "forecast" | "prior" | "date">, now: number): boolean {
459
+ if (event.actual?.trim()) return true;
460
+ return !event.forecast?.trim() && !event.prior?.trim() && event.date.getTime() <= now;
461
+ }
462
+
412
463
  /**
413
464
  * A `--from` the stored calendar does not reach back to, which Gloom Cloud refuses
414
465
  * with "The calendar reaches back 400 days": an input error naming the earliest
@@ -429,15 +480,18 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
429
480
  const rawArgs = [...args];
430
481
  const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
431
482
  const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
483
+ const search = takeOption(rawArgs, "--search")?.trim() ?? null;
484
+ if (search === "") ctx.fail("--search needs the words of an event's name.", "Such as econ --search nonfarm.");
432
485
  const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
433
486
  const tzOption = takeOption(rawArgs, "--tz");
434
487
  const viewerZone = tzOption == null ? null : canonicalTimeZone(tzOption);
435
488
  if (tzOption != null && !viewerZone) {
436
489
  ctx.fail(`Unknown time zone "${tzOption}".`, "Use an IANA zone name, such as America/New_York, Europe/London or Asia/Tokyo.");
437
490
  }
438
- rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --impact.");
491
+ rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country, --impact and --search.");
439
492
  await withCliServices(ctx, async () => {
440
- const todayStart = Math.floor(Date.now() / DAY_MS) * DAY_MS;
493
+ const now = Date.now();
494
+ const todayStart = Math.floor(now / DAY_MS) * DAY_MS;
441
495
  // An earlier day reads the stored calendar, which keeps past events after the week's listing drops them.
442
496
  // A server without it, a server error or no network lists the current calendar (cached when offline) instead.
443
497
  const stored = fromOption != null && fromOption < isoDate(new Date(todayStart))
@@ -445,7 +499,9 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
445
499
  : null;
446
500
  const past = stored?.events ? stored : null;
447
501
  const unread = stored && !stored.events && stored.failure ? `The stored calendar could not be read (${stored.failure}); listing the current calendar` : null;
448
- const { data: events, fetchedAt, stale } = past ? { data: past.events, fetchedAt: Date.now(), stale: false } : await loadCalendar(ctx.cliOptions.refresh);
502
+ const { data: events, fetchedAt, stale, source } = past
503
+ ? { data: past.events, fetchedAt: Date.now(), stale: false, source: "network" as const }
504
+ : await loadCalendar(ctx.cliOptions.refresh);
449
505
  // The calendar lists from today (UTC). --from moves the first day; --tail
450
506
  // alone keeps the newest events of the whole calendar, the past week's too.
451
507
  const from = fromOption ?? (ctx.cliOptions.tail != null ? null : new Date(todayStart).toISOString().slice(0, 10));
@@ -464,9 +520,10 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
464
520
  // Countries asked for that nothing in the window is for: a quiet week there, not an unknown country.
465
521
  const inWindow = events.filter((event) => event.date.getTime() >= fromTime);
466
522
  const unscheduled = countriesWithoutEvents(country, inWindow);
467
- const rows = inWindow
468
- .filter((event) => matchesCountry(event, country) && matchesImpact(event, impact))
469
- .sort((left, right) => left.date.getTime() - right.date.getTime())
523
+ const listed = inWindow
524
+ .filter((event) => matchesCountry(event, country) && matchesImpact(event, impact) && (search == null || matchesEventSearch(event, search)))
525
+ .sort((left, right) => left.date.getTime() - right.date.getTime());
526
+ const rows = listed
470
527
  .map((event) => {
471
528
  // The viewer's clock (--tz) for a row with a time: its date and time there.
472
529
  const clock = viewerZone && !hasNoReleaseTime(event) ? zonedClock(event.date.getTime(), viewerZone) : null;
@@ -480,7 +537,8 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
480
537
  ...(viewerZone ? { viewerDate: clock?.date ?? null, viewerTime: clock?.time ?? null } : {}),
481
538
  country: eventCountry(event),
482
539
  impact: event.impact,
483
- event: event.event,
540
+ // As the calendar names it: the Fed's decision is the upper bound of its range.
541
+ event: econEventName(event),
484
542
  actual: figures.actual,
485
543
  forecast: figures.forecast,
486
544
  prior: figures.prior,
@@ -497,25 +555,34 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
497
555
  // Who publishes the figure (Freddie Mac for the 30-year mortgage rate), the period it covers and its page.
498
556
  publisher: event.source?.name ?? null,
499
557
  referencePeriod: event.referencePeriod ?? null,
500
- // When the figure came out: the row's time once it has an actual (its day alone when it states no time), null before.
501
- released: event.actual?.trim() ? (hasNoReleaseTime(event) ? isoDate(event.date).slice(0, 10) : isoDate(event.date)) : null,
558
+ // When the figure came out: the row's time once it has an actual, and for a row with no figures (a speech, a
559
+ // meeting) once its time has passed; its day alone when it states no time, null before.
560
+ released: cameOut(event, now) ? (hasNoReleaseTime(event) ? isoDate(event.date).slice(0, 10) : isoDate(event.date)) : null,
502
561
  sourceUrl: event.source?.url ?? null,
503
- // The same indicator from one release to the next; empty until Gloom Cloud names it.
562
+ // The same indicator from one release to the next, as Gloom Cloud names it.
504
563
  indicatorId: event.indicatorId ?? null,
564
+ // The feed the forecast was read from and when, for a row with a forecast.
565
+ consensusFeed: event.consensus?.source ?? null,
566
+ consensusReadAt: event.consensus?.readAt ?? null,
505
567
  };
506
568
  });
507
569
  // A filter of countries only, none of which has anything in the window, says so as its empty list.
508
570
  const allUnscheduled = unscheduled.length > 0 && unscheduled.length === country.split(",").length;
509
571
  const notes = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
510
- rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null]
572
+ rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null,
573
+ // A dash under Forecast on a release still to come is a consensus not in yet, not a missing figure.
574
+ listed.some(awaitsConsensus) ? NO_CONSENSUS_NOTE : null]
511
575
  .filter((note): note is string => !!note);
512
576
  const footnote = notes.join("\n");
577
+ // The calendar as read, not its rows: those are scheduled, mostly ahead. One that could not be
578
+ // refreshed is the copy an earlier read saved, and says when that was.
579
+ const read = rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale });
580
+ const calendarFreshness = read && source === "stale-fallback" ? { ...read, savedAt: new Date(fetchedAt).toISOString() } : read;
513
581
  ctx.printResult({
514
582
  data: rows,
515
583
  ...(unread ? { warnings: [unread] } : {}),
516
- metadata: { country, impact, from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
517
- // The calendar as read, not its rows: those are scheduled, mostly ahead.
518
- freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale }),
584
+ metadata: { country, impact, ...(search != null ? { search } : {}), from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
585
+ freshness: calendarFreshness,
519
586
  }, {
520
587
  dateKey: "date",
521
588
  defaultLimit: 50,
@@ -535,7 +602,7 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
535
602
  }),
536
603
  // The exports add what a script needs to read a row; the text table keeps the columns it had.
537
604
  columns: [...econHeadColumns(viewerZone), ...econFigureColumns(false), ...econExportColumns],
538
- textColumns: [...econHeadColumns(viewerZone), ...econFigureColumns(true), ECON_PUBLISHER_COLUMN],
605
+ textColumns: [...econTextHeadColumns(viewerZone, rows.some((row) => !!publisherClock(row.localTime, row.timeZone))), ...econFigureColumns(true), ECON_PUBLISHER_COLUMN],
539
606
  });
540
607
  });
541
608
  }
@@ -561,6 +628,36 @@ function isUnsupportedSeries(error: unknown): boolean {
561
628
  return error.details?.code === "unsupported_series" || /^Unsupported FRED series\b/i.test(message);
562
629
  }
563
630
 
631
+ /**
632
+ * Runs `run` with the FRED series cache saving what it fetches in the profile,
633
+ * where the app's rates and macro panes keep it: a refresh that fails then
634
+ * answers from the copy an earlier read saved, and the report says so.
635
+ */
636
+ function withSavedFredSeries<T>(ctx: Parameters<CliCommandDef["execute"]>[1], run: () => Promise<T>): Promise<T> {
637
+ return withConfigData(ctx, async ({ persistence }) => {
638
+ attachFredSeriesPersistence(createCommandPluginPersistence(persistence.resources, `plugin:${ratesMacroPluginMeta.stateId}`));
639
+ try {
640
+ return await run();
641
+ } finally {
642
+ resetFredSeriesPersistence();
643
+ }
644
+ });
645
+ }
646
+
647
+ /**
648
+ * A FRED series from Gloom Cloud, or the copy an earlier read saved when it
649
+ * cannot be refreshed (marked as such). A refusal asking again will not change
650
+ * (a series Gloom Cloud no longer serves, access denied) is said, not covered.
651
+ */
652
+ async function loadFredSeries(
653
+ seriesId: string,
654
+ params: { startDate?: string; limit?: number; sortOrder: "asc" | "desc" },
655
+ ): Promise<CloudFredSeriesPayload> {
656
+ const result = await loadCachedFredSeries({ seriesId, ...params }, () => apiClient.getCloudFredSeries(seriesId, params));
657
+ if (result.source === "stale-fallback" && isPermanentClientError(result.error)) throw result.error;
658
+ return result.data;
659
+ }
660
+
564
661
  async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execute"]>[1]) {
565
662
  let catalog;
566
663
  try {
@@ -606,9 +703,19 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
606
703
  ctx.fail(change ? `Unknown change "${change}".` : "--change needs 1D or 1W.", "Use 1D (from the previous observation) or 1W (from a week before).");
607
704
  }
608
705
  const seriesId = requireOneArg(args, FRED_USAGE, "series", ctx).toUpperCase();
706
+ await withSavedFredSeries(ctx, () => printFredSeries(ctx, { seriesId, start, startDate, sortOrder, horizon }));
707
+ }
708
+
709
+ async function printFredSeries(ctx: Parameters<CliCommandDef["execute"]>[1], { seriesId, start, startDate, sortOrder, horizon }: {
710
+ seriesId: string;
711
+ start: string | undefined;
712
+ startDate: string;
713
+ sortOrder: "asc" | "desc";
714
+ horizon: ReturnType<typeof parseFredChange>;
715
+ }) {
609
716
  // A change needs the observation it is measured from: the fetch reaches back before the first row shown.
610
717
  const fetchStart = horizon ? fredChangeFetchStart(startDate) : startDate;
611
- const data = await apiClient.getCloudFredSeries(seriesId, { startDate: fetchStart, sortOrder }).catch((error: unknown) => {
718
+ const data = await loadFredSeries(seriesId, { startDate: fetchStart, sortOrder }).catch((error: unknown) => {
612
719
  if (isUnsupportedSeries(error)) ctx.fail(`Unsupported FRED series ${seriesId}. List supported series with: gloom fred --list`);
613
720
  throw error;
614
721
  });
@@ -622,7 +729,9 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
622
729
  const releasedOf = (observation: { publisher?: string; publishedAt?: string; releasedOn?: string }) =>
623
730
  observation.releasedOn ?? (observation.publisher?.trim() ? observation.publishedAt : undefined);
624
731
  // A value FRED does not list yet carries its publisher and when it published, in every export.
732
+ // A row from the copy an earlier read saved, the refresh having failed, carries its mark: the source line says so.
625
733
  const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string; releasedOn?: string }) => ({
734
+ ...savedCopyMark(data),
626
735
  date: observation.date,
627
736
  value: fredExportValue(seriesId, observation.value),
628
737
  ...(changes ? { change: changes.get(observation.date)?.change ?? null, changeFrom: changes.get(observation.date)?.changeFrom ?? null } : {}),
@@ -641,7 +750,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
641
750
  computedFredSeriesNote(seriesId),
642
751
  ].filter((note): note is string => note != null);
643
752
  // Nothing on or after the start: name where the series ends, so an empty answer is not a dead end.
644
- const latest = rows.length > 0 ? null : await apiClient.getCloudFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
753
+ const latest = rows.length > 0 ? null : await loadFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
645
754
  .then((payload) => payload.observations[0] ?? null).catch(logFailure("cli.commands-overview", "Read latest economic series observation failed", null));
646
755
  // CSV and NDJSON are rows alone: an empty one would read as a series with no data, so it fails and says where the series ends.
647
756
  if (rows.length === 0 && (ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson")) {
@@ -713,10 +822,12 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
713
822
  const startDate = parseStartDate(takeOption(args, "--start"), ctx);
714
823
  rejectArgs(args, "yield-curve [--start <date>]", ctx);
715
824
  const series = Object.keys(YIELD_TENORS);
716
- const results = await Promise.all(series.map(async (seriesId) => {
717
- const data = await apiClient.getCloudFredSeries(seriesId, { startDate, sortOrder: "desc" });
825
+ const results = await withSavedFredSeries(ctx, () => Promise.all(series.map(async (seriesId) => {
826
+ const data = await loadFredSeries(seriesId, { startDate, sortOrder: "desc" });
718
827
  const [latest, previous] = data.observations;
719
828
  return {
829
+ // A tenor from the copy an earlier read saved, the refresh having failed, says so in the source line.
830
+ ...savedCopyMark(data),
720
831
  seriesId,
721
832
  date: latest?.date ?? "",
722
833
  // Yields in percent, which the exports' `currency_or_unit` column says (`pct`), as `fred` does.
@@ -729,7 +840,7 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
729
840
  latest,
730
841
  publishedAt: latest?.publisher ? null : fredPublicationTime(data.info, latest?.date),
731
842
  };
732
- }));
843
+ })));
733
844
  // Tenors whose latest yield FRED does not list yet name the Treasury and when it published them.
734
845
  const ahead = fredProvenance(results.flatMap((row) => row.latest ? [row.latest] : []));
735
846
  // The curve shown was published with its last tenor: the newest of their FRED updates, when each has one.
@@ -826,8 +937,11 @@ export const overviewCliCommands: CliCommandDef[] = [
826
937
  options: [{
827
938
  flags: "--session <basis>",
828
939
  description: "Rank gainers and losers by the regular-session move (regular, the default) or by the pre-market or after-hours move from the close (extended)",
940
+ }, {
941
+ flags: "--min-float <shares>",
942
+ description: "Keep the gainers, losers or most active whose float (the one EQS serves) is at least this many shares, such as 10M or 500k; a row with no float is left out",
829
943
  }],
830
- examples: ["movers", "movers losers --limit 10", "movers gainers --session extended", "movers trending"],
944
+ examples: ["movers", "movers losers --limit 10", "movers gainers --session extended", "movers gainers --min-float 10M", "movers trending"],
831
945
  },
832
946
  execute: runMoverCommand,
833
947
  },
@@ -858,10 +972,11 @@ export const overviewCliCommands: CliCommandDef[] = [
858
972
  options: [
859
973
  { flags: "--country <regions or countries>", description: "US, G7, EU, all (default), or countries by their two-letter codes, comma separated, such as MX or MX,BR,ZA" },
860
974
  { flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
975
+ { flags: "--search <words>", description: "Events whose name has these words, case, spaces and hyphens aside, such as nonfarm or fed funds" },
861
976
  { flags: "--from <date>", description: `First day listed, as ${DATE_OPTION_EXAMPLES} (default today, UTC); an earlier day lists past events from the stored calendar and says where it starts` },
862
977
  { flags: "--tz <zone>", description: "Add a column with each release's time on that clock, an IANA zone such as America/New_York; with the weekday when it falls on another day than the UTC date" },
863
978
  ],
864
- examples: ["econ", "econ --country US --impact high", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
979
+ examples: ["econ", "econ --country US --impact high", "econ --search nonfarm", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
865
980
  "econ --country BR --tz America/New_York", "econ --tail 20"],
866
981
  },
867
982
  execute: runEcon,
@@ -133,7 +133,7 @@ export async function searchCandidatesForCli({
133
133
  const quote = BARE_SYMBOL.test(symbol) && candidates.some((candidate) => candidate.symbol.toUpperCase() === symbol)
134
134
  ? await (quoted ?? loadSearchedQuote(symbol, savedExchange, dataProvider))
135
135
  : null;
136
- return withQuotedNames(symbol, withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote)), quote)
136
+ return withQuotedNames(symbol, withUsLineOfPrimary(withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote))), quote)
137
137
  .slice(0, totalLimit);
138
138
  } catch (error) {
139
139
  // Not "No matches found": a script or a reader would take that for a name nothing lists.
@@ -299,6 +299,26 @@ function withHomeListingPrimary(query: string, candidates: TickerSearchCandidate
299
299
  return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];
300
300
  }
301
301
 
302
+ /**
303
+ * A company whose primary listing is abroad lists the US line of the same
304
+ * symbol right after it: the line `quote SHEL` reads (a bare symbol is its US
305
+ * listing), which a name search ("shell") would otherwise rank below the
306
+ * company's lines on other venues and cut. Any other list keeps its order.
307
+ */
308
+ function withUsLineOfPrimary(candidates: TickerSearchCandidate[]): TickerSearchCandidate[] {
309
+ const primary = candidates.find((candidate) => candidate.category === "Primary Listing");
310
+ if (!primary || primary.kind !== "search" || isUsListingExchange(candidateExchange(primary))) return candidates;
311
+ const symbol = primary.symbol.toUpperCase();
312
+ const company = resolveSearchName(primary);
313
+ const us = candidates.find((candidate) => candidate !== primary && candidate.kind === "search" && !candidate.contractKey
314
+ && candidate.category !== "Funds & Derivatives" && candidate.symbol.toUpperCase() === symbol
315
+ && isUsListingExchange(candidateExchange(candidate)) && sameCompany(resolveSearchName(candidate), company));
316
+ if (!us) return candidates;
317
+ const rest = candidates.filter((candidate) => candidate !== us);
318
+ const at = rest.indexOf(primary) + 1;
319
+ return [...rest.slice(0, at), { ...us, category: "Other Listings" as const }, ...rest.slice(at)];
320
+ }
321
+
302
322
  /** The candidate on the venue the quote prices; the US exchanges share one symbol, so a quote on any of them is its one US line. */
303
323
  function quotedCandidate(candidates: readonly TickerSearchCandidate[], quote: Quote | null): TickerSearchCandidate | undefined {
304
324
  const quoted = canonicalExchange(quote?.listingExchangeName || quote?.exchangeName);