@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.
Files changed (371) hide show
  1. package/.gitignore +8 -0
  2. package/audit/mode-transition-audit.d.ts +11 -0
  3. package/audit/mode-transition-audit.js +29 -0
  4. package/balance-utils.d.ts +36 -0
  5. package/balance-utils.js +98 -0
  6. package/bridge/bridge.d.ts +109 -0
  7. package/bridge/bridge.js +1036 -0
  8. package/bridge/config.d.ts +43 -0
  9. package/bridge/config.js +176 -0
  10. package/bridge/connector.d.ts +52 -0
  11. package/bridge/connector.js +293 -0
  12. package/bridge/event-replay-buffer.d.ts +43 -0
  13. package/bridge/event-replay-buffer.js +109 -0
  14. package/bridge/gateway/event-parser.d.ts +209 -0
  15. package/bridge/gateway/event-parser.js +793 -0
  16. package/bridge/gateway/gateway-config.d.ts +39 -0
  17. package/bridge/gateway/gateway-config.js +86 -0
  18. package/bridge/gateway/gateway-http-client.d.ts +50 -0
  19. package/bridge/gateway/gateway-http-client.js +165 -0
  20. package/bridge/gateway/gateway-ws-client.d.ts +116 -0
  21. package/bridge/gateway/gateway-ws-client.js +417 -0
  22. package/bridge/gateway/poller.d.ts +146 -0
  23. package/bridge/gateway/poller.js +505 -0
  24. package/bridge/gateway/tool-discovery.d.ts +25 -0
  25. package/bridge/gateway/tool-discovery.js +198 -0
  26. package/bridge/index.d.ts +2 -0
  27. package/bridge/index.js +253 -0
  28. package/bridge/logger.d.ts +2 -0
  29. package/bridge/logger.js +2 -0
  30. package/bridge/provider.d.ts +156 -0
  31. package/bridge/provider.js +2 -0
  32. package/bridge/providers/emergency-commands.d.ts +54 -0
  33. package/bridge/providers/emergency-commands.js +235 -0
  34. package/bridge/providers/gateway.d.ts +322 -0
  35. package/bridge/providers/gateway.js +2302 -0
  36. package/bridge/providers/mock.d.ts +37 -0
  37. package/bridge/providers/mock.js +385 -0
  38. package/bridge/providers/onboarding-commands.d.ts +83 -0
  39. package/bridge/providers/onboarding-commands.js +213 -0
  40. package/bridge/providers/risk-calculator.d.ts +96 -0
  41. package/bridge/providers/risk-calculator.js +369 -0
  42. package/bridge/setup.d.ts +32 -0
  43. package/bridge/setup.js +226 -0
  44. package/bridge/types.d.ts +584 -0
  45. package/bridge/types.js +50 -0
  46. package/bridge/utils/reconnect.d.ts +6 -0
  47. package/bridge/utils/reconnect.js +6 -0
  48. package/bridge/utils/skill-signing.d.ts +51 -0
  49. package/bridge/utils/skill-signing.js +138 -0
  50. package/bridge/utils/skill-version.d.ts +17 -0
  51. package/bridge/utils/skill-version.js +74 -0
  52. package/ccxt/binance-ban-gate.d.ts +47 -0
  53. package/ccxt/binance-ban-gate.js +409 -0
  54. package/ccxt/binance-private.d.ts +325 -0
  55. package/ccxt/binance-private.js +1415 -0
  56. package/ccxt/binance-public.d.ts +18 -0
  57. package/ccxt/binance-public.js +147 -0
  58. package/config/agent-config-client.d.ts +55 -0
  59. package/config/agent-config-client.js +145 -0
  60. package/config/agent-config-poller.d.ts +25 -0
  61. package/config/agent-config-poller.js +100 -0
  62. package/config/brackets-config.d.ts +22 -0
  63. package/config/brackets-config.js +58 -0
  64. package/config/gate-store.d.ts +18 -0
  65. package/config/gate-store.js +61 -0
  66. package/config/plugin-config-io.d.ts +181 -0
  67. package/config/plugin-config-io.js +84 -0
  68. package/config/position-review-config.d.ts +35 -0
  69. package/config/position-review-config.js +105 -0
  70. package/config/tool-gate.d.ts +53 -0
  71. package/config/tool-gate.js +125 -0
  72. package/config/user-data-stream-config.d.ts +85 -0
  73. package/config/user-data-stream-config.js +224 -0
  74. package/connector-supervisor.d.ts +36 -0
  75. package/connector-supervisor.js +149 -0
  76. package/exchange-adapter.d.ts +49 -0
  77. package/exchange-adapter.js +4 -0
  78. package/index.d.ts +30 -0
  79. package/index.js +2030 -0
  80. package/ingest/pending-entry-metadata.d.ts +52 -0
  81. package/ingest/pending-entry-metadata.js +182 -0
  82. package/ingest/position-auto-capture.d.ts +98 -0
  83. package/ingest/position-auto-capture.js +394 -0
  84. package/ingest/position-decisions-client.d.ts +318 -0
  85. package/ingest/position-decisions-client.js +296 -0
  86. package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  87. package/ingest/reconcile-db-vs-exchange.js +114 -0
  88. package/ingest/reconciler-cleanup.d.ts +37 -0
  89. package/ingest/reconciler-cleanup.js +147 -0
  90. package/ingest/rest-gap-filler.d.ts +191 -0
  91. package/ingest/rest-gap-filler.js +565 -0
  92. package/ingest/touched-symbols-store.d.ts +25 -0
  93. package/ingest/touched-symbols-store.js +96 -0
  94. package/ingest/trade-store-client.d.ts +40 -0
  95. package/ingest/trade-store-client.js +125 -0
  96. package/ingest/ws-ingest.d.ts +43 -0
  97. package/ingest/ws-ingest.js +126 -0
  98. package/learning/setup-family.d.ts +21 -0
  99. package/learning/setup-family.js +103 -0
  100. package/lifecycle/install-signal-handlers.d.ts +33 -0
  101. package/lifecycle/install-signal-handlers.js +112 -0
  102. package/lifecycle/shutdown-coordinator.d.ts +43 -0
  103. package/lifecycle/shutdown-coordinator.js +131 -0
  104. package/live/bracket-id.d.ts +18 -0
  105. package/live/bracket-id.js +81 -0
  106. package/live/bracket-ledger.d.ts +54 -0
  107. package/live/bracket-ledger.js +267 -0
  108. package/live/bracket-manager.d.ts +82 -0
  109. package/live/bracket-manager.js +478 -0
  110. package/live/bracket-params.d.ts +22 -0
  111. package/live/bracket-params.js +124 -0
  112. package/live/bracket-reconciler.d.ts +95 -0
  113. package/live/bracket-reconciler.js +573 -0
  114. package/live/bracket-types.d.ts +102 -0
  115. package/live/bracket-types.js +8 -0
  116. package/live/deposit-tracker.d.ts +62 -0
  117. package/live/deposit-tracker.js +97 -0
  118. package/live/emergency-controls.d.ts +32 -0
  119. package/live/emergency-controls.js +226 -0
  120. package/live/exchange-errors.d.ts +12 -0
  121. package/live/exchange-errors.js +130 -0
  122. package/live/exchange-info-cache.d.ts +35 -0
  123. package/live/exchange-info-cache.js +119 -0
  124. package/live/fact-subscriber.d.ts +78 -0
  125. package/live/fact-subscriber.js +182 -0
  126. package/live/intent-journal.d.ts +42 -0
  127. package/live/intent-journal.js +122 -0
  128. package/live/listen-key-manager.d.ts +70 -0
  129. package/live/listen-key-manager.js +169 -0
  130. package/live/live-adapter.d.ts +264 -0
  131. package/live/live-adapter.js +1665 -0
  132. package/live/live-balance-enricher.d.ts +32 -0
  133. package/live/live-balance-enricher.js +104 -0
  134. package/live/live-bracket-api.d.ts +13 -0
  135. package/live/live-bracket-api.js +20 -0
  136. package/live/live-state-store.d.ts +194 -0
  137. package/live/live-state-store.js +450 -0
  138. package/live/local-signal-service.d.ts +57 -0
  139. package/live/local-signal-service.js +146 -0
  140. package/live/local-strategy-evaluator.d.ts +62 -0
  141. package/live/local-strategy-evaluator.js +127 -0
  142. package/live/microstructure-assembler.d.ts +54 -0
  143. package/live/microstructure-assembler.js +148 -0
  144. package/live/order-poller.d.ts +29 -0
  145. package/live/order-poller.js +125 -0
  146. package/live/position-state-store.d.ts +83 -0
  147. package/live/position-state-store.js +237 -0
  148. package/live/proposal-decision-listener.d.ts +64 -0
  149. package/live/proposal-decision-listener.js +288 -0
  150. package/live/proposal-manager.d.ts +76 -0
  151. package/live/proposal-manager.js +140 -0
  152. package/live/rate-limiter.d.ts +47 -0
  153. package/live/rate-limiter.js +159 -0
  154. package/live/reconciler.d.ts +39 -0
  155. package/live/reconciler.js +175 -0
  156. package/live/setup-buckets.d.ts +7 -0
  157. package/live/setup-buckets.js +33 -0
  158. package/live/slippage-tracker.d.ts +45 -0
  159. package/live/slippage-tracker.js +78 -0
  160. package/live/stop-watcher.d.ts +34 -0
  161. package/live/stop-watcher.js +158 -0
  162. package/live/user-data-active-probe.d.ts +54 -0
  163. package/live/user-data-active-probe.js +180 -0
  164. package/live/user-data-stream-controller.d.ts +200 -0
  165. package/live/user-data-stream-controller.js +579 -0
  166. package/live/user-data-stream-ws.d.ts +22 -0
  167. package/live/user-data-stream-ws.js +63 -0
  168. package/live/user-data-stream.d.ts +243 -0
  169. package/live/user-data-stream.js +704 -0
  170. package/logger.d.ts +2 -0
  171. package/logger.js +2 -0
  172. package/mfe.d.ts +21 -0
  173. package/mfe.js +68 -0
  174. package/onboarding/mode-ladder.d.ts +1 -0
  175. package/onboarding/mode-ladder.js +3 -0
  176. package/onboarding/runtime.d.ts +71 -0
  177. package/onboarding/runtime.js +153 -0
  178. package/openclaw.plugin.json +94 -0
  179. package/package.json +27 -0
  180. package/paper-adapter.d.ts +24 -0
  181. package/paper-adapter.js +91 -0
  182. package/persistence/state-manager.d.ts +42 -0
  183. package/persistence/state-manager.js +164 -0
  184. package/pinned-plan.d.ts +9 -0
  185. package/pinned-plan.js +23 -0
  186. package/risk/pre-trade-check.d.ts +38 -0
  187. package/risk/pre-trade-check.js +345 -0
  188. package/risk/pre-trade-types.d.ts +60 -0
  189. package/risk/pre-trade-types.js +3 -0
  190. package/scripts/assemble.mjs +114 -0
  191. package/shadow/shadow-tracker.d.ts +36 -0
  192. package/shadow/shadow-tracker.js +151 -0
  193. package/shadow/types.d.ts +42 -0
  194. package/shadow/types.js +20 -0
  195. package/shared/indicators-extended.d.ts +52 -0
  196. package/shared/indicators-extended.js +291 -0
  197. package/shared/indicators.d.ts +15 -0
  198. package/shared/indicators.js +114 -0
  199. package/signals/conditions/registry.d.ts +16 -0
  200. package/signals/conditions/registry.js +1274 -0
  201. package/signals/conditions/types.d.ts +1 -0
  202. package/signals/conditions/types.js +4 -0
  203. package/signals/direction-rules.d.ts +3 -0
  204. package/signals/direction-rules.js +24 -0
  205. package/signals/entry-rules.d.ts +6 -0
  206. package/signals/entry-rules.js +33 -0
  207. package/signals/serialize-context.d.ts +4 -0
  208. package/signals/serialize-context.js +39 -0
  209. package/signals/stop-rules.d.ts +3 -0
  210. package/signals/stop-rules.js +48 -0
  211. package/signals/strategy-adapter.d.ts +14 -0
  212. package/signals/strategy-adapter.js +122 -0
  213. package/signals/types.d.ts +1 -0
  214. package/signals/types.js +8 -0
  215. package/simulator/exchange-simulator.d.ts +93 -0
  216. package/simulator/exchange-simulator.js +684 -0
  217. package/simulator/fill-engine.d.ts +53 -0
  218. package/simulator/fill-engine.js +276 -0
  219. package/simulator/paper-market-feed.d.ts +26 -0
  220. package/simulator/paper-market-feed.js +104 -0
  221. package/simulator/realistic-fills.d.ts +59 -0
  222. package/simulator/realistic-fills.js +175 -0
  223. package/simulator/types.d.ts +219 -0
  224. package/simulator/types.js +43 -0
  225. package/skills/reefclaw/SKILL.md +97 -0
  226. package/strategy/builtin-strategies.d.ts +2 -0
  227. package/strategy/builtin-strategies.js +109 -0
  228. package/strategy/condition-registry.d.ts +3 -0
  229. package/strategy/condition-registry.js +153 -0
  230. package/strategy/evaluator.d.ts +67 -0
  231. package/strategy/evaluator.js +93 -0
  232. package/tools/assessment-validation.d.ts +118 -0
  233. package/tools/assessment-validation.js +415 -0
  234. package/tools/attach-brackets.d.ts +34 -0
  235. package/tools/attach-brackets.js +363 -0
  236. package/tools/audit-bracket-protection.d.ts +49 -0
  237. package/tools/audit-bracket-protection.js +527 -0
  238. package/tools/cancel-all-orders.d.ts +7 -0
  239. package/tools/cancel-all-orders.js +5 -0
  240. package/tools/cancel-order.d.ts +10 -0
  241. package/tools/cancel-order.js +14 -0
  242. package/tools/check-position-health.d.ts +46 -0
  243. package/tools/check-position-health.js +194 -0
  244. package/tools/clear-exchange-credentials.d.ts +24 -0
  245. package/tools/clear-exchange-credentials.js +70 -0
  246. package/tools/close-position.d.ts +22 -0
  247. package/tools/close-position.js +449 -0
  248. package/tools/create-order.d.ts +54 -0
  249. package/tools/create-order.js +338 -0
  250. package/tools/exit-gate.d.ts +58 -0
  251. package/tools/exit-gate.js +162 -0
  252. package/tools/fetch-balance.d.ts +5 -0
  253. package/tools/fetch-balance.js +4 -0
  254. package/tools/fetch-ohlcv.d.ts +11 -0
  255. package/tools/fetch-ohlcv.js +8 -0
  256. package/tools/fetch-open-orders.d.ts +7 -0
  257. package/tools/fetch-open-orders.js +4 -0
  258. package/tools/fetch-positions.d.ts +7 -0
  259. package/tools/fetch-positions.js +4 -0
  260. package/tools/fetch-ticker.d.ts +11 -0
  261. package/tools/fetch-ticker.js +5 -0
  262. package/tools/get-agent-profile.d.ts +4 -0
  263. package/tools/get-agent-profile.js +6 -0
  264. package/tools/get-analytics.d.ts +6 -0
  265. package/tools/get-analytics.js +7 -0
  266. package/tools/get-backtest.d.ts +12 -0
  267. package/tools/get-backtest.js +91 -0
  268. package/tools/get-basis.d.ts +7 -0
  269. package/tools/get-basis.js +7 -0
  270. package/tools/get-bracket-config.d.ts +11 -0
  271. package/tools/get-bracket-config.js +24 -0
  272. package/tools/get-cascade-risk.d.ts +7 -0
  273. package/tools/get-cascade-risk.js +8 -0
  274. package/tools/get-crypto-metrics.d.ts +18 -0
  275. package/tools/get-crypto-metrics.js +45 -0
  276. package/tools/get-cvd.d.ts +6 -0
  277. package/tools/get-cvd.js +6 -0
  278. package/tools/get-divergences.d.ts +6 -0
  279. package/tools/get-divergences.js +6 -0
  280. package/tools/get-funding-context.d.ts +6 -0
  281. package/tools/get-funding-context.js +16 -0
  282. package/tools/get-liquidation-levels.d.ts +7 -0
  283. package/tools/get-liquidation-levels.js +7 -0
  284. package/tools/get-liquidation-pulse.d.ts +9 -0
  285. package/tools/get-liquidation-pulse.js +22 -0
  286. package/tools/get-market-breadth.d.ts +6 -0
  287. package/tools/get-market-breadth.js +8 -0
  288. package/tools/get-market-intel.d.ts +19 -0
  289. package/tools/get-market-intel.js +116 -0
  290. package/tools/get-market-structure.d.ts +47 -0
  291. package/tools/get-market-structure.js +198 -0
  292. package/tools/get-my-mined-patterns.d.ts +20 -0
  293. package/tools/get-my-mined-patterns.js +61 -0
  294. package/tools/get-my-proposed-learnings.d.ts +20 -0
  295. package/tools/get-my-proposed-learnings.js +55 -0
  296. package/tools/get-my-recent-reviews.d.ts +22 -0
  297. package/tools/get-my-recent-reviews.js +66 -0
  298. package/tools/get-orderbook.d.ts +21 -0
  299. package/tools/get-orderbook.js +32 -0
  300. package/tools/get-pattern-scan.d.ts +7 -0
  301. package/tools/get-pattern-scan.js +8 -0
  302. package/tools/get-regime.d.ts +6 -0
  303. package/tools/get-regime.js +7 -0
  304. package/tools/get-relevant-learnings.d.ts +21 -0
  305. package/tools/get-relevant-learnings.js +65 -0
  306. package/tools/get-resting-liquidity.d.ts +6 -0
  307. package/tools/get-resting-liquidity.js +11 -0
  308. package/tools/get-risk-scenario.d.ts +29 -0
  309. package/tools/get-risk-scenario.js +47 -0
  310. package/tools/get-risk-summary.d.ts +51 -0
  311. package/tools/get-risk-summary.js +118 -0
  312. package/tools/get-sentiment.d.ts +4 -0
  313. package/tools/get-sentiment.js +6 -0
  314. package/tools/get-session-review.d.ts +7 -0
  315. package/tools/get-session-review.js +8 -0
  316. package/tools/get-setup-detail.d.ts +7 -0
  317. package/tools/get-setup-detail.js +303 -0
  318. package/tools/get-signals.d.ts +15 -0
  319. package/tools/get-signals.js +54 -0
  320. package/tools/get-sizing.d.ts +6 -0
  321. package/tools/get-sizing.js +6 -0
  322. package/tools/get-trade-feedback.d.ts +7 -0
  323. package/tools/get-trade-feedback.js +8 -0
  324. package/tools/get-trade-flow.d.ts +7 -0
  325. package/tools/get-trade-flow.js +7 -0
  326. package/tools/get-volume-analysis.d.ts +21 -0
  327. package/tools/get-volume-analysis.js +74 -0
  328. package/tools/get-volume-profile.d.ts +7 -0
  329. package/tools/get-volume-profile.js +7 -0
  330. package/tools/helpers.d.ts +25 -0
  331. package/tools/helpers.js +38 -0
  332. package/tools/intel-api.d.ts +14 -0
  333. package/tools/intel-api.js +52 -0
  334. package/tools/intel-cache.d.ts +25 -0
  335. package/tools/intel-cache.js +133 -0
  336. package/tools/list-strategies.d.ts +7 -0
  337. package/tools/list-strategies.js +6 -0
  338. package/tools/modify-stop.d.ts +17 -0
  339. package/tools/modify-stop.js +81 -0
  340. package/tools/modify-target.d.ts +17 -0
  341. package/tools/modify-target.js +71 -0
  342. package/tools/propose-learning.d.ts +22 -0
  343. package/tools/propose-learning.js +65 -0
  344. package/tools/query-review-outcomes.d.ts +30 -0
  345. package/tools/query-review-outcomes.js +64 -0
  346. package/tools/query-trades.d.ts +21 -0
  347. package/tools/query-trades.js +37 -0
  348. package/tools/record-position-reviews.d.ts +38 -0
  349. package/tools/record-position-reviews.js +147 -0
  350. package/tools/save-strategy.d.ts +16 -0
  351. package/tools/save-strategy.js +46 -0
  352. package/tools/scan-pairs.d.ts +18 -0
  353. package/tools/scan-pairs.js +220 -0
  354. package/tools/score-setup.d.ts +31 -0
  355. package/tools/score-setup.js +268 -0
  356. package/tools/set-bracket-requirement.d.ts +18 -0
  357. package/tools/set-bracket-requirement.js +81 -0
  358. package/tools/set-exchange-credentials.d.ts +25 -0
  359. package/tools/set-exchange-credentials.js +80 -0
  360. package/tools/set-trading-mode.d.ts +26 -0
  361. package/tools/set-trading-mode.js +135 -0
  362. package/tools/test-exchange-credentials.d.ts +16 -0
  363. package/tools/test-exchange-credentials.js +100 -0
  364. package/tools/toggle-strategy.d.ts +8 -0
  365. package/tools/toggle-strategy.js +8 -0
  366. package/trading-params-cache.d.ts +26 -0
  367. package/trading-params-cache.js +52 -0
  368. package/types.d.ts +110 -0
  369. package/types.js +7 -0
  370. package/util/plugin-paths.d.ts +3 -0
  371. 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
+ }