@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,478 @@
1
+ // BracketManager — orchestrates the lifecycle of exchange-side bracket orders.
2
+ //
3
+ // Separation of concerns:
4
+ // - LiveAdapter owns the ENTRY order (submit, wait for fill).
5
+ // - BracketManager owns the SL/TP orders (attach, modify, cancel).
6
+ // - BracketLedger owns persistent state (survives restart).
7
+ //
8
+ // Retry policy (locked decision #2):
9
+ // - 3 attempts over ~5s total
10
+ // - On any leg failure, cancel the successful sibling from that attempt
11
+ // before retrying, so the exchange state never runs ahead of the ledger.
12
+ // - After exhausting retries, transitions ledger to 'failed' and emits a
13
+ // critical event. LiveAdapter listens for that event and triggers the
14
+ // auto-flatten (the manager itself does not close positions).
15
+ //
16
+ // Modes:
17
+ // - Both SL and TP provided → target state 'active'
18
+ // - Only one provided → target state 'partial' (matches requireStopLoss /
19
+ // requireTakeProfit toggles being independently flippable)
20
+ // - Neither provided → Manager.registerEntry rejects; LiveAdapter
21
+ // should skip bracket registration entirely in that case.
22
+ import { EventEmitter } from 'node:events';
23
+ import { logger, formatError } from '../logger.js';
24
+ import { buildStopLossParams, buildTakeProfitParams } from './bracket-params.js';
25
+ import { assertNotShuttingDown, trackOp } from '../lifecycle/shutdown-coordinator.js';
26
+ const TAG = 'bracket-manager';
27
+ const DEFAULT_MAX_ATTEMPTS = 3;
28
+ const DEFAULT_BACKOFF = (attempt) => attempt === 1 ? 1_000 : 2_000;
29
+ const DEFAULT_SLA_MS = 3_000;
30
+ const TERMINAL_STATES = new Set([
31
+ 'triggered_sl',
32
+ 'triggered_tp',
33
+ 'cancelled',
34
+ 'failed',
35
+ ]);
36
+ function isTerminalState(state) {
37
+ return TERMINAL_STATES.has(state);
38
+ }
39
+ export class BracketManager extends EventEmitter {
40
+ api;
41
+ ledger;
42
+ maxAttempts;
43
+ backoff;
44
+ slaMs;
45
+ now;
46
+ sleep;
47
+ constructor(api, ledger, opts = {}) {
48
+ super();
49
+ this.api = api;
50
+ this.ledger = ledger;
51
+ this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
52
+ this.backoff = opts.retryBackoffMs ?? DEFAULT_BACKOFF;
53
+ this.slaMs = opts.slaMs ?? DEFAULT_SLA_MS;
54
+ this.now = opts.now ?? Date.now;
55
+ this.sleep = opts.sleep ?? ((ms) => new Promise(r => setTimeout(r, ms)));
56
+ }
57
+ /** Called by LiveAdapter after submitting the entry but before fill.
58
+ * Creates the ledger row in 'pending_entry' state.
59
+ *
60
+ * Ledger hygiene: if a terminal-state row exists for this symbol (from a
61
+ * prior trade cycle that closed, got cancelled, or had its brackets
62
+ * stripped), remove it before inserting the new pending_entry. Otherwise
63
+ * the stale row's bracketId/slCid/tpCid would survive the upsert and
64
+ * mislead later callers — most visibly, `modify_stop` / `modify_target`
65
+ * would see `state: 'cancelled'` and refuse to operate even though a fresh
66
+ * bracket is now active. */
67
+ registerEntry(req, bracketId, entryCid) {
68
+ if (req.stopPrice === undefined && req.targetPrice === undefined) {
69
+ throw new Error('registerEntry: at least one of stopPrice or targetPrice is required');
70
+ }
71
+ const existing = this.ledger.getBySymbol(req.symbol);
72
+ // ANTI-CLOBBER ROOT FIX (2026-05-16 SOL/INJ spiral disease). A
73
+ // NON-terminal row (pending_entry / attaching / active / partial) is a
74
+ // live or in-flight bracket for the CURRENT position cycle, holding the
75
+ // bracketId + slCid/tpCid that match the orders live on the exchange.
76
+ // A single entry produces MULTIPLE fill signals — the real WS
77
+ // ORDER_TRADE_UPDATE, the create_order confirmation, the (stale-skill)
78
+ // position-poll SYNTHETIC fill, and any agent attach_brackets — each of
79
+ // which can re-enter registerEntry. The original code then upsert'd
80
+ // state:'pending_entry' with a FRESH bracketId unconditionally, wiping
81
+ // the active bracket's identity from the ledger. The ledger row no
82
+ // longer matched the live exchange orders, so the orphan sweep saw them
83
+ // as unmatched (→ cancel), classify saw stale cids (→ reattach loop),
84
+ // and audit went unverifiable — the shared root of every bracket spiral
85
+ // this session. Making a duplicate registerEntry a no-op keeps the
86
+ // ledger row matching the live orders. attach_brackets terminalises the
87
+ // row (markState 'cancelled') BEFORE its registerEntry, so its legitimate
88
+ // re-attach is unaffected (existing is terminal → removed below). A new
89
+ // entry on a symbol that still has a non-terminal row is a scale-in/
90
+ // error whose bracket must be adjusted via modify_*, never by silently
91
+ // resetting + re-attaching (that IS the double-bracket spiral).
92
+ if (existing && !isTerminalState(existing.state)) {
93
+ logger.warn(TAG, `registerEntry(${req.symbol}): a non-terminal bracket row already ` +
94
+ `exists (state=${existing.state}, bracketId=${existing.bracketId}, ` +
95
+ `slCid=${existing.slCid ?? '-'}, tpCid=${existing.tpCid ?? '-'}). ` +
96
+ `Treating as a duplicate fill signal — NOT clobbering the live ` +
97
+ `bracket's ledger identity (anti-clobber root fix). Use modify_* ` +
98
+ `to change levels.`);
99
+ return;
100
+ }
101
+ if (existing && isTerminalState(existing.state)) {
102
+ this.ledger.remove(req.symbol);
103
+ }
104
+ this.ledger.upsert({
105
+ bracketId,
106
+ symbol: req.symbol,
107
+ entrySide: req.side,
108
+ entryCid,
109
+ stopPrice: req.stopPrice,
110
+ targetPrice: req.targetPrice,
111
+ state: 'pending_entry',
112
+ });
113
+ this.emitEvent({
114
+ type: 'bracket.entry_submitted',
115
+ bracketId,
116
+ symbol: req.symbol,
117
+ ts: new Date(this.now()).toISOString(),
118
+ });
119
+ }
120
+ /** Called by LiveAdapter when the entry fills. Attaches SL + TP to the
121
+ * exchange with retries. Idempotent — safe to call exactly once per fill.
122
+ *
123
+ * Intentionally NOT gated by `assertNotShuttingDown`: by the time this
124
+ * runs, the entry has already filled and the position is open on Binance.
125
+ * Refusing here would leave a naked position — the drain budget is sized
126
+ * to cover the 3-attempt retry loop instead. New ENTRIES are gated at
127
+ * `LiveAdapter.createOrder`, so once we get here the entry was authorised
128
+ * before the shutdown flag was set. */
129
+ async attachBrackets(symbol, entryAmount) {
130
+ const entry = this.ledger.getBySymbol(symbol);
131
+ if (!entry)
132
+ throw new Error(`attachBrackets: no ledger entry for ${symbol}`);
133
+ if (entry.state !== 'pending_entry') {
134
+ // Idempotency: if already attaching/active, return current status.
135
+ return {
136
+ ok: entry.state === 'active' || entry.state === 'partial',
137
+ latencyMs: entry.attachLatencyMs ?? 0,
138
+ slCid: entry.slCid,
139
+ tpCid: entry.tpCid,
140
+ attempts: 0,
141
+ };
142
+ }
143
+ return trackOp(`attachBrackets(${symbol})`, async () => {
144
+ const startTs = this.now();
145
+ const startedAtIso = new Date(startTs).toISOString();
146
+ this.ledger.markState(symbol, 'attaching', { attachStartedAt: startedAtIso });
147
+ this.emitEvent({
148
+ type: 'bracket.entry_filled',
149
+ bracketId: entry.bracketId,
150
+ symbol,
151
+ ts: startedAtIso,
152
+ });
153
+ const req = {
154
+ symbol,
155
+ side: entry.entrySide,
156
+ stopPrice: entry.stopPrice,
157
+ targetPrice: entry.targetPrice,
158
+ };
159
+ let lastError;
160
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
161
+ const result = await this.attemptAttach(req, entry.bracketId, entryAmount);
162
+ if (result.ok) {
163
+ const latencyMs = this.now() - startTs;
164
+ const targetState = (result.slCid && result.tpCid)
165
+ ? 'active'
166
+ : 'partial';
167
+ this.ledger.markState(symbol, targetState, {
168
+ attachCompletedAt: new Date(this.now()).toISOString(),
169
+ attachLatencyMs: latencyMs,
170
+ slCid: result.slCid,
171
+ tpCid: result.tpCid,
172
+ // Freeze the size the legs were submitted with so a later
173
+ // modifyStop/modifyTarget rebuilds the leg at the SAME size
174
+ // instead of a placeholder (the 1-contract under-protect bug).
175
+ qty: entryAmount,
176
+ });
177
+ this.emitEvent({
178
+ type: 'bracket.attached',
179
+ bracketId: entry.bracketId,
180
+ symbol,
181
+ latencyMs,
182
+ ts: new Date(this.now()).toISOString(),
183
+ });
184
+ if (latencyMs > this.slaMs) {
185
+ this.emitEvent({
186
+ type: 'bracket.sla_breach',
187
+ bracketId: entry.bracketId,
188
+ symbol,
189
+ latencyMs,
190
+ ts: new Date(this.now()).toISOString(),
191
+ });
192
+ }
193
+ return { ok: true, latencyMs, slCid: result.slCid, tpCid: result.tpCid, attempts: attempt };
194
+ }
195
+ lastError = result.error;
196
+ this.emitEvent({
197
+ type: 'bracket.attach_failed',
198
+ bracketId: entry.bracketId,
199
+ symbol,
200
+ error: lastError ?? 'unknown',
201
+ retryCount: attempt,
202
+ ts: new Date(this.now()).toISOString(),
203
+ });
204
+ // Clean up any partial attach (one leg succeeded, the other didn't)
205
+ await this.cancelPartial(symbol, result.slCid, result.tpCid);
206
+ if (attempt < this.maxAttempts) {
207
+ await this.sleep(this.backoff(attempt));
208
+ }
209
+ }
210
+ const latencyMs = this.now() - startTs;
211
+ this.ledger.markState(symbol, 'failed', {
212
+ closeReason: 'attach_failed',
213
+ lastError,
214
+ });
215
+ return { ok: false, latencyMs, attempts: this.maxAttempts, error: lastError };
216
+ });
217
+ }
218
+ /** Move the stop. Cancel-then-resubmit; rolls back to the old stop if the
219
+ * new submission fails, so the position is never left unprotected. */
220
+ async modifyStop(symbol, newStopPrice) {
221
+ // Refuse during shutdown — cancel-then-resubmit is the worst race window
222
+ // (brief naked position between cancel ack and new SL submission).
223
+ assertNotShuttingDown(`modifyStop(${symbol})`);
224
+ if (!Number.isFinite(newStopPrice) || newStopPrice <= 0) {
225
+ throw new Error(`modifyStop: newStopPrice must be positive finite, got ${newStopPrice}`);
226
+ }
227
+ const entry = this.ledger.getBySymbol(symbol);
228
+ if (!entry)
229
+ throw new Error(`modifyStop: no ledger entry for ${symbol}`);
230
+ if (entry.state !== 'active' && entry.state !== 'partial') {
231
+ throw new Error(`modifyStop: cannot modify stop in state ${entry.state}`);
232
+ }
233
+ // Resolve the size the bracket was attached with. This MUST happen before
234
+ // the old SL is cancelled below: if we cancelled first and then couldn't
235
+ // size the replacement, the position would be left naked. Throwing here
236
+ // (old bracket still live) is strictly safer than rebuilding the leg at a
237
+ // placeholder size, which would silently under-protect the position.
238
+ const qty = entry.qty;
239
+ if (qty === undefined || !Number.isFinite(qty) || qty <= 0) {
240
+ throw new Error(`modifyStop: ledger has no valid bracket qty for ${symbol} (qty=${qty}); ` +
241
+ `refusing to resize the stop to a placeholder size — re-attach the bracket first`);
242
+ }
243
+ return trackOp(`modifyStop(${symbol})`, async () => {
244
+ const oldStopPrice = entry.stopPrice;
245
+ const oldSlCid = entry.slCid;
246
+ // Cancel old stop (if present)
247
+ if (oldSlCid) {
248
+ try {
249
+ await this.api.cancelOrderByClientId(oldSlCid, symbol);
250
+ }
251
+ catch (err) {
252
+ throw new Error(`modifyStop: could not cancel old SL: ${formatError(err)}`);
253
+ }
254
+ }
255
+ // Submit new stop
256
+ const leg = buildStopLossParams({ symbol, side: entry.entrySide, stopPrice: newStopPrice }, entry.bracketId, qty);
257
+ try {
258
+ await this.api.submitBracketLeg(leg);
259
+ }
260
+ catch (err) {
261
+ // Rollback: put the old stop back
262
+ if (oldStopPrice !== undefined && oldSlCid) {
263
+ const rollbackLeg = buildStopLossParams({ symbol, side: entry.entrySide, stopPrice: oldStopPrice }, entry.bracketId, qty);
264
+ try {
265
+ await this.api.submitBracketLeg(rollbackLeg);
266
+ }
267
+ catch (rbErr) {
268
+ logger.error(TAG, `modifyStop rollback ALSO failed for ${symbol}: ${formatError(rbErr)}`);
269
+ // Position is now unprotected on the stop side — emit critical event
270
+ this.emitEvent({
271
+ type: 'bracket.attach_failed',
272
+ bracketId: entry.bracketId,
273
+ symbol,
274
+ error: `rollback failed: ${formatError(rbErr)}`,
275
+ retryCount: 0,
276
+ ts: new Date(this.now()).toISOString(),
277
+ });
278
+ }
279
+ }
280
+ throw new Error(`modifyStop: new SL submission failed: ${formatError(err)}`);
281
+ }
282
+ // Success — persist new stop. Same reasoning as attemptAttach: store the
283
+ // client id we chose, not the exchange-assigned id.
284
+ this.ledger.upsert({
285
+ ...entry,
286
+ stopPrice: newStopPrice,
287
+ slCid: leg.params.newClientOrderId,
288
+ });
289
+ this.emitEvent({
290
+ type: 'bracket.modified',
291
+ bracketId: entry.bracketId,
292
+ symbol,
293
+ field: 'stop',
294
+ oldValue: oldStopPrice ?? NaN,
295
+ newValue: newStopPrice,
296
+ ts: new Date(this.now()).toISOString(),
297
+ });
298
+ });
299
+ }
300
+ /** Move the take-profit. Same semantics as modifyStop. */
301
+ async modifyTarget(symbol, newTargetPrice) {
302
+ assertNotShuttingDown(`modifyTarget(${symbol})`);
303
+ if (!Number.isFinite(newTargetPrice) || newTargetPrice <= 0) {
304
+ throw new Error(`modifyTarget: newTargetPrice must be positive finite, got ${newTargetPrice}`);
305
+ }
306
+ const entry = this.ledger.getBySymbol(symbol);
307
+ if (!entry)
308
+ throw new Error(`modifyTarget: no ledger entry for ${symbol}`);
309
+ if (entry.state !== 'active' && entry.state !== 'partial') {
310
+ throw new Error(`modifyTarget: cannot modify target in state ${entry.state}`);
311
+ }
312
+ // See modifyStop: resolve size before cancelling the old TP so we never
313
+ // rebuild the leg at a placeholder size (the 1-contract under-protect bug).
314
+ const qty = entry.qty;
315
+ if (qty === undefined || !Number.isFinite(qty) || qty <= 0) {
316
+ throw new Error(`modifyTarget: ledger has no valid bracket qty for ${symbol} (qty=${qty}); ` +
317
+ `refusing to resize the target to a placeholder size — re-attach the bracket first`);
318
+ }
319
+ return trackOp(`modifyTarget(${symbol})`, async () => {
320
+ const oldTargetPrice = entry.targetPrice;
321
+ const oldTpCid = entry.tpCid;
322
+ if (oldTpCid) {
323
+ try {
324
+ await this.api.cancelOrderByClientId(oldTpCid, symbol);
325
+ }
326
+ catch (err) {
327
+ throw new Error(`modifyTarget: could not cancel old TP: ${formatError(err)}`);
328
+ }
329
+ }
330
+ const leg = buildTakeProfitParams({ symbol, side: entry.entrySide, targetPrice: newTargetPrice }, entry.bracketId, qty);
331
+ try {
332
+ await this.api.submitBracketLeg(leg);
333
+ }
334
+ catch (err) {
335
+ if (oldTargetPrice !== undefined && oldTpCid) {
336
+ const rollbackLeg = buildTakeProfitParams({ symbol, side: entry.entrySide, targetPrice: oldTargetPrice }, entry.bracketId, qty);
337
+ try {
338
+ await this.api.submitBracketLeg(rollbackLeg);
339
+ }
340
+ catch (rbErr) {
341
+ logger.error(TAG, `modifyTarget rollback ALSO failed for ${symbol}: ${formatError(rbErr)}`);
342
+ }
343
+ }
344
+ throw new Error(`modifyTarget: new TP submission failed: ${formatError(err)}`);
345
+ }
346
+ this.ledger.upsert({
347
+ ...entry,
348
+ targetPrice: newTargetPrice,
349
+ tpCid: leg.params.newClientOrderId,
350
+ });
351
+ this.emitEvent({
352
+ type: 'bracket.modified',
353
+ bracketId: entry.bracketId,
354
+ symbol,
355
+ field: 'target',
356
+ oldValue: oldTargetPrice ?? NaN,
357
+ newValue: newTargetPrice,
358
+ ts: new Date(this.now()).toISOString(),
359
+ });
360
+ });
361
+ }
362
+ /** Cancel both bracket legs and mark the ledger cancelled. Called when
363
+ * LiveAdapter closes the position via `close_position` or flatten. Risk-
364
+ * reducing — allowed during shutdown, but tracked so the drain awaits it. */
365
+ async cancelBrackets(symbol, reason) {
366
+ const entry = this.ledger.getBySymbol(symbol);
367
+ if (!entry)
368
+ return; // nothing to cancel
369
+ if (entry.slCid) {
370
+ try {
371
+ await this.api.cancelOrderByClientId(entry.slCid, symbol);
372
+ }
373
+ catch (err) {
374
+ logger.warn(TAG, `cancelBrackets SL ${symbol}: ${formatError(err)}`);
375
+ }
376
+ }
377
+ if (entry.tpCid) {
378
+ try {
379
+ await this.api.cancelOrderByClientId(entry.tpCid, symbol);
380
+ }
381
+ catch (err) {
382
+ logger.warn(TAG, `cancelBrackets TP ${symbol}: ${formatError(err)}`);
383
+ }
384
+ }
385
+ this.ledger.markState(symbol, 'cancelled', { closeReason: 'cancelled_manual' });
386
+ this.emitEvent({
387
+ type: 'bracket.cancelled',
388
+ bracketId: entry.bracketId,
389
+ symbol,
390
+ reason,
391
+ ts: new Date(this.now()).toISOString(),
392
+ });
393
+ }
394
+ /** Called by the reconciler / fill poller when one of the bracket legs
395
+ * fires on the exchange. Transitions the ledger to the matching triggered
396
+ * state and emits. Binance's closePosition:true auto-cancels the sibling. */
397
+ handleBracketTriggered(symbol, triggeredRole) {
398
+ const entry = this.ledger.getBySymbol(symbol);
399
+ if (!entry)
400
+ return;
401
+ const nextState = triggeredRole === 'sl' ? 'triggered_sl' : 'triggered_tp';
402
+ this.ledger.markState(symbol, nextState, {
403
+ closeReason: triggeredRole === 'sl' ? 'triggered_sl' : 'triggered_tp',
404
+ });
405
+ this.emitEvent({
406
+ type: 'bracket.triggered',
407
+ bracketId: entry.bracketId,
408
+ symbol,
409
+ side: triggeredRole,
410
+ ts: new Date(this.now()).toISOString(),
411
+ });
412
+ }
413
+ // ---- Internals ----
414
+ async attemptAttach(req, bracketId, entryAmount) {
415
+ let slCid;
416
+ let tpCid;
417
+ // ROOT-CAUSE FIX 2026-05-16: neutralise any externally-armed
418
+ // countdownCancelAll dead-man's-switch for this symbol BEFORE placing
419
+ // protection, so Binance can't purge the legs ~tens of seconds later
420
+ // (the proven cause of the months-long naked-position spiral). Purely
421
+ // protective + best-effort: the method swallows its own errors and never
422
+ // throws here — a failed disable must never block bracket placement.
423
+ if (this.api.disableCountdownCancelAll) {
424
+ try {
425
+ await this.api.disableCountdownCancelAll(req.symbol);
426
+ }
427
+ catch (err) {
428
+ logger.warn(TAG, `disableCountdownCancelAll(${req.symbol}) threw (ignored): ${formatError(err)}`);
429
+ }
430
+ }
431
+ if (req.stopPrice !== undefined) {
432
+ const leg = buildStopLossParams(req, bracketId, entryAmount);
433
+ try {
434
+ await this.api.submitBracketLeg(leg);
435
+ // Store the CLIENT order id (what we sent), not the exchange-assigned
436
+ // id. The exchange id lives in a different namespace for algo orders
437
+ // (algoId) vs regular orders; cancelOrderByClientId expects the cid
438
+ // we chose, which is deterministic and survives an adapter restart.
439
+ slCid = leg.params.newClientOrderId;
440
+ }
441
+ catch (err) {
442
+ return { ok: false, error: `SL: ${formatError(err)}` };
443
+ }
444
+ }
445
+ if (req.targetPrice !== undefined) {
446
+ const leg = buildTakeProfitParams(req, bracketId, entryAmount);
447
+ try {
448
+ await this.api.submitBracketLeg(leg);
449
+ tpCid = leg.params.newClientOrderId;
450
+ }
451
+ catch (err) {
452
+ return { ok: false, slCid, error: `TP: ${formatError(err)}` };
453
+ }
454
+ }
455
+ return { ok: true, slCid, tpCid };
456
+ }
457
+ async cancelPartial(symbol, slCid, tpCid) {
458
+ if (slCid) {
459
+ try {
460
+ await this.api.cancelOrderByClientId(slCid, symbol);
461
+ }
462
+ catch (err) {
463
+ logger.warn(TAG, `cancelPartial SL ${symbol}: ${formatError(err)}`);
464
+ }
465
+ }
466
+ if (tpCid) {
467
+ try {
468
+ await this.api.cancelOrderByClientId(tpCid, symbol);
469
+ }
470
+ catch (err) {
471
+ logger.warn(TAG, `cancelPartial TP ${symbol}: ${formatError(err)}`);
472
+ }
473
+ }
474
+ }
475
+ emitEvent(event) {
476
+ this.emit('event', event);
477
+ }
478
+ }
@@ -0,0 +1,22 @@
1
+ import type { BracketId, BracketRequest } from './bracket-types.js';
2
+ /** One CCXT-shaped leg ready to hand to `exchange.createOrder(...)`. */
3
+ export interface BracketLegParams {
4
+ symbol: string;
5
+ type: 'STOP_MARKET' | 'TAKE_PROFIT_MARKET';
6
+ side: 'buy' | 'sell';
7
+ amount: number;
8
+ price: undefined;
9
+ params: Record<string, any>;
10
+ }
11
+ /** Return the side that CLOSES a position opened with `entrySide`. */
12
+ export declare function closingSide(entrySide: 'buy' | 'sell'): 'buy' | 'sell';
13
+ /** Build the stop-loss leg. Throws if stopPrice is missing. */
14
+ export declare function buildStopLossParams(req: BracketRequest, bracketId: BracketId, entryAmount: number): BracketLegParams;
15
+ /** Build the take-profit leg. Throws if targetPrice is missing. */
16
+ export declare function buildTakeProfitParams(req: BracketRequest, bracketId: BracketId, entryAmount: number): BracketLegParams;
17
+ /** Sanity check that a stopPrice is on the correct side of entry — catches
18
+ * off-by-flipped-sign errors before Binance rejects with -2021 "would
19
+ * immediately trigger." Returns null if OK, error message otherwise. */
20
+ export declare function validateStopDirection(entrySide: 'buy' | 'sell', entryPrice: number, stopPrice: number): string | null;
21
+ /** Same sanity check for take-profit: must be on the winning side of entry. */
22
+ export declare function validateTargetDirection(entrySide: 'buy' | 'sell', entryPrice: number, targetPrice: number): string | null;
@@ -0,0 +1,124 @@
1
+ // CCXT parameter builder for bracket legs (STOP_MARKET + TAKE_PROFIT_MARKET).
2
+ //
3
+ // One-way mode only (per locked design decision #1). Hedge-mode support can
4
+ // be added later via positionSide — but see §2 of BRACKET_ORDERS_DESIGN.md.
5
+ //
6
+ // Order-flag semantics — `closePosition: true` (position-bound), 2026-05-16.
7
+ //
8
+ // ROOT-CAUSE FIX. We previously used `reduceOnly: true` + explicit `amount`.
9
+ // On 2025-12-09 Binance migrated ALL USD-M conditional orders to the new
10
+ // "Algo Service" (changelog 2025-11-06 / 2025-12-09; classic /fapi/v1/order
11
+ // now rejects them with -4120 → forced onto POST /fapi/v1/algoOrder). The
12
+ // new Algo Service AUTO-CANCELS standalone `reduceOnly`+`quantity`
13
+ // conditional algo orders ~17-53s after placement WHILE THE POSITION IS
14
+ // STILL OPEN (prod-proven 2026-05-15/16 SOL+APT: every prior fix targeted
15
+ // ReefClaw bracket logic — the wrong layer; an airtight-verified
16
+ // `logAlgoCancelChokepoint` tracer recorded ZERO of our cancel calls while
17
+ // Binance reported the leg CANCELED). Binance does NOT document this; it is
18
+ // a silent post-migration behaviour change.
19
+ //
20
+ // `closePosition: true` is the POSITION-BOUND form: Binance ties the order's
21
+ // lifecycle to the position and does NOT auto-cancel it while the position
22
+ // is open. Independently proven: the operator's manually-set Binance-app
23
+ // position TP/SL is exactly this form and it sticks when our reduceOnly one
24
+ // vanishes. Doc (New-Algo-Order): closePosition "close all current
25
+ // long/short position if triggered"; MUST NOT be sent with `quantity`
26
+ // (-4137) or `reduceOnly` (-4138) → `amount` is 0 and neither flag is set.
27
+ //
28
+ // -4130 reassessed: the old comment said a second closePosition order is
29
+ // rejected -4130 ("stop/TP with GTE and closePosition in the direction
30
+ // exists"). That finding "after testnet validation" PREDATES the 2025-12-09
31
+ // algo migration; -4130 is no longer in the current USD-M error-code doc;
32
+ // and the Binance app itself runs a coexisting closePosition SL+TP on a
33
+ // position (opposite trigger types, the standard exchange-native bracket).
34
+ // The deployed cancel/attribution tracers + the bracket reconciler surface
35
+ // any -4130 immediately and loudly, so a recurrence is an observable fast
36
+ // follow-up (asymmetric SL-closePosition / TP-reduceOnly fallback) rather
37
+ // than a silent naked position.
38
+ //
39
+ // closePosition also SIMPLIFIES the model: first trigger closes the whole
40
+ // position; the sibling is auto-cancelled by Binance on position close
41
+ // (EXPIRED, benign); no quantity/partial-fill bracket-resize logic needed.
42
+ //
43
+ // workingType:
44
+ // - CONTRACT_PRICE (Binance default): triggers on last traded price of the
45
+ // pair. Matches what users typically mean by "stop price."
46
+ // - MARK_PRICE: triggers on the mark price (index-derived). More robust to
47
+ // single-pair manipulation but can lag fast moves. Not used by default.
48
+ import { buildBracketCid } from './bracket-id.js';
49
+ /** Return the side that CLOSES a position opened with `entrySide`. */
50
+ export function closingSide(entrySide) {
51
+ return entrySide === 'buy' ? 'sell' : 'buy';
52
+ }
53
+ /** Build the stop-loss leg. Throws if stopPrice is missing. */
54
+ export function buildStopLossParams(req, bracketId, entryAmount) {
55
+ if (req.stopPrice === undefined || !Number.isFinite(req.stopPrice) || req.stopPrice <= 0) {
56
+ throw new Error('buildStopLossParams: stopPrice must be a positive finite number');
57
+ }
58
+ if (!Number.isFinite(entryAmount) || entryAmount <= 0) {
59
+ throw new Error('buildStopLossParams: entryAmount must be a positive finite number');
60
+ }
61
+ return {
62
+ symbol: req.symbol,
63
+ type: 'STOP_MARKET',
64
+ side: closingSide(req.side),
65
+ // closePosition is "close-all" — Binance ignores/forbids a quantity
66
+ // (-4137). amount 0; entryAmount is still validated above so a caller
67
+ // with a bogus size is rejected, and the ledger keeps qty for telemetry.
68
+ amount: 0,
69
+ price: undefined,
70
+ params: {
71
+ stopPrice: req.stopPrice,
72
+ // Position-bound form: Binance keeps it with the position and does NOT
73
+ // auto-cancel it while open (unlike reduceOnly+qty — see file header).
74
+ closePosition: true,
75
+ workingType: 'CONTRACT_PRICE',
76
+ newClientOrderId: buildBracketCid(bracketId, 'stop'),
77
+ },
78
+ };
79
+ }
80
+ /** Build the take-profit leg. Throws if targetPrice is missing. */
81
+ export function buildTakeProfitParams(req, bracketId, entryAmount) {
82
+ if (req.targetPrice === undefined || !Number.isFinite(req.targetPrice) || req.targetPrice <= 0) {
83
+ throw new Error('buildTakeProfitParams: targetPrice must be a positive finite number');
84
+ }
85
+ if (!Number.isFinite(entryAmount) || entryAmount <= 0) {
86
+ throw new Error('buildTakeProfitParams: entryAmount must be a positive finite number');
87
+ }
88
+ return {
89
+ symbol: req.symbol,
90
+ type: 'TAKE_PROFIT_MARKET',
91
+ side: closingSide(req.side),
92
+ amount: 0, // closePosition = close-all; no quantity (-4137)
93
+ price: undefined,
94
+ params: {
95
+ stopPrice: req.targetPrice, // Binance calls the trigger "stopPrice" on TP_MARKET too
96
+ // Position-bound form — see file header (root-cause fix 2026-05-16).
97
+ closePosition: true,
98
+ workingType: 'CONTRACT_PRICE',
99
+ newClientOrderId: buildBracketCid(bracketId, 'target'),
100
+ },
101
+ };
102
+ }
103
+ /** Sanity check that a stopPrice is on the correct side of entry — catches
104
+ * off-by-flipped-sign errors before Binance rejects with -2021 "would
105
+ * immediately trigger." Returns null if OK, error message otherwise. */
106
+ export function validateStopDirection(entrySide, entryPrice, stopPrice) {
107
+ if (entrySide === 'buy' && stopPrice >= entryPrice) {
108
+ return `Stop price ${stopPrice} must be BELOW entry ${entryPrice} for a long position`;
109
+ }
110
+ if (entrySide === 'sell' && stopPrice <= entryPrice) {
111
+ return `Stop price ${stopPrice} must be ABOVE entry ${entryPrice} for a short position`;
112
+ }
113
+ return null;
114
+ }
115
+ /** Same sanity check for take-profit: must be on the winning side of entry. */
116
+ export function validateTargetDirection(entrySide, entryPrice, targetPrice) {
117
+ if (entrySide === 'buy' && targetPrice <= entryPrice) {
118
+ return `Target price ${targetPrice} must be ABOVE entry ${entryPrice} for a long position`;
119
+ }
120
+ if (entrySide === 'sell' && targetPrice >= entryPrice) {
121
+ return `Target price ${targetPrice} must be BELOW entry ${entryPrice} for a short position`;
122
+ }
123
+ return null;
124
+ }