@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,565 @@
1
+ // REST gap-filler — Phase 2 of TRADE_AUDIT_TRAIL_PLAN.
2
+ //
3
+ // WHY THIS EXISTS
4
+ // ---------------
5
+ // The user-data WebSocket is event-driven: Binance only pushes frames when
6
+ // the account moves. A quiet account sits silent for minutes at a time.
7
+ // When a fill lands during a WS reconnect window (stale-watchdog or network
8
+ // blip), the ORDER_TRADE_UPDATE event for that fill is lost forever — it's
9
+ // a pushed stream, not a queryable log. The positions/balance snapshot
10
+ // taken on reconnect reflects the POST-fill state, so truth-check returns
11
+ // OK, but the individual fill row never lands in the `trades` table.
12
+ //
13
+ // In shadow mode this is benign (REST is authoritative for reads). In
14
+ // observe/enforce mode, WS is the sole audit-trail source and missed fills
15
+ // silently degrade correctness of query_trades, daily P&L reconciliation,
16
+ // and downstream analytics. This class closes that gap.
17
+ //
18
+ // WHAT IT DOES
19
+ // ------------
20
+ // On every WS 'connected' event AFTER the initial connect, query
21
+ // `/fapi/v1/userTrades` for every symbol this session has touched, using
22
+ // `since = lastDisconnectTime - safetyBufferMs` so the window overlaps
23
+ // what the WS may already have delivered. Convert each trade to a
24
+ // FillEvent with source='rest_reconcile' and POST via TradeStoreClient.
25
+ //
26
+ // The webapp's ingest endpoint upserts ON CONFLICT (exchange,
27
+ // exchange_trade_id) DO NOTHING — see webapp/src/app/api/internal/trades.
28
+ // Any trade the WS already delivered via source='ws' will lose the race
29
+ // and stay recorded as 'ws' (correct). New trades land with
30
+ // source='rest_reconcile'.
31
+ //
32
+ // WHAT IT DOES NOT DO
33
+ // -------------------
34
+ // - It does NOT touch the LiveStateStore. Positions/balance seeding is
35
+ // the snapshot's job; this class is audit-trail only.
36
+ // - It does NOT gap-fill on the initial connect. The plugin just started —
37
+ // there's no blind window to backfill. Initial snapshot seeds state from
38
+ // REST, which is already authoritative for any fills before startup.
39
+ // - It does NOT retry failed fetches. If fetchMyTrades returns [] because
40
+ // of an error, we log and move on. The next reconnect will retry over
41
+ // a superset window. (Reconnect path only — the startup deep-fill uses
42
+ // fetchMyTradesOrNull and DISTINGUISHES a failed read (null) from a
43
+ // genuinely empty window, aborting the symbol instead of silently
44
+ // truncating the backfill.)
45
+ //
46
+ // SYMBOL TRACKING
47
+ // ---------------
48
+ // Binance requires a symbol per fetchMyTrades call; there's no
49
+ // portfolio-wide trades endpoint. We track every symbol we've seen via
50
+ // WS orderUpdate events + position snapshots. Set never shrinks during
51
+ // the session — a closed position's symbol is still queried on reconnect
52
+ // because late fills (closing trade) may have been missed.
53
+ //
54
+ // Spec:
55
+ // https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List
56
+ import { logger, formatError } from '../logger.js';
57
+ import { TouchedSymbolsStore } from './touched-symbols-store.js';
58
+ const TAG = 'rest-gap-filler';
59
+ /** Binance caps a single userTrades window (startTime..endTime) at 7 days, so
60
+ * the startup deep-fill chunks its lookback into windows no larger than this. */
61
+ const DEEP_FILL_WINDOW_MS = 7 * 24 * 60 * 60_000;
62
+ /** CCXT unified symbol 'BTC/USDT:USDT' → canonical 'BTC/USDT'. Matches the
63
+ * WS-side normalizer so gap-fill rows land under the same symbol key
64
+ * every downstream consumer uses. */
65
+ function normSymbol(s) {
66
+ return (s ?? '').replace(/:[A-Z]+$/, '');
67
+ }
68
+ /** Parse a positive-integer env override, falling back to `def` on missing /
69
+ * non-numeric / non-positive input. */
70
+ function parseEnvInt(raw, def) {
71
+ const n = Number(raw);
72
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : def;
73
+ }
74
+ export class RestGapFiller {
75
+ api;
76
+ stream;
77
+ store;
78
+ client;
79
+ userId;
80
+ exchange;
81
+ safetyBufferMs;
82
+ maxWindowMs;
83
+ perSymbolLimit;
84
+ startupDeepFill;
85
+ startupLookbackMs;
86
+ deepFillPaceMs;
87
+ maxPagesPerWindow;
88
+ now;
89
+ /** Every symbol we've seen via WS orderUpdate, a position snapshot, or
90
+ * the persistent disk store. Never shrinks — a symbol's closing trade
91
+ * may still be pending delivery when its position hits zero, and a
92
+ * symbol traded in a previous session is still a candidate for fills
93
+ * the WS missed during a plugin restart. */
94
+ knownSymbols = new Set();
95
+ /** Optional file-backed persistence so the symbol universe survives
96
+ * plugin restarts. Null in tests that want pure in-memory behaviour. */
97
+ touchedStore;
98
+ /** Timestamp of the last observed disconnect. Bootstrapped to null so
99
+ * the initial window (before the first disconnect) contributes no
100
+ * backfill — no blind window existed before the stream was ever up. */
101
+ lastDisconnectAt = null;
102
+ started = false;
103
+ onResyncRequired;
104
+ onDisconnected;
105
+ onOrderUpdate;
106
+ // Metrics — exposed via getHealth() for operator dashboards.
107
+ reconnectEvents = 0;
108
+ gapFillRuns = 0;
109
+ symbolsQueried = 0;
110
+ tradesBackfilled = 0;
111
+ errorCount = 0;
112
+ constructor(opts) {
113
+ this.api = opts.api;
114
+ this.stream = opts.stream;
115
+ this.store = opts.store;
116
+ this.client = opts.client;
117
+ this.userId = opts.userId;
118
+ this.exchange = opts.exchange ?? 'binance_futures';
119
+ this.safetyBufferMs = opts.safetyBufferMs ?? 10_000;
120
+ this.maxWindowMs = opts.maxWindowMs ?? 60 * 60_000;
121
+ this.perSymbolLimit = opts.perSymbolLimit ?? 500;
122
+ // Startup deep-fill: opt-in via env RC_GAPFILL_STARTUP=on (gated-flag
123
+ // convention — every restart makes paced Binance calls + idempotent webapp
124
+ // POSTs, so it's activated explicitly on deploy, not silently on by default).
125
+ this.startupDeepFill =
126
+ opts.startupDeepFill ?? (process.env.RC_GAPFILL_STARTUP === 'on');
127
+ this.startupLookbackMs =
128
+ opts.startupLookbackMs ??
129
+ parseEnvInt(process.env.RC_GAPFILL_STARTUP_LOOKBACK_MS, DEEP_FILL_WINDOW_MS);
130
+ this.deepFillPaceMs =
131
+ opts.deepFillPaceMs ?? parseEnvInt(process.env.RC_GAPFILL_PACE_MS, 250);
132
+ this.maxPagesPerWindow = opts.maxPagesPerWindow ?? 25;
133
+ this.now = opts.now ?? (() => Date.now());
134
+ // touchedSymbolsStore: explicit null disables persistence; undefined
135
+ // takes the production default (file-backed under ~/.reefclaw).
136
+ this.touchedStore =
137
+ opts.touchedSymbolsStore === null
138
+ ? null
139
+ : (opts.touchedSymbolsStore ?? new TouchedSymbolsStore({ userId: this.userId }));
140
+ this.onResyncRequired = (cause) => this.handleResyncRequired(cause);
141
+ this.onDisconnected = () => this.handleDisconnected();
142
+ this.onOrderUpdate = (ev) => {
143
+ if (!ev?.symbol)
144
+ return;
145
+ const norm = normSymbol(ev.symbol);
146
+ if (this.knownSymbols.has(norm))
147
+ return;
148
+ this.knownSymbols.add(norm);
149
+ // Persist on first sighting only — same-symbol updates are a no-op.
150
+ this.touchedStore?.save(this.knownSymbols);
151
+ };
152
+ }
153
+ start() {
154
+ if (this.started)
155
+ return;
156
+ // `resyncRequired` fires on every successful (re)connect AFTER the
157
+ // initial connect — exactly the hook we need. Initial connect has no
158
+ // blind window so we skip it by design (UserDataStream only emits
159
+ // resyncRequired on the 2nd, 3rd, ... connects).
160
+ this.stream.on('resyncRequired', this.onResyncRequired);
161
+ this.stream.on('disconnected', this.onDisconnected);
162
+ this.stream.on('orderUpdate', this.onOrderUpdate);
163
+ this.started = true;
164
+ // Hydrate first so any open-position seed merges in without erasing
165
+ // the persisted history. Then persist if the merge added anything.
166
+ this.hydrateKnownSymbolsFromDisk();
167
+ this.seedKnownSymbolsFromStore();
168
+ // Background income discovery — recovers symbols that were traded
169
+ // during plugin downtime even when they aren't in the persisted
170
+ // touched-symbols cache (e.g. a brand-new symbol that opened AND
171
+ // closed entirely while the plugin was offline). Discovery resolves
172
+ // BEFORE the startup deep-fill so a symbol traded entirely during the
173
+ // outage is in knownSymbols when its trades are queried. Fire-and-forget
174
+ // so start() never blocks the boot path.
175
+ void (async () => {
176
+ try {
177
+ const lookbackSince = this.now() - this.startupLookbackMs;
178
+ // Match the discovery window to the deep-fill lookback so a
179
+ // multi-day-old symbol is found; falls back to the 1h default
180
+ // window when the deep-fill is disabled.
181
+ await this.discoverSymbolsFromIncome(this.startupDeepFill ? lookbackSince : undefined);
182
+ if (this.startupDeepFill) {
183
+ await this.runStartupDeepFill(lookbackSince);
184
+ }
185
+ }
186
+ catch (err) {
187
+ this.errorCount++;
188
+ logger.error(TAG, `startup discovery/deep-fill chain rejected: ${formatError(err)}`);
189
+ }
190
+ })();
191
+ logger.info(TAG, `Started — userId=${this.userId} safetyBuffer=${this.safetyBufferMs}ms maxWindow=${this.maxWindowMs}ms ` +
192
+ `startupDeepFill=${this.startupDeepFill} startupLookback=${this.startupLookbackMs}ms knownSymbols=${this.knownSymbols.size}`);
193
+ }
194
+ stop() {
195
+ if (!this.started)
196
+ return;
197
+ this.stream.off('resyncRequired', this.onResyncRequired);
198
+ this.stream.off('disconnected', this.onDisconnected);
199
+ this.stream.off('orderUpdate', this.onOrderUpdate);
200
+ this.started = false;
201
+ logger.info(TAG, 'Stopped');
202
+ }
203
+ getHealth() {
204
+ return {
205
+ reconnectEvents: this.reconnectEvents,
206
+ gapFillRuns: this.gapFillRuns,
207
+ symbolsQueried: this.symbolsQueried,
208
+ tradesBackfilled: this.tradesBackfilled,
209
+ errorCount: this.errorCount,
210
+ };
211
+ }
212
+ /** Testing hook — lets unit tests trigger a gap-fill pass with a known
213
+ * time window instead of replaying stream events. */
214
+ async runGapFill(since) {
215
+ this.gapFillRuns++;
216
+ this.seedKnownSymbolsFromStore();
217
+ const symbols = Array.from(this.knownSymbols);
218
+ if (symbols.length === 0) {
219
+ logger.info(TAG, `gap-fill skipped: no known symbols (since=${new Date(since).toISOString()})`);
220
+ return;
221
+ }
222
+ logger.info(TAG, `gap-fill starting: ${symbols.length} symbol(s), since=${new Date(since).toISOString()}`);
223
+ let totalThisRun = 0;
224
+ for (const symbol of symbols) {
225
+ this.symbolsQueried++;
226
+ try {
227
+ // Binance userTrades takes the raw symbol (e.g. 'BTC/USDT:USDT' —
228
+ // CCXT's unified form). We stored normalized keys, add the suffix
229
+ // back for the API call. CCXT handles either form.
230
+ const apiSymbol = symbol.includes(':') ? symbol : `${symbol}:${symbol.split('/')[1]}`;
231
+ const trades = await this.api.fetchMyTrades(apiSymbol, since, this.perSymbolLimit);
232
+ const posted = this.postTrades(symbol, trades);
233
+ totalThisRun += posted;
234
+ }
235
+ catch (err) {
236
+ this.errorCount++;
237
+ logger.warn(TAG, `gap-fill ${symbol} threw: ${formatError(err)} — continuing other symbols`);
238
+ }
239
+ }
240
+ logger.info(TAG, `gap-fill complete: ${totalThisRun} fills backfilled across ${symbols.length} symbol(s)`);
241
+ }
242
+ /** One-shot startup deep-fill — the recurrence fix.
243
+ *
244
+ * The reconnect gap-fill (handleResyncRequired) only covers blind windows
245
+ * while the plugin is RUNNING and caps its lookback at maxWindowMs (1h). A
246
+ * fill that lands while the plugin is DOWN — most damagingly a position's
247
+ * closing trade — is never re-pushed by the event-driven WS and was never
248
+ * gap-filled, so it silently never reaches the `trades` ledger. Over many
249
+ * restarts these missing closes accumulate and make the journal's net-PnL
250
+ * (derived from the ledger) wrong. This pass queries userTrades for every
251
+ * known symbol over a configurable lookback so downtime closes land on the
252
+ * next start. Paginated + 7-day-window-chunked (Binance caps userTrades at
253
+ * 1000 rows / 7-day span per call) and paced to stay clear of the per-IP
254
+ * weight ceiling. Idempotent at the webapp (ON CONFLICT DO NOTHING), so
255
+ * re-querying already-recorded fills is a harmless no-op. */
256
+ async runStartupDeepFill(sinceFloor) {
257
+ this.seedKnownSymbolsFromStore();
258
+ const symbols = Array.from(this.knownSymbols);
259
+ if (symbols.length === 0) {
260
+ logger.info(TAG, 'startup deep-fill skipped: no known symbols');
261
+ return;
262
+ }
263
+ this.gapFillRuns++;
264
+ logger.info(TAG, `startup deep-fill starting: ${symbols.length} symbol(s), since=${new Date(sinceFloor).toISOString()} ` +
265
+ `(lookback=${this.startupLookbackMs}ms, pace=${this.deepFillPaceMs}ms)`);
266
+ let total = 0;
267
+ for (const symbol of symbols) {
268
+ try {
269
+ total += await this.deepFillSymbol(symbol, sinceFloor);
270
+ }
271
+ catch (err) {
272
+ this.errorCount++;
273
+ logger.warn(TAG, `startup deep-fill ${symbol} threw: ${formatError(err)} — continuing other symbols`);
274
+ }
275
+ await this.sleep(this.deepFillPaceMs);
276
+ }
277
+ logger.info(TAG, `startup deep-fill complete: ${total} fill(s) backfilled across ${symbols.length} symbol(s)`);
278
+ }
279
+ /** Deep-fill a single symbol over [sinceFloor, now], chunked into ≤7-day
280
+ * windows (Binance's per-call span cap). Within a window the FIRST page is
281
+ * time-bounded (startTime/endTime); FULL pages continue via Binance's
282
+ * `fromId = lastTradeId + 1` (doc-verified: fromId pages ascending by
283
+ * trade id and CANNOT be combined with startTime/endTime), stopping when
284
+ * a page runs past the window end, comes back short, or hits the page
285
+ * cap. Paging by id — not by `lastTime + 1` — is what keeps
286
+ * same-millisecond trades on a full-page boundary from being skipped.
287
+ *
288
+ * Every fetch goes through the ban-gated `api.fetchMyTradesOrNull`
289
+ * wrapper — we deliberately do NOT use CCXT's built-in `{ paginate: true }`,
290
+ * which re-enters CCXT directly and would bypass the process-wide IP-ban
291
+ * gate. A `null` read (paced / banned / garbled) is a FAILED read, not an
292
+ * empty window: we abort THIS symbol by throwing (caller logs + counts the
293
+ * error and continues other symbols) instead of advancing the cursor —
294
+ * error-as-empty silently truncated the backfill exactly when Binance was
295
+ * shedding us. Returns the number of fills posted. */
296
+ async deepFillSymbol(symbol, sinceFloor) {
297
+ this.symbolsQueried++;
298
+ // Binance userTrades wants CCXT's unified suffixed form ('BTC/USDT:USDT');
299
+ // knownSymbols holds normalized keys, so re-append the quote suffix.
300
+ const apiSymbol = symbol.includes(':') ? symbol : `${symbol}:${symbol.split('/')[1]}`;
301
+ const nowMs = this.now();
302
+ let windowStart = sinceFloor;
303
+ let posted = 0;
304
+ while (windowStart < nowMs) {
305
+ const windowEnd = Math.min(windowStart + DEEP_FILL_WINDOW_MS, nowMs);
306
+ let fromId = null;
307
+ for (let page = 0; page < this.maxPagesPerWindow; page++) {
308
+ const trades = fromId == null
309
+ ? await this.api.fetchMyTradesOrNull(apiSymbol, windowStart, this.perSymbolLimit, { endTime: windowEnd })
310
+ : await this.api.fetchMyTradesOrNull(apiSymbol, undefined, this.perSymbolLimit, { fromId });
311
+ if (trades === null) {
312
+ throw new Error(`deep-fill ${symbol}: fetchMyTrades returned null (read failed — paced/banned/garbled). ` +
313
+ 'Aborting this symbol WITHOUT marking any window complete; the next startup deep-fill retries (posting is idempotent).');
314
+ }
315
+ if (trades.length === 0)
316
+ break;
317
+ // A fromId continuation page walks forward in id order with NO upper
318
+ // time bound — drop anything past this window (the next window's
319
+ // time-bounded first page re-fetches it; posting is idempotent, the
320
+ // filter just keeps the metrics honest).
321
+ const inWindow = trades.filter((t) => this.tradeTime(t) <= windowEnd);
322
+ posted += this.postTrades(symbol, inWindow);
323
+ if (inWindow.length < trades.length)
324
+ break; // paged past the window end
325
+ // A short page means this window is exhausted.
326
+ if (trades.length < this.perSymbolLimit)
327
+ break;
328
+ const lastId = this.tradeId(trades[trades.length - 1]);
329
+ if (lastId == null) {
330
+ // Cannot continue by id (malformed row) — stop rather than loop.
331
+ logger.warn(TAG, `deep-fill ${symbol}: full page with no parseable trade id — stopping pagination for this window`);
332
+ break;
333
+ }
334
+ fromId = lastId + 1; // fromId is inclusive — skip the last posted trade
335
+ await this.sleep(this.deepFillPaceMs);
336
+ }
337
+ windowStart = windowEnd;
338
+ }
339
+ return posted;
340
+ }
341
+ /** Epoch-ms timestamp of a CCXT trade — unified `timestamp` first, raw
342
+ * Binance `info.time` as fallback. 0 when neither is present. */
343
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
344
+ tradeTime(t) {
345
+ const ts = t?.timestamp != null ? Number(t.timestamp) : Number(t?.info?.time);
346
+ return Number.isFinite(ts) ? ts : 0;
347
+ }
348
+ /** Numeric Binance trade id of a CCXT trade — unified `id` first, raw
349
+ * `info.id` fallback. Null when not a finite number (`fromId` is a LONG
350
+ * on the Binance side, so a non-numeric id cannot seed pagination). */
351
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
352
+ tradeId(t) {
353
+ const raw = t?.id ?? t?.info?.id;
354
+ const n = Number(raw);
355
+ return Number.isFinite(n) ? n : null;
356
+ }
357
+ /** Cooperative delay used to pace deep-fill Binance calls. Resolves
358
+ * immediately for a non-positive duration (e.g. tests pass 0). */
359
+ sleep(ms) {
360
+ if (!(ms > 0))
361
+ return Promise.resolve();
362
+ return new Promise((resolve) => setTimeout(resolve, ms));
363
+ }
364
+ // ---- Internals ----
365
+ handleResyncRequired(cause) {
366
+ this.reconnectEvents++;
367
+ const nowMs = this.now();
368
+ // The gap-fill window must cover everything the WS may have missed. Two
369
+ // independent triggers can drive a reconnect:
370
+ // 1. Stale-watchdog / listenKey rotation — happens when WS goes briefly
371
+ // silent but didn't necessarily miss a fill. lastDisconnectAt is
372
+ // ~now and lastEventAt is also recent.
373
+ // 2. Active-probe-detected silent stream — REST sees a fill the WS
374
+ // never delivered. lastDisconnectAt becomes ~now (probe forces
375
+ // reconnect), but lastEventAt may be tens of minutes ago. Using
376
+ // disconnectAt as `since` makes the gap-fill window start AFTER
377
+ // the missed fill, so fetchMyTrades returns 0 every time.
378
+ // Fix: use the OLDER of (lastDisconnectAt, lastEventAt) so the window
379
+ // always covers the actual gap. maxWindow still caps the lookback so
380
+ // we don't backfill multi-hour outages in one shot.
381
+ const disconnectAt = this.lastDisconnectAt ?? nowMs;
382
+ const lastEventAt = this.stream.getHealth().lastEventAt ?? disconnectAt;
383
+ const baseSince = Math.min(disconnectAt, lastEventAt);
384
+ const rawSince = baseSince - this.safetyBufferMs;
385
+ const floorSince = nowMs - this.maxWindowMs;
386
+ const since = Math.max(rawSince, floorSince);
387
+ logger.info(TAG, `resync (cause=${cause}) — scheduling gap-fill since=${new Date(since).toISOString()} ` +
388
+ `(disconnectAt=${new Date(disconnectAt).toISOString()}, lastEventAt=${new Date(lastEventAt).toISOString()})`);
389
+ // Run async; don't block the stream handler. Discovery MUST resolve
390
+ // before runGapFill so a brand-new symbol traded entirely during this
391
+ // outage is in knownSymbols when fetchMyTrades runs. Discovery uses the
392
+ // exact same `since` window as the gap-fill so a single REST call
393
+ // covers everything that may have been missed.
394
+ void (async () => {
395
+ try {
396
+ await this.discoverSymbolsFromIncome(since);
397
+ await this.runGapFill(since);
398
+ }
399
+ catch (err) {
400
+ this.errorCount++;
401
+ logger.error(TAG, `resync gap-fill chain rejected: ${formatError(err)}`);
402
+ }
403
+ })();
404
+ }
405
+ handleDisconnected() {
406
+ this.lastDisconnectAt = this.now();
407
+ }
408
+ seedKnownSymbolsFromStore() {
409
+ const sizeBefore = this.knownSymbols.size;
410
+ try {
411
+ const positions = this.store.getPositions();
412
+ for (const p of positions) {
413
+ if (p?.symbol)
414
+ this.knownSymbols.add(normSymbol(p.symbol));
415
+ }
416
+ }
417
+ catch {
418
+ // Store may not be initialised yet on the first start() call; no-op.
419
+ }
420
+ if (this.knownSymbols.size > sizeBefore) {
421
+ this.touchedStore?.save(this.knownSymbols);
422
+ }
423
+ }
424
+ /** Merge the on-disk symbol history into the in-memory set. Best-effort —
425
+ * load() returns an empty Set on any error so a corrupt cache file is a
426
+ * silent regression to "session-only" behaviour, never a startup blocker. */
427
+ hydrateKnownSymbolsFromDisk() {
428
+ if (!this.touchedStore)
429
+ return;
430
+ const persisted = this.touchedStore.load();
431
+ if (persisted.size === 0)
432
+ return;
433
+ for (const s of persisted)
434
+ this.knownSymbols.add(s);
435
+ logger.info(TAG, `hydrated ${persisted.size} touched symbol(s) from disk`);
436
+ }
437
+ /** Discovery via /fapi/v1/income. Catches symbols traded during a blind
438
+ * window (plugin downtime, mid-session WS outage) that aren't represented
439
+ * in the persisted touched cache or the current open-position seed —
440
+ * i.e. brand-new symbols that opened AND closed entirely during the gap.
441
+ *
442
+ * `since` defaults to the maxWindow lookback (used by start()'s one-shot
443
+ * pass). On every resync, handleResyncRequired passes the reconnect's
444
+ * computed `since` so the discovery window precisely matches the
445
+ * fetchMyTrades window — a single REST call recovers every symbol that
446
+ * could have had activity in the gap.
447
+ *
448
+ * Best-effort: any error logs and returns; gap-fill still runs against
449
+ * whatever knownSymbols already contains. */
450
+ async discoverSymbolsFromIncome(since) {
451
+ if (typeof this.api.fetchRecentTradedSymbols !== 'function') {
452
+ // Older API surfaces / mocks that don't implement the discovery path.
453
+ // Silent skip preserves existing behaviour for unit tests that don't
454
+ // care about cross-restart symbol recovery.
455
+ return;
456
+ }
457
+ const sinceMs = since ?? this.now() - this.maxWindowMs;
458
+ try {
459
+ const symbols = await this.api.fetchRecentTradedSymbols(sinceMs);
460
+ if (!Array.isArray(symbols) || symbols.length === 0)
461
+ return;
462
+ let added = 0;
463
+ for (const s of symbols) {
464
+ const norm = normSymbol(s);
465
+ if (!norm)
466
+ continue;
467
+ if (this.knownSymbols.has(norm))
468
+ continue;
469
+ this.knownSymbols.add(norm);
470
+ added++;
471
+ }
472
+ if (added > 0) {
473
+ this.touchedStore?.save(this.knownSymbols);
474
+ logger.info(TAG, `income discovery added ${added} symbol(s) (since=${new Date(sinceMs).toISOString()}, total=${this.knownSymbols.size})`);
475
+ }
476
+ }
477
+ catch (err) {
478
+ logger.warn(TAG, `income discovery failed: ${formatError(err)} — gap-fill will use existing knownSymbols`);
479
+ }
480
+ }
481
+ /** Convert raw CCXT trade objects to FillEvents and post each to the
482
+ * webapp ingest endpoint. Returns the count successfully posted
483
+ * (posted means handed to the HTTP client; upsert dedup happens in
484
+ * the webapp). Silent on empty input. */
485
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
486
+ postTrades(symbol, raw) {
487
+ if (!Array.isArray(raw) || raw.length === 0)
488
+ return 0;
489
+ let posted = 0;
490
+ for (const t of raw) {
491
+ const fill = this.tradeToFillEvent(symbol, t);
492
+ if (!fill)
493
+ continue;
494
+ this.client.post(this.userId, fill);
495
+ this.tradesBackfilled++;
496
+ posted++;
497
+ }
498
+ return posted;
499
+ }
500
+ /** Map a CCXT trade (Binance /fapi/v1/userTrades) to our FillEvent shape.
501
+ * Returns null on malformed input — tradeId + orderId + price + qty are
502
+ * all required columns on the trades table.
503
+ *
504
+ * Binance raw field names (NOT CCXT-unified) per the docs — we read these
505
+ * via `t.info.*` as the second-choice source after CCXT's unified fields:
506
+ * id, orderId, symbol, side, positionSide, price, qty, quoteQty,
507
+ * commission, commissionAsset, time, buyer, maker, realizedPnl.
508
+ * Spec: https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List
509
+ *
510
+ * Fields NOT returned by userTrades (document-verified):
511
+ * - `reduceOnly` — lives on order endpoints, not trades. Gap-filled rows
512
+ * get reduceOnly=undefined; the primary WS path (source='ws') always
513
+ * has it. Since dedup preserves the first-writer row, trades delivered
514
+ * by WS retain reduceOnly; only genuinely-missed-by-WS trades that the
515
+ * gap-filler rescues will have this field blank. Acceptable gap.
516
+ * - `clientOrderId` — same: lives on the order, not the trade. We infer
517
+ * from the ledger by crosswalking `orderId` if consumers need it. */
518
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
519
+ tradeToFillEvent(symbol, t) {
520
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
521
+ const info = (t?.info ?? {});
522
+ // Binance raw field is `id` (not `tradeId`). CCXT maps raw `id` onto the
523
+ // unified `id` field — primary path is `t.id`; fallback to `info.id` for
524
+ // raw responses that haven't been through CCXT's unifier.
525
+ const tradeId = t?.id != null ? String(t.id) : (info.id != null ? String(info.id) : null);
526
+ const orderId = t?.order != null ? String(t.order) : (info.orderId != null ? String(info.orderId) : null);
527
+ const priceRaw = t?.price != null ? Number(t.price) : Number(info.price);
528
+ const qtyRaw = t?.amount != null ? Number(t.amount) : Number(info.qty);
529
+ const timeRaw = t?.timestamp != null ? Number(t.timestamp) : Number(info.time);
530
+ if (!tradeId || !orderId || !(priceRaw > 0) || !(qtyRaw > 0) || !(timeRaw > 0)) {
531
+ return null;
532
+ }
533
+ const sideRaw = String(t?.side ?? info.side ?? 'buy').toLowerCase();
534
+ const side = sideRaw === 'sell' ? 'SELL' : 'BUY';
535
+ // CCXT's unified `takerOrMaker` is the authoritative source; raw
536
+ // Binance exposes `maker: boolean` under info as the second-choice.
537
+ const maker = typeof t?.takerOrMaker === 'string'
538
+ ? t.takerOrMaker === 'maker'
539
+ : (info.maker != null ? Boolean(info.maker) : undefined);
540
+ const commissionRaw = t?.fee?.cost != null ? Number(t.fee.cost) : Number(info.commission);
541
+ const commissionAsset = typeof t?.fee?.currency === 'string'
542
+ ? t.fee.currency
543
+ : (typeof info.commissionAsset === 'string' ? info.commissionAsset : undefined);
544
+ const realizedPnlRaw = info.realizedPnl != null ? Number(info.realizedPnl) : undefined;
545
+ return {
546
+ exchange: this.exchange,
547
+ exchangeTradeId: tradeId,
548
+ exchangeOrderId: orderId,
549
+ // clientOrderId + reduceOnly are NOT in the Binance userTrades schema.
550
+ // See docstring on this method for why we don't attempt to source them.
551
+ source: 'rest_reconcile',
552
+ userId: this.userId,
553
+ symbol: normSymbol(symbol),
554
+ side,
555
+ quantity: qtyRaw,
556
+ price: priceRaw,
557
+ maker,
558
+ commission: Number.isFinite(commissionRaw) ? commissionRaw : undefined,
559
+ commissionAsset,
560
+ realizedPnl: Number.isFinite(realizedPnlRaw) ? realizedPnlRaw : undefined,
561
+ exchangeTime: timeRaw,
562
+ rawPayload: t,
563
+ };
564
+ }
565
+ }
@@ -0,0 +1,25 @@
1
+ export interface TouchedSymbolsStorageOptions {
2
+ /** Override path — primarily for tests. Default is per-user under ~/.reefclaw. */
3
+ path?: string;
4
+ /** Identifies the file owner; written into the JSON for forensic clarity. */
5
+ userId: string;
6
+ /** Test-only injected clock for `updatedAt`. */
7
+ now?: () => number;
8
+ }
9
+ /** Default per-user path. The userId scoping prevents two parallel plugin
10
+ * instances on the same machine (rare, but possible during ops) from
11
+ * clobbering each other's history. */
12
+ export declare function defaultTouchedSymbolsPath(userId: string): string;
13
+ export declare class TouchedSymbolsStore {
14
+ private readonly path;
15
+ private readonly userId;
16
+ private readonly now;
17
+ constructor(opts: TouchedSymbolsStorageOptions);
18
+ /** Read the persisted set. Returns an empty Set on any error so a corrupt
19
+ * file can't block ingest startup. The corrupt file isn't deleted —
20
+ * the next save() naturally overwrites it via tmp+rename. */
21
+ load(): Set<string>;
22
+ /** Atomically persist the set to disk. Best-effort — failures are logged
23
+ * but never thrown; persistence is a recovery helper, not on the hot path. */
24
+ save(symbols: Set<string>): void;
25
+ }