gloomberb 0.18.4 → 0.18.5

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 (282) 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 +32 -1
  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/data.ts +14 -0
  9. package/src/api-client/index.ts +160 -11
  10. package/src/api-client/live-scan.ts +91 -0
  11. package/src/api-client/members.ts +12 -0
  12. package/src/api-client/request.ts +222 -41
  13. package/src/api-client/research-activity.ts +3 -10
  14. package/src/api-client/types.ts +6 -0
  15. package/src/app/keybindings/index.ts +3 -0
  16. package/src/app/keybindings/labels.ts +18 -1
  17. package/src/app/keybindings/resolve.ts +14 -0
  18. package/src/app/runtime/broker-import.ts +43 -26
  19. package/src/architecture/core-builtin-allowlist.json +1 -1
  20. package/src/cli/commands/fred-series.ts +5 -0
  21. package/src/cli/commands/keybindings.ts +23 -0
  22. package/src/cli/commands/market.ts +68 -22
  23. package/src/cli/commands/options-chain.ts +59 -3
  24. package/src/cli/commands/overview.ts +119 -31
  25. package/src/cli/commands/search.ts +18 -2
  26. package/src/cli/commands/ticker.ts +2 -1
  27. package/src/cli/errors.ts +2 -2
  28. package/src/cli/export-source.ts +29 -1
  29. package/src/cli/glossary.ts +49 -0
  30. package/src/cli/history-rows.ts +30 -7
  31. package/src/cli/known-gaps.ts +5 -2
  32. package/src/cli/listing-arg.ts +16 -5
  33. package/src/cli/pane-functions/freshness.ts +25 -13
  34. package/src/cli/pane-functions/function-help.ts +10 -3
  35. package/src/cli/pane-functions/headless.ts +39 -5
  36. package/src/cli/pane-functions/index.ts +10 -3
  37. package/src/cli/pane-functions/market-session.ts +15 -5
  38. package/src/cli/pane-functions/report.ts +5 -1
  39. package/src/cli/pane-functions/resolver.ts +18 -2
  40. package/src/cli/quote-answer.ts +10 -4
  41. package/src/cli/report-tables.ts +18 -7
  42. package/src/cli/restore-plugins.ts +13 -1
  43. package/src/cli/result.ts +57 -5
  44. package/src/components/chart/composite/composite-chart.tsx +22 -4
  45. package/src/components/chart/composite/format.ts +27 -0
  46. package/src/components/chart/composite/levels.ts +2 -0
  47. package/src/components/chart/composite/panel-series.ts +1 -0
  48. package/src/components/chart/composite/panel-surface.tsx +9 -2
  49. package/src/components/chart/composite/range-statistics.ts +62 -0
  50. package/src/components/chart/composite/scene.ts +7 -3
  51. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  52. package/src/components/chart/composite/types.ts +4 -0
  53. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  54. package/src/components/command-bar/workflow/alert.ts +1 -1
  55. package/src/components/layout/header.tsx +26 -40
  56. package/src/components/layout/pane/footer/index.tsx +1 -0
  57. package/src/components/layout/pane/footer/model.ts +27 -0
  58. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  59. package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
  60. package/src/components/ui/display.tsx +5 -1
  61. package/src/core/state/app/layout.ts +2 -0
  62. package/src/core/state/app/state.ts +9 -0
  63. package/src/core/state/app/types.ts +3 -0
  64. package/src/data/saved-copies.ts +55 -16
  65. package/src/docs/snapshot.json +361 -84
  66. package/src/i18n/es.ts +1 -1
  67. package/src/i18n/ja.ts +1 -1
  68. package/src/i18n/ko.ts +1 -1
  69. package/src/i18n/zh-cn.ts +1 -1
  70. package/src/i18n/zh-tw.ts +1 -1
  71. package/src/market-data/coordinator/financials.ts +10 -2
  72. package/src/market-data/coordinator/index.ts +3 -1
  73. package/src/market-data/coordinator/quotes.ts +4 -2
  74. package/src/market-data/market/change-basis.ts +33 -5
  75. package/src/market-data/market/dated-previous-close.ts +203 -0
  76. package/src/market-data/market/futures-week.ts +68 -0
  77. package/src/market-data/market/status.ts +16 -4
  78. package/src/market-data/options-alternatives.ts +60 -0
  79. package/src/market-data/quotes/contributions.ts +20 -2
  80. package/src/market-data/quotes/resolution.ts +3 -0
  81. package/src/market-data/selectors.ts +3 -0
  82. package/src/notifications/notification-log.ts +6 -4
  83. package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
  84. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  85. package/src/plugins/builtin/alerts/cli.ts +59 -20
  86. package/src/plugins/builtin/alerts/command.ts +7 -0
  87. package/src/plugins/builtin/alerts/constants.ts +1 -1
  88. package/src/plugins/builtin/alerts/fired-log.ts +45 -4
  89. package/src/plugins/builtin/alerts/format.ts +8 -2
  90. package/src/plugins/builtin/alerts/headless.ts +10 -5
  91. package/src/plugins/builtin/alerts/history-rows.ts +5 -2
  92. package/src/plugins/builtin/alerts/history.ts +2 -0
  93. package/src/plugins/builtin/alerts/index.tsx +66 -16
  94. package/src/plugins/builtin/alerts/live.ts +3 -3
  95. package/src/plugins/builtin/alerts/metric-command.ts +61 -0
  96. package/src/plugins/builtin/alerts/metric-data.ts +70 -0
  97. package/src/plugins/builtin/alerts/metric-model.ts +175 -0
  98. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  99. package/src/plugins/builtin/alerts/pane.tsx +42 -29
  100. package/src/plugins/builtin/alerts/storage.ts +33 -10
  101. package/src/plugins/builtin/alerts/types.ts +3 -0
  102. package/src/plugins/builtin/backtest/headless.ts +10 -5
  103. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  104. package/src/plugins/builtin/backtest/presets.ts +29 -3
  105. package/src/plugins/builtin/broker-manager/index.tsx +3 -2
  106. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  107. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  108. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  109. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  110. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  111. package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
  112. package/src/plugins/builtin/catalysts/model.ts +2 -1
  113. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
  114. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  115. package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
  116. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
  117. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
  118. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  119. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  120. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  121. package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
  122. package/src/plugins/builtin/chart-composer/headless.ts +13 -2
  123. package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
  124. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  125. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  126. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  127. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  128. package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
  129. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  130. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  131. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  132. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  133. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  134. package/src/plugins/builtin/chat/index.tsx +23 -0
  135. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  136. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  137. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  138. package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
  139. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  140. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  141. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  142. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  143. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  144. package/src/plugins/builtin/econ/figures.ts +72 -0
  145. package/src/plugins/builtin/econ/index.tsx +33 -3
  146. package/src/plugins/builtin/econ/types.ts +2 -0
  147. package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
  148. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  149. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  150. package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
  151. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  152. package/src/plugins/builtin/exposure/model.ts +13 -1
  153. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  154. package/src/plugins/builtin/futures/index.tsx +1 -0
  155. package/src/plugins/builtin/futures/model.ts +2 -1
  156. package/src/plugins/builtin/futures/table.tsx +2 -2
  157. package/src/plugins/builtin/fx-matrix/client.ts +6 -1
  158. package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
  159. package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
  160. package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
  161. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  162. package/src/plugins/builtin/iv-history/headless.ts +3 -0
  163. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  164. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  165. package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
  166. package/src/plugins/builtin/members/client.ts +19 -4
  167. package/src/plugins/builtin/members/headless.ts +20 -7
  168. package/src/plugins/builtin/members/index.tsx +9 -9
  169. package/src/plugins/builtin/members/model.ts +23 -4
  170. package/src/plugins/builtin/members/pane.tsx +25 -15
  171. package/src/plugins/builtin/options/analytics.ts +26 -2
  172. package/src/plugins/builtin/options/model-spot.ts +5 -2
  173. package/src/plugins/builtin/options/strike-window.ts +2 -1
  174. package/src/plugins/builtin/options/table.ts +1 -7
  175. package/src/plugins/builtin/options/view.tsx +25 -8
  176. package/src/plugins/builtin/options-positioning/client.ts +19 -0
  177. package/src/plugins/builtin/options-positioning/headless.ts +54 -14
  178. package/src/plugins/builtin/options-positioning/index.tsx +3 -3
  179. package/src/plugins/builtin/options-positioning/model.ts +47 -8
  180. package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
  181. package/src/plugins/builtin/perps/freshness.ts +12 -10
  182. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  183. package/src/plugins/builtin/perps/pane.tsx +15 -11
  184. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
  185. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  186. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  187. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  188. package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
  189. package/src/plugins/builtin/scanner/feed.ts +2 -2
  190. package/src/plugins/builtin/scanner/index.ts +24 -1
  191. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  192. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  193. package/src/plugins/builtin/scanner/live-headless.ts +114 -0
  194. package/src/plugins/builtin/scanner/live-model.ts +351 -0
  195. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  196. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  197. package/src/plugins/builtin/series-inventory.ts +1 -0
  198. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  199. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  200. package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
  201. package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
  202. package/src/plugins/builtin/themes/headless.ts +6 -4
  203. package/src/plugins/builtin/themes/model.ts +1 -1
  204. package/src/plugins/builtin/themes/pane.tsx +3 -3
  205. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  206. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  207. package/src/plugins/builtin/vol-surface/client.ts +6 -4
  208. package/src/plugins/builtin/vol-surface/headless.ts +62 -13
  209. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  210. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  211. package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
  212. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  213. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  214. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  215. package/src/plugins/builtin/volatility/series.ts +26 -0
  216. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  217. package/src/plugins/catalog-browser.ts +13 -8
  218. package/src/portfolio/column-values.ts +5 -5
  219. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  220. package/src/renderers/browser/app-services.ts +1 -7
  221. package/src/renderers/browser/config-host.ts +2 -55
  222. package/src/renderers/browser/main.tsx +7 -57
  223. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  224. package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
  225. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  226. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  227. package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
  228. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  229. package/src/renderers/electrobun/bun/index.ts +25 -1
  230. package/src/renderers/electrobun/shared/app-name.ts +17 -0
  231. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  232. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  233. package/src/renderers/opentui/toast-host.tsx +71 -43
  234. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  235. package/src/sources/provider-router/batches.ts +10 -2
  236. package/src/sources/provider-router/financial-routes.ts +3 -3
  237. package/src/sources/provider-router/history.ts +28 -7
  238. package/src/sources/provider-router/index.ts +146 -10
  239. package/src/sources/provider-router/search.ts +13 -2
  240. package/src/state/app/context/index.tsx +3 -0
  241. package/src/sync/auth-transition.ts +19 -9
  242. package/src/sync/baseline.ts +10 -4
  243. package/src/sync/controller.ts +66 -16
  244. package/src/sync/react.ts +4 -1
  245. package/src/sync/types.ts +3 -0
  246. package/src/telemetry/usage-counts.ts +0 -18
  247. package/src/theme/colors.ts +8 -0
  248. package/src/tickers/company-name.ts +59 -0
  249. package/src/tickers/listing-venues.ts +23 -0
  250. package/src/tickers/search/bare-symbol.ts +36 -0
  251. package/src/tickers/search/index.ts +36 -7
  252. package/src/tickers/search/ranking.ts +10 -1
  253. package/src/tickers/search/types.ts +5 -0
  254. package/src/time-series/base-series.ts +13 -5
  255. package/src/time-series/daily-valuation.ts +62 -0
  256. package/src/time-series/field-catalog.ts +1 -3
  257. package/src/time-series/forward-valuation.ts +32 -0
  258. package/src/time-series/fundamentals.ts +155 -7
  259. package/src/time-series/hooks.ts +1 -0
  260. package/src/time-series/price-units.ts +72 -2
  261. package/src/time-series/resolve-context.ts +5 -1
  262. package/src/time-series/resolve.ts +61 -12
  263. package/src/time-series/series-presentation.ts +40 -6
  264. package/src/time-series/spec.ts +2 -1
  265. package/src/time-series/studies.ts +270 -72
  266. package/src/time-series/transforms.ts +1 -0
  267. package/src/time-series/types.ts +5 -0
  268. package/src/time-series/valuation-releases.ts +39 -0
  269. package/src/types/data-provider.ts +9 -1
  270. package/src/types/desktop-window.ts +2 -0
  271. package/src/types/financials.ts +41 -1
  272. package/src/types/headless.ts +42 -4
  273. package/src/types/instrument.ts +3 -0
  274. package/src/types/plugin.ts +1 -1
  275. package/src/updater/command-names.ts +12 -7
  276. package/src/utils/financial-statements.ts +6 -1
  277. package/src/utils/network-failure.ts +9 -6
  278. package/src/utils/option-expiry.ts +48 -1
  279. package/src/utils/options.ts +10 -0
  280. package/src/utils/plugin-proxy-hosts.json +1 -0
  281. package/src/utils/price-history.ts +5 -0
  282. package/src/renderers/browser/first-screen.ts +0 -193
package/README.md CHANGED
@@ -53,7 +53,7 @@ With [Bun](https://bun.sh), on any supported OS:
53
53
  bun install -g gloomberb
54
54
  ```
55
55
 
56
- Then run `gloom`. The command was called `gloomberb` before 0.19, and that name still works. For chart graphics, use a Kitty-compatible terminal such as Ghostty, Kitty, or WezTerm. Windows Terminal draws charts as text; the desktop app draws them in full.
56
+ Then run `gloom`. The command was called `gloomberb` before 0.18.4, and that name still works. For chart graphics, use a Kitty-compatible terminal such as Ghostty, Kitty, or WezTerm. Windows Terminal draws charts as text; the desktop app draws them in full.
57
57
 
58
58
  ## Start
59
59
 
package/README.zh-CN.md CHANGED
@@ -65,7 +65,7 @@ curl -fsSL gloom.sh/install | bash
65
65
  bun install -g gloomberb
66
66
  ```
67
67
 
68
- 运行 `gloom` 即可启动。0.19 之前命令名为 `gloomberb`,该名称仍可使用。要显示图形,请使用兼容 Kitty 图形协议的终端,例如 Ghostty、Kitty 或 WezTerm。直接下载、安装位置与更新方式详见[安装指南(英文)](docs/installation.md)。
68
+ 运行 `gloom` 即可启动。0.18.4 之前命令名为 `gloomberb`,该名称仍可使用。要显示图形,请使用兼容 Kitty 图形协议的终端,例如 Ghostty、Kitty 或 WezTerm。直接下载、安装位置与更新方式详见[安装指南(英文)](docs/installation.md)。
69
69
 
70
70
  ## 开始使用
71
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -74,7 +74,7 @@ index cdaa2bca0fabe577cb667056aa6f8f84211189c2..3839e5232742c2467180c344f51a30f1
74
74
 
75
75
  // Encrypt function
76
76
  diff --git a/dist/api/bun/core/Updater.ts b/dist/api/bun/core/Updater.ts
77
- index 39daf6d341780af8e30c2c50327162863f5977ec..0174d2ce1a9a4fa8ae0fd97367293fc5a794286a 100644
77
+ index 39daf6d341780af8e30c2c50327162863f5977ec..65677cd1d642ef832cf4f90a315d3ffc977909a3 100644
78
78
  --- a/dist/api/bun/core/Updater.ts
79
79
  +++ b/dist/api/bun/core/Updater.ts
80
80
  @@ -132,6 +132,19 @@ let updateInfo: {
@@ -254,6 +254,37 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..0174d2ce1a9a4fa8ae0fd97367293fc5
254
254
 
255
255
  // Ensure launcher binary is executable
256
256
  const launcherPath = join(appBundleDir, "bin", "launcher");
257
+ @@ -1071,14 +1129,18 @@ del "%~f0"
258
+ // current process has exited first. The detached shell survives our
259
+ // exit and polls until the process is gone.
260
+ const pid = process.pid;
261
+ + // The shell, sleep and open by their full paths, from /: a terminal
262
+ + // launch can hand the app a PATH without them, and a spawn into a
263
+ + // folder that is gone fails as if the shell were missing.
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ Bun.spawn(
266
+ [
267
+ - "sh",
268
+ + "/bin/sh",
269
+ "-c",
270
+ - `while kill -0 ${pid} 2>/dev/null; do sleep 0.5; done; sleep 1; open "${runningAppBundlePath}"`,
271
+ + `while kill -0 ${pid} 2>/dev/null; do /bin/sleep 0.5; done; /bin/sleep 1; /usr/bin/open "${runningAppBundlePath}"`,
272
+ ],
273
+ {
274
+ + cwd: "/",
275
+ detached: true,
276
+ stdio: ["ignore", "ignore", "ignore"],
277
+ } as any,
278
+ @@ -1087,7 +1149,8 @@ del "%~f0"
279
+ // On Linux, launch the launcher binary inside the app directory
280
+ const launcherPath = join(runningAppBundlePath, "bin", "launcher");
281
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
282
+ - Bun.spawn(["sh", "-c", `"${launcherPath}" &`], {
283
+ + Bun.spawn(["/bin/sh", "-c", `"${launcherPath}" &`], {
284
+ + cwd: "/",
285
+ detached: true,
286
+ } as any);
287
+ }
257
288
  diff --git a/dist/api/bun/preload/.generated/compiled.ts b/dist/api/bun/preload/.generated/compiled.ts
258
289
  index 3ab1d7d8de8938ae60823650fd3a29b952a76cb6..e0e841e6a87ce41533c6bdda8d29400aaf478e31 100644
259
290
  --- a/dist/api/bun/preload/.generated/compiled.ts
@@ -125,13 +125,17 @@ export async function performEmailAuth(mode: AccountMode, email: string, passwor
125
125
  } else {
126
126
  user = await apiClient.signIn(email, password);
127
127
  }
128
- if (!user.emailVerified) {
129
- await apiClient.sendVerification().catch(logFailure("account-auth", "Verification email request failed"));
130
- }
128
+ if (apiClient.getCurrentUser()?.id !== user.id) throw new Error("Another sign-in replaced this request.");
131
129
  const sessionToken = apiClient.getSessionToken();
132
130
  if (sessionToken) {
133
131
  chatController.adoptSession(sessionToken, user);
134
132
  }
133
+ if (!user.emailVerified) {
134
+ await apiClient.sendVerification().catch(logFailure("account-auth", "Verification email request failed"));
135
+ }
136
+ if (apiClient.getSessionToken() !== sessionToken || apiClient.getCurrentUser()?.id !== user.id) {
137
+ throw new Error("Another sign-in replaced this request.");
138
+ }
135
139
  identifyResearchUser();
136
140
  await chatController.refreshSession().catch(logFailure("account-auth", "Session refresh after email authentication failed"));
137
141
  return user;
@@ -74,6 +74,7 @@ export class CloudAuthApi {
74
74
  username: string,
75
75
  name: string,
76
76
  password: string,
77
+ isCurrent: () => boolean = () => true,
77
78
  ): Promise<AuthUser> {
78
79
  const result = await this.options.request<{ user: AuthUser }>(
79
80
  "/auth/sign-up/email",
@@ -81,7 +82,9 @@ export class CloudAuthApi {
81
82
  method: "POST",
82
83
  body: JSON.stringify({ email, username, name, password }),
83
84
  },
85
+ isCurrent,
84
86
  );
87
+ if (!isCurrent()) throw new Error("Another sign-in replaced this request.");
85
88
  this.options.requireCapturedSession(
86
89
  "Account created, but Gloom could not save the login session. Please try logging in again.",
87
90
  );
@@ -89,14 +92,16 @@ export class CloudAuthApi {
89
92
  return result.user;
90
93
  }
91
94
 
92
- async signIn(email: string, password: string): Promise<AuthUser> {
95
+ async signIn(email: string, password: string, isCurrent: () => boolean = () => true): Promise<AuthUser> {
93
96
  const result = await this.options.request<{ user: AuthUser }>(
94
97
  "/auth/sign-in/email",
95
98
  {
96
99
  method: "POST",
97
100
  body: JSON.stringify({ email, password }),
98
101
  },
102
+ isCurrent,
99
103
  );
104
+ if (!isCurrent()) throw new Error("Another sign-in replaced this request.");
100
105
  this.options.requireCapturedSession(
101
106
  "Logged in, but Gloom could not save the login session. Please try again.",
102
107
  );
@@ -127,11 +132,11 @@ export class CloudAuthApi {
127
132
  });
128
133
  }
129
134
 
130
- async signOut(): Promise<void> {
135
+ async signOut(isCurrent: () => boolean = () => true): Promise<void> {
131
136
  try {
132
- await this.options.request("/auth/sign-out", { method: "POST" });
137
+ await this.options.request("/auth/sign-out", { method: "POST" }, isCurrent);
133
138
  } finally {
134
- this.options.setSessionToken(null);
139
+ if (isCurrent()) this.options.setSessionToken(null);
135
140
  }
136
141
  }
137
142
 
@@ -1,6 +1,7 @@
1
1
  type QuoteMatchMode = "exact" | "whitespace" | "normalized"
2
2
 
3
- export const CATALYST_TYPES = ["regulatory", "clinical", "litigation", "antitrust", "recall", "sanctions", "export_control", "tariff", "rulemaking", "approval", "advisory", "pdufa"] as const
3
+ /** Regulator events, then the ones a company or its exchange dates itself (results, meetings, deals). */
4
+ export const CATALYST_TYPES = ["regulatory", "clinical", "litigation", "antitrust", "recall", "sanctions", "export_control", "tariff", "rulemaking", "approval", "advisory", "pdufa", "results", "shareholder_meeting", "merger_vote", "deal_closing", "investor_day"] as const
4
5
  export type CatalystType = (typeof CATALYST_TYPES)[number]
5
6
  type DatePrecision = "day" | "month" | "year"
6
7
  interface CatalystIdentifiers {
@@ -51,6 +51,7 @@ import type { CarbonBoard, CarbonHistory, EuropePowerBoard, EuropePowerHistory,
51
51
  import type { GpuBoardPayload, GpuEventsPayload, GpuHistoryPayload, GpuHistoryQuery } from "./gpu";
52
52
  import type { CpiBoardPayload } from "./cpi";
53
53
  import type { TapeRange, TapeSnapshot } from "./tape";
54
+ import { LIVE_SCAN_PATH, type LiveScanPayload, type LiveScanWireDefinition } from "./live-scan";
54
55
  import { signalWithTimeout } from "../utils/async-deadline";
55
56
  import { ApiRequestError } from "./errors";
56
57
  import type { ExchangeRateSnapshot } from "../types/exchange-rate";
@@ -253,6 +254,10 @@ export class CloudDataApi {
253
254
  return this.request<import("./fund-facts").FundFactsPayload>(`/cloud/fund-facts/${encodeURIComponent(ticker)}`, options.signal ? { signal: options.signal } : undefined);
254
255
  }
255
256
 
257
+ getFundBreadth(fund: string) {
258
+ return this.request<import("./members").FundBreadthPayload>(`/cloud/members/${encodeURIComponent(fund)}/breadth`);
259
+ }
260
+
256
261
  getFundChanges(fund: string) {
257
262
  return this.request<import("./members").FundChangesPayload>(`/cloud/members/${encodeURIComponent(fund)}/changes`);
258
263
  }
@@ -897,6 +902,15 @@ export class CloudDataApi {
897
902
  return this.request<CentralBankRatesPayload>("/cloud/econ/central-bank-rates", { signal: AbortSignal.timeout(45_000) });
898
903
  }
899
904
 
905
+ /** A live scan's matches against Gloom Cloud's newest market-wide snapshot for this account's tier. */
906
+ getLiveScan(definition: LiveScanWireDefinition, signal?: AbortSignal) {
907
+ return this.request<LiveScanPayload>(LIVE_SCAN_PATH, {
908
+ method: "POST",
909
+ body: JSON.stringify({ scan: definition }),
910
+ signal: signalWithTimeout(signal, 20_000),
911
+ });
912
+ }
913
+
900
914
  /** Recorded FLOW prints older than the live tape (Pro); the plugin builds `search`. */
901
915
  getScannerFlowHistory(search: string, signal?: AbortSignal) {
902
916
  return this.request<ScannerFlowHistoryPage>(`/market/scanner/flow/history?${search}`, { signal });
@@ -67,6 +67,8 @@ export const ASSIST_ARG_OPTION_LIMIT = 128;
67
67
  const ASSIST_ARG_OPTION_VALUE_MAX_LENGTH = 40;
68
68
  const ASSIST_ARG_OPTION_LABEL_MAX_LENGTH = 60;
69
69
  const ASSIST_REQUEST_TIMEOUT_MS = 6_000;
70
+ const SESSION_REVALIDATION_INITIAL_MS = 1_000;
71
+ const SESSION_REVALIDATION_MAX_MS = 30_000;
70
72
 
71
73
  /**
72
74
  * Fits a command's argument values inside the server caps. The value is what
@@ -94,8 +96,15 @@ interface PendingSessionRequest {
94
96
 
95
97
  class GloomApiClient {
96
98
  private currentUser: AuthUser | null = null;
97
- /** Local identity of a verified user/credential pair, never serialized. */
99
+ /** Local user/credential binding, masked until verified and never serialized. */
98
100
  private verifiedSessionIdentity: object | null = null;
101
+ private verifiedSessionCredential: string | null = null;
102
+ private sessionTransitions = 0;
103
+ private sessionTransition: Promise<void> | null = null;
104
+ /** Device adoption and external session installs supersede pending email auth. */
105
+ private sessionInstallationGeneration = 0;
106
+ private sessionRevalidationGeneration = 0;
107
+ private sessionRevalidationTimer: ReturnType<typeof setTimeout> | null = null;
99
108
  private sessionChecked = false;
100
109
  /** Last few session transitions, content-free, for app://auth. */
101
110
  private authTrace: Array<{ at: number; event: string; token: boolean; user: string }> = [];
@@ -110,7 +119,7 @@ class GloomApiClient {
110
119
  request: (path, options, canApplySession) => this.request(path, options, canApplySession),
111
120
  requireCapturedSession: (message) => this.requireCapturedSession(message),
112
121
  setCurrentUser: (user) => this.setCurrentUser(user),
113
- setSessionToken: (token) => this.setSessionToken(token),
122
+ setSessionToken: (token) => this.installSessionToken(token),
114
123
  updateCurrentUser: (updater) => this.updateCurrentUser(updater),
115
124
  });
116
125
  private readonly socket: CloudApiSocket = new CloudApiSocket({
@@ -123,7 +132,6 @@ class GloomApiClient {
123
132
  markCurrentUserUnverified: () => {
124
133
  if (this.currentUser) {
125
134
  this.currentUser = { ...this.currentUser, emailVerified: false };
126
- this.verifiedSessionIdentity = null;
127
135
  this.emitCurrentUserChange();
128
136
  }
129
137
  },
@@ -165,6 +173,8 @@ class GloomApiClient {
165
173
  }
166
174
 
167
175
  setCookieSessionMode(enabled: boolean): void {
176
+ this.cancelSessionRevalidation();
177
+ this.sessionInstallationGeneration += 1;
168
178
  this.sessionChecked = false;
169
179
  this.verifiedSessionIdentity = null;
170
180
  this.transport.setCookieSessionMode(enabled);
@@ -172,12 +182,21 @@ class GloomApiClient {
172
182
  }
173
183
 
174
184
  setSessionToken(token: string | null): void {
185
+ if (this.transport.getSessionToken() !== token || !token) {
186
+ this.cancelSessionRevalidation();
187
+ this.sessionInstallationGeneration += 1;
188
+ }
189
+ this.installSessionToken(token);
190
+ }
191
+
192
+ private installSessionToken(token: string | null): void {
175
193
  const changed = this.transport.getSessionToken() !== token;
176
194
  this.sessionChecked = false;
177
195
  this.transport.setSessionToken(token);
178
196
  if (changed) this.verifiedSessionIdentity = null;
179
197
  this.traceAuth(changed ? "setSessionToken:changed" : "setSessionToken:same");
180
198
  if (!token) {
199
+ this.cancelSessionRevalidation();
181
200
  this.currentUser = null;
182
201
  this.verifiedSessionIdentity = null;
183
202
  }
@@ -224,7 +243,9 @@ class GloomApiClient {
224
243
 
225
244
  /** Null while a replacement credential still awaits its matching user. */
226
245
  getVerifiedSessionIdentity(): object | null {
227
- return this.isVerified() ? this.verifiedSessionIdentity : null;
246
+ return this.sessionTransitions === 0 && this.isVerified()
247
+ && this.verifiedSessionCredential === this.transport.getSessionToken()
248
+ ? this.verifiedSessionIdentity : null;
228
249
  }
229
250
 
230
251
  /**
@@ -276,11 +297,18 @@ class GloomApiClient {
276
297
  if (this.authTrace.length > 24) this.authTrace.shift();
277
298
  }
278
299
 
279
- private setCurrentUser(user: AuthUser | null): void {
300
+ private setCurrentUser(user: AuthUser | null, confirmSession = true): void {
301
+ const sameAccount = this.currentUser?.id === user?.id;
280
302
  const changed = this.socketEntitlementKey(this.currentUser) !== this.socketEntitlementKey(user);
281
303
  this.traceAuth("setCurrentUser", user);
282
304
  this.currentUser = user;
283
- this.verifiedSessionIdentity = this.isVerified() ? {} : null;
305
+ if (confirmSession) {
306
+ this.clearSessionRevalidationTimer();
307
+ this.verifiedSessionIdentity = user ? {} : null;
308
+ this.verifiedSessionCredential = this.transport.getSessionToken();
309
+ } else if (!sameAccount || !user) {
310
+ this.verifiedSessionIdentity = null;
311
+ }
284
312
  this.socket.syncAuthState({ reconnect: changed });
285
313
  this.emitCurrentUserChange();
286
314
  }
@@ -291,7 +319,9 @@ class GloomApiClient {
291
319
 
292
320
  private updateCurrentUser(updater: (user: AuthUser) => AuthUser): void {
293
321
  if (!this.currentUser) return;
294
- this.setCurrentUser(updater(this.currentUser));
322
+ // A socket/profile update is metadata, not proof that a replacement
323
+ // credential belongs to this user. Keep its existing binding, even null.
324
+ this.setCurrentUser(updater(this.currentUser), false);
295
325
  }
296
326
 
297
327
  private socketEntitlementKey(user: AuthUser | null): string {
@@ -326,11 +356,123 @@ class GloomApiClient {
326
356
  return this.currentUser?.emailVerified ? this.currentUser : null;
327
357
  }
328
358
 
329
- signUp = this.auth.signUp.bind(this.auth);
330
- signIn = this.auth.signIn.bind(this.auth);
359
+ signUp = (email: string, username: string, name: string, password: string) =>
360
+ this.withSessionTransition((isCurrent) => this.auth.signUp(email, username, name, password, isCurrent));
361
+ signIn = (email: string, password: string) =>
362
+ this.withSessionTransition((isCurrent) => this.auth.signIn(email, password, isCurrent));
331
363
  startDeviceSignIn = this.auth.startDeviceSignIn.bind(this.auth);
332
364
  pollDeviceSignIn = this.auth.pollDeviceSignIn.bind(this.auth);
333
- signOut = this.auth.signOut.bind(this.auth);
365
+ signOut = () => this.withSessionTransition((isCurrent) => this.auth.signOut(isCurrent), false);
366
+
367
+ /** Cookies can change before an auth response's body arrives, including ones the browser hides. */
368
+ private withSessionTransition<T>(run: (isCurrent: () => boolean) => Promise<T>, reconcileFailure = true): Promise<T> {
369
+ const previous = this.sessionTransition;
370
+ this.cancelSessionRevalidation();
371
+ let revalidationGeneration = this.sessionRevalidationGeneration;
372
+ const generation = this.sessionInstallationGeneration;
373
+ const isCurrent = () => generation === this.sessionInstallationGeneration;
374
+ this.sessionTransitions += 1;
375
+ this.sessionChecked = false;
376
+ this.verifiedSessionIdentity = null;
377
+ this.sessionRequestGeneration += 1;
378
+ this.emitCurrentUserChange();
379
+ let needsRevalidation = false;
380
+ const operation = (async () => {
381
+ // Serialize credential-changing requests so a late response cannot pair
382
+ // one account's user with another request's installed cookie.
383
+ if (previous) await previous;
384
+ try {
385
+ if (!isCurrent()) throw new Error("Another sign-in replaced this request.");
386
+ // An earlier queued sign-out may have retired retries while clearing its token.
387
+ revalidationGeneration = this.sessionRevalidationGeneration;
388
+ this.sessionRequestGeneration += 1;
389
+ const result = await run(isCurrent);
390
+ if (!isCurrent()) throw new Error("Another sign-in replaced this request.");
391
+ return result;
392
+ } catch (error) {
393
+ if (isCurrent()) {
394
+ this.verifiedSessionIdentity = null;
395
+ this.sessionChecked = false;
396
+ }
397
+ if (isCurrent() && reconcileFailure && this.transport.hasSessionCredential()) {
398
+ // A failed body may already have changed the cookie. A fresh session
399
+ // check is the only way to resume; retry if it cannot validate yet.
400
+ needsRevalidation = true;
401
+ this.sessionRequestGeneration += 1;
402
+ await this.revalidateSession(() => isCurrent()
403
+ && revalidationGeneration === this.sessionRevalidationGeneration).catch((failure) => {
404
+ if (!isCurrent()) return;
405
+ this.verifiedSessionIdentity = null;
406
+ logFailure("cloud.auth", "Restore session after failed sign-in failed")(failure);
407
+ });
408
+ }
409
+ throw error;
410
+ } finally {
411
+ if (isCurrent()) this.sessionRequestGeneration += 1;
412
+ this.sessionTransitions -= 1;
413
+ if (isCurrent() && needsRevalidation) this.scheduleSessionRevalidation(revalidationGeneration);
414
+ this.emitCurrentUserChange();
415
+ }
416
+ })();
417
+ const settled = operation.then(() => {}, () => {
418
+ // Intentional ignore: the caller receives the error while the auth queue continues.
419
+ });
420
+ this.sessionTransition = settled;
421
+ void settled.then(() => { if (this.sessionTransition === settled) this.sessionTransition = null; });
422
+ return operation;
423
+ }
424
+
425
+ private clearSessionRevalidationTimer(): void {
426
+ if (this.sessionRevalidationTimer !== null) clearTimeout(this.sessionRevalidationTimer);
427
+ this.sessionRevalidationTimer = null;
428
+ }
429
+
430
+ private cancelSessionRevalidation(): void {
431
+ this.sessionRevalidationGeneration += 1;
432
+ this.clearSessionRevalidationTimer();
433
+ }
434
+
435
+ /** A failed auth response can replace a cookie even when its recovery read is offline. */
436
+ private scheduleSessionRevalidation(generation: number, delay = SESSION_REVALIDATION_INITIAL_MS): void {
437
+ const isCurrent = () => generation === this.sessionRevalidationGeneration;
438
+ const needsRevalidation = () => isCurrent() && this.sessionTransitions === 0
439
+ && !this.sessionChecked && this.verifiedSessionIdentity === null && this.transport.hasSessionCredential();
440
+ if (!needsRevalidation() || this.sessionRevalidationTimer !== null) return;
441
+ this.sessionRevalidationTimer = setTimeout(() => {
442
+ this.sessionRevalidationTimer = null;
443
+ if (!needsRevalidation()) return;
444
+ void this.revalidateSession(isCurrent).catch((error) => {
445
+ if (isCurrent()) logFailure("cloud.auth", "Retry session validation failed")(error);
446
+ }).finally(() => {
447
+ this.scheduleSessionRevalidation(generation, Math.min(delay * 2, SESSION_REVALIDATION_MAX_MS));
448
+ });
449
+ }, delay);
450
+ this.sessionRevalidationTimer.unref?.();
451
+ }
452
+
453
+ private async revalidateSession(isCurrent: () => boolean): Promise<void> {
454
+ const user = await this.getSession(isCurrent);
455
+ const credential = this.getSessionToken();
456
+ if (user || !credential || !isCurrent()) return;
457
+ // A native null-session response can retain a usable token. Validate it
458
+ // through the account endpoint, without pairing it with a cached user.
459
+ this.sessionChecked = false;
460
+ const previousUser = this.currentUser;
461
+ const canApply = () => isCurrent() && this.getSessionToken() === credential && this.currentUser === previousUser;
462
+ try {
463
+ const profile = await this.auth.getAccountProfile(canApply);
464
+ if (!canApply()) return;
465
+ this.sessionChecked = true;
466
+ this.auth.restoreCachedUser({ ...profile, updatedAt: profile.updatedAt ?? undefined });
467
+ } catch (error) {
468
+ if (!canApply()) return;
469
+ if (error instanceof ApiRequestError && error.status === 401) {
470
+ this.installSessionToken(null);
471
+ return;
472
+ }
473
+ throw error;
474
+ }
475
+ }
334
476
 
335
477
  async getSession(isCurrent: () => boolean = () => true): Promise<AuthUser | null> {
336
478
  if (!isCurrent()) return this.currentUser;
@@ -353,7 +495,10 @@ class GloomApiClient {
353
495
  credential: this.getSessionToken(),
354
496
  promise: this.auth.getSession(canApply)
355
497
  .then(({ user, validated }) => {
356
- if (validated && canApply()) this.sessionChecked = true;
498
+ if (validated && canApply()) {
499
+ this.sessionChecked = true;
500
+ this.clearSessionRevalidationTimer();
501
+ }
357
502
  this.traceAuth(validated ? "getSession:done" : "getSession:retained", user);
358
503
  return user;
359
504
  })
@@ -753,6 +898,8 @@ class GloomApiClient {
753
898
  subscribeScanner = this.socket.subscribeScanner.bind(this.socket);
754
899
 
755
900
  dispose(): void {
901
+ this.cancelSessionRevalidation();
902
+ this.sessionInstallationGeneration += 1;
756
903
  this.sessionRequestGeneration += 1;
757
904
  this.sessionRequest = null;
758
905
  this.socket.dispose();
@@ -799,11 +946,13 @@ class GloomApiClient {
799
946
  getCloudCentralBankRates = this.data.getCloudCentralBankRates.bind(this.data);
800
947
  getMobileAlertHistory = this.data.getMobileAlertHistory.bind(this.data);
801
948
  getScannerFlowHistory = this.data.getScannerFlowHistory.bind(this.data);
949
+ getLiveScan = this.data.getLiveScan.bind(this.data);
802
950
  getCloudEstimateRevisions = this.data.getCloudEstimateRevisions.bind(this.data);
803
951
  getCloudMoneyMarkets = this.data.getCloudMoneyMarkets.bind(this.data);
804
952
  equityScreener = this.data.equityScreener.bind(this.data);
805
953
  getMemberFunds = this.data.getMemberFunds.bind(this.data);
806
954
  getFundMembers = this.data.getFundMembers.bind(this.data);
955
+ getFundBreadth = this.data.getFundBreadth.bind(this.data);
807
956
  getFundChanges = this.data.getFundChanges.bind(this.data);
808
957
  getFundFacts = this.data.getFundFacts.bind(this.data);
809
958
  getCloudExchangeAnnouncements = this.data.getCloudExchangeAnnouncements.bind(this.data);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The live scanner's wire format: a user's own intraday conditions over a
3
+ * universe of US stocks, evaluated by Gloom Cloud against a market-wide
4
+ * snapshot it refreshes every few seconds (real-time for Pro, 15 minutes
5
+ * delayed otherwise). `GET /market/scanner/live?scan=<definition JSON>`.
6
+ */
7
+
8
+ /** Minutes a move, a volume window or an event can look back over. */
9
+ export const LIVE_SCAN_WINDOWS = [1, 5, 15, 30, 60] as const;
10
+ export type LiveScanWindow = (typeof LIVE_SCAN_WINDOWS)[number];
11
+
12
+ export const LIVE_SCAN_INDEXES = ["sp500", "sp400", "sp600", "russell1000", "russell2000"] as const;
13
+ export type LiveScanIndex = (typeof LIVE_SCAN_INDEXES)[number];
14
+
15
+ /** The universe as Gloom Cloud reads it; a watchlist goes as its symbols. */
16
+ type LiveScanWireUniverse =
17
+ | { kind: "all" }
18
+ | { kind: "index"; index: LiveScanIndex }
19
+ | { kind: "sector"; sector: string }
20
+ | { kind: "symbols"; symbols: string[] };
21
+
22
+ export type LiveScanDirection = "up" | "down" | "either";
23
+
24
+ export type LiveScanCondition =
25
+ /** Volume over the window (or the session so far) against the 20-session average's pace. */
26
+ | { kind: "volume_spike"; window: LiveScanWindow | "day"; ratio: number }
27
+ /** Percent move over the last `window` minutes. */
28
+ | { kind: "move"; window: LiveScanWindow; direction: LiveScanDirection; percent: number }
29
+ /** A new session high (low) printed in the last `within` minutes. */
30
+ | { kind: "new_high" | "new_low"; within: LiveScanWindow }
31
+ /** Crossed the session VWAP in the last `within` minutes, and still on that side. */
32
+ | { kind: "vwap_cross"; direction: "above" | "below" | "either"; within: LiveScanWindow }
33
+ /** The session's open against the previous close. */
34
+ | { kind: "gap"; direction: LiveScanDirection; percent: number };
35
+
36
+ export interface LiveScanWireDefinition {
37
+ version: 1;
38
+ universe: LiveScanWireUniverse;
39
+ conditions: LiveScanCondition[];
40
+ minPrice?: number;
41
+ minMarketCap?: number;
42
+ minVolume?: number;
43
+ }
44
+
45
+ type LiveScanStatus = "live" | "starting" | "closed" | "degraded";
46
+
47
+ export interface LiveScanMatch {
48
+ symbol: string;
49
+ name: string | null;
50
+ /** Its US venue (NASDAQ, NYSE, ...), null when the screener does not list it. */
51
+ exchange?: string | null;
52
+ price: number;
53
+ changePercent: number | null;
54
+ volume: number | null;
55
+ /** The session's volume against the 20-session average's pace so far. */
56
+ relativeVolume: number | null;
57
+ /** Price against the session VWAP, in percent. */
58
+ vwapPercent: number | null;
59
+ gapPercent: number | null;
60
+ /**
61
+ * The figure each condition matched on, in the definition's order: a
62
+ * percent for a move or a gap, a multiple for a volume spike, the time of
63
+ * the event (epoch ms) for a new high or low or a VWAP cross.
64
+ */
65
+ values: Array<number | null>;
66
+ /** The latest event a condition matched on. */
67
+ signalAt: number | null;
68
+ /** When the price printed. */
69
+ at: number | null;
70
+ }
71
+
72
+ export interface LiveScanPayload {
73
+ status: LiveScanStatus;
74
+ access: "realtime" | "delayed";
75
+ delayMinutes: number;
76
+ /** The market time the snapshot shows. */
77
+ asOf: number | null;
78
+ /** When the snapshot was refreshed. */
79
+ takenAt: number | null;
80
+ /** How often the snapshot refreshes. */
81
+ refreshMs: number;
82
+ scanned: number;
83
+ matched: number;
84
+ matches: LiveScanMatch[];
85
+ /** Windows the scanner cannot measure yet after it started; a condition over one matches nothing until then. */
86
+ warmingWindows: LiveScanWindow[];
87
+ warnings: string[];
88
+ }
89
+
90
+ /** The scan travels in a POST body: a watchlist of 1,000 symbols would not fit a request line. */
91
+ export const LIVE_SCAN_PATH = "/market/scanner/live";
@@ -30,3 +30,15 @@ export interface FundChangesPayload {
30
30
  estimateLabel: string; attribution: { text: string; url: string } | null;
31
31
  }
32
32
  export interface MemberFundsPayload { funds: (MemberFund & { asOf: string | null })[] }
33
+
34
+ interface BreadthCount { above: number; eligible: number; percent: number | null }
35
+ export interface BreadthDay {
36
+ date: string; asOf: string; membershipAsOf: string; memberCount: number; observed: number;
37
+ advances: number; declines: number; unchanged: number; advanceDeclineEligible: number;
38
+ ma20: BreadthCount; ma50: BreadthCount; ma200: BreadthCount;
39
+ newHighs: number; newLows: number; highLowEligible: number; adLine: number | null;
40
+ }
41
+ export interface FundBreadthPayload {
42
+ fund: MemberFund; basis: "daily-close" | "intraday"; access?: "realtime" | "delayed"; membershipBasis: "current-members";
43
+ latest: BreadthDay | null; history: BreadthDay[]; stale: boolean;
44
+ }