@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,363 @@
1
+ // Tool: attach_brackets — bootstrap exchange-side protection on an existing
2
+ // open live position that is currently naked (no STOP_MARKET / TAKE_PROFIT_MARKET
3
+ // order). Complements modify_stop / modify_target, which can only MOVE an
4
+ // already-active bracket — they can't attach one from nothing.
5
+ //
6
+ // Why this tool exists (2026-04-21 naked-position incident): kill-switch
7
+ // sweeps were stripping bracket protective orders off open positions without
8
+ // flattening them. The agent then had no recovery path because modify_*
9
+ // requires state ∈ {active, partial}. This tool + the bracket-preserving
10
+ // cancel filter in BinancePrivateApi together close the loop.
11
+ //
12
+ // Responsibilities:
13
+ // 1. Validate inputs + mode (live only, brackets enabled).
14
+ // 2. Confirm an open position exists with >0 contracts.
15
+ // 3. Sanity-check stop / target direction vs the position side.
16
+ // 4. Reject if a fresh bracket is already active (caller wants modify_*).
17
+ // 5. Clear any stale bracket orders on the exchange for this symbol.
18
+ // 6. Register a new bracket + attach it. Retries inherit BracketManager policy.
19
+ //
20
+ // Paper mode: rejected with an explanatory error (paper uses per-position
21
+ // metadata + stop-watcher, not exchange orders).
22
+ import { generateBracketId, buildBracketCid } from '../live/bracket-id.js';
23
+ import { validateStopDirection, validateTargetDirection } from '../live/bracket-params.js';
24
+ import { formatError, logger } from '../logger.js';
25
+ const TAG = 'attach-brackets';
26
+ export async function attachBracketsTool(args, deps) {
27
+ // ---- 1. Argument validation ----
28
+ if (!args.symbol || typeof args.symbol !== 'string') {
29
+ return { error: 'symbol is required.' };
30
+ }
31
+ if (args.stop_price === undefined && args.target_price === undefined) {
32
+ return { error: 'at least one of stop_price or target_price must be provided.' };
33
+ }
34
+ if (args.stop_price !== undefined && (!Number.isFinite(args.stop_price) || args.stop_price <= 0)) {
35
+ return { error: 'stop_price must be a positive finite number.' };
36
+ }
37
+ if (args.target_price !== undefined && (!Number.isFinite(args.target_price) || args.target_price <= 0)) {
38
+ return { error: 'target_price must be a positive finite number.' };
39
+ }
40
+ // ---- 2. Mode gate ----
41
+ if (!deps.adapter.isLive) {
42
+ return { error: 'attach_brackets is live-mode only (it places exchange-side STOP/TP orders). In paper mode, use modify_stop / modify_target to set or move the position\'s protective levels in place — the paper stop-watcher enforces metadata.stopPrice.' };
43
+ }
44
+ const live = deps.adapter;
45
+ const mgr = live.getBracketManager?.();
46
+ const ledger = live.getBracketLedger?.();
47
+ if (!mgr || !ledger) {
48
+ return { error: 'Bracket orders are not enabled. Set brackets.mode in plugin-config.json to "observe" or "enforce".' };
49
+ }
50
+ // ---- 3. Position must exist ----
51
+ // getPositionsOrNull() returns null when the fetch FAILED (429 / weight-
52
+ // paced / transient) — position state is UNKNOWN, NOT "no position". If
53
+ // we collapsed null→[] here, attach_brackets would tell the agent "No open
54
+ // position" for a position that is actually live and NAKED on Binance; the
55
+ // agent reads that as "nothing to protect", stops trying, and the naked
56
+ // position rides unprotected. Distinct error → agent retries next
57
+ // heartbeat. Same null≠empty contract as audit/closePosition/flatten.
58
+ const positions = await deps.adapter.getPositionsOrNull(args.symbol);
59
+ if (positions === null) {
60
+ return {
61
+ error: `Exchange data unavailable for ${args.symbol} (likely Binance 429 or transient ` +
62
+ `network) — position state UNKNOWN. NOT treating this as "no position"; ` +
63
+ `bracket attach skipped this cycle. Retry on next heartbeat.`,
64
+ };
65
+ }
66
+ const position = positions.find(p => p.symbol === args.symbol) ?? positions[0];
67
+ if (!position || position.contracts <= 0) {
68
+ return { error: `No open position for ${args.symbol}.` };
69
+ }
70
+ const entrySide = position.side === 'long' ? 'buy' : 'sell';
71
+ // ---- 4. Direction sanity check vs entry price ----
72
+ const refPrice = position.entryPrice > 0 ? position.entryPrice : await deps.adapter.getLastPrice(position.symbol);
73
+ if (refPrice != null && refPrice > 0) {
74
+ if (args.stop_price !== undefined) {
75
+ const msg = validateStopDirection(entrySide, refPrice, args.stop_price);
76
+ if (msg)
77
+ return { error: msg };
78
+ }
79
+ if (args.target_price !== undefined) {
80
+ const msg = validateTargetDirection(entrySide, refPrice, args.target_price);
81
+ if (msg)
82
+ return { error: msg };
83
+ }
84
+ }
85
+ // ---- 5. Handle existing ledger row ----
86
+ // If the ledger has a non-terminal row AND its stored SL/TP cids correspond
87
+ // to live exchange orders, a fresh bracket really is active. Two sub-cases:
88
+ // (a) Caller passed the SAME prices the ledger already stores → idempotent
89
+ // no-op success. This is the agent's defensive re-attach pattern: it
90
+ // sees a flickery audit verdict, calls attach_brackets to "repair",
91
+ // passes the same stop/target it remembers from the entry. Pre-fix
92
+ // this returned an error, the agent escalated to close_position
93
+ // (bracket_integrity), and we lost ~$3 of bracket-integrity flushes
94
+ // on 2026-05-15. With idempotency, the defensive re-call costs
95
+ // nothing and the position survives.
96
+ // (b) Caller passed DIFFERENT prices → they want to change levels. Point
97
+ // them at modify_stop / modify_target so we don't have two paths
98
+ // that mutate the bracket geometry.
99
+ //
100
+ // If the ledger is non-terminal but the cids do NOT match any live exchange
101
+ // order, the row is stale (exchange brackets got cancelled out-of-band,
102
+ // plugin restart + lost attach, etc.). Force the row terminal so
103
+ // registerEntry below can seed a fresh pending_entry. Without this, stale
104
+ // rows deadlock recovery: audit reports has_stop=false (no exchange match),
105
+ // attach refuses (ledger says active), agent can't resolve. Incident:
106
+ // 2026-04-21 ADA death loop — 12 cycles panic-closed in 3 hours.
107
+ const existing = ledger.getBySymbol(position.symbol);
108
+ let clearedStaleLedgerRow = false;
109
+ if (existing && NON_TERMINAL_STATES.has(existing.state)) {
110
+ // Tri-state classification. `unknown` means we COULD NOT positively
111
+ // confirm the ledger's cids against live exchange orders — getOpenOrders
112
+ // threw (429 / weight-paced / sticky cooldown), OR the row has no cids to
113
+ // check (in-flight pending_entry/attaching). We must NOT collapse
114
+ // "unverifiable" into "live" — doing so makes attach_brackets return
115
+ // idempotent_no_op / "already active" for a position that is actually
116
+ // NAKED on the exchange, and the agent files a complacent hold/protected
117
+ // review on an unprotected live position. 2026-05-15 NEAR incident: a
118
+ // weight-paced getOpenOrders → verifyLedgerStale safe-defaulted to "not
119
+ // stale" → idempotent_no_op on an `attaching` row whose brackets were
120
+ // never placed. Same null≠empty / never-coerce-unverifiable-to-a-positive-
121
+ // safety-claim contract as getOpenOrders / getPositions / closePosition.
122
+ const cls = await classifyLedgerVsExchange(live, position.symbol, existing.slCid, existing.tpCid);
123
+ if (cls === 'unknown') {
124
+ logger.warn(TAG, `CANNOT CONFIRM bracket protection for ${position.symbol}: ledger row ` +
125
+ `state=${existing.state} (slCid=${existing.slCid ?? '-'}, tpCid=${existing.tpCid ?? '-'}) ` +
126
+ `but getOpenOrders unavailable or no cids to verify. Refusing to claim ` +
127
+ `protected; NOT clearing/reattaching (could race an in-flight attach).`);
128
+ return {
129
+ error: `Cannot confirm whether ${position.symbol} brackets are live on the exchange ` +
130
+ `(getOpenOrders unavailable — Binance 429 / weight-paced / transient — or the ` +
131
+ `ledger row is still in-flight with no cids yet). NOT assuming the position is ` +
132
+ `protected and NOT reattaching this cycle. Retry on the next heartbeat. If this ` +
133
+ `persists, treat the position as potentially NAKED: verify on the exchange and ` +
134
+ `place a manual stop. Do NOT record a hold/protected review on the basis of this call.`,
135
+ };
136
+ }
137
+ if (cls === 'live') {
138
+ // Idempotency check: same protection levels = no-op success.
139
+ if (pricesMatch(args.stop_price, existing.stopPrice)
140
+ && pricesMatch(args.target_price, existing.targetPrice)) {
141
+ logger.info(TAG, `idempotent no-op for ${position.symbol}: bracket already ${existing.state} ` +
142
+ `at stop=${existing.stopPrice ?? 'none'} target=${existing.targetPrice ?? 'none'} ` +
143
+ `(slCid=${existing.slCid ?? '-'}, tpCid=${existing.tpCid ?? '-'})`);
144
+ return {
145
+ ok: true,
146
+ symbol: position.symbol,
147
+ bracket_id: existing.bracketId,
148
+ entry_side: entrySide,
149
+ stop_price: existing.stopPrice,
150
+ target_price: existing.targetPrice,
151
+ sl_cid: existing.slCid,
152
+ tp_cid: existing.tpCid,
153
+ attach_latency_ms: 0,
154
+ cancelled_stale_bracket_orders: 0,
155
+ cleared_stale_ledger_row: false,
156
+ attempts: 0,
157
+ idempotent_no_op: true,
158
+ note: `Bracket already ${existing.state} on exchange with matching prices — no-op. If the audit reports has_stop=false anyway, suspect audit unreliability, not protection loss.`,
159
+ };
160
+ }
161
+ return {
162
+ error: `Bracket already ${existing.state} for ${position.symbol} ` +
163
+ `at stop=${formatPrice(existing.stopPrice)} target=${formatPrice(existing.targetPrice)}. ` +
164
+ `Use modify_stop / modify_target to change levels, or close_position to exit.`,
165
+ };
166
+ }
167
+ logger.warn(TAG, `Ledger row for ${position.symbol} is stale (state=${existing.state}, slCid=${existing.slCid}, tpCid=${existing.tpCid} not on exchange); forcing terminal to re-attach`);
168
+ try {
169
+ ledger.markState(position.symbol, 'cancelled', {
170
+ closeReason: 'cancelled_auto',
171
+ lastError: 'stale_ledger_detected_by_attach_brackets',
172
+ });
173
+ clearedStaleLedgerRow = true;
174
+ }
175
+ catch (err) {
176
+ return { error: `failed to clear stale ledger row: ${formatError(err)}` };
177
+ }
178
+ }
179
+ // ---- 6. Clear any stale bracket orders on the exchange for this symbol ----
180
+ // The ledger row (if any) is terminal at this point; its stored SL/TP cids
181
+ // may or may not still be live on the exchange. Orphan bracket orders not
182
+ // tracked by the ledger may also exist. Cancel all bracket-cid orders on
183
+ // this symbol so only our fresh pair remains after attach.
184
+ let cancelledOrphans = 0;
185
+ try {
186
+ cancelledOrphans = await live.cancelSymbolBracketOrders(position.symbol);
187
+ }
188
+ catch (err) {
189
+ logger.warn(TAG, `orphan cleanup failed for ${position.symbol}: ${formatError(err)}`);
190
+ }
191
+ // ---- 7. Register + attach ----
192
+ // BracketManager.registerEntry auto-clears terminal rows for the same symbol
193
+ // (see ledger-hygiene change landed alongside this tool).
194
+ const bracketId = generateBracketId();
195
+ try {
196
+ mgr.registerEntry({
197
+ symbol: position.symbol,
198
+ side: entrySide,
199
+ stopPrice: args.stop_price,
200
+ targetPrice: args.target_price,
201
+ }, bracketId, buildBracketCid(bracketId, 'entry'));
202
+ }
203
+ catch (err) {
204
+ return { error: `registerEntry failed: ${formatError(err)}` };
205
+ }
206
+ let attachResult;
207
+ try {
208
+ attachResult = await mgr.attachBrackets(position.symbol, position.contracts);
209
+ }
210
+ catch (err) {
211
+ return { error: `attachBrackets threw: ${formatError(err)}` };
212
+ }
213
+ if (!attachResult.ok) {
214
+ return {
215
+ error: `Bracket attach failed after ${attachResult.attempts} attempt(s): ${attachResult.error ?? 'unknown error'}. ` +
216
+ `Position remains open. Try again with adjusted levels, or close_position to exit.`,
217
+ };
218
+ }
219
+ return {
220
+ ok: true,
221
+ symbol: position.symbol,
222
+ bracket_id: bracketId,
223
+ entry_side: entrySide,
224
+ stop_price: args.stop_price,
225
+ target_price: args.target_price,
226
+ sl_cid: attachResult.slCid,
227
+ tp_cid: attachResult.tpCid,
228
+ attach_latency_ms: attachResult.latencyMs,
229
+ cancelled_stale_bracket_orders: cancelledOrphans,
230
+ cleared_stale_ledger_row: clearedStaleLedgerRow || (!!existing && !NON_TERMINAL_STATES.has(existing.state)),
231
+ attempts: attachResult.attempts,
232
+ };
233
+ }
234
+ // Non-terminal bracket states — a row in any of these means brackets are
235
+ // either in-flight or fully live; attach_brackets refuses to clobber them
236
+ // UNLESS the ledger cids don't match any live exchange order (stale row).
237
+ const NON_TERMINAL_STATES = new Set([
238
+ 'pending_entry',
239
+ 'attaching',
240
+ 'active',
241
+ 'partial',
242
+ ]);
243
+ /** Classify a non-terminal ledger row against live exchange open orders.
244
+ *
245
+ * - `'live'` — getOpenOrders SUCCEEDED and at least one stored cid is
246
+ * positively present in the live open orders. Only this
247
+ * result may produce an idempotent_no_op / "already active"
248
+ * reply: protection is PROVEN on the exchange.
249
+ * - `'stale'` — getOpenOrders SUCCEEDED and NONE of the stored cids are
250
+ * present. The exchange definitively does not have our
251
+ * brackets → clear the row and re-attach.
252
+ * - `'unknown'` — we could NOT positively verify: getOpenOrders threw
253
+ * (429 / weight-paced / sticky cooldown / transient), OR the
254
+ * row has no cids to check (in-flight pending_entry/attaching
255
+ * whose attach hasn't assigned cids yet). We must NOT treat
256
+ * this as `live` (false "protected" on a possibly-naked
257
+ * position — the 2026-05-15 NEAR incident) NOR as `stale`
258
+ * (clearing + reattaching could race a genuine in-flight
259
+ * attach and double up). The caller refuses honestly and
260
+ * tells the agent to retry / verify manually.
261
+ *
262
+ * Same null≠empty / never-coerce-unverifiable-into-a-positive-safety-claim
263
+ * contract as getOpenOrders / getPositions / closePosition / the reconciler. */
264
+ async function classifyLedgerVsExchange(live, symbol, slCid, tpCid) {
265
+ if (!slCid && !tpCid)
266
+ return 'unknown';
267
+ // Cheap positive check first: a stored cid present in a SUCCESSFUL,
268
+ // non-empty getOpenOrders is immediate, zero-extra-weight proof of life.
269
+ // This keeps the common happy-path free. A throw or an empty set is NOT
270
+ // disconfirmation — fall through to the authoritative per-leg resolver.
271
+ try {
272
+ const openOrders = await live.getOpenOrders(symbol);
273
+ const liveCids = new Set();
274
+ for (const o of openOrders) {
275
+ if (o.clientOrderId)
276
+ liveCids.add(o.clientOrderId);
277
+ }
278
+ if ((slCid && liveCids.has(slCid)) || (tpCid && liveCids.has(tpCid))) {
279
+ return 'live';
280
+ }
281
+ }
282
+ catch (err) {
283
+ logger.warn(TAG, `classifyLedgerVsExchange: getOpenOrders(${symbol}) unavailable ` +
284
+ `(${formatError(err)}) — deferring to the per-leg liveness resolver, ` +
285
+ `NOT assuming protected or stale`);
286
+ }
287
+ // No positive snapshot match (empty, threw, or non-empty-without-our-cids).
288
+ // The SOL/INJ spiral root: a *successful* empty /fapi/v1/openAlgoOrders is
289
+ // NOT proof a leg is gone. Ask the authoritative per-leg resolver instead
290
+ // of inferring from absence — trusted-WS fast-path → weight-1 REST
291
+ // /fapi/v1/algoOrder → 'unknown' floor:
292
+ // any leg 'live' → 'live' (do NOT clobber)
293
+ // ALL present legs 'terminal' → 'stale' (POSITIVELY confirmed gone —
294
+ // the only state that licenses repair)
295
+ // otherwise (any 'unknown') → 'unknown' (d59e51b safe floor)
296
+ const cids = [slCid, tpCid].filter((c) => !!c);
297
+ const verdicts = await Promise.all(cids.map(c => live.resolveBracketLegLiveness(c)));
298
+ if (verdicts.some(v => v === 'live'))
299
+ return 'live';
300
+ if (verdicts.length > 0 && verdicts.every(v => v === 'terminal')) {
301
+ // #2 — AUTHORITATIVE action-layer spiral guard (2026-05-16 INJ).
302
+ // The LEDGER's cids are positively terminal — but that is NOT proof the
303
+ // position is unprotected. A prior reattach's FRESH bracket may be live
304
+ // on the exchange under a NEW cid the ledger never caught up to.
305
+ // Returning 'stale' here licenses the clear+reattach that cancels that
306
+ // live bracket → naked → audit unprotected → attach → spiral. Before
307
+ // licensing repair, positively prove NO other rc-* leg for this symbol
308
+ // is live. Only positive evidence licenses the destructive action —
309
+ // same invariant as d59e51b / getOpenOrders / the reconciler.
310
+ const sweep = live.findTrustedLiveBracketCidsForSymbol(symbol);
311
+ if (sweep.trusted && sweep.liveCids.length > 0) {
312
+ logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} ledger cids (slCid=${slCid ?? '-'}, ` +
313
+ `tpCid=${tpCid ?? '-'}) are terminal BUT the trusted WS shows live ` +
314
+ `rc-* leg(s) under other cid(s) [${sweep.liveCids.join(', ')}] — a prior ` +
315
+ `reattach's fresh bracket is live and the ledger row lagged. Returning ` +
316
+ `'live' (NOT 'stale') to break the clear+reattach spiral; the position ` +
317
+ `IS protected on the exchange.`);
318
+ return 'live';
319
+ }
320
+ if (!sweep.trusted) {
321
+ // WS store untrusted → we cannot rule out a live fresh bracket under
322
+ // an untracked cid using the zero-weight signal, and the spiral REST
323
+ // is weight-paced. Refuse the destructive 'stale' (retry next cycle);
324
+ // a genuinely-naked position is still caught by the reconciler /
325
+ // ALGO_UPDATE / audit backstops. Worst case is a one-cycle delayed
326
+ // repair — far safer than cancelling a live bracket on a false 'stale'.
327
+ logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} ledger cids REST-confirmed terminal ` +
328
+ `but the WS store is UNTRUSTED — cannot positively rule out a live fresh ` +
329
+ `bracket under an untracked cid (weight-paced REST can't either). ` +
330
+ `Returning 'unknown' (NOT 'stale'); no force-reattach this cycle.`);
331
+ return 'unknown';
332
+ }
333
+ logger.info(TAG, `classifyLedgerVsExchange: ${symbol} legs POSITIVELY confirmed terminal ` +
334
+ `via per-leg resolver AND the trusted WS shows no other live rc-* leg ` +
335
+ `(slCid=${slCid ?? '-'}, tpCid=${tpCid ?? '-'}) — 'stale', safe to ` +
336
+ `clear+reattach.`);
337
+ return 'stale';
338
+ }
339
+ logger.warn(TAG, `classifyLedgerVsExchange: ${symbol} bracket liveness UNVERIFIABLE ` +
340
+ `(slCid=${slCid ?? '-'}, tpCid=${tpCid ?? '-'}; verdicts=${JSON.stringify(verdicts)}) ` +
341
+ `— returning 'unknown', NOT 'stale'. No force-reattach this cycle.`);
342
+ return 'unknown';
343
+ }
344
+ /** Compare a requested price against the ledger's stored price for the same
345
+ * bracket leg. Match means: both undefined (caller didn't ask for that leg
346
+ * and ledger doesn't have it) OR both defined within 0.01% relative
347
+ * tolerance (covers exchange rounding of trigger prices vs the operator's
348
+ * request). Any other shape is a mismatch — interpret as the caller wanting
349
+ * to CHANGE the level, route to modify_*. */
350
+ function pricesMatch(requested, stored) {
351
+ if (requested === undefined && stored === undefined)
352
+ return true;
353
+ if (requested === undefined || stored === undefined)
354
+ return false;
355
+ if (!Number.isFinite(requested) || !Number.isFinite(stored))
356
+ return false;
357
+ if (stored === 0)
358
+ return requested === 0;
359
+ return Math.abs(requested - stored) / Math.abs(stored) <= 1e-4;
360
+ }
361
+ function formatPrice(p) {
362
+ return p === undefined ? 'none' : String(p);
363
+ }
@@ -0,0 +1,49 @@
1
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { BracketState } from '../live/bracket-types.js';
3
+ type ProtectionReason = 'bracket_active' | 'manual_order_protecting' | 'no_protective_order' | 'brackets_disabled' | 'stale_ledger_row' | 'ledger_terminal_no_order';
4
+ interface PositionAuditEntry {
5
+ symbol: string;
6
+ side: 'long' | 'short';
7
+ contracts: number;
8
+ entry_price: number;
9
+ mark_price: number;
10
+ has_stop: boolean;
11
+ has_target: boolean;
12
+ stop_price?: number;
13
+ target_price?: number;
14
+ ledger_state: BracketState | 'no_ledger_row';
15
+ reason: ProtectionReason;
16
+ recommended_action: 'none' | 'attach_brackets' | 'close_position' | 'enable_brackets_mode';
17
+ }
18
+ interface AuditBracketProtectionResult {
19
+ ok: true;
20
+ mode: 'paper' | 'live';
21
+ brackets_enabled: boolean;
22
+ total_positions: number;
23
+ protected_count: number;
24
+ unprotected_count: number;
25
+ positions: PositionAuditEntry[];
26
+ }
27
+ export declare function auditBracketProtectionTool(_args: Record<string, never>, deps: {
28
+ adapter: IExchangeAdapter;
29
+ }): Promise<AuditBracketProtectionResult | {
30
+ error: string;
31
+ }>;
32
+ /** Public getter — returns the recent audit verdict samples for a symbol so
33
+ * close_position can copy them into the position's close record for the
34
+ * dashboard Position Timeline drawer. Read-only snapshot; mutating the
35
+ * return value does not affect internal state. */
36
+ export declare function getRecentAuditVerdictHistory(symbol: string): ReadonlyArray<{
37
+ ts: number;
38
+ protected: boolean;
39
+ }>;
40
+ /** Test seam — clears the in-memory flicker history between unit tests. */
41
+ export declare const __testing__: {
42
+ resetFlickerHistory(): void;
43
+ /** Inspect current history for a symbol — used by tests to assert state. */
44
+ getHistory(symbol: string): ReadonlyArray<{
45
+ ts: number;
46
+ protected: boolean;
47
+ }>;
48
+ };
49
+ export {};