@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,417 @@
1
+ // WebSocket client for the OpenClaw gateway.
2
+ // Implements the 3-step handshake, event routing, RPC methods,
3
+ // and reconnection with exponential backoff.
4
+ import WebSocket from 'ws';
5
+ import { logger } from '../logger.js';
6
+ import { computeDelay } from '../utils/reconnect.js';
7
+ const TAG = 'gateway-ws';
8
+ // ---- Protocol constants ----
9
+ // Protocol range we can speak. The gateway picks its own version if it falls
10
+ // inside [min, max]. v3 = OpenClaw ≤2026.4.x (prod); v4 = OpenClaw 2026.6+
11
+ // (fresh installs) — the connect/hello frames and every RPC we use are
12
+ // shape-identical across both (verified live against 2026.6.11; a v3-only pin
13
+ // gets `1002 protocol mismatch` there and the bridge can never connect).
14
+ const MIN_PROTOCOL_VERSION = 3;
15
+ const MAX_PROTOCOL_VERSION = 4;
16
+ const CLIENT_ID = 'openclaw-tui';
17
+ const CLIENT_VERSION = '0.1.0';
18
+ const DEFAULT_RECONNECT = {
19
+ baseDelayMs: 2_000,
20
+ maxDelayMs: 30_000,
21
+ maxAttempts: 20,
22
+ jitterFactor: 0.3,
23
+ };
24
+ // ---- Fatal close codes ----
25
+ /** Auth errors — do not reconnect */
26
+ const FATAL_CLOSE_CODES = [4001];
27
+ const RPC_TIMEOUT_MS = 30_000;
28
+ // ---- Client ----
29
+ export class GatewayWsClient {
30
+ ws = null;
31
+ state = 'disconnected';
32
+ attempt = 0;
33
+ reconnectTimer = null;
34
+ staleWarnTimer = null;
35
+ staleReconnectTimer = null;
36
+ tickIntervalMs = 15_000; // Updated from hello-ok policy
37
+ pendingRpcs = new Map();
38
+ reconnectConfig;
39
+ destroyed = false;
40
+ /** Stored device token from hello-ok for future connects */
41
+ deviceToken = null;
42
+ /** Last hello-ok payload — contains initial snapshot */
43
+ lastHelloOk = null;
44
+ /** Event listeners */
45
+ listeners = new Map();
46
+ gatewayUrl;
47
+ gatewayToken;
48
+ constructor(config, reconnect) {
49
+ // Convert to WebSocket URL: http→ws, https→wss, bare host:port→ws://
50
+ let url = config.gatewayUrl.replace(/\/$/, '');
51
+ if (/^https:\/\//.test(url)) {
52
+ url = url.replace(/^https:\/\//, 'wss://');
53
+ }
54
+ else if (/^http:\/\//.test(url)) {
55
+ url = url.replace(/^http:\/\//, 'ws://');
56
+ }
57
+ else if (!/^wss?:\/\//.test(url)) {
58
+ url = `ws://${url}`;
59
+ }
60
+ this.gatewayUrl = url;
61
+ this.gatewayToken = config.gatewayToken;
62
+ this.reconnectConfig = { ...DEFAULT_RECONNECT, ...reconnect };
63
+ }
64
+ // ---- Public API ----
65
+ /** Start the WebSocket connection and handshake */
66
+ connect() {
67
+ if (this.destroyed)
68
+ return;
69
+ if (this.state === 'connected' || this.state === 'handshaking')
70
+ return;
71
+ this.setState(this.attempt === 0 ? 'connecting' : 'reconnecting');
72
+ const url = this.gatewayUrl;
73
+ logger.info(TAG, `Connecting to gateway (attempt ${this.attempt + 1}): ${url}`);
74
+ const ws = new WebSocket(url);
75
+ this.ws = ws;
76
+ ws.on('open', () => {
77
+ logger.info(TAG, 'WebSocket open, awaiting challenge');
78
+ this.setState('handshaking');
79
+ });
80
+ ws.on('message', (data) => {
81
+ this.handleMessage(data);
82
+ });
83
+ ws.on('close', (code, reason) => {
84
+ const reasonStr = reason.toString();
85
+ logger.info(TAG, `WebSocket closed: ${code} ${reasonStr}`);
86
+ this.ws = null;
87
+ this.clearStaleTimer();
88
+ this.rejectAllPending('Connection closed');
89
+ if (this.destroyed)
90
+ return;
91
+ const wasConnected = this.state === 'connected' || this.state === 'handshaking';
92
+ if (wasConnected) {
93
+ this.emit('disconnected', { code, reason: reasonStr });
94
+ }
95
+ // Fatal close codes: do not reconnect
96
+ if (FATAL_CLOSE_CODES.includes(code)) {
97
+ logger.error(TAG, `Fatal close code ${code} — authentication failed, not reconnecting`);
98
+ this.setState('failed');
99
+ this.emit('failed', { reason: `Authentication failed (code ${code})` });
100
+ return;
101
+ }
102
+ this.scheduleReconnect();
103
+ });
104
+ ws.on('error', (err) => {
105
+ logger.error(TAG, `WebSocket error: ${err.message}`);
106
+ });
107
+ }
108
+ /** Disconnect and clean up. Cannot reconnect after this. */
109
+ destroy() {
110
+ this.destroyed = true;
111
+ this.clearReconnectTimer();
112
+ this.clearStaleTimer();
113
+ this.rejectAllPending('Client destroyed');
114
+ if (this.ws) {
115
+ this.ws.close(1000, 'Client shutting down');
116
+ this.ws = null;
117
+ }
118
+ this.setState('disconnected');
119
+ }
120
+ /**
121
+ * Send an RPC request and wait for the response.
122
+ * Used for: agent (chat), health, status
123
+ */
124
+ async sendRpc(method, params) {
125
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
126
+ throw new Error(`Cannot send RPC: not connected (state=${this.state})`);
127
+ }
128
+ const id = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
129
+ const frame = {
130
+ type: 'req',
131
+ id,
132
+ method,
133
+ ...(params && { params }),
134
+ };
135
+ return new Promise((resolve, reject) => {
136
+ const timer = setTimeout(() => {
137
+ this.pendingRpcs.delete(id);
138
+ reject(new Error(`RPC ${method} timed out after ${RPC_TIMEOUT_MS}ms`));
139
+ }, RPC_TIMEOUT_MS);
140
+ this.pendingRpcs.set(id, { resolve, reject, timer });
141
+ this.ws.send(JSON.stringify(frame));
142
+ logger.debug(TAG, `Sent RPC: ${method} (id=${id})`);
143
+ });
144
+ }
145
+ /** Register an event listener */
146
+ on(event, listener) {
147
+ if (!this.listeners.has(event)) {
148
+ this.listeners.set(event, new Set());
149
+ }
150
+ this.listeners.get(event).add(listener);
151
+ }
152
+ /** Remove an event listener */
153
+ off(event, listener) {
154
+ this.listeners.get(event)?.delete(listener);
155
+ }
156
+ /** Get current connection state */
157
+ getState() {
158
+ return this.state;
159
+ }
160
+ /** Get the hello-ok snapshot from the last successful connection */
161
+ getHelloOk() {
162
+ return this.lastHelloOk;
163
+ }
164
+ /** Get stored device token (for future connects) */
165
+ getDeviceToken() {
166
+ return this.deviceToken;
167
+ }
168
+ // ---- Message handling ----
169
+ handleMessage(data) {
170
+ let frame;
171
+ try {
172
+ frame = JSON.parse(data.toString());
173
+ }
174
+ catch {
175
+ logger.warn(TAG, 'Received unparseable message');
176
+ return;
177
+ }
178
+ // -- Response frames (hello-ok, RPC responses) --
179
+ if (frame.type === 'res') {
180
+ this.handleResponse(frame);
181
+ return;
182
+ }
183
+ // -- Event frames --
184
+ if (frame.type === 'event') {
185
+ this.handleEvent(frame);
186
+ return;
187
+ }
188
+ // -- Request frames from gateway (unexpected for us) --
189
+ if (frame.type === 'req') {
190
+ logger.debug(TAG, `Received request from gateway (unexpected): ${frame.id}`);
191
+ }
192
+ }
193
+ handleResponse(frame) {
194
+ const id = frame.id;
195
+ if (!id)
196
+ return;
197
+ // Check pending RPCs
198
+ const pending = this.pendingRpcs.get(id);
199
+ if (pending) {
200
+ clearTimeout(pending.timer);
201
+ this.pendingRpcs.delete(id);
202
+ if (frame.ok) {
203
+ // Check if this is the hello-ok response to our connect request
204
+ if (frame.payload && typeof frame.payload === 'object') {
205
+ const payload = frame.payload;
206
+ if (payload.type === 'hello-ok') {
207
+ this.handleHelloOk(frame.payload);
208
+ }
209
+ }
210
+ pending.resolve(frame.payload);
211
+ }
212
+ else {
213
+ pending.reject(new Error(frame.error?.message ?? `RPC failed (code ${frame.error?.code ?? 'unknown'})`));
214
+ }
215
+ return;
216
+ }
217
+ // Unexpected response
218
+ if (!frame.ok) {
219
+ logger.warn(TAG, `Unexpected error response (id=${id}): ${frame.error?.message}`);
220
+ }
221
+ }
222
+ handleEvent(frame) {
223
+ const event = frame.event;
224
+ switch (event) {
225
+ case 'connect.challenge':
226
+ this.handleChallenge(frame.payload);
227
+ break;
228
+ case 'agent':
229
+ this.resetStaleTimer();
230
+ if (frame.payload && typeof frame.payload === 'object') {
231
+ const p = frame.payload;
232
+ this.emit('agent', {
233
+ runId: p.runId,
234
+ stream: p.stream,
235
+ data: p.data,
236
+ seq: frame.seq,
237
+ });
238
+ }
239
+ break;
240
+ case 'presence':
241
+ this.resetStaleTimer();
242
+ this.emit('presence', { payload: frame.payload, seq: frame.seq });
243
+ break;
244
+ case 'tick':
245
+ this.resetStaleTimer();
246
+ this.emit('tick', undefined);
247
+ break;
248
+ case 'shutdown':
249
+ logger.warn(TAG, 'Gateway shutting down — will reconnect');
250
+ this.emit('shutdown', undefined);
251
+ // Don't immediately reconnect; the close event will trigger reconnection
252
+ break;
253
+ default:
254
+ logger.debug(TAG, `Unknown gateway event: ${event}`);
255
+ }
256
+ }
257
+ // ---- Handshake ----
258
+ /**
259
+ * Step 1: Receive connect.challenge with {nonce, ts}.
260
+ * Step 2: Send connect request with auth token.
261
+ */
262
+ handleChallenge(payload) {
263
+ logger.info(TAG, `Received challenge (nonce=${payload.nonce?.slice(0, 8)}...)`);
264
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
265
+ logger.error(TAG, 'Cannot respond to challenge: WebSocket not open');
266
+ return;
267
+ }
268
+ const connectId = `connect-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
269
+ const connectFrame = {
270
+ type: 'req',
271
+ id: connectId,
272
+ method: 'connect',
273
+ params: {
274
+ minProtocol: MIN_PROTOCOL_VERSION,
275
+ maxProtocol: MAX_PROTOCOL_VERSION,
276
+ client: {
277
+ id: CLIENT_ID,
278
+ version: CLIENT_VERSION,
279
+ platform: 'node',
280
+ mode: 'backend',
281
+ },
282
+ role: 'operator',
283
+ scopes: ['operator.read', 'operator.write'],
284
+ auth: {
285
+ token: this.gatewayToken,
286
+ ...(this.deviceToken && { deviceToken: this.deviceToken }),
287
+ },
288
+ },
289
+ };
290
+ // Register as pending RPC so the hello-ok response routes through handleResponse
291
+ const timer = setTimeout(() => {
292
+ this.pendingRpcs.delete(connectId);
293
+ logger.error(TAG, 'Connect handshake timed out');
294
+ // Force close to trigger reconnect
295
+ this.ws?.close(4000, 'Handshake timeout');
296
+ }, RPC_TIMEOUT_MS);
297
+ this.pendingRpcs.set(connectId, {
298
+ resolve: () => { },
299
+ reject: (err) => {
300
+ logger.error(TAG, `Handshake failed: ${err.message}`);
301
+ this.ws?.close(4000, 'Handshake rejected');
302
+ },
303
+ timer,
304
+ });
305
+ this.ws.send(JSON.stringify(connectFrame));
306
+ logger.info(TAG, 'Sent connect request');
307
+ }
308
+ /**
309
+ * Step 3: Process hello-ok response.
310
+ * Extracts policy, snapshot, and device token.
311
+ */
312
+ handleHelloOk(payload) {
313
+ logger.info(TAG, `Connected to gateway (protocol=${payload.protocol})`);
314
+ this.lastHelloOk = payload;
315
+ this.attempt = 0; // Reset reconnect counter
316
+ // Parse policy
317
+ if (payload.policy?.tickIntervalMs) {
318
+ this.tickIntervalMs = payload.policy.tickIntervalMs;
319
+ logger.debug(TAG, `Tick interval: ${this.tickIntervalMs}ms`);
320
+ }
321
+ // Store device token for future connects
322
+ if (payload.auth?.deviceToken) {
323
+ this.deviceToken = payload.auth.deviceToken;
324
+ logger.debug(TAG, 'Stored device token');
325
+ }
326
+ // Log snapshot info
327
+ const snap = payload.snapshot;
328
+ if (snap) {
329
+ logger.info(TAG, `Gateway uptime: ${Math.round((snap.uptimeMs ?? 0) / 1000)}s, stateVersion: ${JSON.stringify(snap.stateVersion)}`);
330
+ }
331
+ this.setState('connected');
332
+ this.resetStaleTimer();
333
+ this.emit('connected', payload);
334
+ }
335
+ // ---- Stale detection ----
336
+ /** Reset the stale timers — called on any incoming event/tick */
337
+ resetStaleTimer() {
338
+ this.clearStaleTimer();
339
+ const warnMs = this.tickIntervalMs * 2;
340
+ const reconnectMs = this.tickIntervalMs * 3;
341
+ // Warn at 2x tick interval
342
+ this.staleWarnTimer = setTimeout(() => {
343
+ if (this.state === 'connected') {
344
+ logger.warn(TAG, `No data received for ${warnMs}ms — connection may be stale`);
345
+ }
346
+ }, warnMs);
347
+ // Force reconnect at 3x tick interval
348
+ this.staleReconnectTimer = setTimeout(() => {
349
+ if (this.state === 'connected') {
350
+ logger.error(TAG, `No data received for ${reconnectMs}ms — forcing reconnect`);
351
+ this.ws?.close(4000, 'Stale connection');
352
+ }
353
+ }, reconnectMs);
354
+ }
355
+ clearStaleTimer() {
356
+ if (this.staleWarnTimer) {
357
+ clearTimeout(this.staleWarnTimer);
358
+ this.staleWarnTimer = null;
359
+ }
360
+ if (this.staleReconnectTimer) {
361
+ clearTimeout(this.staleReconnectTimer);
362
+ this.staleReconnectTimer = null;
363
+ }
364
+ }
365
+ // ---- Reconnection ----
366
+ scheduleReconnect() {
367
+ this.attempt++;
368
+ if (this.attempt > this.reconnectConfig.maxAttempts) {
369
+ logger.error(TAG, `Max reconnection attempts (${this.reconnectConfig.maxAttempts}) exceeded`);
370
+ this.setState('failed');
371
+ this.emit('failed', { reason: 'Max reconnection attempts exceeded' });
372
+ return;
373
+ }
374
+ const delay = computeDelay(this.attempt - 1, this.reconnectConfig);
375
+ logger.info(TAG, `Reconnecting in ${Math.round(delay)}ms (attempt ${this.attempt}/${this.reconnectConfig.maxAttempts})`);
376
+ this.setState('reconnecting');
377
+ this.emit('reconnecting', { attempt: this.attempt, delayMs: Math.round(delay) });
378
+ this.reconnectTimer = setTimeout(() => {
379
+ this.reconnectTimer = null;
380
+ this.connect();
381
+ }, delay);
382
+ }
383
+ clearReconnectTimer() {
384
+ if (this.reconnectTimer) {
385
+ clearTimeout(this.reconnectTimer);
386
+ this.reconnectTimer = null;
387
+ }
388
+ }
389
+ // ---- Internal helpers ----
390
+ setState(newState) {
391
+ if (this.state === newState)
392
+ return;
393
+ const prev = this.state;
394
+ this.state = newState;
395
+ logger.debug(TAG, `State: ${prev} → ${newState}`);
396
+ }
397
+ emit(event, payload) {
398
+ const listeners = this.listeners.get(event);
399
+ if (!listeners)
400
+ return;
401
+ for (const listener of listeners) {
402
+ try {
403
+ listener(payload);
404
+ }
405
+ catch (err) {
406
+ logger.error(TAG, `Event listener error (${event}): ${err instanceof Error ? err.message : String(err)}`);
407
+ }
408
+ }
409
+ }
410
+ rejectAllPending(reason) {
411
+ for (const [, pending] of this.pendingRpcs) {
412
+ clearTimeout(pending.timer);
413
+ pending.reject(new Error(reason));
414
+ }
415
+ this.pendingRpcs.clear();
416
+ }
417
+ }
@@ -0,0 +1,146 @@
1
+ import type { GatewayHttpClient } from './gateway-http-client.js';
2
+ import type { ToolMap } from './tool-discovery.js';
3
+ import type { TickerData, CandleData, OrderData, MarketStructureData, CryptoMetricsData, VolumeAnalysisData } from '../types.js';
4
+ export declare const TICKER_INTERVAL_MS = 5000;
5
+ export declare const OHLCV_INTERVAL_MS = 15000;
6
+ export declare const POSITIONS_INTERVAL_MS = 10000;
7
+ export declare const MARKET_STRUCTURE_INTERVAL_MS = 60000;
8
+ /** CCXT position — minimal shape needed by poller */
9
+ export interface CcxtPosition {
10
+ symbol: string;
11
+ side: 'long' | 'short' | string;
12
+ contracts?: number;
13
+ contractSize?: number;
14
+ entryPrice?: number;
15
+ markPrice?: number;
16
+ unrealizedPnl?: number;
17
+ leverage?: number;
18
+ notional?: number;
19
+ [key: string]: unknown;
20
+ }
21
+ /** Callbacks fired by the Poller when fresh data arrives from polls. */
22
+ export interface PollerCallbacks {
23
+ /** Ticker data from fetch_ticker — fires every 1s */
24
+ onTicker: (data: TickerData) => void;
25
+ /** Candle data from fetch_ohlcv — fires every 5s */
26
+ onCandle: (data: CandleData) => void;
27
+ /** Raw CCXT positions from fetch_positions — fires every 3s */
28
+ onPositions: (positions: CcxtPosition[]) => void;
29
+ /** Parsed balance from fetch_balance — fires every 10s */
30
+ onBalance: (balance: {
31
+ currency: string;
32
+ total: number;
33
+ available: number;
34
+ locked: number;
35
+ }) => void;
36
+ /** Parsed open orders from fetch_open_orders — fires every 3s */
37
+ onOpenOrders: (orders: OrderData[]) => void;
38
+ /** Multi-TF analysis from get_market_structure — fires every 30s */
39
+ onMarketStructure?: (data: MarketStructureData) => void;
40
+ /** Crypto metrics (funding rate + OI) — fires every 60s */
41
+ onCryptoMetrics?: (data: CryptoMetricsData) => void;
42
+ /** Volume/thrust analysis — fires every 15s */
43
+ onVolumeAnalysis?: (data: VolumeAnalysisData) => void;
44
+ }
45
+ /** True when an error message carries a Binance rate-limit / IP-ban signal
46
+ * (418 ban, or 429 / -1003 "too many requests" soft limit). Poll methods that
47
+ * wrap their own try/catch MUST rethrow these so guardedExec's catch can arm
48
+ * the process-wide circuit-breaker (applyRateLimitBackoff). Swallowing them
49
+ * internally as "supplementary/non-fatal" silently defeats the ban gate: a
50
+ * fixed-cadence poller keeps hammering a banned IP, and every request during a
51
+ * 418 EXTENDS the ban (see docs/CLAUDE/binance-ratelimit.md). Kept in sync
52
+ * with the classification branches in applyRateLimitBackoff. */
53
+ export declare function isRateLimitShapedError(msg: string): boolean;
54
+ export declare class Poller {
55
+ private readonly http;
56
+ private readonly toolMap;
57
+ private readonly symbol;
58
+ private readonly callbacks;
59
+ private readonly quoteCurrency;
60
+ private running;
61
+ private paused;
62
+ private timers;
63
+ /** Prevents concurrent polls of the same type (if HTTP is slow) */
64
+ private readonly inflight;
65
+ /** Process-wide circuit-breaker. While `Date.now() < cooldownUntilMs`,
66
+ * `guardedExec` skips EVERY poll regardless of type. Set when any poll
67
+ * observes a Binance 418 (IP ban) or 429 (rate-limit) error.
68
+ *
69
+ * Why this exists: pre-2026-05-15 the poller had no ban awareness — on a
70
+ * 418 every one of ~8 pollers (ticker 1s, positions/orders 3s, …) kept
71
+ * firing on its original cadence straight through the ban, and since
72
+ * Binance EXTENDS an IP ban every time you hit it during the ban, the
73
+ * system never recovered on its own (self-perpetuating lockout that
74
+ * zeroed the dashboard + blinded the agent for the whole window). The
75
+ * breaker makes the whole poller go quiet until Binance's stated ban
76
+ * expiry (+slack), or an exponential window for soft 429s. */
77
+ private cooldownUntilMs;
78
+ /** Index into RATE_LIMIT_BACKOFF_MS for consecutive soft 429s. Reset to
79
+ * -1 (→ step 0 on first hit) after any clean poll. */
80
+ private rateLimitStep;
81
+ /** Throttles the "in cooldown, skipping" log to once per cooldown window
82
+ * so a 2-min ban doesn't emit hundreds of identical skip lines. */
83
+ private cooldownLogged;
84
+ /** Per-tool poll-attribution for the current 60s window (observability
85
+ * only). The skill is a SEPARATE process from the plugin but shares the
86
+ * Binance per-IP weight budget; the plugin's ban-gate weight summary
87
+ * cannot see skill polls. This is the skill-side mirror so the IP load
88
+ * split is provable, not guessed. */
89
+ private pollWindowStart;
90
+ private readonly pollCountByTool;
91
+ constructor(http: GatewayHttpClient, toolMap: ToolMap, symbol: string, callbacks: PollerCallbacks, quoteCurrency?: string);
92
+ /**
93
+ * Start all poll loops. Each tool is polled on its own interval.
94
+ * Tools not present in the ToolMap are silently skipped.
95
+ * Each poll fires immediately on start, then repeats on interval.
96
+ */
97
+ start(): void;
98
+ /** Stop all poll loops and clean up timers. */
99
+ stop(): void;
100
+ /** Pause polling (timers keep ticking but polls are skipped). */
101
+ pause(): void;
102
+ /** Resume polling after a pause. */
103
+ resume(): void;
104
+ /**
105
+ * Schedule a poll loop for a logical tool, only if it was discovered.
106
+ * Fires immediately, then repeats on interval.
107
+ */
108
+ private scheduleIfAvailable;
109
+ /**
110
+ * Execute a poll function with error handling and in-flight guard.
111
+ * - Skipped when paused or stopped (checked before AND after acquiring inflight slot)
112
+ * - Skipped if a previous poll of the same type is still in-flight
113
+ * - Errors are logged and swallowed (never crash the provider)
114
+ */
115
+ private guardedExec;
116
+ /** Inspect a failed poll's error for Binance rate-limit signals and arm
117
+ * the process-wide circuit-breaker accordingly.
118
+ *
119
+ * - **418** (IP ban): Binance returns `banned until <epoch-ms>` in the
120
+ * message. We respect that exact timestamp + slack — guessing a
121
+ * shorter window would just re-ban and extend it.
122
+ * - **429 / -1003 "Too many requests"** (soft rate-limit, no ban yet):
123
+ * exponential backoff so we stop climbing toward a 418.
124
+ *
125
+ * Any non-rate-limit error is left alone (normal transient handling). */
126
+ private applyRateLimitBackoff;
127
+ private pollTicker;
128
+ private pollOhlcv;
129
+ private pollPositions;
130
+ private pollBalance;
131
+ private pollOpenOrders;
132
+ private pollMarketStructure;
133
+ /**
134
+ * Fetch funding rate and open interest via the dedicated get_crypto_metrics tool.
135
+ * The plugin tool handles spot→perp symbol conversion and raw CCXT ticker parsing.
136
+ */
137
+ private pollCryptoMetrics;
138
+ /**
139
+ * Fetch 25 bars of 5m OHLCV and compute volume analysis metrics:
140
+ * - Volume expansion ratio (current vs 20-bar avg)
141
+ * - Close position (where close sits in bar's range, 0=low, 1=high)
142
+ * - Directional thrust (volume-weighted close position, -1 to +1)
143
+ * - Buy volume estimate (proxy using close position)
144
+ */
145
+ private pollVolumeAnalysis;
146
+ }