@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,78 @@
1
+ // Slippage tracker — records expected vs actual fill price for every live order.
2
+ // Computes slippage in basis points and tracks statistics per symbol.
3
+ import { logger } from '../logger.js';
4
+ const TAG = 'slippage';
5
+ export class SlippageTracker {
6
+ records = [];
7
+ maxRecords;
8
+ constructor(maxRecords = 1000) {
9
+ this.maxRecords = maxRecords;
10
+ }
11
+ /** Record a fill with slippage measurement. */
12
+ recordFill(params) {
13
+ // Slippage: positive means worse execution for the order side
14
+ // For buy: actual > expected = positive slippage (paid more)
15
+ // For sell: actual < expected = positive slippage (received less)
16
+ let slippageBps;
17
+ if (params.side === 'buy') {
18
+ slippageBps = ((params.actualPrice - params.expectedPrice) / params.expectedPrice) * 10_000;
19
+ }
20
+ else {
21
+ slippageBps = ((params.expectedPrice - params.actualPrice) / params.expectedPrice) * 10_000;
22
+ }
23
+ const record = {
24
+ ...params,
25
+ slippageBps,
26
+ timestamp: Date.now(),
27
+ };
28
+ this.records.push(record);
29
+ // Circular buffer
30
+ if (this.records.length > this.maxRecords) {
31
+ this.records.shift();
32
+ }
33
+ logger.info(TAG, `${params.symbol} ${params.side}: expected=${params.expectedPrice}, actual=${params.actualPrice}, slippage=${slippageBps.toFixed(2)}bps`);
34
+ return record;
35
+ }
36
+ /** Get average slippage in bps for a symbol. */
37
+ getAverageSlippage(symbol) {
38
+ const relevant = symbol
39
+ ? this.records.filter(r => r.symbol === symbol)
40
+ : this.records;
41
+ if (relevant.length === 0)
42
+ return 0;
43
+ return relevant.reduce((s, r) => s + r.slippageBps, 0) / relevant.length;
44
+ }
45
+ /** Get slippage stats summary. */
46
+ getStats(symbol) {
47
+ const relevant = symbol
48
+ ? this.records.filter(r => r.symbol === symbol)
49
+ : this.records;
50
+ if (relevant.length === 0) {
51
+ return { count: 0, avgBps: 0, maxBps: 0, minBps: 0, p95Bps: 0, totalCost: 0 };
52
+ }
53
+ const bps = relevant.map(r => r.slippageBps).sort((a, b) => a - b);
54
+ const p95Idx = Math.floor(bps.length * 0.95);
55
+ return {
56
+ count: relevant.length,
57
+ avgBps: bps.reduce((s, v) => s + v, 0) / bps.length,
58
+ maxBps: bps[bps.length - 1],
59
+ minBps: bps[0],
60
+ p95Bps: bps[p95Idx],
61
+ totalCost: relevant.reduce((s, r) => {
62
+ // Cost of slippage = (actualPrice - expectedPrice) * amount
63
+ const diff = r.side === 'buy'
64
+ ? (r.actualPrice - r.expectedPrice)
65
+ : (r.expectedPrice - r.actualPrice);
66
+ return s + diff * r.amount;
67
+ }, 0),
68
+ };
69
+ }
70
+ /** Get recent records (last N). */
71
+ getRecent(count = 10) {
72
+ return this.records.slice(-count);
73
+ }
74
+ /** Get all records for a symbol. */
75
+ getRecordsForSymbol(symbol) {
76
+ return this.records.filter(r => r.symbol === symbol);
77
+ }
78
+ }
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
3
+ import type { CcxtPosition } from '../types.js';
4
+ export declare const DEFAULT_INTERVAL_MS = 10000;
5
+ export interface StopTriggeredEvent {
6
+ symbol: string;
7
+ side: 'long' | 'short';
8
+ stopPrice: number;
9
+ markPrice: number;
10
+ quantity: number;
11
+ }
12
+ /** Decide whether a position has crossed its stop.
13
+ * Exported for direct unit-testing without spinning up a watcher loop. */
14
+ export declare function isStopBreached(position: CcxtPosition): boolean;
15
+ export declare class PositionWatcher extends EventEmitter {
16
+ private interval;
17
+ private intervalMs;
18
+ private adapter;
19
+ /** Symbols currently awaiting a close RPC — second trigger in the same tick
20
+ * is suppressed so the watcher cannot double-close. Cleared after the close
21
+ * call resolves (success or failure). */
22
+ private closePending;
23
+ /** Symbols we've already logged a breach for this session, to avoid spamming
24
+ * the log every tick while the close is in flight. */
25
+ private notifiedBreach;
26
+ constructor(adapter: IExchangeAdapter, intervalMs?: number);
27
+ start(): void;
28
+ stop(): void;
29
+ /** Run one check cycle. Exposed for tests (skip setInterval). */
30
+ tick(): Promise<void>;
31
+ private fireClose;
32
+ /** For tests. */
33
+ isRunning(): boolean;
34
+ }
@@ -0,0 +1,158 @@
1
+ // Stop watcher — polls open positions and auto-closes any that have crossed
2
+ // their metadata stopPrice, independent of the LLM agent.
3
+ //
4
+ // Policy vs enforcement split:
5
+ // - The LLM owns POLICY: it sets stopPrice at entry and may update it at
6
+ // any time (move to breakeven, trail, widen, or remove entirely by
7
+ // clearing it). The watcher just reads whatever stopPrice currently is.
8
+ // - The watcher owns ENFORCEMENT: if mark crosses stopPrice, it fires a
9
+ // market close immediately rather than waiting for the agent's next
10
+ // 15-min heartbeat. This closes the exit-latency gap the LLM-centric
11
+ // model would otherwise leave open.
12
+ //
13
+ // Scope:
14
+ // - Works in paper mode (PaperAdapter.getPositions returns CcxtPosition with
15
+ // stopPrice populated from position metadata).
16
+ // - In live mode LiveAdapter does NOT track per-position metadata, so
17
+ // getPositions returns positions without stopPrice and the watcher
18
+ // naturally skips them. Wiring live-mode metadata is a separate change
19
+ // for when live trading turns on.
20
+ import { EventEmitter } from 'node:events';
21
+ import { logger, formatError } from '../logger.js';
22
+ const TAG = 'stop-watcher';
23
+ // 2026-05-15 EFFICIENCY_QUICK_WINS Change 1: 3s → 10s. The watcher was
24
+ // designed when it was the ONLY stop-enforcement mechanism; with exchange-
25
+ // native brackets (live in enforce mode) it is now belt-and-suspenders, so
26
+ // 10s latency to catch a watcher-triggered close is fine — Binance fires the
27
+ // bracket stop in ms regardless. Saves ~70 weight/min (100→30). Operator can
28
+ // revert with zero deploy via plugin-config.json `stopWatcher.intervalMs`.
29
+ // See docs/EFFICIENCY_QUICK_WINS_PLAN.md + the Binance ban-gate context.
30
+ export const DEFAULT_INTERVAL_MS = 10_000;
31
+ /** Decide whether a position has crossed its stop.
32
+ * Exported for direct unit-testing without spinning up a watcher loop. */
33
+ export function isStopBreached(position) {
34
+ const stop = position.stopPrice;
35
+ if (stop === undefined || stop === null || !Number.isFinite(stop) || stop <= 0) {
36
+ return false;
37
+ }
38
+ const mark = position.markPrice;
39
+ if (!Number.isFinite(mark) || mark <= 0) {
40
+ return false;
41
+ }
42
+ return position.side === 'long' ? mark <= stop : mark >= stop;
43
+ }
44
+ export class PositionWatcher extends EventEmitter {
45
+ interval = null;
46
+ intervalMs;
47
+ adapter;
48
+ /** Symbols currently awaiting a close RPC — second trigger in the same tick
49
+ * is suppressed so the watcher cannot double-close. Cleared after the close
50
+ * call resolves (success or failure). */
51
+ closePending = new Set();
52
+ /** Symbols we've already logged a breach for this session, to avoid spamming
53
+ * the log every tick while the close is in flight. */
54
+ notifiedBreach = new Set();
55
+ constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS) {
56
+ super();
57
+ this.adapter = adapter;
58
+ this.intervalMs = intervalMs;
59
+ }
60
+ start() {
61
+ if (this.interval) {
62
+ logger.warn(TAG, 'Already running');
63
+ return;
64
+ }
65
+ logger.info(TAG, `Starting stop watcher (interval: ${this.intervalMs}ms)`);
66
+ this.interval = setInterval(() => {
67
+ void this.tick();
68
+ }, this.intervalMs);
69
+ }
70
+ stop() {
71
+ if (this.interval) {
72
+ clearInterval(this.interval);
73
+ this.interval = null;
74
+ }
75
+ this.closePending.clear();
76
+ this.notifiedBreach.clear();
77
+ logger.info(TAG, 'Stopped');
78
+ }
79
+ /** Run one check cycle. Exposed for tests (skip setInterval). */
80
+ async tick() {
81
+ let positions;
82
+ try {
83
+ positions = await this.adapter.getPositionsOrNull();
84
+ }
85
+ catch (err) {
86
+ logger.warn(TAG, `getPositions failed: ${formatError(err)}`);
87
+ return;
88
+ }
89
+ if (positions === null) {
90
+ // Fetch failed (429 / weight-paced / transient) — position state is
91
+ // UNKNOWN, not "flat". Skip this tick WITHOUT touching notifiedBreach /
92
+ // closePending: a null-collapse to [] would wipe the in-flight close
93
+ // guard (openSymbols below would be empty, clearing every closePending
94
+ // entry) and let the NEXT tick fire a SECOND close on a position whose
95
+ // close is already in flight. State repairs on the next successful tick.
96
+ logger.warn(TAG, 'getPositions unavailable (null) — skipping tick, preserving in-flight close guards');
97
+ return;
98
+ }
99
+ // Clear notification flags for any symbol no longer holding a position,
100
+ // so a re-entered position later gets a fresh log line on re-breach.
101
+ const openSymbols = new Set(positions.map(p => p.symbol));
102
+ for (const sym of this.notifiedBreach) {
103
+ if (!openSymbols.has(sym))
104
+ this.notifiedBreach.delete(sym);
105
+ }
106
+ for (const sym of this.closePending) {
107
+ if (!openSymbols.has(sym))
108
+ this.closePending.delete(sym);
109
+ }
110
+ for (const position of positions) {
111
+ if (!isStopBreached(position))
112
+ continue;
113
+ if (this.closePending.has(position.symbol))
114
+ continue;
115
+ if (!this.notifiedBreach.has(position.symbol)) {
116
+ logger.warn(TAG, `STOP BREACHED: ${position.symbol} ${position.side} mark=${position.markPrice} stop=${position.stopPrice} — auto-closing`);
117
+ this.notifiedBreach.add(position.symbol);
118
+ }
119
+ this.closePending.add(position.symbol);
120
+ // Fire-and-forget per symbol so one slow close doesn't stall the tick.
121
+ void this.fireClose(position);
122
+ }
123
+ }
124
+ async fireClose(position) {
125
+ const event = {
126
+ symbol: position.symbol,
127
+ side: position.side,
128
+ stopPrice: position.stopPrice ?? 0,
129
+ markPrice: position.markPrice,
130
+ quantity: position.contracts,
131
+ };
132
+ this.emit('stop_triggered', event);
133
+ try {
134
+ await this.adapter.closePosition(position.symbol, 'stop_watcher');
135
+ logger.info(TAG, `Auto-closed ${position.symbol} (reason=stop_watcher)`);
136
+ this.emit('stop_closed', event);
137
+ }
138
+ catch (err) {
139
+ const msg = formatError(err);
140
+ // "No open position" means the LLM or another path already closed it —
141
+ // that's a successful no-op, not a failure.
142
+ if (msg.includes('No open position')) {
143
+ logger.info(TAG, `${position.symbol} already closed by another path — skipping`);
144
+ }
145
+ else {
146
+ logger.error(TAG, `Auto-close failed for ${position.symbol}: ${msg}`);
147
+ }
148
+ this.emit('stop_failed', { ...event, error: msg });
149
+ }
150
+ finally {
151
+ this.closePending.delete(position.symbol);
152
+ }
153
+ }
154
+ /** For tests. */
155
+ isRunning() {
156
+ return this.interval !== null;
157
+ }
158
+ }
@@ -0,0 +1,54 @@
1
+ import type { BinancePrivateApi } from '../ccxt/binance-private.js';
2
+ import type { UserDataStream } from './user-data-stream.js';
3
+ import type { LiveStateStore } from './live-state-store.js';
4
+ export interface UserDataActiveProbeOptions {
5
+ api: BinancePrivateApi;
6
+ stream: UserDataStream;
7
+ store: LiveStateStore;
8
+ /** How often to run the probe. Default 60_000 (60s). Pass 0 to disable. */
9
+ intervalMs?: number;
10
+ /** Subtracted from `lastEventAt` when computing `since` so we don't miss
11
+ * trades whose timestamp landed in the same millisecond as our last
12
+ * frame. Default 1_000 (1 s). */
13
+ safetyBufferMs?: number;
14
+ /** For unit tests — injected timers + clock. */
15
+ timers?: {
16
+ setInterval?: (fn: () => void, ms: number) => ReturnType<typeof setInterval>;
17
+ clearInterval?: (h: ReturnType<typeof setInterval>) => void;
18
+ };
19
+ /** For unit tests — injected clock used only for log timestamps. */
20
+ now?: () => number;
21
+ }
22
+ export interface UserDataActiveProbeHealth {
23
+ probesRun: number;
24
+ reconnectsTriggered: number;
25
+ errorCount: number;
26
+ lastReconnectReason: string | null;
27
+ lastReconnectAt: number | null;
28
+ }
29
+ export declare class UserDataActiveProbe {
30
+ private readonly api;
31
+ private readonly stream;
32
+ private readonly store;
33
+ private readonly intervalMs;
34
+ private readonly safetyBufferMs;
35
+ private readonly setIntervalFn;
36
+ private readonly clearIntervalFn;
37
+ private readonly now;
38
+ private timer;
39
+ private running;
40
+ private inFlight;
41
+ private probesRun;
42
+ private reconnectsTriggered;
43
+ private errorCount;
44
+ private lastReconnectReason;
45
+ private lastReconnectAt;
46
+ constructor(opts: UserDataActiveProbeOptions);
47
+ start(): void;
48
+ stop(): void;
49
+ getHealth(): UserDataActiveProbeHealth;
50
+ /** Testing hook — runs one probe pass synchronously (well, async, but
51
+ * awaitable). Production uses the interval-driven path; tests can drive
52
+ * this directly without touching fake timers. */
53
+ runProbe(): Promise<void>;
54
+ }
@@ -0,0 +1,180 @@
1
+ // UserDataActiveProbe — proactive REST probe that catches "WS silently
2
+ // stopped delivering" before the staleness watchdog does.
3
+ //
4
+ // WHY THIS EXISTS
5
+ // ---------------
6
+ // On 2026-04-25 we observed the prod user-data stream go silent for 15
7
+ // minutes at a stretch while a real bracket-stop fill happened on Binance.
8
+ // The socket stayed "open" at the OS level (no FIN, no error), the listenKey
9
+ // keep-alive succeeded, but no frames arrived. The 15-min staleness watchdog
10
+ // caught it eventually, but in that window the local store still showed an
11
+ // already-closed position as live. Either alone — adaptive watchdog OR active
12
+ // probe — narrows the window; together they close it.
13
+ //
14
+ // WHAT IT DOES
15
+ // ------------
16
+ // Every `intervalMs` (default 60s):
17
+ // 1. If the stream isn't connected, skip — reconnect logic owns recovery.
18
+ // 2. Read symbols from `store.getPositions()` — only positions where a
19
+ // missed fill matters (a closed position is a non-event).
20
+ // 3. For each symbol, call `api.fetchMyTrades(symbol, since=lastEventAt - 1s)`.
21
+ // 4. If any returned trade has `timestamp > lastEventAt`, the WS missed
22
+ // a fill: log the trade id + symbol and call `stream.requestReconnect`.
23
+ // 5. Stop after the first detection — reconnect will trigger snapshot +
24
+ // gap-fill, and another probe pass on the next tick covers anything new.
25
+ //
26
+ // WHAT IT DOES NOT DO
27
+ // -------------------
28
+ // - Does NOT ingest the trade itself. Reconnect → resyncRequired → gap-filler
29
+ // handles ingest with `source='rest_reconcile'`. Single-responsibility.
30
+ // - Does NOT touch LiveStateStore. Reconnect snapshot reseeds it.
31
+ // - Does NOT run on flat accounts. With no open positions there's nothing to
32
+ // protect — the watchdog's looser idle threshold is appropriate.
33
+ //
34
+ // COST
35
+ // ----
36
+ // `fetchMyTrades` is weight 5 per symbol per call on Binance Futures. With
37
+ // ~5 open positions on a typical account, that's 25 weight/min — well below
38
+ // the 2400/min budget.
39
+ import { logger, formatError } from '../logger.js';
40
+ const TAG = 'user-data-active-probe';
41
+ /** CCXT unified symbol → API form. The reconciler stores `BTC/USDT` (no suffix);
42
+ * CCXT's fetchMyTrades takes either form, but we re-attach the settle suffix
43
+ * to be explicit. */
44
+ function toApiSymbol(s) {
45
+ if (!s)
46
+ return s;
47
+ if (s.includes(':'))
48
+ return s;
49
+ const parts = s.split('/');
50
+ return parts.length === 2 ? `${s}:${parts[1]}` : s;
51
+ }
52
+ export class UserDataActiveProbe {
53
+ api;
54
+ stream;
55
+ store;
56
+ intervalMs;
57
+ safetyBufferMs;
58
+ setIntervalFn;
59
+ clearIntervalFn;
60
+ now;
61
+ timer = null;
62
+ running = false;
63
+ inFlight = false;
64
+ probesRun = 0;
65
+ reconnectsTriggered = 0;
66
+ errorCount = 0;
67
+ lastReconnectReason = null;
68
+ lastReconnectAt = null;
69
+ constructor(opts) {
70
+ this.api = opts.api;
71
+ this.stream = opts.stream;
72
+ this.store = opts.store;
73
+ this.intervalMs = opts.intervalMs ?? 60_000;
74
+ this.safetyBufferMs = opts.safetyBufferMs ?? 1_000;
75
+ this.setIntervalFn = opts.timers?.setInterval ?? ((fn, ms) => setInterval(fn, ms));
76
+ this.clearIntervalFn = opts.timers?.clearInterval ?? ((h) => clearInterval(h));
77
+ this.now = opts.now ?? (() => Date.now());
78
+ }
79
+ start() {
80
+ if (this.running)
81
+ return;
82
+ if (this.intervalMs <= 0) {
83
+ logger.info(TAG, 'disabled (intervalMs=0)');
84
+ return;
85
+ }
86
+ this.running = true;
87
+ this.timer = this.setIntervalFn(() => {
88
+ void this.runProbe().catch((err) => {
89
+ this.errorCount++;
90
+ logger.warn(TAG, `probe pass threw: ${formatError(err)} — continuing`);
91
+ });
92
+ }, this.intervalMs);
93
+ logger.info(TAG, `started (intervalMs=${this.intervalMs}, safetyBufferMs=${this.safetyBufferMs})`);
94
+ }
95
+ stop() {
96
+ if (!this.running)
97
+ return;
98
+ if (this.timer) {
99
+ this.clearIntervalFn(this.timer);
100
+ this.timer = null;
101
+ }
102
+ this.running = false;
103
+ logger.info(TAG, 'stopped');
104
+ }
105
+ getHealth() {
106
+ return {
107
+ probesRun: this.probesRun,
108
+ reconnectsTriggered: this.reconnectsTriggered,
109
+ errorCount: this.errorCount,
110
+ lastReconnectReason: this.lastReconnectReason,
111
+ lastReconnectAt: this.lastReconnectAt,
112
+ };
113
+ }
114
+ /** Testing hook — runs one probe pass synchronously (well, async, but
115
+ * awaitable). Production uses the interval-driven path; tests can drive
116
+ * this directly without touching fake timers. */
117
+ async runProbe() {
118
+ if (!this.running && this.timer === null) {
119
+ // Allow direct invocation in tests even when start() wasn't called.
120
+ }
121
+ if (this.inFlight)
122
+ return;
123
+ this.inFlight = true;
124
+ this.probesRun++;
125
+ try {
126
+ if (!this.stream.isConnected())
127
+ return;
128
+ const health = this.stream.getHealth();
129
+ const lastEventAt = health.lastEventAt;
130
+ if (lastEventAt == null)
131
+ return; // Never received an event; nothing to compare against.
132
+ const positions = this.store.getPositions().filter((p) => p && p.contracts > 0);
133
+ if (positions.length === 0)
134
+ return;
135
+ const symbols = Array.from(new Set(positions.map((p) => p.symbol).filter(Boolean)));
136
+ const since = lastEventAt - this.safetyBufferMs;
137
+ for (const symbol of symbols) {
138
+ let trades;
139
+ try {
140
+ trades = await this.api.fetchMyTrades(toApiSymbol(symbol), since, 50);
141
+ }
142
+ catch (err) {
143
+ this.errorCount++;
144
+ logger.warn(TAG, `fetchMyTrades(${symbol}) threw: ${formatError(err)}`);
145
+ continue;
146
+ }
147
+ if (!Array.isArray(trades) || trades.length === 0)
148
+ continue;
149
+ let newest = null;
150
+ for (const t of trades) {
151
+ if (!t || typeof t !== 'object')
152
+ continue;
153
+ const rec = t;
154
+ const ts = typeof rec.timestamp === 'number' ? rec.timestamp : 0;
155
+ if (ts <= lastEventAt)
156
+ continue;
157
+ const id = typeof rec.id === 'string' ? rec.id : String(rec.id ?? '?');
158
+ if (!newest || ts > newest.timestamp) {
159
+ newest = { id, timestamp: ts };
160
+ }
161
+ }
162
+ if (newest) {
163
+ const lagMs = this.now() - newest.timestamp;
164
+ const reason = `active_probe_silent_stream`;
165
+ logger.error(TAG, `silent-stream detected: ${symbol} trade id=${newest.id} t=${new Date(newest.timestamp).toISOString()} ` +
166
+ `> lastEventAt=${new Date(lastEventAt).toISOString()} (lagMs=${lagMs}) — forcing reconnect`);
167
+ this.reconnectsTriggered++;
168
+ this.lastReconnectReason = `${reason}:${symbol}`;
169
+ this.lastReconnectAt = this.now();
170
+ this.stream.requestReconnect(reason);
171
+ // One reconnect per pass — don't fire repeatedly across symbols.
172
+ return;
173
+ }
174
+ }
175
+ }
176
+ finally {
177
+ this.inFlight = false;
178
+ }
179
+ }
180
+ }