@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,527 @@
1
+ // Tool: audit_bracket_protection — returns every open live position with a
2
+ // verdict on whether exchange-side SL / TP protection is currently attached.
3
+ // Agent runs this at SESSION START and every heartbeat; any position with
4
+ // `has_stop=false` or `has_target=false` must be resolved (attach_brackets
5
+ // with new levels, or close_position).
6
+ //
7
+ // Cross-references three sources of truth to catch every failure mode:
8
+ // 1. Live positions (adapter.getPositions)
9
+ // 2. Open orders on the exchange (adapter.getOpenOrders) — ground truth
10
+ // for SL/TP orders currently live
11
+ // 3. Bracket ledger (for state + stored price levels)
12
+ //
13
+ // Order checks are by clientOrderId prefix AND by symbol + type, so we catch
14
+ // both (a) bracket orders with reefclaw-managed cids and (b) manually-placed
15
+ // STOP_MARKET / TAKE_PROFIT_MARKET orders from the Binance UI that happen to
16
+ // protect the position. Either is good enough for "position is protected".
17
+ import { parseBracketCid } from '../live/bracket-id.js';
18
+ import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
19
+ import { formatError, logger } from '../logger.js';
20
+ const TAG = 'audit-bracket-protection';
21
+ // Per-symbol verdict history for flicker detection. When audit verdicts
22
+ // transition protected→unprotected→protected repeatedly inside a short
23
+ // window, the bracket subsystem itself is unreliable — protection on the
24
+ // exchange isn't changing, our reading of it is. That's the signature of
25
+ // the 2026-05-15 spiral (ATOM/LINK/NEAR all flushed via bracket_integrity
26
+ // while their brackets were really still live). Emitting a structured warn
27
+ // when we see it surfaces the bug to the operator BEFORE the agent
28
+ // escalates to close_position.
29
+ const FLICKER_WINDOW_MS = 5 * 60 * 1000;
30
+ const FLICKER_THRESHOLD = 3; // 3 verdict flips in the window → warn
31
+ // Retention bumped to 60 min so close_position can copy the last hour of
32
+ // audit verdicts into position_closes.close_assessment.metadata.bracket_health_history
33
+ // for post-mortem analysis on the dashboard's Position Timeline drawer.
34
+ // 120 samples × ~120 bytes = ~15KB worst-case per symbol; trivial.
35
+ const HISTORY_RETENTION_MS = 60 * 60 * 1000;
36
+ const HISTORY_MAX_PER_SYMBOL = 120;
37
+ const verdictHistory = new Map();
38
+ let lastFlickerWarnedAt = new Map();
39
+ const FLICKER_WARN_COOLDOWN_MS = 60 * 1000; // don't repeat the warn more than once a minute per symbol
40
+ export async function auditBracketProtectionTool(_args, deps) {
41
+ try {
42
+ const isLive = deps.adapter.isLive;
43
+ // Paper mode: stop protection lives in position metadata + stop-watcher,
44
+ // not exchange orders. Report positions with their metadata for
45
+ // completeness, but mark them all protected (the stop-watcher model is
46
+ // the contract in paper).
47
+ if (!isLive) {
48
+ const positions = await deps.adapter.getPositions();
49
+ const entries = positions
50
+ .filter(p => p.contracts > 0)
51
+ .map(p => ({
52
+ symbol: p.symbol,
53
+ side: p.side,
54
+ contracts: p.contracts,
55
+ entry_price: p.entryPrice,
56
+ mark_price: p.markPrice,
57
+ has_stop: p.stopPrice !== undefined,
58
+ has_target: p.targetPrice !== undefined,
59
+ stop_price: p.stopPrice,
60
+ target_price: p.targetPrice,
61
+ ledger_state: 'no_ledger_row',
62
+ reason: p.stopPrice !== undefined ? 'manual_order_protecting' : 'no_protective_order',
63
+ recommended_action: p.stopPrice !== undefined ? 'none' : 'close_position',
64
+ }));
65
+ const unprotected = entries.filter(e => !e.has_stop).length;
66
+ return {
67
+ ok: true,
68
+ mode: 'paper',
69
+ brackets_enabled: false,
70
+ total_positions: entries.length,
71
+ protected_count: entries.length - unprotected,
72
+ unprotected_count: unprotected,
73
+ positions: entries,
74
+ };
75
+ }
76
+ // Live mode.
77
+ const live = deps.adapter;
78
+ const ledger = live.getBracketLedger?.() ?? null;
79
+ const bracketsEnabled = ledger !== null;
80
+ // Fetch positions + open orders. If `getOpenOrders` throws (it does so
81
+ // on null = fetch failure / 429 — see live-adapter.ts), we return a
82
+ // clear error instead of falling through with an empty list. The old
83
+ // behaviour was: null collapsed to []; audit said has_stop=false;
84
+ // agent ran attach_brackets; cancelSymbolBracketOrders no-op'd; new
85
+ // brackets piled up on top of orders Binance hadn't surfaced through
86
+ // the 429'd fetch. 2026-05-14 ATOM ran ~17 spurious re-attach cycles
87
+ // before the operator noticed. With this gate, a single 429 just
88
+ // delays the audit one heartbeat — far safer than fabricating
89
+ // "unprotected" verdicts.
90
+ // Weight discipline (2026-05-15, measured): the unscoped getOpenOrders
91
+ // sweep is 80 weight (40 regular + 40 algo). Pre-fix this ran on EVERY
92
+ // audit invocation — including when flat — and the agent's SKILL.md
93
+ // recovery loop (audit→attach) re-runs it every ~3s, so prod measured
94
+ // ~560-640 weight/min from this ONE call, ~40% of the pacer ceiling,
95
+ // which starved every other read and blinded the agent. Two cuts that
96
+ // do NOT change the audit verdict:
97
+ // 1. `openOrders` is consumed ONLY inside `activePositions.map(...)`,
98
+ // so when there are no active positions it is provably unused —
99
+ // skip the call entirely (return the trivial 0-position verdict).
100
+ // 2. When positions exist, fetch open orders SCOPED to those symbols
101
+ // (1 regular + 1 algo = 2 weight each) instead of one 80-weight
102
+ // unscoped sweep. Audit only ever inspects orders for symbols that
103
+ // have a position; cross-symbol orphan detection is the bracket
104
+ // reconciler's job (it keeps its 60s unscoped sweep — the
105
+ // authoritative orphan/naked backstop, unchanged).
106
+ // The null-vs-empty {error} contract is preserved for the calls that
107
+ // matter (verifying real open positions have stops). When flat, the
108
+ // reconciler + user-data WS + ALGO_UPDATE remain the safety net.
109
+ let positions, openOrders;
110
+ try {
111
+ // null = positions fetch FAILED (429 / weight-paced / transient).
112
+ // getPositions() would collapse that to [] → activeSymbols=[] → this
113
+ // audit returns a trivial "0 positions, all protected" verdict during
114
+ // a rate-limit storm, masking a real naked position for a heartbeat.
115
+ // Treat it identically to the getOpenOrders-null case below: surface
116
+ // {error} so the agent skips this cycle and MUST NOT run attach_brackets
117
+ // (see CLAUDE.md audit-vs-action contract).
118
+ const positionsOrNull = await deps.adapter.getPositionsOrNull();
119
+ if (positionsOrNull === null) {
120
+ throw new Error('positions fetch returned null (exchange data unavailable)');
121
+ }
122
+ positions = positionsOrNull;
123
+ const activeSymbols = [
124
+ ...new Set(positions.filter(p => p.contracts > 0).map(p => p.symbol)),
125
+ ];
126
+ if (activeSymbols.length === 0) {
127
+ openOrders = [];
128
+ }
129
+ else {
130
+ const perSymbol = await Promise.all(activeSymbols.map(s => deps.adapter.getOpenOrders(s)));
131
+ openOrders = perSymbol.flat();
132
+ }
133
+ }
134
+ catch (err) {
135
+ const msg = formatError(err);
136
+ logger.warn(TAG, `audit skipped — exchange data unavailable: ${msg}`);
137
+ return {
138
+ error: `Exchange data unavailable (likely Binance 429 or transient network). ` +
139
+ `Skipping audit this cycle — current bracket state on Binance is unchanged. ` +
140
+ `Retry on next heartbeat. Underlying: ${msg}`,
141
+ };
142
+ }
143
+ const activePositions = positions.filter(p => p.contracts > 0);
144
+ // Group open orders by NORMALIZED symbol so position.symbol (may be
145
+ // `:USDT`-suffixed from CCXT fetchPositions) and order.symbol (same
146
+ // source but historically inconsistent across CCXT versions) always
147
+ // collide on the same key. Without this, a suffixed position would miss
148
+ // an un-suffixed order's brackets and audit would return has_stop=false
149
+ // despite the exchange-side stop being live.
150
+ const ordersBySymbol = new Map();
151
+ for (const o of openOrders) {
152
+ if (!o.symbol)
153
+ continue;
154
+ const key = normalizeBracketSymbol(o.symbol);
155
+ const list = ordersBySymbol.get(key) ?? [];
156
+ list.push(o);
157
+ ordersBySymbol.set(key, list);
158
+ }
159
+ // Untrusted-empty contradiction guard (2026-05-15 SOL/INJ spiral).
160
+ // A position whose ledger row is non-terminal WITH stored cids but whose
161
+ // scoped open-orders fetch came back EMPTY is NOT positive evidence the
162
+ // brackets are gone: under weight pressure Binance's algo-orders REST
163
+ // endpoint returns a *successful* [] while ALGO_UPDATE (the authoritative
164
+ // bracket-lifecycle signal) shows the legs still live and never
165
+ // terminated. Emitting a confident has_stop=false / attach_brackets
166
+ // verdict here is what drove the audit → attach → cancel-live-brackets
167
+ // loop AND the bracket_integrity escalation that flushed ATOM/LINK/NEAR.
168
+ // Same null≠empty / require-positive-disconfirmation contract as
169
+ // classifyLedgerVsExchange and the reconciler: surface {error} so the
170
+ // agent skips this cycle and MUST NOT run attach_brackets /
171
+ // close_position(reason='bracket_integrity') (CLAUDE.md audit-vs-action
172
+ // contract). The reconciler + ALGO_UPDATE remain the backstop for any
173
+ // genuinely-naked position on another symbol within one cycle — the
174
+ // file's own stated philosophy ("a single transient gap just delays the
175
+ // audit one heartbeat — far safer than fabricating verdicts").
176
+ const unverifiable = activePositions.filter(p => {
177
+ const normSym = normalizeBracketSymbol(p.symbol);
178
+ if ((ordersBySymbol.get(normSym) ?? []).length > 0)
179
+ return false;
180
+ const lr = ledger?.getBySymbol(p.symbol) ?? null;
181
+ return !!lr
182
+ && (lr.state === 'active' || lr.state === 'partial' || lr.state === 'attaching')
183
+ && (!!lr.slCid || !!lr.tpCid);
184
+ });
185
+ // Before declaring the audit blind, ask the ZERO-WEIGHT authoritative
186
+ // source we already built (Phase 1/2, deployed): the per-cid bracket-leg
187
+ // liveness resolver. `getOpenOrders` came back empty ONLY because the
188
+ // proactive weight pacer shed it (steady-state IP weight pins the soft
189
+ // ceiling 24/7 — the real disease). But ALGO_UPDATE has already pushed
190
+ // every leg's NEW/CANCELED/EXPIRED/TRIGGERED status into the in-memory
191
+ // per-cid map at zero Binance weight. `resolveBracketLegLiveness` reads
192
+ // that (Tier-1, only when the WS store is trusted), falling back to a
193
+ // weight-1 REST query, then to 'unknown'. Pre-fix this branch returned
194
+ // {error} unconditionally → the agent went blind on a live position for
195
+ // HOURS (prod 2026-05-16: INJ/USDT unverifiable every heartbeat 04:29→
196
+ // 05:54 straight) even though the WS knew the brackets were live the
197
+ // whole time. The Phase-2 resolver was dead code in the audit path.
198
+ //
199
+ // stop leg 'live' → position IS protected (positive verdict, w=0)
200
+ // stop leg 'terminal' → positively confirmed naked → attach_brackets
201
+ // 'unknown' → keep the d59e51b safe {error} floor for that
202
+ // symbol only (NEVER infer from absence)
203
+ const wsResolvedProtected = new Set(); // normSym → stop leg confirmed live
204
+ const stillBlind = [];
205
+ if (unverifiable.length > 0) {
206
+ await Promise.all(unverifiable.map(async (p) => {
207
+ const normSym = normalizeBracketSymbol(p.symbol);
208
+ const lr = ledger?.getBySymbol(p.symbol) ?? null;
209
+ // The stop leg is the protective one; resolve it. (A live SL is the
210
+ // bar for "protected"; TP-only is still has_stop=false downstream.)
211
+ let slState = 'unknown';
212
+ if (lr?.slCid) {
213
+ try {
214
+ slState = await live.resolveBracketLegLiveness(lr.slCid);
215
+ }
216
+ catch {
217
+ slState = 'unknown';
218
+ }
219
+ }
220
+ if (slState === 'live') {
221
+ wsResolvedProtected.add(normSym);
222
+ }
223
+ else {
224
+ // #1 (supporting) — the ledger's slCid may be positively terminal
225
+ // while a PRIOR reattach's FRESH stop leg is live on the exchange
226
+ // under a NEW cid the ledger never caught up to (the 2026-05-16 INJ
227
+ // spiral). Declaring the position naked here drives the agent to
228
+ // attach_brackets → clear+reattach → cancels the live leg → spiral.
229
+ // Before that, ask whether ANY trusted-WS rc-*-s stop leg for this
230
+ // symbol is live, not just the ledger's cid. (Authoritative break
231
+ // is #2 in classifyLedgerVsExchange; this keeps the audit verdict
232
+ // consistent so the agent isn't told to attach in the first place.)
233
+ const sweep = live.findTrustedLiveBracketCidsForSymbol(p.symbol);
234
+ const hasLiveStopLeg = sweep.trusted &&
235
+ sweep.liveCids.some(c => parseBracketCid(c)?.role === 'stop');
236
+ if (hasLiveStopLeg) {
237
+ wsResolvedProtected.add(normSym);
238
+ }
239
+ else if (slState === 'terminal') {
240
+ // Positively confirmed gone AND no other live stop leg — let the
241
+ // normal entry builder produce has_stop=false → attach_brackets
242
+ // (licensed: POSITIVE disconfirmation, not inference from absence).
243
+ }
244
+ else {
245
+ stillBlind.push(p);
246
+ }
247
+ }
248
+ }));
249
+ }
250
+ if (stillBlind.length > 0) {
251
+ const blindSyms = stillBlind.map(p => p.symbol).join(', ');
252
+ const seenLive = [...wsResolvedProtected].join(', ') || 'none';
253
+ logger.warn(TAG, `audit unverifiable for [${blindSyms}]: scoped getOpenOrders returned EMPTY ` +
254
+ `(weight-paced) AND the per-cid liveness resolver could not positively confirm ` +
255
+ `(WS untrusted/silent + weight-1 REST unavailable). Empty ≠ "brackets gone". ` +
256
+ `Returning {error} — agent must NOT run attach_brackets or ` +
257
+ `close_position(reason='bracket_integrity') this cycle. ` +
258
+ `(WS positively confirmed LIVE for: [${seenLive}].)`);
259
+ return {
260
+ error: `Cannot confirm bracket protection for [${blindSyms}] — the exchange returned ` +
261
+ `an empty open-orders set (weight-paced) and the zero-weight per-leg liveness ` +
262
+ `resolver could not positively confirm the bracket legs (WS store untrusted ` +
263
+ `or silent on these cids, and the weight-1 REST query was unavailable). An ` +
264
+ `empty result is NOT proof the brackets are gone. Skipping audit this cycle — ` +
265
+ `bracket state on Binance is unchanged. Do NOT run attach_brackets or ` +
266
+ `close_position(reason='bracket_integrity') on the basis of this call. ` +
267
+ `Retry next heartbeat.`,
268
+ };
269
+ }
270
+ if (wsResolvedProtected.size > 0) {
271
+ logger.info(TAG, `per-leg liveness resolver: scoped getOpenOrders was weight-paced-empty but ` +
272
+ `the WS POSITIVELY confirmed the stop leg LIVE for ` +
273
+ `[${[...wsResolvedProtected].join(', ')}] at ZERO Binance weight — ` +
274
+ `reporting protected instead of going blind.`);
275
+ }
276
+ const entries = activePositions.map(p => {
277
+ const normSym = normalizeBracketSymbol(p.symbol);
278
+ const symbolOrders = ordersBySymbol.get(normSym) ?? [];
279
+ // BracketLedger applies normalization internally; pass p.symbol through.
280
+ const ledgerRow = ledger?.getBySymbol(p.symbol) ?? null;
281
+ // WS-confirmed-protected overlay: scoped getOpenOrders was weight-paced
282
+ // to empty for this symbol, so the order-based classification below
283
+ // would falsely report has_stop=false. But the zero-weight per-cid
284
+ // resolver POSITIVELY confirmed the stop leg is live on the exchange.
285
+ // Trust that (it's the authoritative ALGO_UPDATE signal, not inference
286
+ // from an absent REST result). stop_price from the ledger (our intent).
287
+ if (wsResolvedProtected.has(normSym)) {
288
+ return {
289
+ symbol: p.symbol,
290
+ side: p.side,
291
+ contracts: p.contracts,
292
+ entry_price: p.entryPrice,
293
+ mark_price: p.markPrice,
294
+ has_stop: true,
295
+ has_target: ledgerRow?.targetPrice !== undefined,
296
+ stop_price: ledgerRow?.stopPrice,
297
+ target_price: ledgerRow?.targetPrice,
298
+ ledger_state: ledgerRow?.state ?? 'no_ledger_row',
299
+ reason: 'bracket_active',
300
+ recommended_action: 'none',
301
+ };
302
+ }
303
+ // Classify each open order on the symbol: bracket SL, bracket TP,
304
+ // manual SL (non-bracket STOP_MARKET), manual TP (non-bracket TAKE_PROFIT_MARKET).
305
+ let hasBracketStop = false;
306
+ let hasBracketTarget = false;
307
+ let hasManualStop = false;
308
+ let hasManualTarget = false;
309
+ let stopPrice;
310
+ let targetPrice;
311
+ for (const o of symbolOrders) {
312
+ const parsed = o.clientOrderId ? parseBracketCid(o.clientOrderId) : null;
313
+ const triggerPrice = extractTriggerPrice(o);
314
+ if (parsed && parsed.role === 'stop') {
315
+ hasBracketStop = true;
316
+ stopPrice = triggerPrice ?? stopPrice;
317
+ }
318
+ else if (parsed && parsed.role === 'target') {
319
+ hasBracketTarget = true;
320
+ targetPrice = triggerPrice ?? targetPrice;
321
+ }
322
+ else {
323
+ // Prefer Binance's authoritative `orderType` field on the algo
324
+ // response (preserved in `info` by binance-private.ts). This
325
+ // catches externally-placed stops — manual SL via Binance UI —
326
+ // which parseBracketCid can't recognize since they have no
327
+ // `rc-*-s/t` CID. The string heuristic on top-level `type`
328
+ // remains as a secondary signal for legacy / non-algo paths
329
+ // where `info` may be absent.
330
+ // Spec: https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/Current-All-Algo-Open-Orders
331
+ const infoOrderType = String(o.info?.orderType ?? '').toUpperCase();
332
+ const type = (o.type ?? '').toLowerCase();
333
+ const isStopType = infoOrderType === 'STOP_MARKET' || infoOrderType === 'STOP' || infoOrderType === 'TRAILING_STOP_MARKET' ||
334
+ (type.includes('stop') && !type.includes('take'));
335
+ const isTargetType = infoOrderType === 'TAKE_PROFIT_MARKET' || infoOrderType === 'TAKE_PROFIT' ||
336
+ type.includes('take_profit') || type.includes('take-profit') || type === 'take_profit_market';
337
+ if (isStopType) {
338
+ hasManualStop = true;
339
+ stopPrice = triggerPrice ?? stopPrice;
340
+ }
341
+ else if (isTargetType) {
342
+ hasManualTarget = true;
343
+ targetPrice = triggerPrice ?? targetPrice;
344
+ }
345
+ }
346
+ }
347
+ const hasStop = hasBracketStop || hasManualStop;
348
+ const hasTarget = hasBracketTarget || hasManualTarget;
349
+ // Prefer ledger price when available — it's our intent, not a re-derived
350
+ // exchange trigger value (which Binance may round).
351
+ if (ledgerRow?.stopPrice !== undefined)
352
+ stopPrice = ledgerRow.stopPrice;
353
+ if (ledgerRow?.targetPrice !== undefined)
354
+ targetPrice = ledgerRow.targetPrice;
355
+ const ledgerState = ledgerRow?.state ?? 'no_ledger_row';
356
+ let reason;
357
+ if (!bracketsEnabled) {
358
+ reason = 'brackets_disabled';
359
+ }
360
+ else if (hasStop || hasTarget) {
361
+ reason = hasBracketStop || hasBracketTarget ? 'bracket_active' : 'manual_order_protecting';
362
+ }
363
+ else if (ledgerRow && (ledgerRow.state === 'active' || ledgerRow.state === 'partial' || ledgerRow.state === 'attaching')) {
364
+ reason = 'stale_ledger_row';
365
+ }
366
+ else if (ledgerRow && isTerminalState(ledgerRow.state)) {
367
+ reason = 'ledger_terminal_no_order';
368
+ }
369
+ else {
370
+ reason = 'no_protective_order';
371
+ }
372
+ let recommended_action = 'none';
373
+ if (!bracketsEnabled)
374
+ recommended_action = 'enable_brackets_mode';
375
+ else if (!hasStop)
376
+ recommended_action = 'attach_brackets';
377
+ return {
378
+ symbol: p.symbol,
379
+ side: p.side,
380
+ contracts: p.contracts,
381
+ entry_price: p.entryPrice,
382
+ mark_price: p.markPrice,
383
+ has_stop: hasStop,
384
+ has_target: hasTarget,
385
+ stop_price: stopPrice,
386
+ target_price: targetPrice,
387
+ ledger_state: ledgerState,
388
+ reason,
389
+ recommended_action,
390
+ };
391
+ });
392
+ const unprotected = entries.filter(e => !e.has_stop).length;
393
+ // Flicker detection — runs after every audit pass. Cheap (a few map ops
394
+ // per open position) and only emits log when the threshold trips.
395
+ const nowTs = Date.now();
396
+ for (const e of entries) {
397
+ recordVerdictAndCheckFlicker(normalizeBracketSymbol(e.symbol), e.has_stop, nowTs);
398
+ }
399
+ // Diagnostic: when we conclude a live position is unprotected, dump the
400
+ // raw openOrders shape for that symbol so post-mortems can tell whether
401
+ // (a) exchange genuinely had no stop/TP, (b) CCXT returned them but the
402
+ // cid/type didn't parse, or (c) the orders came back under a different
403
+ // symbol key than the position. This is the other half of the bug the
404
+ // 2026-04-21 close_position safeguard catches — we want to see WHY the
405
+ // audit said false, not just catch the symptom.
406
+ for (const e of entries) {
407
+ if (e.has_stop)
408
+ continue;
409
+ const normSym = normalizeBracketSymbol(e.symbol);
410
+ const raw = ordersBySymbol.get(normSym) ?? [];
411
+ const orderSnapshot = raw.map(o => ({
412
+ id: o.id,
413
+ cid: o.clientOrderId ?? null,
414
+ type: o.type ?? null,
415
+ side: o.side ?? null,
416
+ reduceOnly: o.reduceOnly ?? null,
417
+ stopPrice: extractTriggerPrice(o) ?? null,
418
+ infoType: o.info
419
+ ? (o.info.type ?? null)
420
+ : null,
421
+ symbol: o.symbol ?? null,
422
+ }));
423
+ const allSymbols = Array.from(ordersBySymbol.keys());
424
+ logger.warn(TAG, `unprotected verdict for ${e.symbol} (norm=${normSym}): has_stop=${e.has_stop} ` +
425
+ `has_target=${e.has_target} reason=${e.reason} ledger=${e.ledger_state}. ` +
426
+ `openOrders keyed under ${normSym}: ${orderSnapshot.length} ` +
427
+ `(all symbol keys in map: ${JSON.stringify(allSymbols)}). ` +
428
+ `orders=${JSON.stringify(orderSnapshot)}`);
429
+ }
430
+ return {
431
+ ok: true,
432
+ mode: 'live',
433
+ brackets_enabled: bracketsEnabled,
434
+ total_positions: entries.length,
435
+ protected_count: entries.length - unprotected,
436
+ unprotected_count: unprotected,
437
+ positions: entries,
438
+ };
439
+ }
440
+ catch (err) {
441
+ logger.error(TAG, `audit failed: ${formatError(err)}`);
442
+ return { error: `audit_bracket_protection failed: ${formatError(err)}` };
443
+ }
444
+ }
445
+ function isTerminalState(s) {
446
+ return s === 'cancelled' || s === 'failed' || s === 'triggered_sl' || s === 'triggered_tp';
447
+ }
448
+ /** Append a verdict sample for the symbol and emit a flicker warn when the
449
+ * recent history shows ≥ FLICKER_THRESHOLD verdict transitions (true↔false)
450
+ * inside FLICKER_WINDOW_MS. Trims the history to retention bounds. */
451
+ function recordVerdictAndCheckFlicker(symbol, isProtected, nowTs) {
452
+ const history = verdictHistory.get(symbol) ?? [];
453
+ history.push({ ts: nowTs, protected: isProtected });
454
+ // Trim by both age and length.
455
+ const cutoff = nowTs - HISTORY_RETENTION_MS;
456
+ while (history.length > 0 && history[0].ts < cutoff)
457
+ history.shift();
458
+ while (history.length > HISTORY_MAX_PER_SYMBOL)
459
+ history.shift();
460
+ verdictHistory.set(symbol, history);
461
+ // Count verdict transitions inside the flicker window.
462
+ const windowStart = nowTs - FLICKER_WINDOW_MS;
463
+ let transitions = 0;
464
+ let prev;
465
+ for (const sample of history) {
466
+ if (sample.ts < windowStart)
467
+ continue;
468
+ if (prev !== undefined && prev !== sample.protected)
469
+ transitions++;
470
+ prev = sample.protected;
471
+ }
472
+ if (transitions < FLICKER_THRESHOLD)
473
+ return;
474
+ const lastWarned = lastFlickerWarnedAt.get(symbol) ?? 0;
475
+ if (nowTs - lastWarned < FLICKER_WARN_COOLDOWN_MS)
476
+ return;
477
+ lastFlickerWarnedAt.set(symbol, nowTs);
478
+ logger.warn(TAG, `AUDIT FLICKER on ${symbol}: ${transitions} protected↔unprotected transitions ` +
479
+ `in the last ${FLICKER_WINDOW_MS / 60_000} min. ` +
480
+ `Bracket subsystem is reading exchange state inconsistently. ` +
481
+ `Suspect transient fetchOpenOrders gaps, attach_brackets re-entry races, or ` +
482
+ `CCXT symbol-form drift. Operator should investigate BEFORE the agent escalates ` +
483
+ `to close_position(reason='bracket_integrity'). ` +
484
+ `Recent samples: ${history.slice(-6).map(s => `${new Date(s.ts).toISOString().slice(11, 19)}:${s.protected ? 'P' : 'U'}`).join(' ')}`);
485
+ }
486
+ /** Public getter — returns the recent audit verdict samples for a symbol so
487
+ * close_position can copy them into the position's close record for the
488
+ * dashboard Position Timeline drawer. Read-only snapshot; mutating the
489
+ * return value does not affect internal state. */
490
+ export function getRecentAuditVerdictHistory(symbol) {
491
+ const history = verdictHistory.get(normalizeBracketSymbol(symbol)) ?? [];
492
+ // Defensive copy so callers can't mutate the live buffer.
493
+ return history.map(s => ({ ts: s.ts, protected: s.protected }));
494
+ }
495
+ /** Test seam — clears the in-memory flicker history between unit tests. */
496
+ export const __testing__ = {
497
+ resetFlickerHistory() {
498
+ verdictHistory.clear();
499
+ lastFlickerWarnedAt = new Map();
500
+ },
501
+ /** Inspect current history for a symbol — used by tests to assert state. */
502
+ getHistory(symbol) {
503
+ return verdictHistory.get(normalizeBracketSymbol(symbol)) ?? [];
504
+ },
505
+ };
506
+ // CCXT returns STOP_MARKET / TAKE_PROFIT_MARKET trigger prices in one of a
507
+ // few spots depending on version + exchange. Check the most common ones.
508
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
509
+ function extractTriggerPrice(o) {
510
+ const candidates = [
511
+ o?.stopPrice,
512
+ o?.triggerPrice,
513
+ o?.info?.stopPrice,
514
+ o?.info?.triggerPrice,
515
+ o?.params?.stopPrice,
516
+ ];
517
+ for (const c of candidates) {
518
+ if (typeof c === 'number' && Number.isFinite(c) && c > 0)
519
+ return c;
520
+ if (typeof c === 'string') {
521
+ const n = Number(c);
522
+ if (Number.isFinite(n) && n > 0)
523
+ return n;
524
+ }
525
+ }
526
+ return undefined;
527
+ }
@@ -0,0 +1,7 @@
1
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { CcxtOrder } from '../types.js';
3
+ export declare function cancelAllOrdersTool(args: {
4
+ symbol?: string;
5
+ }, deps: {
6
+ adapter: IExchangeAdapter;
7
+ }): Promise<CcxtOrder[]>;
@@ -0,0 +1,5 @@
1
+ // Tool: cancel_all_orders — cancel all open orders (paper or live)
2
+ // NO readiness gate — emergency control (kill switch), must always work.
3
+ export async function cancelAllOrdersTool(args, deps) {
4
+ return deps.adapter.cancelAllOrders(args.symbol);
5
+ }
@@ -0,0 +1,10 @@
1
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { CcxtOrder } from '../types.js';
3
+ export declare function cancelOrderTool(args: {
4
+ id: string;
5
+ symbol?: string;
6
+ }, deps: {
7
+ adapter: IExchangeAdapter;
8
+ }): Promise<CcxtOrder | {
9
+ error: string;
10
+ }>;
@@ -0,0 +1,14 @@
1
+ // Tool: cancel_order — cancel a single order (paper or live)
2
+ // NO readiness gate — emergency control, must always work.
3
+ import { formatError } from '../logger.js';
4
+ export async function cancelOrderTool(args, deps) {
5
+ if (!args.id) {
6
+ return { error: 'Order ID is required.' };
7
+ }
8
+ try {
9
+ return await deps.adapter.cancelOrder(args.id, args.symbol);
10
+ }
11
+ catch (err) {
12
+ return { error: formatError(err) };
13
+ }
14
+ }
@@ -0,0 +1,46 @@
1
+ interface PositionInput {
2
+ symbol: string;
3
+ side: 'LONG' | 'SHORT';
4
+ entry_price: number;
5
+ current_price: number;
6
+ stop_price: number;
7
+ target_price?: number;
8
+ quantity: number;
9
+ entry_time: string;
10
+ }
11
+ interface CheckHealthArgs {
12
+ positions: PositionInput[];
13
+ account_balance: number;
14
+ }
15
+ interface PositionHealth {
16
+ symbol: string;
17
+ side: string;
18
+ pnl: number;
19
+ pnlPercent: number;
20
+ pnlR: number;
21
+ distanceToStopPercent: number;
22
+ distanceToStopR: number;
23
+ stopStatus: 'SAFE' | 'CLOSE' | 'CRITICAL' | 'BREACHED';
24
+ distanceToTargetPercent: number | null;
25
+ distanceToTargetR: number | null;
26
+ targetReached: boolean;
27
+ heatPercent: number;
28
+ notionalPercent: number;
29
+ timeInTradeMinutes: number;
30
+ timeInTradeFormatted: string;
31
+ timeStatus: 'FRESH' | 'NORMAL' | 'EXTENDED' | 'STALE';
32
+ alerts: string[];
33
+ }
34
+ interface HealthSummary {
35
+ positions: PositionHealth[];
36
+ portfolio: {
37
+ totalHeat: number;
38
+ totalNotional: number;
39
+ totalPnl: number;
40
+ positionCount: number;
41
+ heatStatus: 'LOW' | 'MODERATE' | 'ELEVATED' | 'HIGH' | 'CRITICAL';
42
+ alerts: string[];
43
+ };
44
+ }
45
+ export declare function checkPositionHealthTool(args: CheckHealthArgs): HealthSummary;
46
+ export {};