@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,409 @@
|
|
|
1
|
+
// Process-wide Binance rate-limit / IP-ban gate.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists (2026-05-15 incident): the plugin runs ~6 independent
|
|
4
|
+
// Binance pollers (LiveBalanceEnricher, position reconciler, bracket
|
|
5
|
+
// reconciler, user-data active probe, deposit tracker, public ticker/OHLCV)
|
|
6
|
+
// plus agent-driven tool calls. NONE of them shared any ban awareness. When
|
|
7
|
+
// Binance returned a 418 (IP ban), every poller kept firing on its own
|
|
8
|
+
// cadence, and since Binance EXTENDS an IP ban on every request received
|
|
9
|
+
// DURING the ban, the system never recovered on its own — a
|
|
10
|
+
// self-perpetuating lockout that zeroed the dashboard and blinded the agent
|
|
11
|
+
// for the whole window (REST dead; only the user-data WS kept working,
|
|
12
|
+
// which is exactly what Binance's error message tells you to use).
|
|
13
|
+
//
|
|
14
|
+
// This module is a SINGLETON (module-level state) deliberately: the ban is
|
|
15
|
+
// per-IP, not per-adapter-instance, so the gate must be shared across every
|
|
16
|
+
// BinancePrivateApi / BinancePublicApi instance in the process. The
|
|
17
|
+
// per-instance BinanceRateLimiter (rate-limiter.ts) tracks weight windows
|
|
18
|
+
// but has no ban model and is not consulted before every call — this gate
|
|
19
|
+
// is the hard stop that complements it.
|
|
20
|
+
//
|
|
21
|
+
// Binance taxonomy (USD-M Futures, General Info docs):
|
|
22
|
+
// - 429 / -1003 "Too many requests": soft rate-limit violation, no ban yet.
|
|
23
|
+
// - 418: auto-ban for CONTINUING to send after a 429. Message carries an
|
|
24
|
+
// explicit `banned until <epoch-ms>` we must respect — guessing a
|
|
25
|
+
// shorter window just re-bans and extends it.
|
|
26
|
+
//
|
|
27
|
+
// Integration contract (see binance-private.ts / binance-public.ts):
|
|
28
|
+
// - `assertNotBanned(ctx)` is called as the FIRST line inside each
|
|
29
|
+
// fetch/order method's existing try-block. While banned it throws a
|
|
30
|
+
// `BinanceBannedError`, which the method's EXISTING catch absorbs into
|
|
31
|
+
// its existing contract (return null / [] / rethrow) — so callers see
|
|
32
|
+
// the same shape they'd see for any failed fetch, but ZERO network
|
|
33
|
+
// traffic is generated during the ban. That's what lets the ban expire.
|
|
34
|
+
// - `noteBinanceError(err)` is called in each catch to arm/extend the gate.
|
|
35
|
+
// - `noteSuccess()` is called after a clean call to relax the soft-429
|
|
36
|
+
// backoff ladder.
|
|
37
|
+
import { logger } from '../logger.js';
|
|
38
|
+
const TAG = 'binance-ban-gate';
|
|
39
|
+
/** Soft-429 exponential backoff schedule (ms). Advance one step per
|
|
40
|
+
* consecutive 429; a clean call resets to step -1 (→ step 0 on next hit).
|
|
41
|
+
* Capped so we still probe ~every 2 min — enough to notice the limit
|
|
42
|
+
* clearing without climbing back toward a 418. Mirrors the skill-side
|
|
43
|
+
* poller circuit-breaker for behavioural consistency. */
|
|
44
|
+
const RATE_LIMIT_BACKOFF_MS = [5_000, 15_000, 45_000, 90_000, 120_000];
|
|
45
|
+
/** Slack added on top of Binance's stated `banned until` before we resume —
|
|
46
|
+
* clock skew + margin so the first post-ban call doesn't land on the
|
|
47
|
+
* boundary and immediately re-ban. */
|
|
48
|
+
const BAN_RESUME_SLACK_MS = 8_000;
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Proactive weight-budget pacer (prevention half — 2026-05-15).
|
|
51
|
+
//
|
|
52
|
+
// The reactive 418/429 backoff above only engages AFTER Binance has already
|
|
53
|
+
// rate-limited us. The recurring incident driver is the agent heartbeat
|
|
54
|
+
// SCAN burst: scan_pairs across the configured symbol set + per-candidate
|
|
55
|
+
// drill-downs fire dozens of fetchTicker/fetchOrderBook/fetchOHLCV calls within seconds,
|
|
56
|
+
// spiking the 1-min IP weight from baseline to 2000-2400 (Binance cap 2400)
|
|
57
|
+
// and tripping 418 at the window edge — every heartbeat.
|
|
58
|
+
//
|
|
59
|
+
// This is a per-IP, process-wide budget (every BinancePrivateApi /
|
|
60
|
+
// BinancePublicApi call already funnels through assertNotBanned), so the
|
|
61
|
+
// pacer lives here. It self-limits at a SOFT ceiling well below Binance's
|
|
62
|
+
// hard 2400 cap: once the rolling 1-min estimate would cross the ceiling,
|
|
63
|
+
// non-order reads fast-fail (same proven absorbed-by-existing-catch contract
|
|
64
|
+
// as the ban error) until the 1-min window rolls. Result: the burst is
|
|
65
|
+
// shed BEFORE it can reach 418 territory — the ban becomes structurally
|
|
66
|
+
// unreachable in normal operation instead of merely recovered-from.
|
|
67
|
+
//
|
|
68
|
+
// Local estimation (incremented on every allowed call) leads the headers,
|
|
69
|
+
// which lag by in-flight concurrency; noteResponseWeight() reconciles the
|
|
70
|
+
// estimate up to Binance truth when a response header is available.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
const WEIGHT_WINDOW_MS = 60_000;
|
|
73
|
+
/** Binance USD-M Futures hard IP cap is 2400 weight / 1-min. We self-limit
|
|
74
|
+
* under it with ~400 weight of headroom (absorbs in-flight concurrency +
|
|
75
|
+
* header-vs-local lag; reactive 418/429 gate is the hard backstop).
|
|
76
|
+
*
|
|
77
|
+
* 2026-05-15 calibration (evidence-based, NOT a guess): after the measured
|
|
78
|
+
* efficiency pass — audit no longer sweeps unscoped when flat, algoRefresh
|
|
79
|
+
* 15s→60s, skill ticker/ohlcv/market-structure cadence cuts, all verified
|
|
80
|
+
* via the per-context + skill poll-window instrumentation — the residual
|
|
81
|
+
* flat-state IP weight is ~1650-1750/min of IRREDUCIBLE legitimate live-
|
|
82
|
+
* trading polling (positions, balance, open-orders, OHLCV, income,
|
|
83
|
+
* transfers, the 60s safety reconcilers). 1600 was below that floor, so
|
|
84
|
+
* the pacer shed legitimate heartbeat reads indefinitely and blinded the
|
|
85
|
+
* agent. Measured Binance x-mbx-used-weight-1m peaks ~1750-1900 << 2400,
|
|
86
|
+
* so 2000 stops the false shedding while keeping ~400 real headroom.
|
|
87
|
+
* Env-overridable for emergency tuning without a code change. */
|
|
88
|
+
const WEIGHT_SOFT_CEILING = (() => {
|
|
89
|
+
const raw = Number(process.env.RC_BINANCE_WEIGHT_CEILING);
|
|
90
|
+
return Number.isFinite(raw) && raw >= 200 && raw <= 2400 ? raw : 2000;
|
|
91
|
+
})();
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Safety-reserve priority tiering (2026-05-16).
|
|
94
|
+
//
|
|
95
|
+
// The pacer above sheds ALL non-order reads uniformly at the soft ceiling.
|
|
96
|
+
// That let the recurring real-money failure persist: a heartbeat SCAN burst
|
|
97
|
+
// (fetchTicker/fetchOHLCV/fetchOrderBook across the symbol set) pins the 1-min IP
|
|
98
|
+
// weight at the ceiling, so the bracket audit's getOpenOrders /
|
|
99
|
+
// queryAlgoOrder / fetchPositions were shed every heartbeat and the agent
|
|
100
|
+
// went blind on live-position protection for HOURS (2026-05-14 ATOM →
|
|
101
|
+
// 2026-05-16 INJ 04:29→05:54 straight). Absolute weight cuts (Option A
|
|
102
|
+
// income poll; 983cef0 reconciler/deposit) lower the floor; this makes the
|
|
103
|
+
// starvation STRUCTURALLY impossible regardless of future polling creep.
|
|
104
|
+
//
|
|
105
|
+
// Mechanism: low-priority cosmetic/market-data contexts (SHED_FIRST) are
|
|
106
|
+
// paced against COSMETIC_SOFT_CEILING; the reserve band
|
|
107
|
+
// [COSMETIC_SOFT_CEILING, WEIGHT_SOFT_CEILING] is reachable ONLY by
|
|
108
|
+
// safety-critical reads. This is strictly additive — nothing is made more
|
|
109
|
+
// lenient than today; only SHED_FIRST contexts shed earlier, so a safety
|
|
110
|
+
// read can never be newly starved by this change.
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/** Weight reserved exclusively for safety-critical reads. Env-overridable;
|
|
113
|
+
* clamp 0–1000 (0 disables tiering → exact pre-2026-05-16 behaviour). */
|
|
114
|
+
const SAFETY_RESERVE = (() => {
|
|
115
|
+
const raw = Number(process.env.RC_BINANCE_SAFETY_RESERVE);
|
|
116
|
+
return Number.isFinite(raw) && raw >= 0 && raw <= 1000 ? raw : 400;
|
|
117
|
+
})();
|
|
118
|
+
/** Effective ceiling for SHED_FIRST contexts. Floored at 200 so a large
|
|
119
|
+
* reserve can't drive it to zero and shed cosmetic reads forever. */
|
|
120
|
+
const COSMETIC_SOFT_CEILING = Math.max(200, WEIGHT_SOFT_CEILING - SAFETY_RESERVE);
|
|
121
|
+
/** Approx Binance request weight per call context (the string passed to
|
|
122
|
+
* assertNotBanned). Conservative — over-estimating paces slightly early,
|
|
123
|
+
* which is the safe direction. Unknown contexts default to 5. */
|
|
124
|
+
const CONTEXT_WEIGHT = {
|
|
125
|
+
fetchTicker: 2,
|
|
126
|
+
fetchTickerRaw: 2,
|
|
127
|
+
fetchOrderBook: 5,
|
|
128
|
+
fetchOHLCV: 2,
|
|
129
|
+
fetchFundingRate: 1,
|
|
130
|
+
fetchOpenInterest: 1,
|
|
131
|
+
fetchBalance: 5,
|
|
132
|
+
fetchPositions: 5,
|
|
133
|
+
// BinancePrivateApi.fetchOpenOrders merges TWO REST calls (regular
|
|
134
|
+
// /fapi/v1/openOrders + algo /fapi/v1/openAlgoOrders), each 1 scoped /
|
|
135
|
+
// 40 unscoped (doc-verified developers.binance.com 2026-05-15). The call
|
|
136
|
+
// site passes the scoped/unscoped context so this is exact, not a guess.
|
|
137
|
+
fetchOpenOrders: 2, // 1 regular + 1 algo (symbol given)
|
|
138
|
+
fetchOpenOrdersAll: 80, // 40 regular + 40 algo (symbol omitted)
|
|
139
|
+
fetchMyTrades: 5,
|
|
140
|
+
fetchOrder: 1, // GET /fapi/v1/order (also used for by-clientId)
|
|
141
|
+
// GET /fapi/v1/algoOrder — single conditional-order status by clientAlgoId.
|
|
142
|
+
// Weight 1, doc-verified developers.binance.com/.../Query-Algo-Order
|
|
143
|
+
// (2026-05-15). The per-order bracket-spiral disambiguator: a successful
|
|
144
|
+
// empty /fapi/v1/openAlgoOrders is NOT proof a leg is gone; this asks
|
|
145
|
+
// about ONE exact order id. Same weight as the scoped snapshot, but it
|
|
146
|
+
// answers the actual question instead of inferring from absence.
|
|
147
|
+
queryAlgoOrder: 1,
|
|
148
|
+
fetchPositionMode: 30, // GET /fapi/v1/positionSide/dual (doc-verified)
|
|
149
|
+
fetchTransfers: 30,
|
|
150
|
+
fetchTodayIncomeBreakdown: 30,
|
|
151
|
+
// Same /fapi/v1/income call (weight 30) but the never-paced variant the
|
|
152
|
+
// boot + UTC-day-rollover KPI anchor uses. Still counted so the estimate
|
|
153
|
+
// stays honest; just not shed (see NEVER_PACE doc).
|
|
154
|
+
fetchTodayIncomeBreakdownForced: 30,
|
|
155
|
+
fetchRecentTradedSymbols: 30,
|
|
156
|
+
createListenKey: 1,
|
|
157
|
+
keepAliveListenKey: 1,
|
|
158
|
+
closeListenKey: 1,
|
|
159
|
+
};
|
|
160
|
+
/** Contexts that consume weight but must NEVER be blocked by the pacer:
|
|
161
|
+
* order/cancel paths (must always reach the exchange — same invariant as
|
|
162
|
+
* the ban gate), listenKey lifecycle (a paced-out keep-alive could let the
|
|
163
|
+
* user-data WS key expire; weight is trivial = 1), and the FORCED KPI
|
|
164
|
+
* income anchor. The latter (`fetchTodayIncomeBreakdownForced`) is the
|
|
165
|
+
* /fapi/v1/income call made only at boot and at UTC-day rollover — it
|
|
166
|
+
* establishes `sessionStartNav = wallet_at_midnight`, the single value the
|
|
167
|
+
* ENTIRE Day-P&L / Realized KPI chain is derived from. It runs at most
|
|
168
|
+
* ~twice a day, so pacing it buys ~0 weight relief but, if shed, the
|
|
169
|
+
* failed fetch used to corrupt the anchor to wallet_now and zero Day P&L
|
|
170
|
+
* (2026-05-16 KPI incident). Routine same-day income refreshes stay
|
|
171
|
+
* SHED_FIRST/cosmetic (the anchor is invariant within a UTC day, so a
|
|
172
|
+
* paced miss there is a harmless no-op). All NEVER_PACE contexts still
|
|
173
|
+
* COUNT toward the window so the estimate stays honest. */
|
|
174
|
+
const NEVER_PACE = new Set([
|
|
175
|
+
'createOrder', 'cancelOrder', 'cancelAllOrders', 'cancelOrderByClientId',
|
|
176
|
+
'createBracketOrder', 'createListenKey', 'keepAliveListenKey', 'closeListenKey',
|
|
177
|
+
'fetchTodayIncomeBreakdownForced',
|
|
178
|
+
]);
|
|
179
|
+
/** Low-priority contexts that shed FIRST under weight pressure (paced
|
|
180
|
+
* against COSMETIC_SOFT_CEILING, not the full ceiling). Two safe-to-shed
|
|
181
|
+
* groups: dashboard/KPI anchors that explicitly tolerate minute-scale
|
|
182
|
+
* staleness (income / transfers / recent-symbols) and public market-data
|
|
183
|
+
* scan reads that have a WS alternative and are re-fetched next heartbeat.
|
|
184
|
+
*
|
|
185
|
+
* DELIBERATELY EXCLUDES the bracket/position safety reads —
|
|
186
|
+
* fetchOpenOrders, fetchOpenOrdersAll, fetchPositions, queryAlgoOrder,
|
|
187
|
+
* fetchOrder, fetchMyTrades, fetchBalance, fetchPositionMode — which keep
|
|
188
|
+
* the full WEIGHT_SOFT_CEILING (today's behaviour, never made stricter
|
|
189
|
+
* here). fetchBalance/fetchMyTrades stay protected because they feed
|
|
190
|
+
* risk/sizing + fill reconciliation, not just display. */
|
|
191
|
+
const SHED_FIRST = new Set([
|
|
192
|
+
'fetchTodayIncomeBreakdown', 'fetchTransfers', 'fetchRecentTradedSymbols',
|
|
193
|
+
'fetchTicker', 'fetchTickerRaw', 'fetchOHLCV', 'fetchOrderBook',
|
|
194
|
+
'fetchFundingRate', 'fetchOpenInterest',
|
|
195
|
+
]);
|
|
196
|
+
let weightWindowStart = 0;
|
|
197
|
+
let weightUsed = 0;
|
|
198
|
+
/** Throttle the "weight pacer engaged" log to once per window. */
|
|
199
|
+
let pacerLogged = false;
|
|
200
|
+
/** Per-context attribution for the current window (observability only —
|
|
201
|
+
* NOT consulted by any decision). Lets prod prove WHICH callers drive the
|
|
202
|
+
* weight so durable cadence cuts target the real consumer instead of a
|
|
203
|
+
* guess. `allowed` = weight let through; `shed` = paced-out call count. */
|
|
204
|
+
const weightByContext = new Map();
|
|
205
|
+
const shedByContext = new Map();
|
|
206
|
+
function rollWeightWindowIfDue(now) {
|
|
207
|
+
if (now - weightWindowStart >= WEIGHT_WINDOW_MS) {
|
|
208
|
+
if (weightUsed > 0 || shedByContext.size > 0) {
|
|
209
|
+
const fmt = (m, n) => [...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, n)
|
|
210
|
+
.map(([k, v]) => `${k}=${v}`).join(' ');
|
|
211
|
+
// `weightUsed` (=total) is reconciled UP to Binance's IP-wide
|
|
212
|
+
// x-mbx-used-weight-1m header (noteResponseWeight); `weightByContext`
|
|
213
|
+
// only sees calls that passed through THIS gate (plugin reads). The
|
|
214
|
+
// difference is weight Binance charged this IP that the gate never
|
|
215
|
+
// saw — the skill process's own REST polls, ccxt-internal calls
|
|
216
|
+
// (loadMarkets/fetchTime), or anything else sharing the IP/key.
|
|
217
|
+
// Naming it explicitly is the whole point of the 2026-05-18
|
|
218
|
+
// "instrument, don't guess" pass: the per-context map summed to
|
|
219
|
+
// ~275 while total was ~1900 and the ~1600 gap was invisible,
|
|
220
|
+
// folded silently into `total`. Print ALL contexts (no top-N
|
|
221
|
+
// truncation) so no real consumer can hide either.
|
|
222
|
+
const gatedSum = [...weightByContext.values()].reduce((s, v) => s + v, 0);
|
|
223
|
+
const ungatedIp = Math.max(0, weightUsed - gatedSum);
|
|
224
|
+
logger.info(TAG, `weight window summary: total=${weightUsed}/${WEIGHT_SOFT_CEILING} ` +
|
|
225
|
+
`gated=${gatedSum} ungated_ip=${ungatedIp} ` +
|
|
226
|
+
`allowed[${fmt(weightByContext, 100)}] shed[${fmt(shedByContext, 100)}]`);
|
|
227
|
+
}
|
|
228
|
+
weightWindowStart = now;
|
|
229
|
+
weightUsed = 0;
|
|
230
|
+
pacerLogged = false;
|
|
231
|
+
weightByContext.clear();
|
|
232
|
+
shedByContext.clear();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function contextWeight(ctx) {
|
|
236
|
+
return CONTEXT_WEIGHT[ctx] ?? 5;
|
|
237
|
+
}
|
|
238
|
+
/** Thrown by `assertNotBanned` while the gate is closed. Carries the
|
|
239
|
+
* remaining cooldown so callers/telemetry can reason about it. Methods
|
|
240
|
+
* catch this in their existing catch block — it is NOT meant to escape
|
|
241
|
+
* the API layer with special handling; it degrades to the method's normal
|
|
242
|
+
* failure contract. */
|
|
243
|
+
export class BinanceBannedError extends Error {
|
|
244
|
+
retryAfterMs;
|
|
245
|
+
constructor(retryAfterMs, context) {
|
|
246
|
+
super(`Binance rate-limit/IP-ban gate is OPEN (retry in ~${Math.ceil(retryAfterMs / 1000)}s) — skipping ${context} to let the ban expire instead of extending it`);
|
|
247
|
+
this.retryAfterMs = retryAfterMs;
|
|
248
|
+
this.name = 'BinanceBannedError';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Thrown by the proactive weight pacer. Subclass of BinanceBannedError so
|
|
252
|
+
* every `instanceof BinanceBannedError` check and every method's existing
|
|
253
|
+
* catch keep working unchanged — it degrades to the same null/[]/throw
|
|
254
|
+
* contract (treated exactly like a transient fetch failure). The distinct
|
|
255
|
+
* name lets journal/telemetry tell prevention apart from a real IP ban. */
|
|
256
|
+
export class BinanceWeightPacedError extends BinanceBannedError {
|
|
257
|
+
constructor(retryAfterMs, context) {
|
|
258
|
+
super(retryAfterMs, context);
|
|
259
|
+
this.name = 'BinanceWeightPacedError';
|
|
260
|
+
this.message =
|
|
261
|
+
`Binance weight pacer engaged (1-min window reached the ~${WEIGHT_SOFT_CEILING}/2400 ` +
|
|
262
|
+
`soft ceiling, retry in ~${Math.ceil(retryAfterMs / 1000)}s) — shedding ${context} ` +
|
|
263
|
+
`to stay below the 418 threshold instead of bursting into an IP ban`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let banUntilMs = 0;
|
|
267
|
+
let rateLimitStep = -1;
|
|
268
|
+
/** Throttles the "gate open, skipping" log to once per cooldown window so a
|
|
269
|
+
* multi-minute ban doesn't emit hundreds of identical lines. */
|
|
270
|
+
let cooldownLogged = false;
|
|
271
|
+
/** Fast pre-flight check. Call as the first line inside a Binance call's
|
|
272
|
+
* try-block. Throws `BinanceBannedError` (no network) while the gate is
|
|
273
|
+
* open. */
|
|
274
|
+
export function assertNotBanned(context) {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
if (now < banUntilMs) {
|
|
277
|
+
const remaining = banUntilMs - now;
|
|
278
|
+
if (!cooldownLogged) {
|
|
279
|
+
logger.warn(TAG, `gate OPEN — pausing ALL Binance calls for ~${Math.ceil(remaining / 1000)}s (418/429 backoff). First skipped: ${context}`);
|
|
280
|
+
cooldownLogged = true;
|
|
281
|
+
}
|
|
282
|
+
throw new BinanceBannedError(remaining, context);
|
|
283
|
+
}
|
|
284
|
+
if (cooldownLogged) {
|
|
285
|
+
logger.info(TAG, 'gate CLOSED — Binance calls resuming');
|
|
286
|
+
cooldownLogged = false;
|
|
287
|
+
}
|
|
288
|
+
// --- Proactive weight pacing (prevention; runs only when not banned) ---
|
|
289
|
+
rollWeightWindowIfDue(now);
|
|
290
|
+
const w = contextWeight(context);
|
|
291
|
+
if (!NEVER_PACE.has(context)) {
|
|
292
|
+
// Priority tiering: cosmetic/market-data reads (SHED_FIRST) pace against
|
|
293
|
+
// the lower COSMETIC_SOFT_CEILING so the reserve band up to the full
|
|
294
|
+
// ceiling stays exclusively available to safety-critical reads. Anything
|
|
295
|
+
// not in SHED_FIRST keeps the full ceiling — identical to pre-2026-05-16
|
|
296
|
+
// behaviour, never more lenient.
|
|
297
|
+
const isCosmetic = SHED_FIRST.has(context);
|
|
298
|
+
const effectiveCeiling = isCosmetic ? COSMETIC_SOFT_CEILING : WEIGHT_SOFT_CEILING;
|
|
299
|
+
if (weightUsed + w > effectiveCeiling) {
|
|
300
|
+
const retryAfterMs = Math.max(0, weightWindowStart + WEIGHT_WINDOW_MS - now);
|
|
301
|
+
if (!pacerLogged) {
|
|
302
|
+
logger.warn(TAG, `weight pacer ENGAGED — 1-min IP weight ~${weightUsed}/${WEIGHT_SOFT_CEILING} ` +
|
|
303
|
+
`(cosmetic ceiling ${COSMETIC_SOFT_CEILING}, hard cap 2400); shedding ` +
|
|
304
|
+
`${isCosmetic ? 'cosmetic' : 'non-order'} reads ~${Math.ceil(retryAfterMs / 1000)}s until the window rolls. First shed: ${context}`);
|
|
305
|
+
pacerLogged = true;
|
|
306
|
+
}
|
|
307
|
+
shedByContext.set(context, (shedByContext.get(context) ?? 0) + 1);
|
|
308
|
+
throw new BinanceWeightPacedError(retryAfterMs, context);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Allowed (or NEVER_PACE) → the request will go out; count its weight
|
|
312
|
+
// immediately so the estimate leads (not lags) the in-flight burst.
|
|
313
|
+
weightUsed += w;
|
|
314
|
+
weightByContext.set(context, (weightByContext.get(context) ?? 0) + w);
|
|
315
|
+
}
|
|
316
|
+
/** Inspect a failed call's error and arm/extend the gate on a Binance
|
|
317
|
+
* rate-limit signal. Non-rate-limit errors are ignored (normal transient
|
|
318
|
+
* handling stays in the caller). Safe to call with any error shape. */
|
|
319
|
+
export function noteBinanceError(err) {
|
|
320
|
+
// A BinanceBannedError means the gate was already open and we short-
|
|
321
|
+
// circuited — nothing to arm.
|
|
322
|
+
if (err instanceof BinanceBannedError)
|
|
323
|
+
return;
|
|
324
|
+
const msg = err instanceof Error ? err.message : typeof err === 'string' ? err : String(err);
|
|
325
|
+
// 418 — hard IP ban with an explicit expiry Binance gives us.
|
|
326
|
+
if (msg.includes('418') || /banned until/i.test(msg)) {
|
|
327
|
+
const m = /banned until (\d{10,})/i.exec(msg);
|
|
328
|
+
const stated = m ? Number(m[1]) : 0;
|
|
329
|
+
const base = Number.isFinite(stated) && stated > Date.now()
|
|
330
|
+
? stated
|
|
331
|
+
: Date.now() + RATE_LIMIT_BACKOFF_MS[RATE_LIMIT_BACKOFF_MS.length - 1];
|
|
332
|
+
const target = base + BAN_RESUME_SLACK_MS;
|
|
333
|
+
if (target > banUntilMs) {
|
|
334
|
+
banUntilMs = target;
|
|
335
|
+
cooldownLogged = false; // force a fresh "gate OPEN ~Ns" line
|
|
336
|
+
logger.error(TAG, `Binance 418 IP BAN — gate engaged, ALL Binance calls paused ~${Math.ceil((target - Date.now()) / 1000)}s (Binance-stated expiry +slack)`);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// 429 / -1003 — soft rate-limit, no ban yet. Exponential backoff so we
|
|
341
|
+
// stop climbing toward a 418.
|
|
342
|
+
if (msg.includes('429') ||
|
|
343
|
+
msg.includes('-1003') ||
|
|
344
|
+
/too many requests/i.test(msg)) {
|
|
345
|
+
rateLimitStep = Math.min(rateLimitStep + 1, RATE_LIMIT_BACKOFF_MS.length - 1);
|
|
346
|
+
const window = RATE_LIMIT_BACKOFF_MS[rateLimitStep];
|
|
347
|
+
const target = Date.now() + window;
|
|
348
|
+
if (target > banUntilMs) {
|
|
349
|
+
banUntilMs = target;
|
|
350
|
+
cooldownLogged = false;
|
|
351
|
+
logger.warn(TAG, `Binance 429 — gate backoff step ${rateLimitStep}, ALL Binance calls paused ${window / 1000}s`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Relax the soft-429 ladder after a clean call. (A hard 418 window is NOT
|
|
356
|
+
* shortened by this — only Binance's stated expiry clears it.) */
|
|
357
|
+
export function noteSuccess() {
|
|
358
|
+
if (rateLimitStep !== -1)
|
|
359
|
+
rateLimitStep = -1;
|
|
360
|
+
}
|
|
361
|
+
/** Reconcile the local weight estimate against Binance truth from a response
|
|
362
|
+
* header (`x-mbx-used-weight-1m`). Best-effort: callers pass whatever header
|
|
363
|
+
* bag they have; absent/garbage values are ignored. The local estimate
|
|
364
|
+
* leads (counts in-flight calls Binance hasn't tallied yet), so we take the
|
|
365
|
+
* MAX within the window — never let a lagging header lower our guard. A
|
|
366
|
+
* header far BELOW the estimate means Binance's minute rolled; re-anchor the
|
|
367
|
+
* window to Binance's clock so we don't shed for a stale local total. */
|
|
368
|
+
export function noteResponseWeight(headers) {
|
|
369
|
+
if (!headers)
|
|
370
|
+
return;
|
|
371
|
+
const raw = headers['x-mbx-used-weight-1m'] ?? headers['X-MBX-USED-WEIGHT-1M'];
|
|
372
|
+
if (raw == null)
|
|
373
|
+
return;
|
|
374
|
+
const actual = parseInt(String(raw), 10);
|
|
375
|
+
if (!Number.isFinite(actual) || actual < 0)
|
|
376
|
+
return;
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
rollWeightWindowIfDue(now);
|
|
379
|
+
if (actual + 50 < weightUsed) {
|
|
380
|
+
// Binance reports much less than our local tally → its 1-min counter
|
|
381
|
+
// reset on the exchange side. Re-anchor to Binance truth.
|
|
382
|
+
weightWindowStart = now;
|
|
383
|
+
weightUsed = actual;
|
|
384
|
+
pacerLogged = false;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (actual > weightUsed)
|
|
388
|
+
weightUsed = actual;
|
|
389
|
+
}
|
|
390
|
+
/** Test/diagnostic helper — current gate state. */
|
|
391
|
+
export function getBanGateState() {
|
|
392
|
+
return {
|
|
393
|
+
banUntilMs,
|
|
394
|
+
remainingMs: Math.max(0, banUntilMs - Date.now()),
|
|
395
|
+
rateLimitStep,
|
|
396
|
+
weightUsed,
|
|
397
|
+
weightCeiling: WEIGHT_SOFT_CEILING,
|
|
398
|
+
cosmeticCeiling: COSMETIC_SOFT_CEILING,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/** Test-only reset. Not used in production code paths. */
|
|
402
|
+
export function __resetBanGateForTests() {
|
|
403
|
+
banUntilMs = 0;
|
|
404
|
+
rateLimitStep = -1;
|
|
405
|
+
cooldownLogged = false;
|
|
406
|
+
weightWindowStart = 0;
|
|
407
|
+
weightUsed = 0;
|
|
408
|
+
pacerLogged = false;
|
|
409
|
+
}
|