@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.
- package/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- 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
|
+
}
|