@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,449 @@
1
+ // Tool: close_position — close a position at market (paper or live)
2
+ // NO readiness gate — emergency control (flatten), must always work.
3
+ //
4
+ // SKILL.md v2.10.0 gate (Position Management Discipline): the agent must
5
+ // submit a documented `reason` and (except for operator_command) a
6
+ // structured `assessment` describing the reasoning behind the close. The
7
+ // LLM's value is context-sensitive reasoning; we enforce the reasoning
8
+ // at the API boundary via a schema, not a time floor.
9
+ //
10
+ // 2026-04-21 false-bracket_integrity safeguard: audit_bracket_protection
11
+ // was observed reporting has_stop=false while the reconciler (same data
12
+ // source, same call) consistently confirmed 2 live bracket orders on the
13
+ // exchange. Agent used this false negative to trigger close_position with
14
+ // reason=bracket_integrity, burning ~8–20 bps per cycle, 14+ cycles today.
15
+ // Fix: before executing a bracket_integrity close, the plugin independently
16
+ // verifies the ledger + exchange state. If the bracket is genuinely live
17
+ // (ledger active/partial AND an exchange order matches the stored cid),
18
+ // reject the close — audit was wrong, the position is protected. This is
19
+ // a same-signal re-check at the API boundary, not a duplicate of the
20
+ // audit tool; it catches the observed class of false negatives without
21
+ // trusting the audit's own verdict.
22
+ import { formatError, logger } from '../logger.js';
23
+ import { fetchCurrentPrice, fetchOrderBook, isError } from './helpers.js';
24
+ import { validateClosePosition, } from './assessment-validation.js';
25
+ import { onClosePositionFilled } from '../ingest/position-auto-capture.js';
26
+ import { evaluateExitGate, } from './exit-gate.js';
27
+ import { getRecentAuditVerdictHistory } from './audit-bracket-protection.js';
28
+ import { loadExitGateMode } from '../config/position-review-config.js';
29
+ const TAG = 'close-position';
30
+ // Bracket-integrity circuit-breaker. Counts SUCCESSFUL close_position calls
31
+ // with reason='bracket_integrity' in a rolling 60-min window. When the 3rd
32
+ // such close is attempted, reject — repeated bracket-integrity flushes in
33
+ // the same hour means EITHER the bracket subsystem is actually broken (in
34
+ // which case the operator needs to know, not the agent quietly burning more
35
+ // positions) OR the audit tool is false-negativing (same outcome: stop the
36
+ // burn). 2026-05-15 session ran this exact pattern 3 times in 4 hours and
37
+ // realised −$3.20 of bracket-integrity flushes before the operator caught it.
38
+ const BRACKET_INTEGRITY_WINDOW_MS = 60 * 60 * 1000;
39
+ const BRACKET_INTEGRITY_LIMIT = 3;
40
+ let bracketIntegrityCloses = []; // timestamps of successful closes
41
+ export async function closePositionTool(args, deps) {
42
+ // Schema gate FIRST — reject before touching the exchange.
43
+ const v = validateClosePosition(args);
44
+ if (!v.ok) {
45
+ logger.warn(TAG, `close_position rejected: ${v.error} (symbol=${args.symbol}, reason=${args.reason ?? 'missing'})`);
46
+ return { error: v.error ?? 'Invalid close_position arguments.' };
47
+ }
48
+ // Exit-gate check — see docs/EXIT_GATE_DESIGN.md. The gate is orthogonal to
49
+ // the bracket_integrity safeguard below; both can fire on the same call.
50
+ // Behaviour by mode:
51
+ // off — gate skipped entirely (no input fetch, no log).
52
+ // shadow — gate runs, verdict logged to close_assessment.metadata.gate_eval,
53
+ // close_position always proceeds.
54
+ // observe — gate runs; on BLOCK we still proceed but the verdict is logged
55
+ // and a warning printed to journalctl.
56
+ // enforce — gate runs; on BLOCK we hard-reject with the recovery hint.
57
+ const gateMode = deps.exitGateMode ?? loadExitGateMode();
58
+ let gateVerdict = null;
59
+ // 0a (2026-06-16): hoist the gate inputs to function scope so the post-close
60
+ // capture block can persist a full inputs snapshot. Previously `inputs` was a
61
+ // block-local const that went out of scope before the capture write, which is
62
+ // why only {mode,outcome,tier,code} ever reached the journal (the §5 `inputs`
63
+ // snapshot was never constructed). See docs/EXIT_DISCIPLINE_BUILD_PLAN.md
64
+ // Step 0a + docs/EXIT_GATE_DESIGN.md §5.
65
+ let gateInputs = null;
66
+ if (gateMode !== 'off') {
67
+ gateInputs = await buildExitGateInputs(args, deps);
68
+ gateVerdict = evaluateExitGate(gateInputs);
69
+ logger.info(TAG, `exit_gate mode=${gateMode} ${gateVerdict.outcome} tier=${gateVerdict.tier} code=${gateVerdict.code} (symbol=${args.symbol}, reason=${args.reason})`);
70
+ if (gateVerdict.outcome === 'BLOCK' && gateMode === 'enforce') {
71
+ logger.warn(TAG, `close_position REJECTED by exit gate (enforce): ${gateVerdict.code} — ${gateVerdict.recovery_hint ?? '(no hint)'}`);
72
+ return {
73
+ error: `close_position blocked by exit gate (${gateVerdict.code}). ` +
74
+ (gateVerdict.recovery_hint ?? 'See docs/EXIT_GATE_DESIGN.md.'),
75
+ };
76
+ }
77
+ if (gateVerdict.outcome === 'BLOCK' && gateMode === 'observe') {
78
+ logger.warn(TAG, `close_position WOULD BE BLOCKED by exit gate (observe): ${gateVerdict.code} — ${gateVerdict.recovery_hint ?? '(no hint)'} — proceeding because mode=observe`);
79
+ }
80
+ }
81
+ // In paper mode, fetch latest ticker + order book for realistic fills
82
+ if (!deps.adapter.isLive) {
83
+ const paperDeps = { binanceApi: deps.binanceApi, simulator: deps.adapter.getSimulator() };
84
+ const [priceResult] = await Promise.all([
85
+ fetchCurrentPrice(args.symbol, paperDeps),
86
+ fetchOrderBook(args.symbol, paperDeps),
87
+ ]);
88
+ if (isError(priceResult))
89
+ return priceResult;
90
+ }
91
+ // Log the assessment so every agent close is auditable in the journal.
92
+ // We log after validation (we've confirmed the shape is sound) and before
93
+ // the adapter call so the record is written even if the close fails.
94
+ const reason = args.reason;
95
+ const summary = formatAssessmentSummary(args.assessment);
96
+ logger.info(TAG, `close_position ${args.symbol} reason=${reason}${summary ? ' | ' + summary : ''}`);
97
+ // Circuit-breaker: if we've already let 2 bracket_integrity closes through
98
+ // in the last hour, the 3rd is almost certainly the audit lying to us
99
+ // rather than a real protection failure. Force the operator to either ack
100
+ // by retrying with a different reason (regime_flip / risk_limit /
101
+ // operator_command) or investigate the bracket subsystem.
102
+ if (reason === 'bracket_integrity' && deps.adapter.isLive) {
103
+ const now = Date.now();
104
+ const windowStart = now - BRACKET_INTEGRITY_WINDOW_MS;
105
+ bracketIntegrityCloses = bracketIntegrityCloses.filter(ts => ts >= windowStart);
106
+ if (bracketIntegrityCloses.length >= BRACKET_INTEGRITY_LIMIT - 1) {
107
+ logger.warn(TAG, `BRACKET-INTEGRITY CIRCUIT-BREAKER TRIPPED for ${args.symbol}: ` +
108
+ `${bracketIntegrityCloses.length} prior bracket_integrity closes in the last ` +
109
+ `${BRACKET_INTEGRITY_WINDOW_MS / 60_000} min. Refusing the ${bracketIntegrityCloses.length + 1}th. ` +
110
+ `Audit-vs-reality bug is likelier than ${bracketIntegrityCloses.length + 1} consecutive real protection losses.`);
111
+ return {
112
+ error: `close_position rejected: circuit-breaker tripped — ` +
113
+ `${bracketIntegrityCloses.length} bracket_integrity closes already fired in the last ` +
114
+ `${BRACKET_INTEGRITY_WINDOW_MS / 60_000} min. This pattern almost always means the audit ` +
115
+ `is unreliable, not that brackets are repeatedly failing on the exchange. ` +
116
+ `Investigation steps: (1) run audit_bracket_protection and inspect 'reason' + ` +
117
+ `ledger_state; (2) check the journal for 'AUDIT FLICKER' warnings; ` +
118
+ `(3) if you truly need to exit, use reason='operator_command' (bypasses this check) ` +
119
+ `or escalate to the operator.`,
120
+ };
121
+ }
122
+ }
123
+ // False-bracket_integrity safeguard — see module header comment.
124
+ if (reason === 'bracket_integrity' && deps.adapter.isLive) {
125
+ const integrity = await verifyBracketsGenuinelyMissing(deps.adapter, args.symbol);
126
+ if (integrity.protected) {
127
+ logger.warn(TAG, `close_position REJECTED — reason=bracket_integrity but plugin verified brackets live: ${integrity.detail} (symbol=${args.symbol}). This is the audit-vs-reality bug; the position is protected. If you want to close anyway, use reason=regime_flip, risk_limit, or operator_command.`);
128
+ return {
129
+ error: `close_position rejected: bracket_integrity claim contradicted by plugin (${integrity.detail}). ` +
130
+ `Brackets are live on exchange. Use a different reason if you want to exit.`,
131
+ };
132
+ }
133
+ logger.info(TAG, `bracket_integrity safeguard: ${integrity.detail} — allowing close`);
134
+ }
135
+ try {
136
+ const order = await deps.adapter.closePosition(args.symbol);
137
+ // Successful close — record the bracket_integrity timestamp so the
138
+ // circuit-breaker can see the trend on the next attempt.
139
+ if (reason === 'bracket_integrity' && deps.adapter.isLive) {
140
+ bracketIntegrityCloses.push(Date.now());
141
+ }
142
+ // Auto-capture close to the Position Decision Journal — fail-open.
143
+ if (deps.autoCapture) {
144
+ const a = args.assessment;
145
+ const assessmentForCapture = {
146
+ ...a ?? {},
147
+ };
148
+ if (gateVerdict) {
149
+ // 0a: persist the full inputs snapshot + would_have_blocked so shadow
150
+ // verdicts are auditable from the journal (EXIT_GATE_DESIGN.md §5).
151
+ // Keys are snake_case per the canonical-JSONB-key rule; every input is
152
+ // null-coalesced so a MISSING input is recorded explicitly (that is the
153
+ // point — it makes the inputs_unavailable_* skips diagnosable). The key
154
+ // stays `outcome` (not §5's older `verdict`) to match the 148 historical
155
+ // rows. `gateInputs` is non-null whenever `gateVerdict` is set (both are
156
+ // assigned together when gateMode!=='off'); `gi` guard is for the type.
157
+ const gi = gateInputs;
158
+ const stopAtBE = gi && gi.side && typeof gi.stopPrice === 'number' && typeof gi.entryPrice === 'number'
159
+ ? gi.side === 'long'
160
+ ? gi.stopPrice >= gi.entryPrice
161
+ : gi.stopPrice <= gi.entryPrice
162
+ : null;
163
+ assessmentForCapture.metadata = {
164
+ ...assessmentForCapture.metadata ?? {},
165
+ gate_eval: {
166
+ mode: gateMode,
167
+ outcome: gateVerdict.outcome,
168
+ tier: gateVerdict.tier,
169
+ code: gateVerdict.code,
170
+ would_have_blocked: gateVerdict.outcome === 'BLOCK',
171
+ ...(gateVerdict.discipline_failure
172
+ ? { discipline_failure: gateVerdict.discipline_failure }
173
+ : {}),
174
+ ...(gi
175
+ ? {
176
+ inputs: {
177
+ mfe_r_peak: gi.mfe_r_peak ?? null,
178
+ current_r: gi.current_r ?? null,
179
+ stop_at_BE: stopAtBE,
180
+ last_review_verdict: gi.lastReview?.verdict ?? null,
181
+ last_review_age_minutes: gi.lastReview
182
+ ? Math.round((gi.now_ms - gi.lastReview.review_at_ms) / 6000) / 10
183
+ : null,
184
+ thesis_status: gi.lastReview?.thesis_status ?? null,
185
+ // vol_shock is never computed by buildExitGateInputs yet, so
186
+ // this is always false until the 3xATR shock is wired — do
187
+ // NOT read it as a real signal during the soak.
188
+ vol_shock: gi.volShock ?? false,
189
+ // Provenance (set in buildExitGateInputs): which source
190
+ // supplied each value — makes the inputs_unavailable_* and
191
+ // computed-fallback paths diagnosable from the journal.
192
+ mfe_src: gi.mfe_src ?? null,
193
+ current_r_src: gi.current_r_src ?? null,
194
+ },
195
+ }
196
+ : {}),
197
+ },
198
+ };
199
+ }
200
+ // Forensics: snapshot the audit verdict history (last 60min, up to
201
+ // 120 samples) so the dashboard Position Timeline drawer can flag
202
+ // closes that look like audit-flicker artefacts rather than real
203
+ // market exits. The flicker-warn line in journal logs the same data
204
+ // in real time; this lets us audit historical closes after the fact.
205
+ // 2026-05-15 spiral root-cause work, item #8 of the bundle.
206
+ if (deps.adapter.isLive) {
207
+ const history = getRecentAuditVerdictHistory(args.symbol);
208
+ if (history.length > 0) {
209
+ assessmentForCapture.metadata = {
210
+ ...assessmentForCapture.metadata ?? {},
211
+ bracket_health_history: history,
212
+ };
213
+ }
214
+ }
215
+ onClosePositionFilled(deps.autoCapture, {
216
+ symbol: args.symbol,
217
+ closeReason: reason,
218
+ closeAssessment: assessmentForCapture,
219
+ scorecardVerdict: typeof a?.current_score === 'number' ? undefined : undefined,
220
+ confluenceScore: typeof a?.current_score === 'number' ? a.current_score : undefined,
221
+ regime: a?.current_regime,
222
+ regimeConfidence: undefined,
223
+ }, order).catch((err) => {
224
+ // eslint-disable-next-line no-console
225
+ console.warn(`[position-auto-capture] close capture threw: ${formatError(err)}`);
226
+ });
227
+ }
228
+ return order;
229
+ }
230
+ catch (err) {
231
+ return { error: formatError(err) };
232
+ }
233
+ }
234
+ /** Independent check used by the bracket_integrity safeguard. Looks at the
235
+ * plugin's bracket ledger + a fresh exchange-side open-orders fetch; returns
236
+ * `{protected: true}` only when BOTH confirm a live bracket (ledger row in a
237
+ * non-terminal state with stored cids, AND at least one exchange order
238
+ * carrying a matching `rc-<bracketId>-s` or `rc-<bracketId>-t` cid). Any
239
+ * uncertainty (no ledger row, cids not stored, fetch error, nothing on
240
+ * exchange) returns `{protected: false}` so the close is allowed — we only
241
+ * block when we can definitively prove the audit was wrong. */
242
+ async function verifyBracketsGenuinelyMissing(adapter, symbol) {
243
+ const ledger = adapter.getBracketLedger?.();
244
+ if (!ledger)
245
+ return { protected: false, detail: 'brackets disabled' };
246
+ const row = ledger.getBySymbol(symbol);
247
+ if (!row)
248
+ return { protected: false, detail: 'no ledger row' };
249
+ const NON_TERMINAL = new Set(['pending_entry', 'attaching', 'active', 'partial']);
250
+ if (!NON_TERMINAL.has(row.state)) {
251
+ return { protected: false, detail: `ledger state=${row.state} (terminal)` };
252
+ }
253
+ if (!row.slCid && !row.tpCid) {
254
+ return { protected: false, detail: `ledger state=${row.state} but no cids stored` };
255
+ }
256
+ let open = [];
257
+ try {
258
+ open = await adapter.getOpenOrders(symbol);
259
+ }
260
+ catch (err) {
261
+ return { protected: false, detail: `getOpenOrders failed: ${formatError(err)}` };
262
+ }
263
+ const liveCids = new Set();
264
+ for (const o of open) {
265
+ if (o.clientOrderId)
266
+ liveCids.add(o.clientOrderId);
267
+ }
268
+ const slLive = Boolean(row.slCid && liveCids.has(row.slCid));
269
+ const tpLive = Boolean(row.tpCid && liveCids.has(row.tpCid));
270
+ // Block the close only when BOTH bracket legs are live — that's the only
271
+ // shape that proves the audit was definitively wrong (the original 2026-04-21
272
+ // symbol-mapping bug). A position with only one leg live is half-bracketed:
273
+ // the audit's `has_stop=false` (or `has_target=false`) is correct, the
274
+ // operator/agent needs to flatten or re-attach, and the safeguard would
275
+ // wrongly trap them. Per the docstring: only block when we can prove the
276
+ // audit was wrong; one-leg-live does not prove that.
277
+ if (slLive && tpLive) {
278
+ return {
279
+ protected: true,
280
+ detail: `ledger state=${row.state}, exchange has SL(${row.slCid}) + TP(${row.tpCid}) open (${open.length} total orders on ${symbol})`,
281
+ };
282
+ }
283
+ const observed = [];
284
+ if (slLive)
285
+ observed.push(`SL(${row.slCid}) only`);
286
+ if (tpLive)
287
+ observed.push(`TP(${row.tpCid}) only`);
288
+ if (observed.length === 0)
289
+ observed.push('neither cid on exchange');
290
+ return {
291
+ protected: false,
292
+ detail: `ledger state=${row.state} (slCid=${row.slCid ?? '-'}, tpCid=${row.tpCid ?? '-'}) — ${observed.join(', ')} (${open.length} orders)`,
293
+ };
294
+ }
295
+ /** Gather inputs for the exit gate from adapter + bracket ledger + state store.
296
+ * Best-effort: any missing piece becomes `undefined` so the pure gate function
297
+ * can decide whether to PASS('skipped') or proceed. Never throws — gate
298
+ * evaluation must never crash close_position.
299
+ */
300
+ async function buildExitGateInputs(args, deps) {
301
+ const reason = (args.reason ?? 'regime_flip');
302
+ const a = args.assessment;
303
+ const inputs = {
304
+ reason,
305
+ now_ms: Date.now(),
306
+ mfe_r_peak: typeof a?.mfe_r_peak === 'number' ? a.mfe_r_peak : undefined,
307
+ current_r: typeof a?.r_multiple_at_close === 'number' ? a.r_multiple_at_close : undefined,
308
+ };
309
+ // Provenance: agent-supplied values win (set the src tags now); the adapter
310
+ // and computed fallbacks below only fire when these stay undefined.
311
+ if (inputs.mfe_r_peak !== undefined)
312
+ inputs.mfe_src = 'assessment';
313
+ if (inputs.current_r !== undefined)
314
+ inputs.current_r_src = 'assessment';
315
+ // Bracket ledger — stop price + (canonical) ledger entry.
316
+ // Only LiveAdapter exposes getBracketLedger; paper adapter falls back.
317
+ try {
318
+ const ledger = deps.adapter.getBracketLedger?.();
319
+ if (ledger) {
320
+ const row = ledger.getBySymbol(args.symbol);
321
+ if (row) {
322
+ if (typeof row.stopPrice === 'number' && Number.isFinite(row.stopPrice)) {
323
+ inputs.stopPrice = row.stopPrice;
324
+ }
325
+ if (row.entrySide === 'buy')
326
+ inputs.side = 'long';
327
+ else if (row.entrySide === 'sell')
328
+ inputs.side = 'short';
329
+ }
330
+ }
331
+ }
332
+ catch (err) {
333
+ logger.warn(TAG, `exit_gate input fetch (bracket ledger): ${formatError(err)}`);
334
+ }
335
+ // Position — for entryPrice + side fallback, plus mfeR + bracket fallbacks.
336
+ // LiveAdapter.getPositions decorates each row with a running mfeR (updated
337
+ // from mark price on every call) and a bracket snapshot. When the agent
338
+ // omits `assessment.mfe_r_peak` (observed on ~77% of closes during the
339
+ // 2026-05-14 shadow soak), we read mfeR from the adapter so the gate can
340
+ // still tier the close instead of returning inputs_unavailable_mfe.
341
+ try {
342
+ const positions = await deps.adapter.getPositions(args.symbol);
343
+ const pos = positions.find((p) => p.contracts && p.contracts > 0);
344
+ if (pos) {
345
+ if (typeof pos.entryPrice === 'number' && Number.isFinite(pos.entryPrice)) {
346
+ inputs.entryPrice = pos.entryPrice;
347
+ }
348
+ if (!inputs.side && pos.side === 'long')
349
+ inputs.side = 'long';
350
+ if (!inputs.side && pos.side === 'short')
351
+ inputs.side = 'short';
352
+ if (inputs.mfe_r_peak === undefined
353
+ && typeof pos.mfeR === 'number'
354
+ && Number.isFinite(pos.mfeR)) {
355
+ inputs.mfe_r_peak = pos.mfeR;
356
+ inputs.mfe_src = 'pos.mfeR';
357
+ }
358
+ if (inputs.stopPrice === undefined
359
+ && pos.bracket
360
+ && typeof pos.bracket.slPrice === 'number'
361
+ && Number.isFinite(pos.bracket.slPrice)) {
362
+ inputs.stopPrice = pos.bracket.slPrice;
363
+ }
364
+ // 0b: plugin-computed fallbacks for current_r (SIGNED) + an mfe peak-FLOOR
365
+ // when the agent AND the adapter both left them undefined — the
366
+ // inputs_unavailable_* skips (~16-30% of evals). R denominator is the
367
+ // frozen entry→stop distance (originalStopPrice preferred; else the bracket
368
+ // stop resolved above). Both gated on `=== undefined` so agent-supplied
369
+ // values still win. current_r MUST stay signed (the T1-escape check
370
+ // compares current_r <= -0.5R); the mfe value is a NON-NEGATIVE FLOOR (true
371
+ // peak >= current excursion) — it lets the gate tier the close instead of
372
+ // skipping, at the cost of under-classifying a round-tripped winner as T1
373
+ // (acceptable: errs toward BLOCK in enforce). A position with no frozen
374
+ // stop (no originalStopPrice, no bracket) still legitimately skips.
375
+ // See docs/EXIT_DISCIPLINE_BUILD_PLAN.md Step 0b.
376
+ const denomStop = typeof pos.originalStopPrice === 'number' && Number.isFinite(pos.originalStopPrice)
377
+ ? pos.originalStopPrice
378
+ : inputs.stopPrice;
379
+ if (inputs.side
380
+ && typeof pos.entryPrice === 'number' && Number.isFinite(pos.entryPrice)
381
+ && typeof pos.markPrice === 'number' && Number.isFinite(pos.markPrice)
382
+ && typeof denomStop === 'number' && Number.isFinite(denomStop)) {
383
+ const denom = Math.abs(pos.entryPrice - denomStop);
384
+ if (denom > 0) {
385
+ const fav = inputs.side === 'long'
386
+ ? pos.markPrice - pos.entryPrice
387
+ : pos.entryPrice - pos.markPrice;
388
+ if (inputs.current_r === undefined) {
389
+ inputs.current_r = fav / denom; // signed
390
+ inputs.current_r_src = 'computed';
391
+ }
392
+ if (inputs.mfe_r_peak === undefined) {
393
+ inputs.mfe_r_peak = Math.max(0, fav / denom); // non-negative floor
394
+ inputs.mfe_src = 'computed_floor';
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ catch (err) {
401
+ logger.warn(TAG, `exit_gate input fetch (positions): ${formatError(err)}`);
402
+ }
403
+ // Last review — from state store, for contradicts-own-review + review override.
404
+ try {
405
+ const entry = deps.autoCapture?.stateStore?.get(args.symbol);
406
+ if (entry?.lastVerdict && typeof entry.lastReviewAt === 'number') {
407
+ inputs.lastReview = {
408
+ verdict: entry.lastVerdict,
409
+ thesis_status: entry.lastThesisStatus ?? 'intact',
410
+ review_at_ms: entry.lastReviewAt,
411
+ };
412
+ }
413
+ }
414
+ catch (err) {
415
+ logger.warn(TAG, `exit_gate input fetch (state store): ${formatError(err)}`);
416
+ }
417
+ return inputs;
418
+ }
419
+ /** Test seam — clears the circuit-breaker state between unit tests. */
420
+ export const __testing__ = {
421
+ resetCircuitBreaker() {
422
+ bracketIntegrityCloses = [];
423
+ },
424
+ /** Inspect current circuit-breaker state — for assertions. */
425
+ getCircuitBreakerCount() {
426
+ const windowStart = Date.now() - BRACKET_INTEGRITY_WINDOW_MS;
427
+ return bracketIntegrityCloses.filter(ts => ts >= windowStart).length;
428
+ },
429
+ };
430
+ function formatAssessmentSummary(a) {
431
+ if (!a)
432
+ return '';
433
+ const parts = [];
434
+ if (typeof a.entry_score === 'number')
435
+ parts.push(`entry=${a.entry_score}`);
436
+ if (typeof a.current_score === 'number')
437
+ parts.push(`current=${a.current_score}`);
438
+ if (a.entry_regime)
439
+ parts.push(`er=${a.entry_regime}`);
440
+ if (a.current_regime)
441
+ parts.push(`cr=${a.current_regime}`);
442
+ if (a.contradicting_metric)
443
+ parts.push(`metric="${a.contradicting_metric.slice(0, 80)}"`);
444
+ if (typeof a.position_age_seconds === 'number')
445
+ parts.push(`age=${a.position_age_seconds}s`);
446
+ if (typeof a.r_multiple_at_close === 'number')
447
+ parts.push(`R=${a.r_multiple_at_close}`);
448
+ return parts.join(' ');
449
+ }
@@ -0,0 +1,54 @@
1
+ import type { BinancePublicApi } from '../ccxt/binance-public.js';
2
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
3
+ import type { CcxtOrder } from '../types.js';
4
+ import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
5
+ import type { ProposalManager } from '../live/proposal-manager.js';
6
+ export declare function createOrderTool(args: {
7
+ symbol: string;
8
+ side: string;
9
+ type: string;
10
+ amount: number;
11
+ price?: number;
12
+ stopPrice?: number;
13
+ setup_type?: string;
14
+ thesis?: string;
15
+ target_price?: number;
16
+ regime?: string;
17
+ regime_confidence?: number;
18
+ scorecard_verdict?: string;
19
+ confluence_score?: number;
20
+ invalidation_price?: number;
21
+ realization_rule?: unknown;
22
+ supersedes_id?: string;
23
+ }, deps: {
24
+ binanceApi: BinancePublicApi;
25
+ adapter: IExchangeAdapter;
26
+ autoCapture?: AutoCaptureContext;
27
+ /** Approval-mode wiring. The same ProposalManager handles both modes;
28
+ * `approvalMode` selects the branch:
29
+ *
30
+ * 'off' — fire normally. proposalManager is ignored.
31
+ * 'shadow' — Phase A telemetry. Dual-write proposal row AND fire
32
+ * the real order. Used to collect setup distribution
33
+ * before flipping per_trade. See APPROVAL_MODE_DESIGN.md §12.
34
+ * 'per_trade' — THE feature. Propose only — DO NOT fire. Return
35
+ * pending_approval; the operator approves on the dashboard
36
+ * and ProposalDecisionListener fires via this same tool
37
+ * with proposalManager omitted (which goes through the
38
+ * normal real-fire path).
39
+ *
40
+ * The listener-side fire is the one path that calls createOrderTool
41
+ * WITHOUT proposalManager — it bypasses the propose branch and runs
42
+ * the real adapter.createOrder. */
43
+ proposalManager?: ProposalManager;
44
+ userId?: string;
45
+ approvalMode?: 'off' | 'shadow' | 'per_trade';
46
+ }): Promise<CcxtOrder | {
47
+ error: string;
48
+ } | {
49
+ status: 'pending_approval';
50
+ proposal_id: string;
51
+ hard_expires_at: string;
52
+ setup_bucket: 'momentum' | 'mean_reversion';
53
+ message: string;
54
+ }>;