@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
@@ -282,7 +282,7 @@ export function calculateKellySizing({
282
282
  price?: number | null;
283
283
  }): KellySizingResult {
284
284
  const warnings: string[] = [];
285
- if (!finite(bankroll) || bankroll <= 0 || !finite(currentValue)) {
285
+ if (!finite(currentValue)) {
286
286
  return {
287
287
  mode,
288
288
  bankroll: 0,
@@ -301,7 +301,7 @@ export function calculateKellySizing({
301
301
  riskFraction: 0,
302
302
  expectedReturn: 0,
303
303
  expectedLogGrowth: 0,
304
- warnings: [!finite(currentValue) ? "Current holding value unavailable; check FX rates." : "No positive bankroll available."],
304
+ warnings: ["Current holding value unavailable; check FX rates."],
305
305
  clipReasons: [],
306
306
  recommendationLabel: "Unavailable",
307
307
  };
@@ -342,20 +342,24 @@ export function calculateKellySizing({
342
342
 
343
343
  const safeCurrentValue = finite(currentValue) ? Math.max(0, currentValue) : 0;
344
344
  const safePrice = price != null && finite(price) && price > 0 ? price : null;
345
- const targetValue = clippedFraction * bankroll;
346
- const addTrimValue = targetValue - safeCurrentValue;
347
- const estimatedUnits = safePrice ? addTrimValue / safePrice : null;
345
+ // Sizes are shares of the bankroll; only the money amounts need one.
346
+ const hasBankroll = finite(bankroll) && bankroll > 0;
347
+ const targetValue = hasBankroll ? clippedFraction * bankroll : null;
348
+ const addTrimValue = targetValue == null ? null : targetValue - safeCurrentValue;
349
+ const estimatedUnits = safePrice && addTrimValue != null ? addTrimValue / safePrice : null;
348
350
  const riskFraction = clippedFraction * downsideLossFraction;
349
- const riskValue = riskFraction * bankroll;
351
+ const riskValue = hasBankroll ? riskFraction * bankroll : null;
352
+ // Nothing held is 0% of any bankroll; a holding with no bankroll has no share.
353
+ const currentFraction = hasBankroll ? safeCurrentValue / bankroll : safeCurrentValue > 0 ? null : 0;
350
354
 
351
355
  if (fullKellyFraction === 0 && solve.expectedReturn <= 0) warnings.push("No positive Kelly edge.");
352
356
  if (downsideLossFraction <= 0) warnings.push("No downside assumption; risk caps cannot bind.");
353
357
 
354
358
  return {
355
359
  mode,
356
- bankroll,
360
+ bankroll: hasBankroll ? bankroll : 0,
357
361
  currentValue: safeCurrentValue,
358
- currentFraction: safeCurrentValue / bankroll,
362
+ currentFraction,
359
363
  price: safePrice,
360
364
  fullKellyFraction,
361
365
  fractionalKellyFraction,
@@ -372,7 +372,7 @@ export function KellySizerPane({ focused, width, height }: PaneProps) {
372
372
  const sensitivity = useMemo(() => buildSensitivityGrid(sizingMode, activeDraft), [activeDraft, sizingMode]);
373
373
  const curveMaxFraction = useMemo(
374
374
  () => getKellyCurveMaxFraction(sizingMode, activeDraft, [
375
- result.currentFraction,
375
+ result.currentFraction ?? 0,
376
376
  result.clippedFraction,
377
377
  result.fullKellyFraction,
378
378
  ]),
@@ -384,7 +384,9 @@ export function KellySizerPane({ focused, width, height }: PaneProps) {
384
384
  );
385
385
  const curveXAxisLabels = useMemo(() => buildKellyCurveXAxisLabels(curveMaxFraction), [curveMaxFraction]);
386
386
  const currentGrowth = useMemo(
387
- () => calculateExpectedLogGrowthAtFraction(sizingMode, activeDraft, result.currentFraction),
387
+ () => result.currentFraction == null
388
+ ? null
389
+ : calculateExpectedLogGrowthAtFraction(sizingMode, activeDraft, result.currentFraction),
388
390
  [activeDraft, sizingMode, result.currentFraction],
389
391
  );
390
392
  const targetGrowth = useMemo(
@@ -393,15 +395,16 @@ export function KellySizerPane({ focused, width, height }: PaneProps) {
393
395
  );
394
396
  const curveMarkers = useMemo<StaticChartXMarker[]>(() => {
395
397
  if (!Number.isFinite(curveMaxFraction) || curveMaxFraction <= 0) return [];
396
- const targetLabel = result.clipReasons[0]?.replace(/^max /, "") ?? "target";
398
+ // Caps apply in turn, so with both binding the last one set the target.
399
+ const targetLabel = result.clipReasons.at(-1)?.replace(/^max /, "") ?? "target";
397
400
  const markers: StaticChartXMarker[] = [
398
- {
401
+ ...(result.currentFraction == null ? [] : [{
399
402
  id: "current",
400
403
  xRatio: result.currentFraction / curveMaxFraction,
401
404
  label: "current",
402
405
  color: colors.textDim,
403
406
  lineChar: "┊",
404
- },
407
+ }]),
405
408
  {
406
409
  id: "target",
407
410
  xRatio: result.clippedFraction / curveMaxFraction,
@@ -474,9 +477,6 @@ export function KellySizerPane({ focused, width, height }: PaneProps) {
474
477
  usePaneFooter(KELLY_PANE_ID, () => ({
475
478
  info: historyStatus
476
479
  ? [{ id: "history", parts: [historyStatus] }]
477
- : bankroll <= 0 && ticker
478
- // Every size reads 0% until there is a bankroll; say so where status lives.
479
- ? [{ id: "bankroll", parts: [{ text: "no bankroll", tone: "warning" as const }] }]
480
480
  : result.warnings.length > 0
481
481
  ? [{ id: "warning", parts: [{ text: result.warnings[0]!, tone: "warning" as const }] }]
482
482
  : result.clipReasons.length > 0
@@ -488,7 +488,7 @@ export function KellySizerPane({ focused, width, height }: PaneProps) {
488
488
  { id: "search", key: "/", label: "search", onPress: focusTickerSearch },
489
489
  ...(viewInBar ? [] : [{ id: "sensitivity", key: "s", label: showSensitivity ? "ensitivity off" : "ensitivity", onPress: toggleSensitivity }]),
490
490
  ],
491
- }), [activeInputId, bankroll, editSelectedField, focusTickerSearch, historyStatus?.text, historyStatus?.tone, result.clipReasons, result.warnings, selectedField?.id, selectedField?.label, showSensitivity, ticker, toggleSensitivity, viewInBar]);
491
+ }), [activeInputId, editSelectedField, focusTickerSearch, historyStatus?.text, historyStatus?.tone, result.clipReasons, result.warnings, selectedField?.id, selectedField?.label, showSensitivity, ticker, toggleSensitivity, viewInBar]);
492
492
 
493
493
  const portfolioTabs = useMemo(
494
494
  () => config.portfolios.map((portfolio) => ({ label: portfolio.name, value: portfolio.id })),
@@ -11,7 +11,7 @@ import { resolveChartPalette } from "../../../components/chart/core/palette";
11
11
  import type { StaticChartXMarker } from "../../../components/chart/static";
12
12
  import { colors, priceColor } from "../../../theme/colors";
13
13
  import { Box } from "../../../ui";
14
- import { displayWidth, formatCurrency, formatNumber } from "../../../utils/format";
14
+ import { displayWidth, formatCurrency, formatNumber, MISSING_VALUE } from "../../../utils/format";
15
15
  import type { KellySizingResult, SensitivityGrid } from "./types";
16
16
  import type { KellyHistoryInputs } from "./history";
17
17
  import { formatPct, formatSignedPct } from "./view";
@@ -32,10 +32,15 @@ export function buildKellyHistoryItems(inputs: KellyHistoryInputs): StatItem[] {
32
32
  ];
33
33
  }
34
34
 
35
+ /** What the first money figure says while there is no bankroll to size in. */
36
+ const BANKROLL_HINT = "Set a bankroll";
37
+
35
38
  /**
36
39
  * The sizing result as one band under the inputs, laid out in the FieldGrid's
37
40
  * columns: the Kelly fractions, then the move in money, then risk, then log
38
- * growth at full Kelly, at the current size and at the target.
41
+ * growth at full Kelly, at the current size and at the target. Money figures
42
+ * read a dash until there is a bankroll; the fractions and growth never wait
43
+ * for one.
39
44
  */
40
45
  export function buildKellyResultItems({
41
46
  result,
@@ -47,12 +52,13 @@ export function buildKellyResultItems({
47
52
  }: {
48
53
  result: KellySizingResult;
49
54
  baseCurrency: string;
50
- currentGrowth: number;
55
+ /** Null when the holding is not a share of any bankroll. */
56
+ currentGrowth: number | null;
51
57
  targetGrowth: number;
52
58
  width: number;
53
59
  columns: number;
54
60
  }): StatItem[] {
55
- const addTrim = formatCurrency(result.addTrimValue, baseCurrency);
61
+ const addTrim = result.addTrimValue == null ? MISSING_VALUE : formatCurrency(result.addTrimValue, baseCurrency);
56
62
  const units = result.estimatedUnits == null ? null : `${formatNumber(result.estimatedUnits, 1)} units`;
57
63
  // The units ride beside the amount when the column has room for label,
58
64
  // amount and units (plus the cell inset and gaps); otherwise they take a
@@ -68,14 +74,16 @@ export function buildKellyResultItems({
68
74
  value: formatPct(result.clippedFraction, 2),
69
75
  tone: result.clipReasons.length > 0 ? "positive" : undefined,
70
76
  },
71
- { id: "current", label: "Current %", value: formatPct(result.currentFraction, 1) },
72
- { id: "target", label: "Target val", value: formatCurrency(result.targetValue, baseCurrency) },
77
+ { id: "current", label: "Current %", value: result.currentFraction == null ? MISSING_VALUE : formatPct(result.currentFraction, 1) },
78
+ result.targetValue == null
79
+ ? { id: "target", label: "Target val", value: MISSING_VALUE, detail: BANKROLL_HINT }
80
+ : { id: "target", label: "Target val", value: formatCurrency(result.targetValue, baseCurrency) },
73
81
  {
74
82
  id: "addTrim",
75
83
  label: "Add / trim",
76
84
  value: addTrim,
77
85
  detail: unitsInline ? units ?? undefined : undefined,
78
- color: priceColor(result.addTrimValue),
86
+ color: result.addTrimValue == null ? undefined : priceColor(result.addTrimValue),
79
87
  },
80
88
  ...(units != null && !unitsInline
81
89
  ? [{ id: "units", label: "Units", value: formatNumber(result.estimatedUnits!, 1) }]
@@ -83,9 +91,10 @@ export function buildKellyResultItems({
83
91
  {
84
92
  id: "risk",
85
93
  label: "Risk",
86
- value: formatCurrency(result.riskValue, baseCurrency),
94
+ value: result.riskValue == null ? MISSING_VALUE : formatCurrency(result.riskValue, baseCurrency),
87
95
  detail: formatPct(result.riskFraction, 2),
88
- tone: "negative",
96
+ // Nothing at risk is not a loss.
97
+ tone: result.riskValue != null && result.riskValue > 0 ? "negative" : undefined,
89
98
  },
90
99
  { id: "worstLoss", label: "Worst loss", value: formatPct(result.downsideLossFraction, 1) },
91
100
  {
@@ -95,7 +104,7 @@ export function buildKellyResultItems({
95
104
  color: priceColor(result.expectedReturn),
96
105
  },
97
106
  { id: "fullGrowth", label: "Full growth", value: formatSignedPct(result.expectedLogGrowth) },
98
- { id: "growthNow", label: "Growth now", value: formatSignedPct(currentGrowth) },
107
+ { id: "growthNow", label: "Growth now", value: currentGrowth == null ? MISSING_VALUE : formatSignedPct(currentGrowth) },
99
108
  { id: "growthTarget", label: "Growth tgt", value: formatSignedPct(targetGrowth) },
100
109
  ];
101
110
  }
@@ -69,25 +69,31 @@ export interface KellySolveResult {
69
69
  warning?: string;
70
70
  }
71
71
 
72
+ /**
73
+ * The fractions, expected return and growth need no bankroll. The money fields
74
+ * (target, add or trim, units, risk) are null until there is one, and
75
+ * `currentFraction` is null for a holding that has no bankroll to be a share of.
76
+ */
72
77
  export interface KellySizingResult {
73
78
  mode: KellySizingMode;
74
79
  bankroll: number;
75
80
  currentValue: number;
76
- currentFraction: number;
81
+ currentFraction: number | null;
77
82
  price: number | null;
78
83
  fullKellyFraction: number;
79
84
  fractionalKellyFraction: number;
80
85
  unclippedFraction: number;
81
86
  clippedFraction: number;
82
- targetValue: number;
83
- addTrimValue: number;
87
+ targetValue: number | null;
88
+ addTrimValue: number | null;
84
89
  estimatedUnits: number | null;
85
90
  downsideLossFraction: number;
86
- riskValue: number;
91
+ riskValue: number | null;
87
92
  riskFraction: number;
88
93
  expectedReturn: number;
89
94
  expectedLogGrowth: number;
90
95
  warnings: string[];
96
+ /** The caps that cut the size, in the order applied; the last one set the target. */
91
97
  clipReasons: string[];
92
98
  recommendationLabel: string;
93
99
  }
@@ -5,9 +5,9 @@ export const MACRO_RELEASE_TABLE: {
5
5
  coveredThrough: string;
6
6
  days: Record<"cpi" | "jobs" | "fomc", Record<number, readonly string[]>>;
7
7
  } = {
8
- checkedAt: "2026-10-15",
8
+ checkedAt: "2026-10-28",
9
9
  /** The last day the lists are complete for: later sessions are left out, release day or not. */
10
- coveredThrough: "2026-10-14",
10
+ coveredThrough: "2026-10-27",
11
11
  days: {
12
12
  // BLS Consumer Price Index, 8:30 ET.
13
13
  cpi: {
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { createTtlCache } from "../../../data/ttl-cache";
2
3
  import type { MarketHeatmapResult, MarketHeatmapUniverseId } from "../../../api-client/market-discovery";
3
4
  export type { MarketHeatmapUniverseId } from "../../../api-client/market-discovery";
4
5
 
@@ -39,8 +40,7 @@ const MAX_MARKET_HEATMAP_COUNT = 500;
39
40
  export function heatmapCheckIntervalMs(board: Pick<MarketHeatmapResult, "access"> | null | undefined): number {
40
41
  return board?.access === "realtime" ? 15_000 : 60_000;
41
42
  }
42
- const activeFetches = new Map<string, Promise<MarketHeatmapBoard>>();
43
- const memoryCache = new Map<string, { expiresAt: number; result: MarketHeatmapBoard }>();
43
+ const boards = createTtlCache<MarketHeatmapBoard>({ ttlMs: heatmapCheckIntervalMs });
44
44
 
45
45
  /**
46
46
  * One tile per symbol. A company listed on two exchanges (KHC on NASDAQ and
@@ -75,43 +75,18 @@ async function loadMarketHeatmap(universe: MarketHeatmapUniverseId, count: numbe
75
75
  };
76
76
  }
77
77
 
78
- export async function fetchMarketHeatmap(
78
+ export function fetchMarketHeatmap(
79
79
  universe: MarketHeatmapUniverseId,
80
80
  options?: MarketHeatmapFetchOptions,
81
81
  sources?: MarketHeatmapSources,
82
82
  ): Promise<MarketHeatmapBoard> {
83
83
  const count = Math.max(1, Math.min(MAX_MARKET_HEATMAP_COUNT, Math.round(options?.count ?? DEFAULT_COUNT)));
84
- const cacheKey = `${universe}:${count}`;
85
- const useCache = options?.cache !== false && !sources?.client;
86
- const now = Date.now();
87
- const cached = memoryCache.get(cacheKey);
88
- if (useCache && !options?.forceRefresh && cached && cached.expiresAt > now) {
89
- return cached.result;
90
- }
91
-
92
- if (useCache) {
93
- const active = activeFetches.get(cacheKey);
94
- if (active) return active;
95
- }
96
-
97
- const fetchPromise = loadMarketHeatmap(universe, count, sources)
98
- .then((result) => {
99
- if (useCache) {
100
- memoryCache.set(cacheKey, { result, expiresAt: Date.now() + heatmapCheckIntervalMs(result) });
101
- }
102
- return result;
103
- })
104
- .finally(() => {
105
- if (activeFetches.get(cacheKey) === fetchPromise) {
106
- activeFetches.delete(cacheKey);
107
- }
108
- });
109
-
110
- if (useCache) activeFetches.set(cacheKey, fetchPromise);
111
- return fetchPromise;
84
+ const load = () => loadMarketHeatmap(universe, count, sources);
85
+ // A board asked for uncached, or through another client, neither reads nor fills the shared cache.
86
+ if (options?.cache === false || sources?.client) return load();
87
+ return boards.load(`${universe}:${count}`, load, { force: options?.forceRefresh });
112
88
  }
113
89
 
114
90
  export function resetMarketHeatmapCache(): void {
115
- activeFetches.clear();
116
- memoryCache.clear();
91
+ boards.clear();
117
92
  }
@@ -24,6 +24,7 @@ import {
24
24
  type PreferredMoversOptions,
25
25
  type TrendingSymbol,
26
26
  } from "./screener";
27
+ import { meetsMinFloat } from "./min-float";
27
28
 
28
29
  export interface MarketMoverTabResult extends MarketMoversResult {
29
30
  tab: ScreenerTabId;
@@ -100,10 +101,14 @@ export function marketMoverQuoteError(result: Pick<MarketMoverTabResult, "unavai
100
101
  export async function loadMarketMoverTab(
101
102
  tab: ScreenerTabId,
102
103
  provider: DataProvider | null,
103
- options?: Pick<PreferredMoversOptions, "forceRefresh" | "metadataWaitMs"> & { basis?: MoverRankBasis },
104
+ options?: Pick<PreferredMoversOptions, "forceRefresh" | "metadataWaitMs"> & {
105
+ basis?: MoverRankBasis;
106
+ /** Keep the rows with a float of at least this many shares. Trending carries no float and ignores it. */
107
+ minFloat?: number | null;
108
+ },
104
109
  dependencies: MarketMoverTabDependencies = defaultDependencies,
105
110
  ): Promise<MarketMoverTabResult> {
106
- const { basis, ...fetchOptions } = options ?? {};
111
+ const { basis, minFloat = null, ...fetchOptions } = options ?? {};
107
112
  if (tab === "trending") {
108
113
  const trending = await dependencies.fetchTrending(LIST_ROWS, fetchOptions);
109
114
  const hydrated = await hydrateTrending(trending.data, provider);
@@ -116,13 +121,15 @@ export async function loadMarketMoverTab(
116
121
  }
117
122
  const category = CATEGORY_MAP[tab];
118
123
  // The server picks the candidates on the basis the list ranks by, so after the close they are the session's movers.
119
- const result = await dependencies.fetchPreferred(category, category === "most_actives" ? LIST_ROWS : MOVER_CANDIDATES, {
124
+ // A minimum float drops rows, so Most Active then reads as many candidates as the price lists.
125
+ const result = await dependencies.fetchPreferred(category, category === "most_actives" && minFloat == null ? LIST_ROWS : MOVER_CANDIDATES, {
120
126
  ...fetchOptions,
121
127
  session: basis ?? "regular",
122
128
  });
123
129
  // Each row read as `ticker` reads it, then ranked by the move its Chg% shows.
124
130
  const { quotes, rankedBy } = rankMoverQuotes(category, result.quotes.map(moverRegularSession), basis);
125
- return { ...result, quotes: quotes.slice(0, LIST_ROWS), rankedBy, tab };
131
+ const kept = quotes.filter((quote) => meetsMinFloat(quote.floatShares, minFloat));
132
+ return { ...result, quotes: kept.slice(0, LIST_ROWS), rankedBy, tab };
126
133
  }
127
134
 
128
135
  /** The server refused the list for the account's plan (gaps, without Pro). */
@@ -136,12 +143,23 @@ export class SessionMoversAccessError extends Error {
136
143
  export async function loadSessionMovers(
137
144
  view: CloudSessionMoversCategory,
138
145
  side: CloudSessionMoversSide,
139
- options?: { forceRefresh?: boolean },
146
+ options?: { forceRefresh?: boolean; minFloat?: number | null },
140
147
  ): Promise<CloudSessionMoversPayload> {
141
- const response = await apiClient.getCloudMarketScreener(view, 25, options?.forceRefresh ? "refresh" : "cache-first", side);
148
+ const minFloat = options?.minFloat ?? null;
149
+ // A minimum float drops rows, so it reads the most the server lists.
150
+ const response = await apiClient.getCloudMarketScreener(view, minFloat == null ? LIST_ROWS : MOVER_CANDIDATES, options?.forceRefresh ? "refresh" : "cache-first", side);
142
151
  if (response.reasonCode === "PRO_REQUIRED") throw new SessionMoversAccessError();
143
152
  if ((response.status === "success" || response.status === "partial") && response.data) {
144
- return response.stale ? { ...response.data, stale: true } : response.data;
153
+ const payload = response.stale ? { ...response.data, stale: true } : response.data;
154
+ return minFloat == null ? payload : { ...payload, items: withMinFloat(payload.items, minFloat) };
145
155
  }
146
156
  throw new Error("Session movers temporarily unavailable");
147
157
  }
158
+
159
+ /** The rows with a float of at least `minFloat`, the first LIST_ROWS of them, ranked again from 1. */
160
+ export function withMinFloat<T extends { rank: number; floatShares: number | null }>(items: readonly T[], minFloat: number): T[] {
161
+ return items
162
+ .filter((item) => meetsMinFloat(item.floatShares, minFloat))
163
+ .slice(0, LIST_ROWS)
164
+ .map((item, index) => ({ ...item, rank: index + 1 }));
165
+ }
@@ -14,6 +14,7 @@ import { createRows, fiftyTwoWeekPositionPercent, formatMoverPrice, MOVER_RANKIN
14
14
  import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
15
15
  import type { HeadlessPaneColumn } from "../../../types/headless";
16
16
  import { isSessionTab, resolveSide } from "./session";
17
+ import { hasPartialFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "./min-float";
17
18
 
18
19
  const rowPrice = (value: unknown, row: Record<string, unknown>) => formatMoverPrice(typeof value === "number" ? value : null, typeof row.currency === "string" ? row.currency : "", moverReferencePrice(row as unknown as MarketMoverRow));
19
20
 
@@ -69,15 +70,19 @@ function sessionColumns(view: CloudSessionMoversCategory) {
69
70
  ];
70
71
  }
71
72
 
73
+ const FLOAT_COLUMN: HeadlessPaneColumn = { key: "floatShares", header: "Float", align: "right", format: compact };
74
+
72
75
  export interface MarketMoversHeadlessDependencies {
73
76
  load(
74
77
  args: HeadlessPaneLoadArgs,
75
78
  tab: ScreenerTabId,
76
79
  provider: DataProvider,
80
+ minFloat: number | null,
77
81
  ): Promise<MarketMoverTabResult>;
78
82
  loadSession(
79
83
  view: CloudSessionMoversCategory,
80
84
  side: CloudSessionMoversSide,
85
+ minFloat: number | null,
81
86
  ): Promise<CloudSessionMoversPayload>;
82
87
  }
83
88
 
@@ -89,8 +94,8 @@ export interface MarketMoversHeadlessDependencies {
89
94
  const ONE_SHOT_METADATA_WAIT_MS = 10_000;
90
95
 
91
96
  const defaultDependencies: MarketMoversHeadlessDependencies = {
92
- load: (_args, tab, provider) => loadMarketMoverTab(tab, provider, { metadataWaitMs: ONE_SHOT_METADATA_WAIT_MS }),
93
- loadSession: (view, side) => loadSessionMovers(view, side),
97
+ load: (_args, tab, provider, minFloat) => loadMarketMoverTab(tab, provider, { metadataWaitMs: ONE_SHOT_METADATA_WAIT_MS, minFloat }),
98
+ loadSession: (view, side, minFloat) => loadSessionMovers(view, side, { minFloat }),
94
99
  };
95
100
 
96
101
  export function createMarketMoversHeadless(
@@ -128,6 +133,14 @@ export function createMarketMoversHeadless(
128
133
  ],
129
134
  defaultValue: "up",
130
135
  pluginState: { pluginId: "market-overview", key: "sessionSide" },
136
+ }, {
137
+ key: "min-float",
138
+ description: "Keep the rows whose float (the one EQS serves) is at least this many shares, such as 10M or 500k; any lifts it. A row with no float is left out. Trending carries no float and ignores it.",
139
+ type: "string",
140
+ placeholder: "shares",
141
+ example: "--min-float 10M",
142
+ normalize: normalizeMinFloat,
143
+ pluginState: { pluginId: "market-overview", key: "minFloat" },
131
144
  }],
132
145
  columns: COLUMNS,
133
146
  describe: (args) => isSessionTab(String(args.options.list))
@@ -135,21 +148,26 @@ export function createMarketMoversHeadless(
135
148
  : `Market Movers | ${String(args.options.list)}`,
136
149
  async load(args, ctx) {
137
150
  const tab = args.options.list as TabId;
151
+ const minFloatValue = typeof args.options["min-float"] === "string" ? args.options["min-float"] : null;
152
+ const minFloat = minFloatShares(minFloatValue);
138
153
  if (isSessionTab(tab)) {
139
154
  const side = resolveSide(tab, String(args.options.side));
140
- const payload = await dependencies.loadSession(tab, side);
155
+ const payload = await dependencies.loadSession(tab, side, minFloat);
141
156
  return {
142
157
  columns: sessionColumns(tab),
143
158
  rows: payload.items.map((item) => ({
144
159
  ...item,
145
160
  events: item.catalysts.map((catalyst) => CATALYST_LABEL[catalyst] ?? catalyst).join(" "),
146
161
  })),
147
- metadata: { list: tab, side, session: payload.session, phase: payload.phase, asOf: payload.asOf, stale: payload.stale === true },
162
+ ...(hasPartialFloat(payload.items) ? { notes: [PARTIAL_FLOAT_NOTE] } : {}),
163
+ metadata: { list: tab, side, session: payload.session, phase: payload.phase, asOf: payload.asOf, stale: payload.stale === true, minFloat },
148
164
  // A Pro account's list is the live board; the free one says it is delayed.
149
165
  ...(payload.access === "delayed" ? {} : { freshness: { status: "live" as const } }),
150
166
  };
151
167
  }
152
- const result = await dependencies.load(args, tab, ctx.marketData);
168
+ // Trending carries no float, so a minimum does not apply to it.
169
+ const floatFiltered = minFloat != null && tab !== "trending";
170
+ const result = await dependencies.load(args, tab, ctx.marketData, floatFiltered ? minFloat : null);
153
171
  const ranked = createRows(result.quotes);
154
172
  // Last and Change % are the regular session; an extended print is a column of its own.
155
173
  const extendedSessions = moverExtendedSessions(ranked);
@@ -168,16 +186,18 @@ export function createMarketMoversHeadless(
168
186
  // Beside an extended column, say which move the order follows.
169
187
  const rankedBy = result.rankedBy ?? null;
170
188
  const quoteError = marketMoverQuoteError(result);
189
+ const columns = [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5), ...(floatFiltered ? [FLOAT_COLUMN] : [])];
190
+ const notes = [
191
+ ...(rankedBy && rankedBy !== "volume" && extendedSessions.length > 0 ? [`Ranked by ${MOVER_RANKING_LABELS[rankedBy]}.`] : []),
192
+ ...(minFloat != null && !floatFiltered ? ["Trending carries no float, so --min-float does not apply to it."] : []),
193
+ ...(floatFiltered && hasPartialFloat(rows) ? [PARTIAL_FLOAT_NOTE] : []),
194
+ ];
171
195
  return {
172
- ...(extendedSessions.length > 0
173
- ? { columns: [...COLUMNS.slice(0, 5), ...extendedSessions.map(extendedColumn), ...COLUMNS.slice(5)] }
174
- : {}),
196
+ ...(columns.length !== COLUMNS.length ? { columns } : {}),
175
197
  rows,
176
198
  ...(quoteError ? { complete: false, unavailableSymbols: result.unavailableSymbols, errors: [quoteError] } : {}),
177
- ...(rankedBy && rankedBy !== "volume" && extendedSessions.length > 0
178
- ? { notes: [`Ranked by ${MOVER_RANKING_LABELS[rankedBy]}.`] }
179
- : {}),
180
- metadata: { list: tab, source: result.source, stale: result.stale, rankedBy },
199
+ ...(notes.length > 0 ? { notes } : {}),
200
+ metadata: { list: tab, source: result.source, stale: result.stale, rankedBy, minFloat: floatFiltered ? minFloat : null },
181
201
  // A Pro account's Gainers, Losers and Most Active are the real-time lists.
182
202
  ...(result.source === "cloud" ? { freshness: { status: "live" as const } } : {}),
183
203
  };
@@ -1,7 +1,7 @@
1
1
  import { Box, useUiCapabilities } from "../../../ui";
2
2
  import { nextHeaderSort } from "../../../utils/sort-values";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { DataTableView, EmptyState, usePaneFooter, usePaneTabs, type DataTableKeyEvent } from "../../../components";
4
+ import { DataTableView, EmptyState, QueryBar, usePaneFooter, usePaneTabs, type DataTableKeyEvent } from "../../../components";
5
5
  import { handleRefreshKey } from "../../../components/data-table/table-pane";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
7
  import type { PluginModule } from "../plugin-module";
@@ -41,6 +41,7 @@ import { loadMarketMoverTab, marketMoverQuoteError } from "./client";
41
41
  import { summaryFooterSegments } from "./footer";
42
42
  import { isSessionTab, resolveActiveTab, usSessionAt, type UsSession } from "./session";
43
43
  import { SessionMoversBody } from "./session-body";
44
+ import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
44
45
  import { marketMoversHeadless } from "./headless";
45
46
  import { buildMarketMoverColumns, renderMarketMoverCell } from "./table";
46
47
  import type { ExtendedSession } from "../../../market-data/market/status";
@@ -152,16 +153,21 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
152
153
  }) {
153
154
  const dataProvider = useAssetData();
154
155
  const { pinTicker } = usePluginTickerActions();
156
+ const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
157
+ // Trending carries no float, so the minimum is not offered there.
158
+ const floatFiltered = activeTab !== "trending";
159
+ const minFloat = floatFiltered ? minFloatShares(minFloatValue) : null;
160
+ const listKey = `${activeTab}:${minFloat ?? ""}`;
155
161
  const [quotes, setQuotes] = useState<MoverQuote[]>([]);
156
- const [loadedTab, setLoadedTab] = useState<TabId | null>(null);
157
- const visibleQuotes = loadedTab === activeTab ? quotes : NO_QUOTES;
162
+ const [loadedKey, setLoadedKey] = useState<string | null>(null);
163
+ const visibleQuotes = loadedKey === listKey ? quotes : NO_QUOTES;
158
164
  // The first load starts before the effect runs; an empty board is not "no data".
159
165
  const [loading, setLoading] = useState(true);
160
166
  const [loadError, setLoadError] = useState<string | null>(null);
161
167
  const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
162
168
  const [sortPreference, setSortPreference] = useState<MarketMoverSortPreference>(DEFAULT_SORT_PREFERENCE);
163
169
  const [moversStale, setMoversStale] = useState(false);
164
- const [lastSuccess, setLastSuccess] = useState<{ tab: ScreenerTabId; at: number } | null>(null);
170
+ const [lastSuccess, setLastSuccess] = useState<{ key: string; at: number } | null>(null);
165
171
 
166
172
  const fetchGenRef = useRef(0);
167
173
 
@@ -192,8 +198,8 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
192
198
  // A pre-market or after-hours column only while a row has such a print.
193
199
  const extendedSessionsKey = moverExtendedSessions(rankedRows).join(",");
194
200
  const columns = useMemo(
195
- () => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
196
- [extendedSessionsKey, width],
201
+ () => buildMarketMoverColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], { float: minFloat != null }),
202
+ [extendedSessionsKey, minFloat, width],
197
203
  );
198
204
  const rows = useMemo(() => sortRows(rankedRows, sortPreference), [rankedRows, sortPreference]);
199
205
  const selectedIdx = selectedSymbol
@@ -223,6 +229,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
223
229
  try {
224
230
  const result = await loadMarketMoverTab(tab, dataProvider, {
225
231
  forceRefresh: options?.forceRefresh,
232
+ minFloat,
226
233
  });
227
234
  if (fetchGenRef.current !== gen) return;
228
235
 
@@ -230,12 +237,12 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
230
237
  // A partial list shows only its current rows; a total failure keeps this list's prior rows.
231
238
  if (!quoteError || result.quotes.length > 0) {
232
239
  setQuotes(result.quotes);
233
- setLoadedTab(tab);
240
+ setLoadedKey(`${tab}:${minFloat ?? ""}`);
234
241
  if (!options?.background) setSelectedSymbol(null);
235
242
  }
236
243
  setMoversStale(result.stale);
237
244
  setLoadError(quoteError);
238
- if (!quoteError && !result.stale) setLastSuccess({ tab, at: Date.now() });
245
+ if (!quoteError && !result.stale) setLastSuccess({ key: `${tab}:${minFloat ?? ""}`, at: Date.now() });
239
246
  } catch {
240
247
  if (fetchGenRef.current !== gen) return;
241
248
  setMoversStale(true);
@@ -244,7 +251,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
244
251
  finally {
245
252
  if (fetchGenRef.current === gen && !options?.background) setLoading(false);
246
253
  }
247
- }, [dataProvider]);
254
+ }, [dataProvider, minFloat]);
248
255
 
249
256
  useEffect(() => {
250
257
  setSelectedSymbol(null);
@@ -254,7 +261,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
254
261
  const backgroundRefresh = useCallback(() => {
255
262
  void loadTab(activeTab, { background: true });
256
263
  }, [activeTab, loadTab]);
257
- useAutoRefresh(lastSuccess?.tab === activeTab ? lastSuccess.at : null, backgroundRefresh);
264
+ useAutoRefresh(lastSuccess?.key === listKey ? lastSuccess.at : null, backgroundRefresh);
258
265
 
259
266
  const openSymbol = useCallback((row: MarketMoverRow) => {
260
267
  pinTicker(moverKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
@@ -281,16 +288,21 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
281
288
  id: "feed",
282
289
  parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
283
290
  }] : []),
284
- ...(moversStale && loadedTab === activeTab ? [{
291
+ ...(moversStale && loadedKey === listKey ? [{
285
292
  id: "stale",
286
293
  parts: [{ text: "stale", tone: "muted" as const }],
287
294
  }] : []),
295
+ // The FLOAT column shows while a minimum is set; a float the source does not date says so once.
296
+ ...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
288
297
  ],
289
- }), [activeTab, feedStatus, loadedTab, loadError, loading, moversStale, summaryQuotes]);
298
+ }), [columns, feedStatus, listKey, loadedKey, loadError, loading, moversStale, rows, summaryQuotes]);
290
299
 
291
300
  return (
292
301
  <DataTableView<MarketMoverRow, MarketMoverColumn>
293
302
  focused={focused}
303
+ rootBefore={floatFiltered ? (
304
+ <QueryBar width={Math.max(1, width - 2)} filters={[minFloatFilter(minFloatValue, setMinFloatValue)]} />
305
+ ) : undefined}
294
306
  selection={{
295
307
  kind: "id",
296
308
  selectedId: selectedSymbol,
@@ -298,7 +310,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
298
310
  onChange: (symbol) => setSelectedSymbol(symbol),
299
311
  }}
300
312
  onRootKeyDown={handleTableKeyDown}
301
- resetScrollKey={activeTab}
313
+ resetScrollKey={listKey}
302
314
  sortable
303
315
  columns={columns}
304
316
  items={rows}
@@ -309,7 +321,7 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
309
321
  onActivate={openSymbol}
310
322
  renderCell={renderCell}
311
323
  selectedTextOverridesCellColor
312
- emptyStateTitle={loading ? "Loading movers..." : loadError ?? "No movers returned."}
324
+ emptyStateTitle={loading ? "Loading movers..." : loadError ?? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers returned.")}
313
325
  emptyContent={loadError ? (
314
326
  <Box paddingX={1} paddingY={1}>
315
327
  <EmptyState title={loadError} message="Try again in a moment." />