@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,114 @@
1
+ // DB-authoritative orphan close (Increment 0 of the durable orphan-position fix).
2
+ //
3
+ // The problem the existing backstops can't solve: a position closed on the
4
+ // exchange (bracket SL/TP fill or manual close) while the gateway was DOWN, or
5
+ // whose state-store entry was already wiped, leaves webapp `positions.status`
6
+ // stuck 'open' forever. reconcileStateStoreOnStartup can't help — it iterates
7
+ // only LOCAL state-store entries (a wiped entry is invisible) and passes
8
+ // lastContracts:0 (which trips the fillSize>0 guard, so it never posts a close).
9
+ //
10
+ // This pass is DB-authoritative: it asks the webapp for the open set (keyed by
11
+ // the DB row id, not local state), diffs against the EXCHANGE TRUTH snapshot,
12
+ // and posts a synthetic close for any open DB row whose symbol is absent from
13
+ // the exchange. Keying off the DB row id means a missing webappPositionId /
14
+ // wiped state-store / untracked contracts can't defeat it for the common
15
+ // one-row-per-symbol case.
16
+ //
17
+ // Known limitations (all fail SAFE — only false-negatives, never a wrong close):
18
+ // - Diff is symbol-membership only. If a symbol has BOTH a genuine orphan and
19
+ // a freshly re-opened live position (two open rows, same symbol, different
20
+ // positionOpenAt), the symbol is on the exchange so NEITHER is closed — the
21
+ // orphan persists until the next boot when that symbol is flat. A later
22
+ // increment can make this count-aware (close the M−N oldest-by-openedAt when
23
+ // the DB has M open rows but the exchange has N<M for the symbol).
24
+ // - First-writer-wins on position_closes UNIQUE(position_id): if a real
25
+ // close_position's journal POST is still retrying at boot, a synthetic close
26
+ // can land first and the accurate PnL becomes a no-op. Synthetic rows are
27
+ // tagged (metadata.synthetic) so a later increment could upgrade them.
28
+ // - closeAt is stamped at boot, not the real (earlier) exchange close time, so
29
+ // time-in-position analytics on these rows are approximate. Tagged synthetic.
30
+ //
31
+ // Safety (mandatory): the caller MUST pass a TRUSTED exchange snapshot
32
+ // (getPositionsOrNull() !== null). A failed REST fetch collapses to [] in
33
+ // getPositions(), which would make EVERY open row look orphaned — never run
34
+ // this pass off an untrusted/empty-on-error snapshot.
35
+ //
36
+ // Idempotency: position_closes is UNIQUE(position_id) with onConflictDoNothing,
37
+ // and the parent status flip is idempotent — a real close_position (exact PnL)
38
+ // that lands first always wins the UNIQUE. closeReason='reconciler_observed_flat'
39
+ // + metadata.synthetic=true tags these for KPI exclusion.
40
+ import { logger } from '../logger.js';
41
+ const TAG = 'reconcile-db-vs-exchange';
42
+ /** Canonicalise a symbol to its settle-suffix-free form (BTC/USDT:USDT → BTC/USDT). */
43
+ function canonical(symbol) {
44
+ return symbol.split(':')[0];
45
+ }
46
+ /**
47
+ * Close webapp `positions` rows that are status='open' but absent from the
48
+ * (trusted) exchange snapshot. Returns the number of synthetic closes posted.
49
+ *
50
+ * @param exchangeSymbols symbols from a TRUSTED snapshot (getPositionsOrNull()
51
+ * !== null). Pass only when the fetch genuinely succeeded.
52
+ */
53
+ export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Date.now()) {
54
+ if (!ctx.decisionsClient || !ctx.userId)
55
+ return 0;
56
+ const resp = await ctx.decisionsClient.getOpenPositions(ctx.userId);
57
+ if (!resp) {
58
+ // null = fetch failed (network / terminal). NEVER treat as "no open rows"
59
+ // (null≠empty) — skip; the next boot / periodic sweep retries.
60
+ logger.warn(TAG, 'DB open-positions fetch returned null — skipping reconcile (will retry)');
61
+ return 0;
62
+ }
63
+ if (resp.positions.length === 0)
64
+ return 0;
65
+ const exchangeSet = new Set();
66
+ for (const s of exchangeSymbols)
67
+ exchangeSet.add(canonical(s));
68
+ const orphans = resp.positions.filter((p) => !exchangeSet.has(canonical(p.symbol)));
69
+ if (orphans.length === 0) {
70
+ logger.info(TAG, `DB reconcile: all ${resp.positions.length} open row(s) present on exchange`);
71
+ return 0;
72
+ }
73
+ logger.warn(TAG, `DB reconcile: ${orphans.length} open DB row(s) absent from exchange — closing: ${orphans.map((o) => o.symbol).join(', ')}`);
74
+ let posted = 0;
75
+ for (const o of orphans) {
76
+ if (!(o.remainingSize > 0) || !(o.entryPrice > 0)) {
77
+ logger.warn(TAG, `${o.symbol}: bad size/price from DB (size=${o.remainingSize}, entry=${o.entryPrice}) — skipping close`);
78
+ continue;
79
+ }
80
+ const close = {
81
+ positionId: o.id,
82
+ closeAt: nowMs,
83
+ closeReason: 'reconciler_observed_flat',
84
+ closeAssessment: {
85
+ source: 'db_exchange_sweep_boot',
86
+ synthetic: true,
87
+ note: 'Position open in DB but absent from the exchange snapshot. It was ' +
88
+ 'closed on-exchange via a path that bypassed close_position (bracket ' +
89
+ 'SL/TP fill or manual close). Synthetic close; PnL/R/MFE recomputed ' +
90
+ 'server-side from the entry + last review.',
91
+ openedAtMs: o.openedAt,
92
+ },
93
+ // Synthetic-but-neutral context. Tagged so KPI aggregation can exclude it.
94
+ scorecardVerdict: 'unknown',
95
+ confluenceScore: 0,
96
+ regime: 'UNKNOWN',
97
+ regimeConfidence: 0,
98
+ // The true exit price wasn't captured; use the entry so the server-side
99
+ // recompute yields ~0 realized PnL (honest "unknown") rather than a
100
+ // fabricated number. R/MFE/give-back come from the last review row.
101
+ fillPrice: o.entryPrice,
102
+ fillSize: o.remainingSize,
103
+ realizedPnl: 0,
104
+ realizedR: 0,
105
+ mfeRAtClose: 0,
106
+ giveBackPctAtClose: 0,
107
+ metadata: { synthetic: true, source: 'boot_reconcile' },
108
+ };
109
+ ctx.decisionsClient.postClose(ctx.userId, close);
110
+ posted++;
111
+ logger.info(TAG, `synthetic close posted ${o.symbol} (positionId=${o.id.slice(0, 8)}…, size=${o.remainingSize})`);
112
+ }
113
+ return posted;
114
+ }
@@ -0,0 +1,37 @@
1
+ import type { PositionDecisionsClient } from './position-decisions-client.js';
2
+ import type { PositionStateStore } from '../live/position-state-store.js';
3
+ export interface ReconcilerCleanupContext {
4
+ decisionsClient?: PositionDecisionsClient;
5
+ stateStore?: PositionStateStore;
6
+ userId?: string;
7
+ /** Last-price fetcher. Plugin-side passes a closure over
8
+ * `adapter.getLastPrice`. Tests inject a mock. Returning `null` skips
9
+ * the webapp close post (state-store still cleaned). */
10
+ lastPriceFn?: (symbol: string) => Promise<number | null>;
11
+ }
12
+ export interface CloseObservedArgs {
13
+ /** Symbol observed flat. Either canonical (`BTC/USDT`) or settle-suffix
14
+ * (`BTC/USDT:USDT`) form — state-store normalises internally. */
15
+ symbol: string;
16
+ /** Last contracts seen by reconciler before exchange showed flat. Used as
17
+ * fillSize on the synthetic close payload. */
18
+ lastContracts: number;
19
+ /** Epoch ms — when reconciler observed the close. Defaults to Date.now(). */
20
+ observedAtMs?: number;
21
+ }
22
+ export declare function onReconcilerObservedClose(ctx: ReconcilerCleanupContext, args: CloseObservedArgs): Promise<void>;
23
+ /** Startup-time reconciliation between state-store and exchange truth.
24
+ *
25
+ * Why this exists: the in-memory reconciler only emits `closed` drifts on
26
+ * the exact transition "was on exchange last poll, gone now". State-store
27
+ * entries that persist across a close-bypass that happened BEFORE the plugin
28
+ * started are invisible to that loop — they sit in `position-state.json`
29
+ * forever and the next entry on the same symbol gets mistaken for a scale-in.
30
+ *
31
+ * This pass runs once at boot, after the live adapter is initialised:
32
+ * for every state-store entry, if the symbol isn't on the current exchange
33
+ * snapshot, fire the same cleanup as the runtime hook would.
34
+ *
35
+ * Returns the count of entries cleaned (for log + telemetry).
36
+ */
37
+ export declare function reconcileStateStoreOnStartup(ctx: ReconcilerCleanupContext, exchangeSymbols: Iterable<string>): Promise<number>;
@@ -0,0 +1,147 @@
1
+ // Reconciler-driven cleanup for the close-bypass-leaks-state class-of-bug.
2
+ //
3
+ // Background: when a position closes via any path other than close_position()
4
+ // — i.e., bracket SL/TP fill, opposite-side market order, or external close on
5
+ // the exchange UI — onClosePositionFilled never runs. Two pieces of state leak:
6
+ //
7
+ // 1. webapp `positions.status` stays 'open' indefinitely (until a manual
8
+ // `scripts/reconcile-open-positions.py` sweep).
9
+ // 2. `position-state.json` keeps the entry with its stale `webappPositionId`.
10
+ // The next entry on the same symbol is then mistaken for a scale-in and
11
+ // reviews silently drop because the cached id no longer maps to a live
12
+ // DB row.
13
+ //
14
+ // All three close-bypass paths converge on the same observable: the next
15
+ // reconciler tick (60 s cadence) sees the symbol gone from `getPositions()`
16
+ // and emits a `drift_detected` event with `type='closed'`. We hook there.
17
+ //
18
+ // Two-phase cleanup:
19
+ // Phase 1 — synthetic close to webapp (best-effort): if the state-store
20
+ // entry has a `webappPositionId` and we can fetch a last price,
21
+ // POST `/api/internal/position-closes` with a stub close payload
22
+ // tagged `closeReason='reconciler_observed_flat'`. Webapp
23
+ // recomputes realized PnL/R/MFE server-side from positionEntries +
24
+ // last review (the same path the close auto-capture uses).
25
+ // Phase 2 — state-store cleanup (unconditional): drop the entry. This is
26
+ // the more important half — it prevents the next entry on the
27
+ // same symbol from being mistaken for a scale-in.
28
+ //
29
+ // Idempotency:
30
+ // - webapp endpoint dedups via UNIQUE on position_id (onConflictDoNothing).
31
+ // - state-store remove is a no-op if the entry is already gone.
32
+ // Re-firing on subsequent reconciler ticks is safe.
33
+ //
34
+ // Failure mode tradeoff: when no last-price is reachable, we skip the webapp
35
+ // post but still clean state-store. The DB row stays 'open' until manual
36
+ // reconcile (existing operator workflow). State-store cleanup alone closes
37
+ // the next-entry-mistaken-for-scale-in failure mode, which is the harder one
38
+ // to recover from.
39
+ import { logger } from '../logger.js';
40
+ const TAG = 'reconciler-cleanup';
41
+ export async function onReconcilerObservedClose(ctx, args) {
42
+ if (!ctx.stateStore)
43
+ return;
44
+ const entry = ctx.stateStore.get(args.symbol);
45
+ if (!entry) {
46
+ // Already clean. Could happen if close_position fired between the WS
47
+ // close fill and this reconciler tick — onClosePositionFilled removed the
48
+ // entry first. Silent no-op.
49
+ return;
50
+ }
51
+ const webappPositionId = entry.webappPositionId;
52
+ const observedAtMs = args.observedAtMs ?? Date.now();
53
+ if (webappPositionId && ctx.decisionsClient && ctx.userId) {
54
+ let lastPrice = null;
55
+ if (ctx.lastPriceFn) {
56
+ try {
57
+ lastPrice = await ctx.lastPriceFn(args.symbol);
58
+ }
59
+ catch (err) {
60
+ logger.warn(TAG, `lastPriceFn(${args.symbol}) threw: ${err instanceof Error ? err.message : String(err)} — synthetic close will be skipped`);
61
+ }
62
+ }
63
+ const fillSize = Math.abs(args.lastContracts);
64
+ if (lastPrice !== null && lastPrice > 0 && fillSize > 0) {
65
+ const close = {
66
+ positionId: webappPositionId,
67
+ closeAt: observedAtMs,
68
+ closeReason: 'reconciler_observed_flat',
69
+ closeAssessment: {
70
+ note: 'Position observed flat on exchange via reconciler. Close path ' +
71
+ 'bypassed close_position (likely bracket SL/TP fill, opposite-side ' +
72
+ 'market order, or external close). Synthetic close row.',
73
+ observedAtMs,
74
+ lastContracts: args.lastContracts,
75
+ },
76
+ scorecardVerdict: 'NO_GO',
77
+ confluenceScore: 0,
78
+ regime: 'unknown',
79
+ regimeConfidence: 0.5,
80
+ fillPrice: lastPrice,
81
+ fillSize,
82
+ // Webapp recomputes these server-side from positionEntries + last
83
+ // review when all four arrive as zero (same code path the close
84
+ // auto-capture uses).
85
+ realizedPnl: 0,
86
+ realizedR: 0,
87
+ mfeRAtClose: 0,
88
+ giveBackPctAtClose: 0,
89
+ };
90
+ ctx.decisionsClient.postClose(ctx.userId, close);
91
+ logger.info(TAG, `synthetic close posted ${args.symbol} ` +
92
+ `(positionId=${webappPositionId.slice(0, 8)}…, ` +
93
+ `price=${lastPrice}, size=${fillSize})`);
94
+ }
95
+ else {
96
+ logger.warn(TAG, `${args.symbol}: cannot post synthetic close ` +
97
+ `(lastPrice=${lastPrice ?? 'null'}, size=${fillSize}); ` +
98
+ `state-store entry will still be cleaned. DB row may stay ` +
99
+ `status='open' until reconcile-open-positions.py runs.`);
100
+ }
101
+ }
102
+ else if (!webappPositionId) {
103
+ logger.info(TAG, `${args.symbol}: orphan entry (no webappPositionId, parent row never created) — cleaning state-store only`);
104
+ }
105
+ ctx.stateStore.remove(args.symbol);
106
+ }
107
+ /** Startup-time reconciliation between state-store and exchange truth.
108
+ *
109
+ * Why this exists: the in-memory reconciler only emits `closed` drifts on
110
+ * the exact transition "was on exchange last poll, gone now". State-store
111
+ * entries that persist across a close-bypass that happened BEFORE the plugin
112
+ * started are invisible to that loop — they sit in `position-state.json`
113
+ * forever and the next entry on the same symbol gets mistaken for a scale-in.
114
+ *
115
+ * This pass runs once at boot, after the live adapter is initialised:
116
+ * for every state-store entry, if the symbol isn't on the current exchange
117
+ * snapshot, fire the same cleanup as the runtime hook would.
118
+ *
119
+ * Returns the count of entries cleaned (for log + telemetry).
120
+ */
121
+ export async function reconcileStateStoreOnStartup(ctx, exchangeSymbols) {
122
+ if (!ctx.stateStore)
123
+ return 0;
124
+ // Normalise the exchange symbol list — state-store stores canonical form.
125
+ const exchangeSet = new Set();
126
+ for (const sym of exchangeSymbols) {
127
+ exchangeSet.add(sym.split(':')[0]);
128
+ }
129
+ const stale = ctx.stateStore.getAll().filter((e) => !exchangeSet.has(e.symbol));
130
+ if (stale.length === 0) {
131
+ logger.info(TAG, `startup reconcile: state-store clean (${ctx.stateStore.size()} entries match exchange)`);
132
+ return 0;
133
+ }
134
+ logger.warn(TAG, `startup reconcile: ${stale.length} stale state-store entr${stale.length === 1 ? 'y' : 'ies'} ` +
135
+ `not on exchange — cleaning: ${stale.map((e) => e.symbol).join(', ')}`);
136
+ for (const entry of stale) {
137
+ await onReconcilerObservedClose(ctx, {
138
+ symbol: entry.symbol,
139
+ // We don't know the closing size — pass 0 so the synthetic close
140
+ // post is skipped (size validation in cleanup). State-store entry
141
+ // is still removed. Operator runs reconcile-open-positions.py to
142
+ // close any matching DB rows.
143
+ lastContracts: 0,
144
+ });
145
+ }
146
+ return stale.length;
147
+ }
@@ -0,0 +1,191 @@
1
+ import type { BinancePrivateApi } from '../ccxt/binance-private.js';
2
+ import type { UserDataStream } from '../live/user-data-stream.js';
3
+ import type { LiveStateStore } from '../live/live-state-store.js';
4
+ import type { TradeStoreClient } from './trade-store-client.js';
5
+ import { TouchedSymbolsStore } from './touched-symbols-store.js';
6
+ export interface RestGapFillerOptions {
7
+ api: BinancePrivateApi;
8
+ stream: UserDataStream;
9
+ store: LiveStateStore;
10
+ client: TradeStoreClient;
11
+ userId: string;
12
+ /** Defaults to 'binance_futures'. */
13
+ exchange?: string;
14
+ /** How far BEFORE the disconnect to start the backfill window. A 10s
15
+ * buffer compensates for clock skew between the client and exchange
16
+ * and for any in-flight frames we may have already processed.
17
+ * Dedupe at the webapp prevents duplicate rows. */
18
+ safetyBufferMs?: number;
19
+ /** Hard floor on the backfill window. A long outage shouldn't trigger
20
+ * a fetchMyTrades that walks back days. Binance returns up to 1000
21
+ * trades per symbol per call; past that you need multiple paginated
22
+ * calls. Default 1h. If a real outage exceeds this, operator runs the
23
+ * Phase 2 continuous reconciler separately. */
24
+ maxWindowMs?: number;
25
+ /** Limit on trades per symbol per call. Binance cap is 1000. */
26
+ perSymbolLimit?: number;
27
+ /** Run a one-shot deep backfill on start() that recovers fills which landed
28
+ * while the plugin was DOWN — the reconnect path (handleResyncRequired) only
29
+ * covers gaps while the plugin is RUNNING, so a position's closing trade that
30
+ * fills during downtime is never re-pushed by the WS and accumulates silently
31
+ * across restarts. Defaults to OFF; activate via env `RC_GAPFILL_STARTUP=on`. */
32
+ startupDeepFill?: boolean;
33
+ /** How far back the startup deep-fill looks. Default 7 days (env
34
+ * `RC_GAPFILL_STARTUP_LOOKBACK_MS`). Sized to cover realistic inter-restart
35
+ * downtime; a longer outage is a one-off the operator backfills manually. */
36
+ startupLookbackMs?: number;
37
+ /** Delay between paced Binance calls in the deep-fill (per page + per symbol),
38
+ * to stay clear of the per-IP weight ceiling. Default 250ms
39
+ * (env `RC_GAPFILL_PACE_MS`). */
40
+ deepFillPaceMs?: number;
41
+ /** Safety cap on paginated pages per 7-day window in the deep-fill — bounds
42
+ * the worst case if a symbol churned a huge number of fills. Default 25. */
43
+ maxPagesPerWindow?: number;
44
+ /** Persistent symbol store. Defaults to a file-backed implementation
45
+ * under ~/.reefclaw/touched-symbols-<userId>.json. Tests inject a
46
+ * store pointed at a tmpdir path or a stub. Pass `null` to disable
47
+ * persistence entirely (in-memory only). */
48
+ touchedSymbolsStore?: TouchedSymbolsStore | null;
49
+ /** For unit tests — injected clock. */
50
+ now?: () => number;
51
+ }
52
+ export interface RestGapFillerHealth {
53
+ reconnectEvents: number;
54
+ gapFillRuns: number;
55
+ symbolsQueried: number;
56
+ tradesBackfilled: number;
57
+ errorCount: number;
58
+ }
59
+ export declare class RestGapFiller {
60
+ private readonly api;
61
+ private readonly stream;
62
+ private readonly store;
63
+ private readonly client;
64
+ private readonly userId;
65
+ private readonly exchange;
66
+ private readonly safetyBufferMs;
67
+ private readonly maxWindowMs;
68
+ private readonly perSymbolLimit;
69
+ private readonly startupDeepFill;
70
+ private readonly startupLookbackMs;
71
+ private readonly deepFillPaceMs;
72
+ private readonly maxPagesPerWindow;
73
+ private readonly now;
74
+ /** Every symbol we've seen via WS orderUpdate, a position snapshot, or
75
+ * the persistent disk store. Never shrinks — a symbol's closing trade
76
+ * may still be pending delivery when its position hits zero, and a
77
+ * symbol traded in a previous session is still a candidate for fills
78
+ * the WS missed during a plugin restart. */
79
+ private readonly knownSymbols;
80
+ /** Optional file-backed persistence so the symbol universe survives
81
+ * plugin restarts. Null in tests that want pure in-memory behaviour. */
82
+ private readonly touchedStore;
83
+ /** Timestamp of the last observed disconnect. Bootstrapped to null so
84
+ * the initial window (before the first disconnect) contributes no
85
+ * backfill — no blind window existed before the stream was ever up. */
86
+ private lastDisconnectAt;
87
+ private started;
88
+ private readonly onResyncRequired;
89
+ private readonly onDisconnected;
90
+ private readonly onOrderUpdate;
91
+ private reconnectEvents;
92
+ private gapFillRuns;
93
+ private symbolsQueried;
94
+ private tradesBackfilled;
95
+ private errorCount;
96
+ constructor(opts: RestGapFillerOptions);
97
+ start(): void;
98
+ stop(): void;
99
+ getHealth(): RestGapFillerHealth;
100
+ /** Testing hook — lets unit tests trigger a gap-fill pass with a known
101
+ * time window instead of replaying stream events. */
102
+ runGapFill(since: number): Promise<void>;
103
+ /** One-shot startup deep-fill — the recurrence fix.
104
+ *
105
+ * The reconnect gap-fill (handleResyncRequired) only covers blind windows
106
+ * while the plugin is RUNNING and caps its lookback at maxWindowMs (1h). A
107
+ * fill that lands while the plugin is DOWN — most damagingly a position's
108
+ * closing trade — is never re-pushed by the event-driven WS and was never
109
+ * gap-filled, so it silently never reaches the `trades` ledger. Over many
110
+ * restarts these missing closes accumulate and make the journal's net-PnL
111
+ * (derived from the ledger) wrong. This pass queries userTrades for every
112
+ * known symbol over a configurable lookback so downtime closes land on the
113
+ * next start. Paginated + 7-day-window-chunked (Binance caps userTrades at
114
+ * 1000 rows / 7-day span per call) and paced to stay clear of the per-IP
115
+ * weight ceiling. Idempotent at the webapp (ON CONFLICT DO NOTHING), so
116
+ * re-querying already-recorded fills is a harmless no-op. */
117
+ runStartupDeepFill(sinceFloor: number): Promise<void>;
118
+ /** Deep-fill a single symbol over [sinceFloor, now], chunked into ≤7-day
119
+ * windows (Binance's per-call span cap). Within a window the FIRST page is
120
+ * time-bounded (startTime/endTime); FULL pages continue via Binance's
121
+ * `fromId = lastTradeId + 1` (doc-verified: fromId pages ascending by
122
+ * trade id and CANNOT be combined with startTime/endTime), stopping when
123
+ * a page runs past the window end, comes back short, or hits the page
124
+ * cap. Paging by id — not by `lastTime + 1` — is what keeps
125
+ * same-millisecond trades on a full-page boundary from being skipped.
126
+ *
127
+ * Every fetch goes through the ban-gated `api.fetchMyTradesOrNull`
128
+ * wrapper — we deliberately do NOT use CCXT's built-in `{ paginate: true }`,
129
+ * which re-enters CCXT directly and would bypass the process-wide IP-ban
130
+ * gate. A `null` read (paced / banned / garbled) is a FAILED read, not an
131
+ * empty window: we abort THIS symbol by throwing (caller logs + counts the
132
+ * error and continues other symbols) instead of advancing the cursor —
133
+ * error-as-empty silently truncated the backfill exactly when Binance was
134
+ * shedding us. Returns the number of fills posted. */
135
+ private deepFillSymbol;
136
+ /** Epoch-ms timestamp of a CCXT trade — unified `timestamp` first, raw
137
+ * Binance `info.time` as fallback. 0 when neither is present. */
138
+ private tradeTime;
139
+ /** Numeric Binance trade id of a CCXT trade — unified `id` first, raw
140
+ * `info.id` fallback. Null when not a finite number (`fromId` is a LONG
141
+ * on the Binance side, so a non-numeric id cannot seed pagination). */
142
+ private tradeId;
143
+ /** Cooperative delay used to pace deep-fill Binance calls. Resolves
144
+ * immediately for a non-positive duration (e.g. tests pass 0). */
145
+ private sleep;
146
+ private handleResyncRequired;
147
+ private handleDisconnected;
148
+ private seedKnownSymbolsFromStore;
149
+ /** Merge the on-disk symbol history into the in-memory set. Best-effort —
150
+ * load() returns an empty Set on any error so a corrupt cache file is a
151
+ * silent regression to "session-only" behaviour, never a startup blocker. */
152
+ private hydrateKnownSymbolsFromDisk;
153
+ /** Discovery via /fapi/v1/income. Catches symbols traded during a blind
154
+ * window (plugin downtime, mid-session WS outage) that aren't represented
155
+ * in the persisted touched cache or the current open-position seed —
156
+ * i.e. brand-new symbols that opened AND closed entirely during the gap.
157
+ *
158
+ * `since` defaults to the maxWindow lookback (used by start()'s one-shot
159
+ * pass). On every resync, handleResyncRequired passes the reconnect's
160
+ * computed `since` so the discovery window precisely matches the
161
+ * fetchMyTrades window — a single REST call recovers every symbol that
162
+ * could have had activity in the gap.
163
+ *
164
+ * Best-effort: any error logs and returns; gap-fill still runs against
165
+ * whatever knownSymbols already contains. */
166
+ private discoverSymbolsFromIncome;
167
+ /** Convert raw CCXT trade objects to FillEvents and post each to the
168
+ * webapp ingest endpoint. Returns the count successfully posted
169
+ * (posted means handed to the HTTP client; upsert dedup happens in
170
+ * the webapp). Silent on empty input. */
171
+ private postTrades;
172
+ /** Map a CCXT trade (Binance /fapi/v1/userTrades) to our FillEvent shape.
173
+ * Returns null on malformed input — tradeId + orderId + price + qty are
174
+ * all required columns on the trades table.
175
+ *
176
+ * Binance raw field names (NOT CCXT-unified) per the docs — we read these
177
+ * via `t.info.*` as the second-choice source after CCXT's unified fields:
178
+ * id, orderId, symbol, side, positionSide, price, qty, quoteQty,
179
+ * commission, commissionAsset, time, buyer, maker, realizedPnl.
180
+ * Spec: https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Account-Trade-List
181
+ *
182
+ * Fields NOT returned by userTrades (document-verified):
183
+ * - `reduceOnly` — lives on order endpoints, not trades. Gap-filled rows
184
+ * get reduceOnly=undefined; the primary WS path (source='ws') always
185
+ * has it. Since dedup preserves the first-writer row, trades delivered
186
+ * by WS retain reduceOnly; only genuinely-missed-by-WS trades that the
187
+ * gap-filler rescues will have this field blank. Acceptable gap.
188
+ * - `clientOrderId` — same: lives on the order, not the trade. We infer
189
+ * from the ledger by crosswalking `orderId` if consumers need it. */
190
+ private tradeToFillEvent;
191
+ }