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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/data.ts +14 -0
- package/src/api-client/index.ts +160 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/members.ts +12 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/types.ts +6 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +68 -22
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/overview.ts +119 -31
- package/src/cli/commands/search.ts +18 -2
- package/src/cli/commands/ticker.ts +2 -1
- package/src/cli/errors.ts +2 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +49 -0
- package/src/cli/history-rows.ts +30 -7
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +16 -5
- package/src/cli/pane-functions/freshness.ts +25 -13
- package/src/cli/pane-functions/function-help.ts +10 -3
- package/src/cli/pane-functions/headless.ts +39 -5
- package/src/cli/pane-functions/index.ts +10 -3
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +18 -2
- package/src/cli/quote-answer.ts +10 -4
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/components/chart/composite/composite-chart.tsx +22 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +9 -2
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/scene.ts +7 -3
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/ui/display.tsx +5 -1
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/saved-copies.ts +55 -16
- package/src/docs/snapshot.json +361 -84
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/market/change-basis.ts +33 -5
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli.ts +59 -20
- package/src/plugins/builtin/alerts/command.ts +7 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/fired-log.ts +45 -4
- package/src/plugins/builtin/alerts/format.ts +8 -2
- package/src/plugins/builtin/alerts/headless.ts +10 -5
- package/src/plugins/builtin/alerts/history-rows.ts +5 -2
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +66 -16
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +42 -29
- package/src/plugins/builtin/alerts/storage.ts +33 -10
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
- package/src/plugins/builtin/chart-composer/headless.ts +13 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
- package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/client.ts +6 -1
- package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
- package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/iv-history/headless.ts +3 -0
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +25 -8
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/pane.tsx +15 -11
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -3
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +6 -4
- package/src/plugins/builtin/vol-surface/headless.ts +62 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/portfolio/column-values.ts +5 -5
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +66 -16
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/theme/colors.ts +8 -0
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +155 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +5 -1
- package/src/time-series/resolve.ts +61 -12
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/spec.ts +2 -1
- package/src/time-series/studies.ts +270 -72
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +5 -0
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +41 -1
- package/src/types/headless.ts +42 -4
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +1 -1
- package/src/updater/command-names.ts +12 -7
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +5 -0
- 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.
|
|
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.
|
|
68
|
+
运行 `gloom` 即可启动。0.18.4 之前命令名为 `gloomberb`,该名称仍可使用。要显示图形,请使用兼容 Kitty 图形协议的终端,例如 Ghostty、Kitty 或 WezTerm。直接下载、安装位置与更新方式详见[安装指南(英文)](docs/installation.md)。
|
|
69
69
|
|
|
70
70
|
## 开始使用
|
|
71
71
|
|
package/package.json
CHANGED
|
@@ -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..
|
|
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 (
|
|
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;
|
package/src/api-client/auth.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/api-client/data.ts
CHANGED
|
@@ -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 });
|
package/src/api-client/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
330
|
-
|
|
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
|
|
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())
|
|
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
|
+
}
|