@reefclaw/openclaw-plugin 0.1.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.
- package/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- package/util/plugin-paths.js +15 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
// UserDataStream — the authenticated Binance Futures user-data WebSocket.
|
|
2
|
+
//
|
|
3
|
+
// What it does:
|
|
4
|
+
// 1. Mints a listenKey (via ListenKeyManager) and opens a WebSocket to
|
|
5
|
+
// `wss://fstream.binance.com/private/ws/<listenKey>` (testnet: `stream.binancefuture.com`).
|
|
6
|
+
// 2. Parses incoming frames into typed events (ACCOUNT_UPDATE,
|
|
7
|
+
// ORDER_TRADE_UPDATE, ALGO_UPDATE, MARGIN_CALL, ACCOUNT_CONFIG_UPDATE,
|
|
8
|
+
// listenKeyExpired).
|
|
9
|
+
// 3. Emits those events for the parent (typically LiveStateStore +
|
|
10
|
+
// LiveAdapter) to apply.
|
|
11
|
+
// 4. Handles reconnects with exponential backoff + jitter, staleness
|
|
12
|
+
// watchdog, graduated alert thresholds, and listenKey rotation.
|
|
13
|
+
//
|
|
14
|
+
// What it does NOT do:
|
|
15
|
+
// - No state management. The store (`LiveStateStore`) applies events.
|
|
16
|
+
// - No REST snapshot logic. The parent calls its own REST on
|
|
17
|
+
// `'reconnect'` / `'connected'` so the store can re-seed.
|
|
18
|
+
// - No 60s reconciler. That lives separately (see PositionReconciler).
|
|
19
|
+
//
|
|
20
|
+
// Testability:
|
|
21
|
+
// - `webSocketFactory` is injectable → tests use a mock WebSocket.
|
|
22
|
+
// - `timers` is injectable → fake-timer tests drive backoff + watchdog.
|
|
23
|
+
// - `listenKeyManager` is injectable → tests don't hit the network.
|
|
24
|
+
//
|
|
25
|
+
// Rule of thumb: this class is a thin, well-logged translator from Binance
|
|
26
|
+
// framing to our domain events. All business logic belongs upstream.
|
|
27
|
+
import { EventEmitter } from 'node:events';
|
|
28
|
+
import { ListenKeyManager } from './listen-key-manager.js';
|
|
29
|
+
import { logger, formatError } from '../logger.js';
|
|
30
|
+
const TAG = 'user-data-stream';
|
|
31
|
+
const REAL_TIMERS = {
|
|
32
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
33
|
+
clearTimeout: (h) => clearTimeout(h),
|
|
34
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
35
|
+
clearInterval: (h) => clearInterval(h),
|
|
36
|
+
now: () => Date.now(),
|
|
37
|
+
random: () => Math.random(),
|
|
38
|
+
};
|
|
39
|
+
// ---- The class ----
|
|
40
|
+
export class UserDataStream extends EventEmitter {
|
|
41
|
+
api;
|
|
42
|
+
opts;
|
|
43
|
+
webSocketFactory;
|
|
44
|
+
timers;
|
|
45
|
+
listenKeyManager;
|
|
46
|
+
translateSymbol;
|
|
47
|
+
ws = null;
|
|
48
|
+
status = 'stopped';
|
|
49
|
+
reconnectTimer = null;
|
|
50
|
+
watchdogInterval = null;
|
|
51
|
+
lastEventAt = null;
|
|
52
|
+
lastFrameAt = null;
|
|
53
|
+
lastConnectedAt = null;
|
|
54
|
+
consecutiveReconnects = 0;
|
|
55
|
+
totalReconnects = 0;
|
|
56
|
+
stopRequested = false;
|
|
57
|
+
firstConnect = true;
|
|
58
|
+
/** First-N raw ALGO_UPDATE payloads logged at INFO so operators can verify
|
|
59
|
+
* the parser against real Binance traffic without scraping the WS at
|
|
60
|
+
* TRACE. Capped to avoid log volume; resets per process restart. */
|
|
61
|
+
algoUpdateRawLogged = 0;
|
|
62
|
+
static ALGO_UPDATE_RAW_LOG_CAP = 20;
|
|
63
|
+
constructor(api, options) {
|
|
64
|
+
super();
|
|
65
|
+
this.api = api;
|
|
66
|
+
this.opts = {
|
|
67
|
+
wsBaseUrl: options.wsBaseUrl,
|
|
68
|
+
reconnectBaseMs: options.reconnectBaseMs ?? 1_000,
|
|
69
|
+
reconnectCapMs: options.reconnectCapMs ?? 60_000,
|
|
70
|
+
reconnectJitterPct: options.reconnectJitterPct ?? 0.2,
|
|
71
|
+
staleEventWatchdogMs: options.staleEventWatchdogMs ?? 6 * 60_000,
|
|
72
|
+
warnThreshold: options.warnThreshold ?? 3,
|
|
73
|
+
errorThreshold: options.errorThreshold ?? 10,
|
|
74
|
+
fatalThreshold: options.fatalThreshold ?? 30,
|
|
75
|
+
};
|
|
76
|
+
this.webSocketFactory = options.webSocketFactory;
|
|
77
|
+
this.timers = {
|
|
78
|
+
setTimeout: options.timers?.setTimeout ?? REAL_TIMERS.setTimeout,
|
|
79
|
+
clearTimeout: options.timers?.clearTimeout ?? REAL_TIMERS.clearTimeout,
|
|
80
|
+
setInterval: options.timers?.setInterval ?? REAL_TIMERS.setInterval,
|
|
81
|
+
clearInterval: options.timers?.clearInterval ?? REAL_TIMERS.clearInterval,
|
|
82
|
+
now: options.timers?.now ?? REAL_TIMERS.now,
|
|
83
|
+
random: options.timers?.random ?? REAL_TIMERS.random,
|
|
84
|
+
};
|
|
85
|
+
const makeListenKeyMgr = options.listenKeyManagerFactory
|
|
86
|
+
?? ((a, o) => new ListenKeyManager(a, o));
|
|
87
|
+
this.listenKeyManager = makeListenKeyMgr(api, options.listenKey);
|
|
88
|
+
this.translateSymbol = options.symbolTranslator
|
|
89
|
+
?? ((id) => api.symbolFromMarketId(id));
|
|
90
|
+
this.listenKeyManager.on('keyKeepAliveFailed', (reason) => {
|
|
91
|
+
logger.warn(TAG, `listenKey keep-alive failed — forcing reconnect: ${reason}`);
|
|
92
|
+
// Forcing a reconnect will re-mint a fresh key as part of start().
|
|
93
|
+
this.forceReconnect('listen_key_keepalive_failed');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Start the stream. Idempotent — returns immediately if already running. */
|
|
97
|
+
async start() {
|
|
98
|
+
if (this.status !== 'stopped') {
|
|
99
|
+
logger.warn(TAG, `start() called while status=${this.status} — ignored`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.stopRequested = false;
|
|
103
|
+
this.consecutiveReconnects = 0;
|
|
104
|
+
this.firstConnect = true;
|
|
105
|
+
await this.openConnection();
|
|
106
|
+
this.startWatchdog();
|
|
107
|
+
}
|
|
108
|
+
/** Graceful stop. Closes WS + stops listenKey. Idempotent. */
|
|
109
|
+
async stop() {
|
|
110
|
+
this.stopRequested = true;
|
|
111
|
+
this.clearReconnectTimer();
|
|
112
|
+
this.stopWatchdog();
|
|
113
|
+
if (this.ws) {
|
|
114
|
+
try {
|
|
115
|
+
this.ws.close(1000, 'client shutdown');
|
|
116
|
+
}
|
|
117
|
+
catch { /* best-effort */ }
|
|
118
|
+
this.ws = null;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await this.listenKeyManager.stop();
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.warn(TAG, `listenKey stop failed (non-fatal): ${formatError(err)}`);
|
|
125
|
+
}
|
|
126
|
+
this.setStatus('stopped');
|
|
127
|
+
}
|
|
128
|
+
/** Current health snapshot for observability. */
|
|
129
|
+
getHealth() {
|
|
130
|
+
return {
|
|
131
|
+
status: this.status,
|
|
132
|
+
currentListenKey: this.listenKeyManager.getKey(),
|
|
133
|
+
lastEventAt: this.lastEventAt,
|
|
134
|
+
lastFrameAt: this.lastFrameAt,
|
|
135
|
+
lastConnectedAt: this.lastConnectedAt,
|
|
136
|
+
consecutiveReconnectAttempts: this.consecutiveReconnects,
|
|
137
|
+
totalReconnects: this.totalReconnects,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Whether the stream is currently open. Convenience for tests + callers. */
|
|
141
|
+
isConnected() {
|
|
142
|
+
return this.status === 'open';
|
|
143
|
+
}
|
|
144
|
+
// ---- Internal: connection lifecycle ----
|
|
145
|
+
async openConnection() {
|
|
146
|
+
if (this.stopRequested)
|
|
147
|
+
return;
|
|
148
|
+
this.setStatus('connecting');
|
|
149
|
+
let key;
|
|
150
|
+
try {
|
|
151
|
+
key = await this.listenKeyManager.start();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
logger.error(TAG, `listenKey mint failed: ${formatError(err)}`);
|
|
155
|
+
this.scheduleReconnect('listen_key_mint_failed');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const url = `${this.opts.wsBaseUrl}/${key}`;
|
|
159
|
+
logger.info(TAG, `Opening WS to ${this.opts.wsBaseUrl}/<listenKey> (hash=${hash8(key)})`);
|
|
160
|
+
let socket;
|
|
161
|
+
try {
|
|
162
|
+
socket = this.webSocketFactory(url);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.error(TAG, `webSocketFactory threw: ${formatError(err)}`);
|
|
166
|
+
this.scheduleReconnect('ws_factory_failed');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.ws = socket;
|
|
170
|
+
this.attachWsHandlers(socket);
|
|
171
|
+
}
|
|
172
|
+
attachWsHandlers(socket) {
|
|
173
|
+
socket.on('open', () => {
|
|
174
|
+
if (this.ws !== socket)
|
|
175
|
+
return; // stale
|
|
176
|
+
this.lastConnectedAt = this.timers.now();
|
|
177
|
+
this.lastEventAt = this.lastConnectedAt;
|
|
178
|
+
this.lastFrameAt = this.lastConnectedAt;
|
|
179
|
+
this.consecutiveReconnects = 0;
|
|
180
|
+
this.setStatus('open');
|
|
181
|
+
logger.info(TAG, 'WS connected');
|
|
182
|
+
this.emit('connected');
|
|
183
|
+
if (!this.firstConnect) {
|
|
184
|
+
this.emit('resyncRequired', 'reconnect');
|
|
185
|
+
}
|
|
186
|
+
this.firstConnect = false;
|
|
187
|
+
});
|
|
188
|
+
socket.on('message', (data) => {
|
|
189
|
+
if (this.ws !== socket)
|
|
190
|
+
return;
|
|
191
|
+
const now = this.timers.now();
|
|
192
|
+
this.lastEventAt = now; // data event — drives the active probe's freshness check
|
|
193
|
+
this.lastFrameAt = now; // any frame — drives the dead-socket watchdog
|
|
194
|
+
this.handleFrame(data);
|
|
195
|
+
});
|
|
196
|
+
socket.on('ping', (payload) => {
|
|
197
|
+
if (this.ws !== socket)
|
|
198
|
+
return;
|
|
199
|
+
// Server ping = liveness signal even on a fully-idle account. Binance
|
|
200
|
+
// documents these every 3 min on the user-data stream, so counting them
|
|
201
|
+
// toward `lastFrameAt` keeps the watchdog quiet between data frames.
|
|
202
|
+
// Note: `lastEventAt` is intentionally NOT updated — pings carry no data
|
|
203
|
+
// so the active REST probe still sees them as silence (correct, the probe
|
|
204
|
+
// is checking for missed FILLS not missed liveness).
|
|
205
|
+
this.lastFrameAt = this.timers.now();
|
|
206
|
+
// Most `ws` clients pong automatically; in case a custom factory doesn't,
|
|
207
|
+
// reply here. Binance closes the socket if no pong arrives within 10 min.
|
|
208
|
+
try {
|
|
209
|
+
socket.pong?.(payload);
|
|
210
|
+
}
|
|
211
|
+
catch { /* best-effort */ }
|
|
212
|
+
});
|
|
213
|
+
socket.on('pong', () => {
|
|
214
|
+
if (this.ws !== socket)
|
|
215
|
+
return;
|
|
216
|
+
// Server-sent pong (in response to a client-initiated ping if any). Same
|
|
217
|
+
// liveness role as `ping` above. Kept for observability symmetry.
|
|
218
|
+
this.lastFrameAt = this.timers.now();
|
|
219
|
+
});
|
|
220
|
+
socket.on('error', (err) => {
|
|
221
|
+
if (this.ws !== socket)
|
|
222
|
+
return;
|
|
223
|
+
logger.warn(TAG, `WS error: ${formatError(err)}`);
|
|
224
|
+
// 'close' will follow. Don't schedule reconnect here to avoid double-firing.
|
|
225
|
+
});
|
|
226
|
+
socket.on('close', (code, reason) => {
|
|
227
|
+
if (this.ws !== socket)
|
|
228
|
+
return;
|
|
229
|
+
let reasonStr;
|
|
230
|
+
if (reason == null)
|
|
231
|
+
reasonStr = '';
|
|
232
|
+
else if (typeof reason === 'string')
|
|
233
|
+
reasonStr = reason;
|
|
234
|
+
else
|
|
235
|
+
reasonStr = reason.toString('utf-8');
|
|
236
|
+
logger.warn(TAG, `WS closed code=${code ?? '?'} reason=${reasonStr || '(none)'}`);
|
|
237
|
+
this.ws = null;
|
|
238
|
+
// Stop the keep-alive loop WITHOUT DELETEing the key; the next connect
|
|
239
|
+
// re-POSTs. Binance's re-POST returns the SAME active key (doc-verified),
|
|
240
|
+
// so the previous un-awaited DELETE here could land AFTER the reconnect's
|
|
241
|
+
// POST and invalidate the key the fresh socket just authenticated with.
|
|
242
|
+
// The graceful path (stop() below / UserDataStream.stop()) still DELETEs —
|
|
243
|
+
// no reconnect follows there.
|
|
244
|
+
this.listenKeyManager.stopKeepAlive();
|
|
245
|
+
if (this.stopRequested) {
|
|
246
|
+
this.setStatus('stopped');
|
|
247
|
+
this.emit('disconnected', reasonStr || 'client stop', false);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.emit('disconnected', reasonStr || `code=${code ?? '?'}`, true);
|
|
251
|
+
this.scheduleReconnect(reasonStr || 'ws_close');
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
handleFrame(raw) {
|
|
255
|
+
let parsed;
|
|
256
|
+
try {
|
|
257
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf-8');
|
|
258
|
+
parsed = JSON.parse(text);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
logger.warn(TAG, `frame parse failed (ignored): ${formatError(err)}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!parsed || typeof parsed !== 'object')
|
|
265
|
+
return;
|
|
266
|
+
const obj = parsed;
|
|
267
|
+
const eventType = typeof obj.e === 'string' ? obj.e : null;
|
|
268
|
+
if (!eventType) {
|
|
269
|
+
// Binance occasionally sends pong-like keepalive frames w/o 'e'. Swallow.
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
switch (eventType) {
|
|
274
|
+
case 'ACCOUNT_UPDATE': {
|
|
275
|
+
const ev = parseAccountUpdate(obj, this.translateSymbol);
|
|
276
|
+
if (ev)
|
|
277
|
+
this.emit('accountUpdate', ev);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'ORDER_TRADE_UPDATE': {
|
|
281
|
+
const ev = parseOrderUpdate(obj, this.translateSymbol);
|
|
282
|
+
if (ev)
|
|
283
|
+
this.emit('orderUpdate', ev);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'ALGO_UPDATE': {
|
|
287
|
+
// Binance fires this for STOP_MARKET / TAKE_PROFIT_MARKET /
|
|
288
|
+
// TRAILING_STOP_MARKET conditional orders. Pre-2026-05-15 we
|
|
289
|
+
// swallowed this at the default branch — that left the plugin
|
|
290
|
+
// blind to exchange-side bracket cancellation, which is the
|
|
291
|
+
// root cause of the bracket-spiral class of bug. We now parse +
|
|
292
|
+
// emit; the downstream listener (controller + LiveAdapter) is
|
|
293
|
+
// responsible for the action.
|
|
294
|
+
const ev = parseAlgoUpdate(obj, this.translateSymbol);
|
|
295
|
+
if (ev) {
|
|
296
|
+
if (this.algoUpdateRawLogged < UserDataStream.ALGO_UPDATE_RAW_LOG_CAP) {
|
|
297
|
+
// Capped to avoid flooding the journal under heavy bracket
|
|
298
|
+
// traffic. Operators read these first-N samples once after
|
|
299
|
+
// deploy to verify the parser matches Binance's actual shape.
|
|
300
|
+
logger.info(TAG, `ALGO_UPDATE raw (${this.algoUpdateRawLogged + 1}/` +
|
|
301
|
+
`${UserDataStream.ALGO_UPDATE_RAW_LOG_CAP}): ${JSON.stringify(obj)}`);
|
|
302
|
+
this.algoUpdateRawLogged++;
|
|
303
|
+
}
|
|
304
|
+
this.emit('algoUpdate', ev);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case 'TRADE_LITE': {
|
|
309
|
+
// Binance's newer lightweight fill notification (late-2024 addition).
|
|
310
|
+
// Same information as ORDER_TRADE_UPDATE but sent earlier for lower
|
|
311
|
+
// fill-latency. ORDER_TRADE_UPDATE follows with the canonical state
|
|
312
|
+
// transition, so we deliberately don't double-apply — TRADE_LITE is
|
|
313
|
+
// informational here. Log at debug-ish level (info) so operators
|
|
314
|
+
// can confirm Binance is sending it.
|
|
315
|
+
logger.info(TAG, 'TRADE_LITE received (canonical state follows in ORDER_TRADE_UPDATE)');
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case 'MARGIN_CALL': {
|
|
319
|
+
const ev = parseMarginCall(obj, this.translateSymbol);
|
|
320
|
+
if (ev)
|
|
321
|
+
this.emit('marginCall', ev);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case 'ACCOUNT_CONFIG_UPDATE': {
|
|
325
|
+
const ev = parseAccountConfigUpdate(obj, this.translateSymbol);
|
|
326
|
+
if (ev)
|
|
327
|
+
this.emit('accountConfigUpdate', ev);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'listenKeyExpired': {
|
|
331
|
+
logger.warn(TAG, 'listenKey expired notice from Binance — forcing reconnect');
|
|
332
|
+
this.emit('listenKeyExpired');
|
|
333
|
+
this.forceReconnect('listen_key_expired');
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
// Unknown event type. Binance adds new types over time; swallow at
|
|
338
|
+
// WARN so we notice but don't crash.
|
|
339
|
+
logger.warn(TAG, `unknown event type=${eventType} — swallowed`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
logger.warn(TAG, `event handler for ${eventType} threw: ${formatError(err)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ---- Internal: reconnect + watchdog ----
|
|
347
|
+
scheduleReconnect(reason) {
|
|
348
|
+
this.clearReconnectTimer();
|
|
349
|
+
if (this.stopRequested)
|
|
350
|
+
return;
|
|
351
|
+
this.consecutiveReconnects += 1;
|
|
352
|
+
this.totalReconnects += 1;
|
|
353
|
+
// Graduated alerts — easier to tune alerting later.
|
|
354
|
+
if (this.consecutiveReconnects >= this.opts.fatalThreshold) {
|
|
355
|
+
logger.error(TAG, `RECONNECT-FATAL attempt=${this.consecutiveReconnects} reason=${reason}`);
|
|
356
|
+
}
|
|
357
|
+
else if (this.consecutiveReconnects >= this.opts.errorThreshold) {
|
|
358
|
+
logger.error(TAG, `RECONNECT-ERROR attempt=${this.consecutiveReconnects} reason=${reason}`);
|
|
359
|
+
}
|
|
360
|
+
else if (this.consecutiveReconnects >= this.opts.warnThreshold) {
|
|
361
|
+
logger.warn(TAG, `RECONNECT-WARN attempt=${this.consecutiveReconnects} reason=${reason}`);
|
|
362
|
+
}
|
|
363
|
+
const delay = this.computeBackoffMs(this.consecutiveReconnects);
|
|
364
|
+
logger.info(TAG, `reconnect attempt=${this.consecutiveReconnects} delayMs=${delay} reason=${reason}`);
|
|
365
|
+
this.setStatus('reconnecting');
|
|
366
|
+
this.emit('reconnecting', this.consecutiveReconnects, delay);
|
|
367
|
+
this.reconnectTimer = this.timers.setTimeout(() => {
|
|
368
|
+
this.reconnectTimer = null;
|
|
369
|
+
void this.openConnection();
|
|
370
|
+
}, delay);
|
|
371
|
+
}
|
|
372
|
+
computeBackoffMs(attempt) {
|
|
373
|
+
// Exponential: base * 2^(attempt-1). Capped. Then ± jitterPct.
|
|
374
|
+
const exp = this.opts.reconnectBaseMs * 2 ** Math.min(attempt - 1, 20);
|
|
375
|
+
const capped = Math.min(exp, this.opts.reconnectCapMs);
|
|
376
|
+
const jitterRange = capped * this.opts.reconnectJitterPct;
|
|
377
|
+
const jitter = (this.timers.random() * 2 - 1) * jitterRange;
|
|
378
|
+
return Math.max(0, Math.round(capped + jitter));
|
|
379
|
+
}
|
|
380
|
+
clearReconnectTimer() {
|
|
381
|
+
if (this.reconnectTimer) {
|
|
382
|
+
this.timers.clearTimeout(this.reconnectTimer);
|
|
383
|
+
this.reconnectTimer = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Force a reconnect now — used when the server tells us the key expired
|
|
387
|
+
* or when the listenKey keep-alive loop gives up.
|
|
388
|
+
*
|
|
389
|
+
* Invariant: the close handler is responsible for clearing `this.ws`,
|
|
390
|
+
* stopping the listenKey loop, and scheduling the reconnect. We only
|
|
391
|
+
* trigger close() here and let the handler run. If there's no socket to
|
|
392
|
+
* close (manager dropped out-of-band), we schedule directly. */
|
|
393
|
+
forceReconnect(reason) {
|
|
394
|
+
if (this.stopRequested)
|
|
395
|
+
return;
|
|
396
|
+
if (this.ws) {
|
|
397
|
+
// The socket is PRESUMED DEAD here (staleness watchdog fired, listenKey
|
|
398
|
+
// expired, keep-alive gave up, or the active REST probe asked for a
|
|
399
|
+
// resync). Prefer terminate() over close(): `ws.close(4001)` starts a
|
|
400
|
+
// closing HANDSHAKE and the `ws` library waits up to ~30s for the
|
|
401
|
+
// server's reciprocal close frame before it emits 'close' — on a truly
|
|
402
|
+
// dead TCP path that stalls the whole reconnect for the timeout.
|
|
403
|
+
// terminate() drops the socket immediately (emits 'close' with code
|
|
404
|
+
// 1006), so the reconnect fires right away. Same reasoning as the
|
|
405
|
+
// intel-side routed-WS watchdog (binance-routed-ws.ts:
|
|
406
|
+
// `ws.terminate()`). Fall back to close() when the socket doesn't
|
|
407
|
+
// expose terminate() (a browser/test socket without it).
|
|
408
|
+
const sock = this.ws;
|
|
409
|
+
try {
|
|
410
|
+
if (typeof sock.terminate === 'function')
|
|
411
|
+
sock.terminate();
|
|
412
|
+
else
|
|
413
|
+
sock.close(4001, reason);
|
|
414
|
+
}
|
|
415
|
+
catch { /* best-effort — the 'close' handler still runs cleanup once */ }
|
|
416
|
+
// Do NOT null this.ws here — the close handler needs it to pass the
|
|
417
|
+
// `ws === socket` identity check and run cleanup exactly once.
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// No socket in hand — schedule reconnect directly.
|
|
421
|
+
if (this.status !== 'reconnecting') {
|
|
422
|
+
this.scheduleReconnect(reason);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
startWatchdog() {
|
|
426
|
+
// Fire the check every 30s — a 6-min threshold doesn't need finer
|
|
427
|
+
// resolution, and a less chatty interval cuts log noise during the steady
|
|
428
|
+
// state where the watchdog should never fire.
|
|
429
|
+
this.stopWatchdog();
|
|
430
|
+
this.watchdogInterval = this.timers.setInterval(() => {
|
|
431
|
+
if (this.status !== 'open' || this.stopRequested)
|
|
432
|
+
return;
|
|
433
|
+
const now = this.timers.now();
|
|
434
|
+
// Compare against `lastFrameAt` — the most recent INCOMING FRAME of any
|
|
435
|
+
// kind, including server pings (every 3 min on the Binance user-data
|
|
436
|
+
// stream). The earlier design compared against `lastEventAt` (message
|
|
437
|
+
// frames only) and false-fired ~200×/day on idle accounts because
|
|
438
|
+
// Binance pushes no message frames between events on a quiet position.
|
|
439
|
+
const last = this.lastFrameAt ?? this.lastConnectedAt ?? now;
|
|
440
|
+
const idle = now - last;
|
|
441
|
+
if (idle > this.opts.staleEventWatchdogMs) {
|
|
442
|
+
logger.warn(TAG, `staleness watchdog fired: idleMs=${idle} > ${this.opts.staleEventWatchdogMs} (no frames — likely dead socket)`);
|
|
443
|
+
this.emit('staleForcedReconnect', idle);
|
|
444
|
+
this.forceReconnect('stale_event_watchdog');
|
|
445
|
+
}
|
|
446
|
+
}, 30_000);
|
|
447
|
+
}
|
|
448
|
+
/** External trigger for a reconnect — used by the active REST probe when
|
|
449
|
+
* it detects a fill that the WS missed. Same plumbing as the watchdog
|
|
450
|
+
* path; the close handler runs the reconnect schedule. */
|
|
451
|
+
requestReconnect(reason) {
|
|
452
|
+
this.forceReconnect(reason);
|
|
453
|
+
}
|
|
454
|
+
stopWatchdog() {
|
|
455
|
+
if (this.watchdogInterval) {
|
|
456
|
+
this.timers.clearInterval(this.watchdogInterval);
|
|
457
|
+
this.watchdogInterval = null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
setStatus(next) {
|
|
461
|
+
this.status = next;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// ---- Frame parsers ----
|
|
465
|
+
function num(v) {
|
|
466
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
467
|
+
return v;
|
|
468
|
+
if (typeof v === 'string') {
|
|
469
|
+
const n = Number(v);
|
|
470
|
+
if (Number.isFinite(n))
|
|
471
|
+
return n;
|
|
472
|
+
}
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
function str(v) {
|
|
476
|
+
return typeof v === 'string' ? v : String(v ?? '');
|
|
477
|
+
}
|
|
478
|
+
function optStr(v) {
|
|
479
|
+
if (v == null)
|
|
480
|
+
return undefined;
|
|
481
|
+
return typeof v === 'string' ? v : String(v);
|
|
482
|
+
}
|
|
483
|
+
const IDENTITY = (s) => s;
|
|
484
|
+
export function parseAccountUpdate(obj, translateSymbol = IDENTITY) {
|
|
485
|
+
const a = obj.a;
|
|
486
|
+
if (!a)
|
|
487
|
+
return null;
|
|
488
|
+
const positionsRaw = Array.isArray(a.P) ? a.P : [];
|
|
489
|
+
const balancesRaw = Array.isArray(a.B) ? a.B : [];
|
|
490
|
+
const positions = positionsRaw.map((p) => {
|
|
491
|
+
const qtySigned = num(p.pa);
|
|
492
|
+
const positionSide = str(p.ps);
|
|
493
|
+
// In one-way mode, side is derived from the sign of pa.
|
|
494
|
+
// In hedge mode, ps is LONG or SHORT directly.
|
|
495
|
+
let side;
|
|
496
|
+
if (positionSide === 'LONG')
|
|
497
|
+
side = qtySigned > 0 ? 'long' : 'flat';
|
|
498
|
+
else if (positionSide === 'SHORT')
|
|
499
|
+
side = qtySigned < 0 ? 'short' : 'flat';
|
|
500
|
+
else {
|
|
501
|
+
if (qtySigned > 0)
|
|
502
|
+
side = 'long';
|
|
503
|
+
else if (qtySigned < 0)
|
|
504
|
+
side = 'short';
|
|
505
|
+
else
|
|
506
|
+
side = 'flat';
|
|
507
|
+
}
|
|
508
|
+
const mtRaw = str(p.mt).toLowerCase();
|
|
509
|
+
const marginType = mtRaw === 'isolated' ? 'isolated' : 'cross';
|
|
510
|
+
return {
|
|
511
|
+
symbol: translateSymbol(str(p.s)),
|
|
512
|
+
side,
|
|
513
|
+
quantity: Math.abs(qtySigned),
|
|
514
|
+
entryPrice: num(p.ep),
|
|
515
|
+
unrealizedPnl: num(p.up),
|
|
516
|
+
marginType,
|
|
517
|
+
positionSide,
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
const balances = balancesRaw.map((b) => ({
|
|
521
|
+
asset: str(b.a),
|
|
522
|
+
walletBalance: num(b.wb),
|
|
523
|
+
crossWallet: num(b.cw),
|
|
524
|
+
balanceChange: num(b.bc),
|
|
525
|
+
}));
|
|
526
|
+
return {
|
|
527
|
+
eventTime: num(obj.E),
|
|
528
|
+
transactionTime: num(obj.T),
|
|
529
|
+
reason: str(a.m),
|
|
530
|
+
positions,
|
|
531
|
+
balances,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
export function parseOrderUpdate(obj, translateSymbol = IDENTITY) {
|
|
535
|
+
const o = obj.o;
|
|
536
|
+
if (!o)
|
|
537
|
+
return null;
|
|
538
|
+
const orderStatus = str(o.X);
|
|
539
|
+
let status;
|
|
540
|
+
switch (orderStatus) {
|
|
541
|
+
case 'NEW':
|
|
542
|
+
case 'PARTIALLY_FILLED':
|
|
543
|
+
status = 'open';
|
|
544
|
+
break;
|
|
545
|
+
case 'FILLED':
|
|
546
|
+
status = 'closed';
|
|
547
|
+
break;
|
|
548
|
+
case 'CANCELED':
|
|
549
|
+
case 'EXPIRED':
|
|
550
|
+
case 'REJECTED':
|
|
551
|
+
case 'EXPIRED_IN_MATCH':
|
|
552
|
+
status = 'canceled';
|
|
553
|
+
break;
|
|
554
|
+
default:
|
|
555
|
+
status = 'open';
|
|
556
|
+
}
|
|
557
|
+
const typeRaw = str(o.o).toUpperCase();
|
|
558
|
+
let type;
|
|
559
|
+
switch (typeRaw) {
|
|
560
|
+
case 'LIMIT':
|
|
561
|
+
type = 'limit';
|
|
562
|
+
break;
|
|
563
|
+
case 'MARKET':
|
|
564
|
+
type = 'market';
|
|
565
|
+
break;
|
|
566
|
+
case 'STOP_MARKET':
|
|
567
|
+
type = 'stop_market';
|
|
568
|
+
break;
|
|
569
|
+
case 'TAKE_PROFIT_MARKET':
|
|
570
|
+
type = 'take_profit_market';
|
|
571
|
+
break;
|
|
572
|
+
default: type = 'other';
|
|
573
|
+
}
|
|
574
|
+
const sideRaw = str(o.S).toUpperCase();
|
|
575
|
+
const side = sideRaw === 'SELL' ? 'sell' : 'buy';
|
|
576
|
+
const origPrice = o.p != null && str(o.p) !== '' ? num(o.p) : null;
|
|
577
|
+
const avgPrice = o.ap != null && num(o.ap) > 0 ? num(o.ap) : null;
|
|
578
|
+
const lastFillPrice = o.L != null && num(o.L) > 0 ? num(o.L) : null;
|
|
579
|
+
// Audit-trail fields. `o.t` is Binance's stable trade id, present only on
|
|
580
|
+
// executionType='TRADE' (fill events); for NEW / CANCELED / EXPIRED it's
|
|
581
|
+
// either absent or 0. We store null in those cases so callers don't
|
|
582
|
+
// accidentally upsert with a meaningless id.
|
|
583
|
+
const tradeIdRaw = o.t;
|
|
584
|
+
const tradeId = tradeIdRaw != null && str(tradeIdRaw) !== '' && str(tradeIdRaw) !== '0'
|
|
585
|
+
? str(tradeIdRaw)
|
|
586
|
+
: null;
|
|
587
|
+
return {
|
|
588
|
+
eventTime: num(obj.E),
|
|
589
|
+
transactionTime: num(obj.T),
|
|
590
|
+
symbol: translateSymbol(str(o.s)),
|
|
591
|
+
orderId: str(o.i),
|
|
592
|
+
clientOrderId: optStr(o.c),
|
|
593
|
+
side,
|
|
594
|
+
type,
|
|
595
|
+
status,
|
|
596
|
+
executionType: str(o.x),
|
|
597
|
+
origQuantity: num(o.q),
|
|
598
|
+
cumulativeFilled: num(o.z),
|
|
599
|
+
lastFilledQty: num(o.l),
|
|
600
|
+
avgPrice,
|
|
601
|
+
origPrice,
|
|
602
|
+
lastFillPrice,
|
|
603
|
+
commission: num(o.n),
|
|
604
|
+
commissionAsset: str(o.N),
|
|
605
|
+
timeInForce: str(o.f),
|
|
606
|
+
tradeId,
|
|
607
|
+
reduceOnly: o.R === true,
|
|
608
|
+
maker: o.m === true,
|
|
609
|
+
realizedPnl: num(o.rp),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
export function parseMarginCall(obj, translateSymbol = IDENTITY) {
|
|
613
|
+
const positionsRaw = Array.isArray(obj.p) ? obj.p : [];
|
|
614
|
+
return {
|
|
615
|
+
eventTime: num(obj.E),
|
|
616
|
+
crossWallet: num(obj.cw),
|
|
617
|
+
positions: positionsRaw.map((p) => ({
|
|
618
|
+
symbol: translateSymbol(str(p.s)),
|
|
619
|
+
positionSide: str(p.ps),
|
|
620
|
+
positionAmt: num(p.pa),
|
|
621
|
+
marginType: str(p.mt).toLowerCase() === 'isolated' ? 'isolated' : 'cross',
|
|
622
|
+
isolatedWallet: num(p.iw),
|
|
623
|
+
markPrice: num(p.mp),
|
|
624
|
+
unrealizedPnl: num(p.up),
|
|
625
|
+
maintMargin: num(p.mm),
|
|
626
|
+
})),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
export function parseAlgoUpdate(obj, translateSymbol = IDENTITY) {
|
|
630
|
+
// Field shape verified 2026-05-15 against the authoritative Binance doc
|
|
631
|
+
// (developers.binance.com → USD-M Futures → User Data Streams → Event
|
|
632
|
+
// Algo Order Update) AND a raw prod ALGO_UPDATE capture. The earlier
|
|
633
|
+
// implementation guessed `ao`/`a` wrapper + `c`/`sp`/`as`/`r` keys — ALL
|
|
634
|
+
// wrong; it parsed nothing. Binance wraps the algo-order detail under `o`:
|
|
635
|
+
// { "e":"ALGO_UPDATE","T","E","o":{ caid, aid, at, o, s, S, ps, f, q,
|
|
636
|
+
// X, ai, ap, aq, act, tp, p, V, wt, pm, cp, pP, R, tt, gtd, rm } }
|
|
637
|
+
// where (doc-exact):
|
|
638
|
+
// caid = Client Algo Id aid = Algo Id s = Symbol
|
|
639
|
+
// S = Side o = Order Type X = Algo Status
|
|
640
|
+
// tp = Trigger Price q = Quantity R = Reduce Only
|
|
641
|
+
// f = Time In Force V = STP Mode rm = Failure Reason
|
|
642
|
+
// ai = Order Id (matching-engine order id, NOT the algo id)
|
|
643
|
+
const o = obj.o;
|
|
644
|
+
if (!o)
|
|
645
|
+
return null;
|
|
646
|
+
const rawSym = optStr(o.s);
|
|
647
|
+
if (!rawSym)
|
|
648
|
+
return null;
|
|
649
|
+
const sideRaw = str(o.S).toUpperCase();
|
|
650
|
+
const side = sideRaw === 'SELL' ? 'sell' : 'buy';
|
|
651
|
+
// Algo Status `X`: NEW / TRIGGERING / TRIGGERED / FINISHED / CANCELED /
|
|
652
|
+
// REJECTED / EXPIRED. Semantics that matter downstream (doc-confirmed):
|
|
653
|
+
// CANCELED = MANUALLY canceled (an explicit cancelOrder from us)
|
|
654
|
+
// EXPIRED = SYSTEM-canceled (e.g. GTE_GTC conditional + position closed
|
|
655
|
+
// → Binance auto-cancels the surviving sibling). EXPECTED.
|
|
656
|
+
const statusRaw = str(o.X).toUpperCase();
|
|
657
|
+
// Trigger price is `tp` (doc-exact). `p` is the (limit) order price, 0 for
|
|
658
|
+
// market-type triggers.
|
|
659
|
+
const trigRaw = o.tp;
|
|
660
|
+
const triggerPrice = trigRaw != null && num(trigRaw) > 0 ? num(trigRaw) : null;
|
|
661
|
+
return {
|
|
662
|
+
eventTime: num(obj.E),
|
|
663
|
+
transactionTime: num(obj.T),
|
|
664
|
+
symbol: translateSymbol(rawSym),
|
|
665
|
+
algoId: str(o.aid ?? ''),
|
|
666
|
+
clientAlgoId: optStr(o.caid) ?? null,
|
|
667
|
+
side,
|
|
668
|
+
orderType: str(o.o ?? '').toUpperCase(),
|
|
669
|
+
status: statusRaw,
|
|
670
|
+
triggerPrice,
|
|
671
|
+
quantity: num(o.q),
|
|
672
|
+
reduceOnly: o.R === true,
|
|
673
|
+
// `rm` = Failure Reason (doc-exact). Present on REJECTED/EXPIRED.
|
|
674
|
+
reason: optStr(o.rm),
|
|
675
|
+
raw: obj,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
export function parseAccountConfigUpdate(obj, translateSymbol = IDENTITY) {
|
|
679
|
+
const ac = obj.ac; // leverage
|
|
680
|
+
const ai = obj.ai; // multi-asset mode
|
|
681
|
+
if (ac) {
|
|
682
|
+
const rawSym = optStr(ac.s);
|
|
683
|
+
return {
|
|
684
|
+
eventTime: num(obj.E),
|
|
685
|
+
kind: 'leverage',
|
|
686
|
+
symbol: rawSym ? translateSymbol(rawSym) : undefined,
|
|
687
|
+
leverage: num(ac.l),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
if (ai) {
|
|
691
|
+
return {
|
|
692
|
+
eventTime: num(obj.E),
|
|
693
|
+
kind: 'multi_asset_mode',
|
|
694
|
+
multiAssetMode: Boolean(ai.j),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
eventTime: num(obj.E),
|
|
699
|
+
kind: 'other',
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function hash8(key) {
|
|
703
|
+
return key.length > 8 ? key.slice(0, 8) : '(short)';
|
|
704
|
+
}
|