gloomberb 0.18.4 → 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 (600) 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 +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  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 +31 -6
  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/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  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/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -1,5 +1,5 @@
1
1
  import type { PaneFooterSegment } from "../../../components";
2
- import { priceColor } from "../../../theme/colors";
2
+ import { shownPriceColor } from "../../../theme/colors";
3
3
  import { formatPercentRaw } from "../../../utils/format";
4
4
  import { INDEX_SHORT } from "./model";
5
5
  import type { MarketSummaryQuote } from "./screener";
@@ -12,7 +12,7 @@ export function summaryFooterSegments(summaryQuotes: readonly MarketSummaryQuote
12
12
  id: `summary:${idx.symbol}`,
13
13
  parts: [
14
14
  { text: short, tone: "label" as const },
15
- { text: formatPercentRaw(idx.changePercent), tone: "value" as const, color: priceColor(idx.changePercent), bold: true },
15
+ { text: formatPercentRaw(idx.changePercent), tone: "value" as const, color: shownPriceColor(idx.changePercent, 2), bold: true },
16
16
  ],
17
17
  };
18
18
  });
@@ -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." />
@@ -0,0 +1,74 @@
1
+ import type { PaneFooterSegment } from "../../../components";
2
+ import type { QueryBarSelectFilter } from "../../../components/ui/query-bar";
3
+ import { formatCompact } from "../../../utils/format";
4
+
5
+ /** No minimum: every row, with or without a float. */
6
+ export const ANY_FLOAT = "any";
7
+ /** The minimums the pane's menu offers; `--min-float` takes any other. */
8
+ const PRESETS = ["1M", "5M", "10M", "20M", "50M", "100M"] as const;
9
+ const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9 };
10
+
11
+ /** Shares in `text` (10M, 500k, 2.5M, 25000000), or null when it is not a share count. */
12
+ function parseShares(text: string): number | null {
13
+ const match = /^((?:\d+(?:\.\d*)?|\.\d+))([kmb])?$/i.exec(text.trim().replace(/[_,]/g, ""));
14
+ if (!match) return null;
15
+ const value = Number(match[1]) * (match[2] ? SCALE[match[2].toLowerCase()]! : 1);
16
+ return Number.isFinite(value) ? value : null;
17
+ }
18
+
19
+ /** The minimum a saved or typed value sets, in shares; null for none (`any`, 0 or a value that is not a count). */
20
+ export function minFloatShares(value: string | null | undefined): number | null {
21
+ if (!value || value.trim().toLowerCase() === ANY_FLOAT) return null;
22
+ const shares = parseShares(value);
23
+ return shares != null && shares > 0 ? shares : null;
24
+ }
25
+
26
+ /** `--min-float` as the pane keeps it: `10M` for 10m or 10000000, `any` for none. Throws on a value that is not a share count. */
27
+ export function normalizeMinFloat(value: string): string {
28
+ if (value.trim().toLowerCase() === ANY_FLOAT) return ANY_FLOAT;
29
+ const shares = parseShares(value);
30
+ if (shares == null) throw new Error(`--min-float takes a share count such as 10M or 500k, or any; got "${value}".`);
31
+ return shares > 0 ? formatCompact(shares) : ANY_FLOAT;
32
+ }
33
+
34
+ /** A row passes with a float at or above the minimum; a row whose float is not known does not. */
35
+ export function meetsMinFloat(floatShares: number | null | undefined, minimum: number | null): boolean {
36
+ return minimum == null || (floatShares != null && floatShares >= minimum);
37
+ }
38
+
39
+ /** The pane's Min float menu: Any and the presets, with a minimum set from the CLI kept as its own choice. */
40
+ export function minFloatFilter(value: string, onChange: (value: string) => void): QueryBarSelectFilter {
41
+ const current = minFloatShares(value) == null ? ANY_FLOAT : value;
42
+ const presets: string[] = [...PRESETS];
43
+ const options = [ANY_FLOAT, ...(presets.includes(current) || current === ANY_FLOAT ? [] : [current]), ...presets]
44
+ .map((option) => ({ value: option, label: option === ANY_FLOAT ? "Any" : option }));
45
+ return {
46
+ id: "min-float",
47
+ label: "Min float",
48
+ title: "Minimum float",
49
+ value: current,
50
+ defaultValue: ANY_FLOAT,
51
+ options,
52
+ onChange,
53
+ };
54
+ }
55
+
56
+ /** What a list says under its Float column while a shown float is partial. */
57
+ export const PARTIAL_FLOAT_NOTE = "Float is partial: the source does not say when it measured it.";
58
+
59
+ /** Whether any row's float is partial, which the list then says once. */
60
+ export function hasPartialFloat(rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): boolean {
61
+ return rows.some((row) => row.floatShares != null && row.floatSharesState === "partial");
62
+ }
63
+
64
+ /** The footer's word for a FLOAT column of partial floats, or none. */
65
+ export function partialFloatFooter(shown: boolean, rows: readonly { floatShares?: number | null; floatSharesState?: string }[]): PaneFooterSegment[] {
66
+ return shown && hasPartialFloat(rows)
67
+ ? [{ id: "float-partial", parts: [{ text: "FLOAT", tone: "label" }, { text: "partial, undated", tone: "muted" }] }]
68
+ : [];
69
+ }
70
+
71
+ /** The empty list's title while a minimum hides every row. */
72
+ export function minFloatEmptyTitle(value: string): string {
73
+ return `No movers with a float of ${value} or more.`;
74
+ }
@@ -50,7 +50,8 @@ type MarketMoverColumnId =
50
50
  | "volume"
51
51
  | "volumeRatio"
52
52
  | "range"
53
- | "marketCap";
53
+ | "marketCap"
54
+ | "floatShares";
54
55
  export type MarketMoverColumn = DataTableColumn & { id: MarketMoverColumnId };
55
56
  /**
56
57
  * A list row. Price and change are the regular session's, as `ticker` reads
@@ -123,6 +124,8 @@ function getSortValue(
123
124
  return fiftyTwoWeekPositionPercent(row.price, row.fiftyTwoWeekLow, row.fiftyTwoWeekHigh);
124
125
  case "marketCap":
125
126
  return row.marketCap ?? null;
127
+ case "floatShares":
128
+ return row.floatShares ?? null;
126
129
  }
127
130
  }
128
131
 
@@ -220,6 +220,17 @@ function parseScreenerSessions(row: Record<string, unknown>): CloudMoverSessionF
220
220
  return sessions;
221
221
  }
222
222
 
223
+ /** A positive float with its state, or nothing: a row without one fails any minimum. */
224
+ function floatOf(source: { floatShares?: unknown; floatSharesState?: unknown; floatSharesReason?: unknown } | null | undefined): Pick<ScreenerQuote, "floatShares" | "floatSharesState" | "floatSharesReason"> {
225
+ const shares = screenerNumber(source?.floatShares);
226
+ if (shares == null || !(shares > 0)) return {};
227
+ return {
228
+ floatShares: shares,
229
+ ...(source?.floatSharesState === "partial" ? { floatSharesState: "partial" as const } : {}),
230
+ ...(source?.floatSharesState === "partial" && typeof source.floatSharesReason === "string" ? { floatSharesReason: source.floatSharesReason } : {}),
231
+ };
232
+ }
233
+
223
234
  /** Validate numeric fields at the API boundary so absent values stay unavailable. */
224
235
  export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
225
236
  if (!data || typeof data !== "object" || !("quotes" in data) || !Array.isArray(data.quotes)) {
@@ -239,6 +250,7 @@ export function parseScreenerResponse(data: unknown): ScreenerQuote[] {
239
250
  previousClose: screenerNumber(q.previousClose) ?? undefined, exchange: typeof q.exchange === "string" ? q.exchange : "",
240
251
  lastUpdated: typeof q.lastUpdated === "number" && Number.isFinite(q.lastUpdated) && q.lastUpdated > 0 ? q.lastUpdated : undefined,
241
252
  sessions: parseScreenerSessions(q),
253
+ ...floatOf(q),
242
254
  }];
243
255
  });
244
256
  }
@@ -325,6 +337,8 @@ function mergeCloudScreenerItem(
325
337
  lastUpdated: item.lastUpdated,
326
338
  previousClose: screenerNumber(item.previousClose) ?? undefined,
327
339
  sessions: parseScreenerSessions(item as unknown as Record<string, unknown>),
340
+ // The live list's own float, the delayed list's when it sends none, each with its own state.
341
+ ...floatOf(item.floatShares != null ? item : metadata),
328
342
  };
329
343
  }
330
344
 
@@ -20,6 +20,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
20
20
  import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
21
21
  import { ProWall, SignInWall } from "../cloud/auth-actions";
22
22
  import { loadSessionMovers, SessionMoversAccessError } from "./client";
23
+ import { ANY_FLOAT, minFloatEmptyTitle, minFloatFilter, minFloatShares, partialFloatFooter } from "./min-float";
23
24
  import { summaryFooterSegments } from "./footer";
24
25
  import type { MarketSummaryQuote } from "./screener";
25
26
  import {
@@ -104,7 +105,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
104
105
  const { pinTicker } = usePluginTickerActions();
105
106
  const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
106
107
  const side = resolveSide(view, savedSide);
107
- const listKey = `${view}:${side}`;
108
+ const [minFloatValue, setMinFloatValue] = usePluginPaneState<string>("minFloat", ANY_FLOAT);
109
+ const minFloat = minFloatShares(minFloatValue);
110
+ const listKey = `${view}:${side}:${minFloat ?? ""}`;
108
111
  const [loaded, setLoaded] = useState<LoadedList | null>(null);
109
112
  // The first frame would otherwise claim an empty list.
110
113
  const [loading, setLoading] = useState(true);
@@ -132,9 +135,9 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
132
135
  setLoadError(null);
133
136
  }
134
137
  try {
135
- const next = await loadSessionMovers(view, side, options);
138
+ const next = await loadSessionMovers(view, side, { ...options, minFloat });
136
139
  if (fetchGenRef.current !== gen) return;
137
- setLoaded({ key: `${view}:${side}`, payload: next });
140
+ setLoaded({ key: `${view}:${side}:${minFloat ?? ""}`, payload: next });
138
141
  setLoadError(null);
139
142
  setLastLoadedAt(Date.now());
140
143
  } catch (error) {
@@ -147,7 +150,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
147
150
  } finally {
148
151
  if (fetchGenRef.current === gen && !options?.background) setLoading(false);
149
152
  }
150
- }, [onDenied, side, view]);
153
+ }, [minFloat, onDenied, side, view]);
151
154
 
152
155
  useEffect(() => {
153
156
  setSelectedId(null);
@@ -167,7 +170,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
167
170
  freshnessScopeKey: `market-movers:${listKey}`,
168
171
  liveStreaming,
169
172
  });
170
- const columns = useMemo(() => buildSessionMoverColumns(view, width), [view, width]);
173
+ const columns = useMemo(() => buildSessionMoverColumns(view, width, { float: minFloat != null }), [minFloat, view, width]);
171
174
  // The sort survives a change of list, but not onto a column this list or width does not show.
172
175
  const sort = columns.some((column) => column.id === sortPreference.columnId) ? sortPreference : DEFAULT_SORT;
173
176
  const rows = useMemo(
@@ -199,8 +202,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
199
202
  parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
200
203
  }] : []),
201
204
  ...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
205
+ // A float the source does not date says so once, while the FLOAT column shows.
206
+ ...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
202
207
  ],
203
- }), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
208
+ }), [asOfLabel, columns, feedStatus, loadError, loading, payload?.stale, rows, sessionLabel, summaryQuotes]);
204
209
 
205
210
  const openRow = useCallback((row: SessionMoverRow) => {
206
211
  pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
@@ -217,7 +222,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
217
222
  const sides = sessionSides(view);
218
223
  // Until this list's first answer the table is loading, including the frame
219
224
  // between a change of list and its request going out.
220
- const emptyTitle = payload ? "No movers yet." : loadError ?? "Loading movers...";
225
+ const emptyTitle = payload ? (minFloat != null ? minFloatEmptyTitle(minFloatValue) : "No movers yet.") : loadError ?? "Loading movers...";
221
226
  return (
222
227
  <DataTableView<SessionMoverRow, SessionMoverColumn>
223
228
  focused={focused}
@@ -231,7 +236,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
231
236
  value: side,
232
237
  options: sides,
233
238
  onChange: (value: CloudSessionMoversSide) => setSavedSide(value),
234
- }]}
239
+ }, minFloatFilter(minFloatValue, setMinFloatValue)]}
235
240
  />
236
241
  )}
237
242
  selection={{ kind: "id", selectedId, getId: rowKey, onChange: setSelectedId }}
@@ -21,9 +21,10 @@ const VOLUME_LABEL: Record<CloudSessionMoversCategory, string> = {
21
21
  * Pre-market and after hours: last, change from the reference close, session
22
22
  * volume, relative volume, float and the catalyst. Gaps add the gap and the
23
23
  * distance from VWAP. The name gives way first, then the columns a narrow pane
24
- * can do without (the day's volume, then the float).
24
+ * can do without (the day's volume, then the float, which stays while a
25
+ * minimum float is set: `float`).
25
26
  */
26
- export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number): SessionMoverColumn[] {
27
+ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width: number, options: { float?: boolean } = {}): SessionMoverColumn[] {
27
28
  const columns: SessionMoverColumn[] = [
28
29
  { id: "rank", label: "#", width: 3, align: "left" },
29
30
  { id: "symbol", label: "TICKER", width: 8, align: "left" },
@@ -37,7 +38,7 @@ export function buildSessionMoverColumns(view: CloudSessionMoversCategory, width
37
38
  { id: "floatShares", label: "FLOAT", width: 7, align: "right" },
38
39
  { id: "catalyst", label: "EVENT", width: 6, align: "left" },
39
40
  ];
40
- const optional: SessionMoverColumnId[] = view === "gaps" ? ["volume", "floatShares"] : ["floatShares"];
41
+ const optional: SessionMoverColumnId[] = [...(view === "gaps" ? ["volume" as const] : []), ...(options.float ? [] : ["floatShares" as const])];
41
42
  // Measured the way the table draws it: a header widens its column, and a
42
43
  // left-aligned column after a right-aligned one gets a lead gap. The name
43
44
  // sits between left-aligned columns, so it adds only its width and a gutter.
@@ -26,15 +26,20 @@ function fiftyTwoWeekPosition(price: number | null, low: number | undefined, hig
26
26
  }
27
27
 
28
28
  const EXTENDED_COLUMN_WIDTH = 9;
29
- /** With an extended column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
29
+ /** With an extended or float column, a narrow pane drops MCAP, then 52W%, before the name gets narrower than this. */
30
30
  const EXTENDED_MIN_NAME_WIDTH = 12;
31
31
 
32
32
  /**
33
33
  * The list's columns at `width`. A pre-market or after-hours column follows
34
- * CHG% for each session in `extendedSessions`; to make room for it a narrow
35
- * pane gives up MCAP, then 52W%, before it cuts the names short.
34
+ * CHG% for each session in `extendedSessions`, and FLOAT ends the row while a
35
+ * minimum float is set (`float`); to make room for them a narrow pane gives up
36
+ * MCAP, then 52W%, before it cuts the names short.
36
37
  */
37
- export function buildMarketMoverColumns(width: number, extendedSessions: readonly ExtendedSession[] = []): MarketMoverColumn[] {
38
+ export function buildMarketMoverColumns(
39
+ width: number,
40
+ extendedSessions: readonly ExtendedSession[] = [],
41
+ options: { float?: boolean } = {},
42
+ ): MarketMoverColumn[] {
38
43
  const lead: MarketMoverColumn[] = [
39
44
  { id: "rank", label: "#", width: 3, align: "left" },
40
45
  { id: "symbol", label: "TICKER", width: 8, align: "left" },
@@ -55,12 +60,13 @@ export function buildMarketMoverColumns(width: number, extendedSessions: readonl
55
60
  { id: "volumeRatio", label: "V/AVG", width: 6, align: "right" },
56
61
  { id: "range", label: "52W%", width: 6, align: "right" },
57
62
  { id: "marketCap", label: "MCAP", width: 8, align: "right" },
63
+ ...(options.float ? [{ id: "floatShares" as const, label: "FLOAT", width: 7, align: "right" as const }] : []),
58
64
  ];
59
65
  // Measured the way the table draws them (a header and its sort mark widen a
60
66
  // column, gaps between them); the name adds its own width and a gap.
61
67
  const nameRoom = () => width - getTableWidth([...lead, ...quote, ...trailing]) - 1;
62
68
  for (const id of ["marketCap", "range"] as const) {
63
- if (extendedSessions.length === 0 || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
69
+ if ((extendedSessions.length === 0 && !options.float) || nameRoom() >= EXTENDED_MIN_NAME_WIDTH) break;
64
70
  trailing.splice(trailing.findIndex((column) => column.id === id), 1);
65
71
  }
66
72
  return [...lead, { id: "name", label: "NAME", width: Math.max(6, nameRoom()), align: "left" }, ...quote, ...trailing];
@@ -121,5 +127,11 @@ export function renderMarketMoverCell(
121
127
  value: row.marketCap,
122
128
  color: colors.textDim,
123
129
  };
130
+ case "floatShares":
131
+ return {
132
+ text: row.floatShares != null ? formatCompact(row.floatShares, { fixedDecimals: true }) : "—",
133
+ value: row.floatShares ?? null,
134
+ color: colors.textDim,
135
+ };
124
136
  }
125
137
  }
@@ -0,0 +1,82 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import type { BreadthDay } from "../../../api-client/members";
3
+ import { ChartTableHeader, DataTableView, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, type DataTableColumn } from "../../../components";
4
+ import { staticSeries } from "../../../components/chart/static/series";
5
+ import { fitChartTableColumns } from "../../../components/chart-table/layout";
6
+ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
7
+ import { useAsyncResource, useAutoRefresh, usePaneVisible, usePluginPaneState } from "../../../public/react";
8
+ import { useThemeColors } from "../../../theme/theme-context";
9
+ import type { PaneProps } from "../../../types/plugin";
10
+ import { cachedBreadth, loadBreadth } from "./client";
11
+ import { useLiveSessionRefresh } from "../shared/volatility/live-session";
12
+ import { decimal } from "./model";
13
+
14
+ const percent = (value: number | null) => value == null ? "--" : `${value.toFixed(1)}%`;
15
+ const CHARTS = [{ value: "adLine", label: "A/D line" }, { value: "ma200", label: "Above 200D" }, { value: "ma50", label: "Above 50D" }, { value: "ma20", label: "Above 20D" }];
16
+ type Metric = "adLine" | "ma200" | "ma50" | "ma20";
17
+ const dateOf = (row: BreadthDay) => new Date(`${row.date}T00:00:00Z`);
18
+ const keyOf = (row: BreadthDay) => row.date;
19
+ const valueOf = (row: BreadthDay, metric: Metric) => metric === "adLine" ? row.adLine : row[metric].percent;
20
+ const COLUMNS: DataTableColumn[] = [
21
+ { id: "date", label: "Date", width: 12, align: "left" },
22
+ ...["advances", "declines", "unchanged", "ma20", "ma50", "ma200", "newHighs", "newLows", "coverage"].map((id, index) => ({
23
+ id, label: ["Adv", "Dec", "Unch", ">20D %", ">50D %", ">200D %", "52W high", "52W low", "A/D covered"][index]!, width: index === 8 ? 13 : 10, flexGrow: 1, align: "right" as const,
24
+ })),
25
+ ];
26
+
27
+ export function Breadth({ fund, width, height, focused, active }: Pick<PaneProps, "width" | "height" | "focused"> & { fund: string; active: boolean }) {
28
+ const colors = useThemeColors();
29
+ const visible = usePaneVisible();
30
+ const resource = useAsyncResource(useCallback((force: boolean) => loadBreadth(fund, force), [fund]), { initialData: useCallback(() => cachedBreadth(fund), [fund]) });
31
+ const data = resource.data?.payload;
32
+ const latest = data?.latest;
33
+ const [metric, setMetric] = usePluginPaneState<Metric>("breadth:metric", "adLine");
34
+ const [selected, setSelected] = usePluginPaneState<string | null>(`breadth:selected:${fund}`, null);
35
+ const history = useMemo(() => data ? [...data.history, ...(data.basis === "intraday" && data.latest ? [data.latest] : [])] : [], [data]);
36
+ const rows = useMemo(() => [...history].reverse(), [history]);
37
+ const columns = useMemo(() => fitChartTableColumns(COLUMNS, width, ["unchanged", "ma20", "ma50", "coverage"]), [width]);
38
+ const series = useMemo(() => [staticSeries(history.map(row => ({ date: dateOf(row), observedAt: new Date(row.asOf), value: valueOf(row, metric) })), {
39
+ id: `breadth-${metric}`, label: metric === "adLine" ? "Cumulative advances − declines" : `% above ${metric.slice(2)}-day average`,
40
+ color: colors.warning, calendarSpaced: true,
41
+ })], [history, metric, colors.warning]);
42
+ const link = useChartTableSelection({ rows, getId: keyOf, getDate: dateOf, selectedId: selected ?? latest?.date ?? null, onSelect: setSelected, focused });
43
+ // The resource keeps a failed load as its error (stale, and a warning below), so this refresh never rejects.
44
+ const sessionOpen = useLiveSessionRefresh(() => resource.load(true), data?.access === "realtime" ? 15_000 : 60_000, active && visible, resource.updatedAt).live;
45
+ useAutoRefresh(resource.updatedAt, () => { if (active && visible && !sessionOpen) void resource.load(data?.basis === "intraday"); });
46
+ usePaneRefreshKey(() => void resource.reload(), { focused, enabled: active });
47
+ usePaneStatusFooter({ registrationId: "members:breadth", loading: !!data && resource.loading,
48
+ stale: !!data && (data.stale || resource.data!.stale || !!resource.error), info: latest ? [
49
+ { id: "as-of", asOf: true, parts: [{ text: `${data?.basis === "intraday" ? data.access === "realtime" ? "live" : "15m delayed" : "close"} ${latest.date}`, tone: "muted" }] },
50
+ { id: "coverage", parts: [{ text: `A/D ${latest.advanceDeclineEligible}/${latest.memberCount}`, tone: latest.advanceDeclineEligible < latest.memberCount * 0.8 ? "warning" : "muted" }] },
51
+ ] : [] });
52
+ usePaneNoticeFooter({ registrationId: "members:breadth-notices", focused, notices: [
53
+ ...(resource.error || resource.data?.refreshError ? [resource.error ?? resource.data!.refreshError!] : []),
54
+ ...(latest && [latest.advanceDeclineEligible, latest.ma20.eligible, latest.ma50.eligible, latest.ma200.eligible, latest.highLowEligible].some(n => n < latest.memberCount * 0.8)
55
+ ? [`Partial coverage: A/D ${latest.advanceDeclineEligible}, 20D ${latest.ma20.eligible}, 50D ${latest.ma50.eligible}, 200D ${latest.ma200.eligible}, 52W ${latest.highLowEligible} of ${latest.memberCount} members. Missing windows are excluded.`] : []),
56
+ ] });
57
+ return <PaneStatusBody subject="fund breadth" loading={!data && resource.loading} error={!data ? resource.error : null}
58
+ empty={!!data && !latest} emptyTitle="Breadth is awaiting daily price history.">
59
+ {data && latest && <DataTableView<BreadthDay> items={rows} columns={columns} rootWidth={width} rootHeight={height} focused={focused}
60
+ getItemKey={keyOf} selection={{ kind: "id", selectedId: selected ?? latest.date, getId: keyOf, onChange: id => setSelected(String(id)) }}
61
+ sortColumnId="date" sortDirection="desc" freezeFirstColumn emptyStateTitle="No daily breadth history."
62
+ renderCell={(row, column) => {
63
+ if (column.id === "date") return { text: row.date };
64
+ if (column.id === "coverage") return { text: `${row.advanceDeclineEligible}/${row.memberCount}` };
65
+ if (column.id === "ma20" || column.id === "ma50" || column.id === "ma200") return { text: percent(row[column.id].percent), value: row[column.id].percent };
66
+ const value = row[column.id as "advances"];
67
+ const eligible = column.id === "newHighs" || column.id === "newLows" ? row.highLowEligible : row.advanceDeclineEligible;
68
+ return { text: eligible ? decimal(value, 0) : "--", value: eligible ? value : null,
69
+ color: column.id === "advances" || column.id === "newHighs" ? colors.positive : column.id === "declines" || column.id === "newLows" ? colors.negative : colors.text };
70
+ }}
71
+ rootBefore={<ChartTableHeader width={width} height={height} tableRows={rows.length} tableColumns={columns}
72
+ query={<QueryBar width={width} meta="History uses current members (survivorship bias)." filters={[{ kind: "select", id: "chart", label: "History", value: metric, options: CHARTS, onChange: value => setMetric(value as Metric) }]} />}
73
+ figures={[
74
+ { label: "Adv / Dec", value: latest.advanceDeclineEligible ? `${latest.advances} / ${latest.declines}` : "--", detail: `${latest.unchanged} unchanged` },
75
+ ...([20, 50, 200] as const).map(window => ({ label: `Above ${window}D`, value: percent(latest[`ma${window}`].percent), detail: `${latest[`ma${window}`].eligible}/${latest.memberCount} covered` })),
76
+ { label: "52W high / low", value: latest.highLowEligible ? `${latest.newHighs} / ${latest.newLows}` : "--", detail: `${latest.highLowEligible}/${latest.memberCount} covered` },
77
+ ]}
78
+ chart={{ series, ...link, formatValue: metric === "adLine" ? value => decimal(value, 0) : percent,
79
+ formatAxisValue: metric === "adLine" ? value => decimal(value, 0) : percent, remoteKind: "member-breadth", empty: "History is awaiting sufficient daily bars." }} />}
80
+ />}
81
+ </PaneStatusBody>;
82
+ }