@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,394 @@
1
+ // Auto-capture helper for the Position Decision Journal.
2
+ //
3
+ // Wires create_order / close_position / fill events into the webapp ingest:
4
+ //
5
+ // ENTRY — onCreateOrderFilled() upserts the parent positions row, gets back
6
+ // the UUID, persists it locally for restart-survival, then posts the
7
+ // entry decision row.
8
+ // CLOSE — onClosePositionFilled() looks up the cached position UUID and posts
9
+ // the close decision row. Local position-state is dropped after the
10
+ // dispatch so the symbol can re-enter cleanly.
11
+ //
12
+ // Auto-capture is mode-agnostic — it runs whenever the decisions client is
13
+ // configured (env vars present), regardless of trading mode (PAPER vs LIVE) or
14
+ // positionReview.mode. Fail-open: a network blip on a webapp POST does not
15
+ // block the trading hot path.
16
+ //
17
+ // PR 1 scope: market orders (immediate fill detection from CcxtOrder.filled).
18
+ // Limit-order fill capture and scale-in detection are deferred to a follow-up
19
+ // that hooks into the WS-ingest pipeline (see POSITION_DECISION_JOURNAL_PLAN
20
+ // §5.1 for the longer-term design).
21
+ import { logger } from '../logger.js';
22
+ import { isBracketCid } from '../live/bracket-id.js';
23
+ const TAG = 'position-auto-capture';
24
+ /** Flatness tolerance for remaining-contracts tracking. Reduce-only fills sum
25
+ * exactly to the position size on Binance, so any residual below this is noise
26
+ * / float rounding and means the position is flat. */
27
+ const FLAT_EPSILON = 1e-6;
28
+ /** Called after a successful adapter.createOrder().
29
+ *
30
+ * For PR 1, we treat any non-zero `filled` quantity as an entry event and
31
+ * upsert the parent position + post the entry decision row. Limit orders that
32
+ * haven't filled yet are skipped (caller can call us again from the WS-ingest
33
+ * pipeline once they fill — that wiring is the follow-up).
34
+ *
35
+ * Always-fail-open: webapp errors are logged but never thrown.
36
+ */
37
+ export async function onCreateOrderFilled(ctx, inputs, order) {
38
+ if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
39
+ return;
40
+ const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
41
+ ? order.filled
42
+ : 0;
43
+ if (filledQty <= 0) {
44
+ // Limit order not yet filled — nothing to capture yet.
45
+ return;
46
+ }
47
+ const fillPrice = typeof order.average === 'number' && Number.isFinite(order.average) && order.average > 0
48
+ ? order.average
49
+ : typeof order.price === 'number' && Number.isFinite(order.price) && order.price > 0
50
+ ? order.price
51
+ : null;
52
+ if (fillPrice === null) {
53
+ logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: missing fill price; skipping capture`);
54
+ return;
55
+ }
56
+ // Need v2.10.0 metadata to build a meaningful entry row. In paper mode the
57
+ // agent may omit them; we still capture an entry but mark missing fields.
58
+ const md = inputs.metadata;
59
+ if (!md) {
60
+ logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: no metadata; skipping (paper without rationale)`);
61
+ return;
62
+ }
63
+ const positionSide = inputs.side === 'buy' ? 'long' : 'short';
64
+ const openedAtMs = Date.now();
65
+ // Skip if state-store already knows about this position (scale-in / partial
66
+ // fill of an existing entry). PR 1 deferral: don't record additional entries.
67
+ // The follow-up that hooks WS-ingest will detect scale-ins explicitly.
68
+ const existing = ctx.stateStore.get(inputs.symbol);
69
+ if (existing) {
70
+ logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: existing position in state-store (openedAt=${existing.openedAt}); treating as scale-in (auto-capture deferred to v2)`);
71
+ return;
72
+ }
73
+ // Local state-store first — ensures restart-survival even if the webapp POST
74
+ // fails on the first attempt.
75
+ ctx.stateStore.upsert({
76
+ symbol: inputs.symbol,
77
+ openedAt: openedAtMs,
78
+ side: positionSide,
79
+ openedFromExchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
80
+ });
81
+ const upsert = {
82
+ symbol: inputs.symbol,
83
+ positionOpenAt: openedAtMs,
84
+ side: positionSide,
85
+ currentSize: filledQty,
86
+ avgEntryPrice: fillPrice,
87
+ mode: ctx.resolveMode?.(),
88
+ };
89
+ // postPosition is awaited — we need the UUID before posting the entry row.
90
+ const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
91
+ if (!positionId) {
92
+ logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: webapp position upsert returned no id; entry row will not be posted (will retry on next observation)`);
93
+ return;
94
+ }
95
+ ctx.stateStore.setWebappId(inputs.symbol, positionId);
96
+ // Track size so a later reduce-only/bracket exit fill can detect flat + close.
97
+ ctx.stateStore.addOpenContracts(inputs.symbol, filledQty);
98
+ // Build entry decision payload from the v2.10.0 metadata.
99
+ // regime_confidence stored 0-100 in PositionMetadata (per validator), but the
100
+ // webapp schema expects 0-1. Normalize.
101
+ const regimeConfNormalized = typeof md.regimeConfidence === 'number'
102
+ ? Math.max(0, Math.min(1, md.regimeConfidence / 100))
103
+ : 0.5;
104
+ const entry = {
105
+ positionId,
106
+ entryAt: openedAtMs,
107
+ isScaleIn: false,
108
+ thesis: md.thesis ?? '(no thesis recorded)',
109
+ setupType: md.setupType ?? 'unknown',
110
+ regime: md.regime ?? 'unknown',
111
+ regimeConfidence: regimeConfNormalized,
112
+ scorecardVerdict: md.scorecardVerdict ?? 'GO',
113
+ confluenceScore: typeof md.confluenceScore === 'number' ? md.confluenceScore : 0,
114
+ fillPrice,
115
+ fillSize: filledQty,
116
+ exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
117
+ metadata: buildEntryPlanMetadata(md),
118
+ };
119
+ ctx.decisionsClient.postEntry(ctx.userId, entry);
120
+ logger.info(TAG, `entry captured ${inputs.symbol} ${positionSide} ${filledQty} @ ${fillPrice} (positionId=${positionId.slice(0, 8)}…)`);
121
+ }
122
+ /** Called after a successful adapter.closePosition() with the v2.10.0 reason +
123
+ * assessment from close_position.ts. Looks up the cached position UUID, posts
124
+ * the close decision row, and drops the local state entry. */
125
+ export async function onClosePositionFilled(ctx, inputs, order) {
126
+ if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
127
+ return;
128
+ const stateEntry = ctx.stateStore.get(inputs.symbol);
129
+ if (!stateEntry || !stateEntry.webappPositionId) {
130
+ logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: no cached webappPositionId; close row will not be posted (entry was not captured)`);
131
+ return;
132
+ }
133
+ const filledQty = typeof order.filled === 'number' && Number.isFinite(order.filled)
134
+ ? order.filled
135
+ : 0;
136
+ const fillPrice = typeof order.average === 'number' && Number.isFinite(order.average) && order.average > 0
137
+ ? order.average
138
+ : typeof order.price === 'number' && Number.isFinite(order.price) && order.price > 0
139
+ ? order.price
140
+ : null;
141
+ if (filledQty <= 0 || fillPrice === null) {
142
+ logger.warn(TAG, `onClosePositionFilled ${inputs.symbol}: missing fill quantity or price; skipping capture`);
143
+ return;
144
+ }
145
+ const closeAtMs = Date.now();
146
+ // Realized PnL + R-multiple require entry-side state we don't have here for
147
+ // PR 1. The skill-side computation already produces these for the webapp;
148
+ // this auto-capture just records what the close tool input told us. Default
149
+ // numeric fields to 0 — webapp accepts and the operator will see the close
150
+ // card with zeros until the v2 ws-ingest hook fills them properly.
151
+ const regimeConfNormalized = typeof inputs.regimeConfidence === 'number'
152
+ ? Math.max(0, Math.min(1, inputs.regimeConfidence / 100))
153
+ : 0.5;
154
+ const close = {
155
+ positionId: stateEntry.webappPositionId,
156
+ closeAt: closeAtMs,
157
+ closeReason: inputs.closeReason,
158
+ closeAssessment: inputs.closeAssessment,
159
+ scorecardVerdict: inputs.scorecardVerdict ?? 'NO_GO',
160
+ confluenceScore: typeof inputs.confluenceScore === 'number' ? inputs.confluenceScore : 0,
161
+ regime: inputs.regime ?? 'unknown',
162
+ regimeConfidence: regimeConfNormalized,
163
+ fillPrice,
164
+ fillSize: filledQty,
165
+ realizedPnl: 0, // populated by v2 ws-ingest hook
166
+ realizedR: 0,
167
+ mfeRAtClose: 0,
168
+ giveBackPctAtClose: 0,
169
+ exchangeTradeId: typeof order.id === 'string' ? order.id : undefined,
170
+ };
171
+ ctx.decisionsClient.postClose(ctx.userId, close);
172
+ // Drop local state — symbol can re-enter as a new position.
173
+ ctx.stateStore.remove(inputs.symbol);
174
+ logger.info(TAG, `close captured ${inputs.symbol} reason=${inputs.closeReason} (positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
175
+ }
176
+ export async function onWsFillObserved(ctx, fill) {
177
+ if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
178
+ return;
179
+ if (fill.fillPrice <= 0 || fill.fillSize <= 0)
180
+ return;
181
+ // Reduce-only fills are exit-side. close_position tool exits are journaled by
182
+ // onClosePositionFilled; bracket SL/TP fills and other reduce-only closes
183
+ // bypass that path entirely and used to leave the journal position open
184
+ // forever (close-bypass class). When closeOnReduceOnlyFill is enabled, detect
185
+ // the position going flat from the fill(s) and journal an exact close.
186
+ if (fill.reduceOnly) {
187
+ await handleReduceOnlyExit(ctx, fill);
188
+ return;
189
+ }
190
+ const stateEntry = ctx.stateStore.get(fill.symbol);
191
+ const fillSide = fill.side === 'buy' ? 'long' : 'short';
192
+ const ts = fill.exchangeTimeMs ?? Date.now();
193
+ if (stateEntry) {
194
+ // Dedup: if the synchronous create_order path already captured this
195
+ // exact order (orderId match), the WS event is the same fill — skip.
196
+ if (stateEntry.openedFromExchangeTradeId === fill.exchangeOrderId) {
197
+ return;
198
+ }
199
+ if (stateEntry.side !== fillSide) {
200
+ // Opposite-side fill on an existing position. Almost certainly a manual
201
+ // flatten that bypassed close_position. Don't journal here — the
202
+ // operator should use close_position with reason+assessment instead.
203
+ logger.warn(TAG, `onWsFillObserved ${fill.symbol}: opposite-side fill on existing ${stateEntry.side} position; skipping (use close_position for rationale capture)`);
204
+ return;
205
+ }
206
+ // SCALE-IN: same symbol, same side, fresh fill.
207
+ if (!stateEntry.webappPositionId) {
208
+ logger.warn(TAG, `onWsFillObserved ${fill.symbol}: scale-in observed but no webappPositionId yet — skipping (parent upsert may have failed; will retry next fill)`);
209
+ return;
210
+ }
211
+ const pending = ctx.pendingEntries?.consume(fill.exchangeOrderId);
212
+ const entry = buildEntryPayload({
213
+ positionId: stateEntry.webappPositionId,
214
+ isScaleIn: true,
215
+ ts,
216
+ fillPrice: fill.fillPrice,
217
+ fillSize: fill.fillSize,
218
+ exchangeTradeId: fill.exchangeTradeId,
219
+ metadata: pending?.metadata,
220
+ });
221
+ ctx.decisionsClient.postEntry(ctx.userId, entry);
222
+ // Grow tracked size so a later reduce-only exit detects flat correctly.
223
+ ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
224
+ logger.info(TAG, `scale-in captured ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
225
+ `(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
226
+ return;
227
+ }
228
+ // No state-store entry → NEW position via WS-driven fill. Replicates the
229
+ // synchronous-market-order path but pulls metadata from the pending cache
230
+ // (which create_order populated when the limit was placed).
231
+ const pending = ctx.pendingEntries?.consume(fill.exchangeOrderId);
232
+ ctx.stateStore.upsert({
233
+ symbol: fill.symbol,
234
+ openedAt: ts,
235
+ side: fillSide,
236
+ openedFromExchangeTradeId: fill.exchangeOrderId,
237
+ });
238
+ const upsert = {
239
+ symbol: fill.symbol,
240
+ positionOpenAt: ts,
241
+ side: fillSide,
242
+ currentSize: fill.fillSize,
243
+ avgEntryPrice: fill.fillPrice,
244
+ mode: ctx.resolveMode?.(),
245
+ };
246
+ const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
247
+ if (!positionId) {
248
+ logger.warn(TAG, `onWsFillObserved ${fill.symbol}: webapp position upsert returned no id; entry row will not be posted`);
249
+ return;
250
+ }
251
+ ctx.stateStore.setWebappId(fill.symbol, positionId);
252
+ // Track size so a later reduce-only/bracket exit fill can detect flat + close.
253
+ ctx.stateStore.addOpenContracts(fill.symbol, fill.fillSize);
254
+ const entry = buildEntryPayload({
255
+ positionId,
256
+ isScaleIn: false,
257
+ ts,
258
+ fillPrice: fill.fillPrice,
259
+ fillSize: fill.fillSize,
260
+ exchangeTradeId: fill.exchangeTradeId,
261
+ metadata: pending?.metadata,
262
+ });
263
+ ctx.decisionsClient.postEntry(ctx.userId, entry);
264
+ logger.info(TAG, `entry captured (ws) ${fill.symbol} ${fillSide} ${fill.fillSize} @ ${fill.fillPrice} ` +
265
+ `(positionId=${positionId.slice(0, 8)}…, metadata=${pending ? 'cached' : 'none'})`);
266
+ }
267
+ /** Reduce-only exit fill handler — the close-bypass fix. When enabled, sums the
268
+ * fill's realised PnL into the tracked position and, once remaining size hits
269
+ * flat, posts an exact close decision row (Binance-exact realised PnL, real
270
+ * exit price) and drops the local state entry. Idempotent at the webapp
271
+ * (position_closes UNIQUE on position_id); fail-open. */
272
+ async function handleReduceOnlyExit(ctx, fill) {
273
+ if (!ctx.decisionsClient || !ctx.userId || !ctx.stateStore)
274
+ return;
275
+ // Gated: when off, preserve the legacy behaviour (reduce-only fills ignored).
276
+ if (!ctx.closeOnReduceOnlyFill)
277
+ return;
278
+ const stateEntry = ctx.stateStore.get(fill.symbol);
279
+ // No tracked entry (already cleaned, or never journaled) → nothing to close.
280
+ if (!stateEntry || !stateEntry.webappPositionId)
281
+ return;
282
+ const updated = ctx.stateStore.applyExitContracts(fill.symbol, fill.fillSize, fill.realizedPnl ?? 0);
283
+ if (!updated) {
284
+ // Size unknown for this entry (opened before tracking existed and not yet
285
+ // seeded). Can't decide flatness from fills — leave the reconciler/ALGO
286
+ // close-bypass path as the backstop.
287
+ logger.info(TAG, `reduce-only fill ${fill.symbol}: size untracked; deferring close to reconciler/ALGO backstop`);
288
+ return;
289
+ }
290
+ const remaining = updated.remainingContracts ?? 0;
291
+ if (remaining > FLAT_EPSILON) {
292
+ logger.info(TAG, `partial exit ${fill.symbol}: ${fill.fillSize} reduce-only fill, ${remaining} contracts remaining`);
293
+ return; // not flat yet — keep accumulating
294
+ }
295
+ // FLAT. Auto-journal an exact close ONLY for exchange-native bracket fills
296
+ // (the cid is one of our bracket legs). Non-bracket reduce-only flats are
297
+ // either close_position's own order (its WS fill can race ahead of
298
+ // onClosePositionFilled) or a manual/external close — both are handled by
299
+ // their own paths (close_position's rich reason+assessment, or the reconciler
300
+ // backstop). Closing here would clobber the agent's close reasoning, so defer.
301
+ const isBracket = fill.clientOrderId ? isBracketCid(fill.clientOrderId) : false;
302
+ if (!isBracket) {
303
+ logger.info(TAG, `${fill.symbol} flat via non-bracket reduce-only fill (cid=${fill.clientOrderId ?? 'none'}) — ` +
304
+ `deferring close to close_position / reconciler backstop (no clobber)`);
305
+ return; // do NOT remove — onClosePositionFilled / backstop owns this close
306
+ }
307
+ const realizedPnl = updated.realizedPnlAccum ?? 0;
308
+ const close = {
309
+ positionId: stateEntry.webappPositionId,
310
+ closeAt: fill.exchangeTimeMs ?? Date.now(),
311
+ closeReason: 'bracket_fill',
312
+ closeAssessment: {
313
+ note: 'Position closed by an exchange-native bracket SL/TP fill (reduce-only). ' +
314
+ 'Auto-journaled from the WS fill — no close_position call (close-bypass path).',
315
+ observedFrom: 'ws_reduce_only_fill',
316
+ clientOrderId: fill.clientOrderId,
317
+ },
318
+ scorecardVerdict: 'NO_GO',
319
+ confluenceScore: 0,
320
+ regime: 'unknown',
321
+ regimeConfidence: 0.5,
322
+ fillPrice: fill.fillPrice,
323
+ fillSize: Math.abs(fill.fillSize),
324
+ // realizedPnl is Binance-exact (summed across exit fills). When it's a true
325
+ // break-even 0 the webapp recomputes from entries server-side — harmless.
326
+ realizedPnl,
327
+ realizedR: 0,
328
+ mfeRAtClose: 0,
329
+ giveBackPctAtClose: 0,
330
+ exchangeTradeId: fill.exchangeTradeId,
331
+ };
332
+ ctx.decisionsClient.postClose(ctx.userId, close);
333
+ ctx.stateStore.remove(fill.symbol);
334
+ logger.info(TAG, `close captured (ws bracket fill) ${fill.symbol} pnl=${realizedPnl.toFixed(4)} ` +
335
+ `(positionId=${stateEntry.webappPositionId.slice(0, 8)}…)`);
336
+ }
337
+ /** Internal helper — builds the PositionEntryPayload from a fill, applying the
338
+ * same regimeConfidence normalisation (0-100 → 0-1) as the synchronous path. */
339
+ function buildEntryPayload(args) {
340
+ const md = args.metadata;
341
+ const regimeConfNormalized = typeof md?.regimeConfidence === 'number'
342
+ ? Math.max(0, Math.min(1, md.regimeConfidence / 100))
343
+ : 0.5;
344
+ return {
345
+ positionId: args.positionId,
346
+ entryAt: args.ts,
347
+ isScaleIn: args.isScaleIn,
348
+ thesis: md?.thesis ?? '(no thesis recorded)',
349
+ setupType: md?.setupType ?? 'unknown',
350
+ regime: md?.regime ?? 'unknown',
351
+ regimeConfidence: regimeConfNormalized,
352
+ scorecardVerdict: md?.scorecardVerdict ?? 'GO',
353
+ confluenceScore: typeof md?.confluenceScore === 'number' ? md.confluenceScore : 0,
354
+ fillPrice: args.fillPrice,
355
+ fillSize: args.fillSize,
356
+ exchangeTradeId: args.exchangeTradeId,
357
+ metadata: buildEntryPlanMetadata(md),
358
+ };
359
+ }
360
+ /** Pinned-plan subset persisted to position_entries.metadata (free-form JSONB).
361
+ * Keys are snake_case per the canonical-JSONB-key rule (CLAUDE.md learning-loop:
362
+ * a camelCase mismatch previously broke learning matching). Returns undefined
363
+ * when neither field is present so we never write an empty object. */
364
+ export function buildEntryPlanMetadata(md) {
365
+ if (!md)
366
+ return undefined;
367
+ const out = {};
368
+ if (typeof md.invalidationPrice === 'number' && Number.isFinite(md.invalidationPrice)) {
369
+ out.invalidation_price = md.invalidationPrice;
370
+ }
371
+ // Pin the planned profit target too — it's the agent's stated target (and the
372
+ // bracket TP in live), surfaced on the dashboard pinned-plan view. Distinct
373
+ // from realization_rule (which is mostly omitted); without this the target is
374
+ // blank on the journal/Trade-card pinned-plan surfaces in the common case.
375
+ if (typeof md.targetPrice === 'number' && Number.isFinite(md.targetPrice)) {
376
+ out.target_price = md.targetPrice;
377
+ }
378
+ const rr = md.realizationRule;
379
+ if (rr) {
380
+ const j = { type: rr.type };
381
+ if (rr.targetPrice != null)
382
+ j.target_price = rr.targetPrice;
383
+ if (rr.trailAfterR != null)
384
+ j.trail_after_r = rr.trailAfterR;
385
+ if (rr.trailDistanceR != null)
386
+ j.trail_distance_r = rr.trailDistanceR;
387
+ if (rr.scale)
388
+ j.scale = rr.scale.map((s) => ({ at_r: s.atR, fraction: s.fraction }));
389
+ if (rr.note)
390
+ j.note = rr.note;
391
+ out.realization_rule = j;
392
+ }
393
+ return Object.keys(out).length > 0 ? out : undefined;
394
+ }