@reefclaw/connect 0.1.9 → 0.1.10
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/assets/plugin/ccxt/binance-private.d.ts +21 -0
- package/assets/plugin/ccxt/binance-private.js +132 -22
- package/assets/plugin/config/plugin-config-io.d.ts +6 -0
- package/assets/plugin/config/tool-gate.js +3 -0
- package/assets/plugin/exchange-adapter.d.ts +16 -0
- package/assets/plugin/index.js +551 -54
- package/assets/plugin/lifecycle/trading-operation-lock.d.ts +17 -0
- package/assets/plugin/lifecycle/trading-operation-lock.js +14 -0
- package/assets/plugin/live/bracket-id.d.ts +2 -3
- package/assets/plugin/live/bracket-id.js +22 -9
- package/assets/plugin/live/live-adapter.d.ts +24 -1
- package/assets/plugin/live/live-adapter.js +114 -2
- package/assets/plugin/live/local-signal-service.js +11 -6
- package/assets/plugin/live/local-strategy-evaluator.js +4 -0
- package/assets/plugin/live/proposal-decision-listener.d.ts +6 -0
- package/assets/plugin/live/proposal-decision-listener.js +4 -0
- package/assets/plugin/live/stop-watcher.d.ts +27 -1
- package/assets/plugin/live/stop-watcher.js +59 -2
- package/assets/plugin/onboarding/runtime.d.ts +13 -0
- package/assets/plugin/onboarding/runtime.js +22 -2
- package/assets/plugin/openclaw.plugin.json +1 -0
- package/assets/plugin/portfolio/wave9-admission.d.ts +67 -0
- package/assets/plugin/portfolio/wave9-admission.js +262 -0
- package/assets/plugin/portfolio/wave9-policy.d.ts +36 -0
- package/assets/plugin/portfolio/wave9-policy.js +183 -0
- package/assets/plugin/signals/conditions/registry.js +50 -0
- package/assets/plugin/simulator/exchange-simulator.js +5 -0
- package/assets/plugin/simulator/fill-engine.js +5 -1
- package/assets/plugin/simulator/types.d.ts +6 -1
- package/assets/plugin/strategy/evaluator.d.ts +3 -0
- package/assets/plugin/strategy/evaluator.js +5 -0
- package/assets/plugin/tools/assessment-validation.d.ts +2 -0
- package/assets/plugin/tools/attach-brackets.d.ts +7 -2
- package/assets/plugin/tools/attach-brackets.js +36 -0
- package/assets/plugin/tools/cancel-all-orders.d.ts +2 -0
- package/assets/plugin/tools/cancel-all-orders.js +4 -1
- package/assets/plugin/tools/cancel-order.d.ts +4 -0
- package/assets/plugin/tools/cancel-order.js +49 -3
- package/assets/plugin/tools/close-position.d.ts +23 -0
- package/assets/plugin/tools/close-position.js +286 -13
- package/assets/plugin/tools/create-order.d.ts +28 -0
- package/assets/plugin/tools/create-order.js +1364 -192
- package/assets/plugin/tools/get-analytics.js +2 -2
- package/assets/plugin/tools/get-basis.js +2 -2
- package/assets/plugin/tools/get-cascade-risk.js +2 -2
- package/assets/plugin/tools/get-crypto-metrics.js +14 -4
- package/assets/plugin/tools/get-cvd.js +2 -2
- package/assets/plugin/tools/get-divergences.js +2 -2
- package/assets/plugin/tools/get-funding-context.js +2 -2
- package/assets/plugin/tools/get-liquidation-levels.js +2 -2
- package/assets/plugin/tools/get-liquidation-pulse.js +2 -2
- package/assets/plugin/tools/get-pattern-scan.js +2 -2
- package/assets/plugin/tools/get-regime.js +2 -2
- package/assets/plugin/tools/get-resting-liquidity.js +2 -2
- package/assets/plugin/tools/get-risk-scenario.js +2 -2
- package/assets/plugin/tools/get-session-review.js +2 -2
- package/assets/plugin/tools/get-setup-detail.js +9 -1
- package/assets/plugin/tools/get-signals.js +2 -2
- package/assets/plugin/tools/get-sizing.js +2 -2
- package/assets/plugin/tools/get-trade-feedback.js +2 -2
- package/assets/plugin/tools/get-trade-flow.js +2 -2
- package/assets/plugin/tools/get-volume-profile.js +2 -2
- package/assets/plugin/tools/get-wave9-status.d.ts +127 -0
- package/assets/plugin/tools/get-wave9-status.js +796 -0
- package/assets/plugin/tools/intel-api.d.ts +20 -0
- package/assets/plugin/tools/intel-api.js +67 -0
- package/assets/plugin/tools/intel-cache.d.ts +1 -1
- package/assets/plugin/tools/intel-cache.js +20 -5
- package/assets/plugin/tools/list-strategies.d.ts +11 -1
- package/assets/plugin/tools/list-strategies.js +17 -0
- package/assets/plugin/tools/modify-stop.d.ts +4 -0
- package/assets/plugin/tools/modify-stop.js +63 -24
- package/assets/plugin/tools/modify-target.d.ts +4 -0
- package/assets/plugin/tools/modify-target.js +62 -23
- package/assets/plugin/tools/scan-pairs.js +19 -8
- package/assets/plugin/tools/toggle-strategy.js +7 -0
- package/assets/plugin/types.d.ts +5 -0
- package/assets/plugin/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/assets/plugin/venues/hyperliquid/hl-balance.js +145 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.js +172 -0
- package/assets/plugin/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/assets/plugin/venues/hyperliquid/hl-cloid.js +82 -0
- package/assets/plugin/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/assets/plugin/venues/hyperliquid/hl-info-cache.js +125 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +353 -0
- package/assets/plugin/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/assets/plugin/venues/hyperliquid/hl-precision.js +176 -0
- package/assets/plugin/venues/hyperliquid/hl-private.d.ts +88 -0
- package/assets/plugin/venues/hyperliquid/hl-private.js +357 -0
- package/assets/plugin/venues/hyperliquid/hl-public.d.ts +31 -4
- package/assets/plugin/venues/hyperliquid/hl-public.js +155 -11
- package/assets/plugin/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/assets/plugin/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/assets/plugin/venues/hyperliquid/hl-user-stream.js +220 -0
- package/assets/plugin/venues/registry.d.ts +23 -9
- package/assets/plugin/venues/registry.js +12 -13
- package/assets/plugin/venues/symbols.d.ts +43 -0
- package/assets/plugin/venues/symbols.js +107 -0
- package/assets/plugin/wave9/live-account-capture.d.ts +67 -0
- package/assets/plugin/wave9/live-account-capture.js +435 -0
- package/assets/plugin/wave9/live-autonomous-protection.d.ts +39 -0
- package/assets/plugin/wave9/live-autonomous-protection.js +112 -0
- package/assets/plugin/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/assets/plugin/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/assets/plugin/wave9/live-execution-ledger.d.ts +107 -0
- package/assets/plugin/wave9/live-execution-ledger.js +498 -0
- package/assets/plugin/wave9/live-position-confirmation.d.ts +18 -0
- package/assets/plugin/wave9/live-position-confirmation.js +111 -0
- package/assets/plugin/wave9/live-residual-protection.d.ts +18 -0
- package/assets/plugin/wave9/live-residual-protection.js +250 -0
- package/assets/plugin/wave9/live-startup-reconciliation.d.ts +38 -0
- package/assets/plugin/wave9/live-startup-reconciliation.js +454 -0
- package/assets/plugin/wave9/live-symbol-ownership.d.ts +20 -0
- package/assets/plugin/wave9/live-symbol-ownership.js +132 -0
- package/assets/plugin/wave9/paper-admission-guard.d.ts +199 -0
- package/assets/plugin/wave9/paper-admission-guard.js +650 -0
- package/assets/plugin/wave9/usdm-evidence-provider.d.ts +42 -0
- package/assets/plugin/wave9/usdm-evidence-provider.js +133 -0
- package/assets/shared/portfolio/wave9-admission.d.ts +67 -0
- package/assets/shared/portfolio/wave9-admission.js +260 -0
- package/assets/shared/portfolio/wave9-policy.d.ts +36 -0
- package/assets/shared/portfolio/wave9-policy.js +176 -0
- package/assets/shared/signals/conditions/registry.js +50 -0
- package/assets/shared/signals/indicators-extended.d.ts +52 -0
- package/assets/shared/signals/indicators-extended.js +284 -0
- package/assets/shared/signals/indicators.d.ts +15 -0
- package/assets/shared/signals/indicators.js +107 -0
- package/dist/daemon.js +104 -0
- package/package.json +1 -1
|
@@ -205,6 +205,11 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
205
205
|
// Surface entry metadata (if available)
|
|
206
206
|
...(p.metadata && {
|
|
207
207
|
setupType: p.metadata.setupType,
|
|
208
|
+
missionId: p.metadata.missionId,
|
|
209
|
+
candidateId: p.metadata.candidateId,
|
|
210
|
+
strategy: p.metadata.strategy,
|
|
211
|
+
accruedFundingUsd: p.metadata.accruedFundingUsd,
|
|
212
|
+
fundingThroughTime: p.metadata.fundingThroughTime,
|
|
208
213
|
thesis: p.metadata.thesis,
|
|
209
214
|
stopPrice: p.metadata.stopPrice,
|
|
210
215
|
targetPrice: p.metadata.targetPrice,
|
|
@@ -53,7 +53,11 @@ export function parseSymbol(symbol) {
|
|
|
53
53
|
if (parts.length !== 2) {
|
|
54
54
|
throw new Error(`Invalid symbol format: ${symbol}. Expected BASE/QUOTE`);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const [quote, settle, ...extra] = parts[1].split(':');
|
|
57
|
+
if (!quote || extra.length > 0 || (settle !== undefined && settle !== quote)) {
|
|
58
|
+
throw new Error(`Invalid symbol format: ${symbol}. Expected BASE/QUOTE or BASE/QUOTE:QUOTE`);
|
|
59
|
+
}
|
|
60
|
+
return { base: parts[0], quote };
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
63
|
* Ensure a currency exists in the wallet with at least zero balance.
|
|
@@ -83,7 +83,7 @@ export interface CurrencyBalance {
|
|
|
83
83
|
* - `exchange_stop` / `exchange_target`: exchange-native STOP_MARKET / TAKE_PROFIT_MARKET triggered on Binance
|
|
84
84
|
* - `cancelled_auto`: bracket reconciler cleaned up after detecting the position closed externally
|
|
85
85
|
* - `operator` / `agent` / `emergency`: explicit initiator */
|
|
86
|
-
export type CloseReason = 'agent' | 'stop_watcher' | 'emergency' | 'operator' | 'bracket_attach_failed' | 'exchange_stop' | 'exchange_target' | 'cancelled_auto';
|
|
86
|
+
export type CloseReason = 'agent' | 'stop_watcher' | 'emergency' | 'operator' | 'bracket_attach_failed' | 'exchange_stop' | 'exchange_target' | 'cancelled_auto' | 'wave9_signal_reversal' | 'liquidated' | 'adl' | 'venue_delisted';
|
|
87
87
|
/** The agent's stated profit-realization plan, pinned at entry. PURE DATA /
|
|
88
88
|
* indication — the plugin does NOT enforce it; it is surfaced back so the agent
|
|
89
89
|
* can hold to its own plan instead of re-litigating. The agent fills the parts
|
|
@@ -107,6 +107,11 @@ export interface RealizationRule {
|
|
|
107
107
|
* moment of close. */
|
|
108
108
|
export interface PositionMetadata {
|
|
109
109
|
setupType?: string;
|
|
110
|
+
missionId?: string;
|
|
111
|
+
candidateId?: string;
|
|
112
|
+
strategy?: 'tsmom_28d_long_flat_reversal_1d' | 'tsmom_28d_short_flat_reversal_1d';
|
|
113
|
+
accruedFundingUsd?: number;
|
|
114
|
+
fundingThroughTime?: string;
|
|
110
115
|
thesis?: string;
|
|
111
116
|
stopPrice?: number;
|
|
112
117
|
targetPrice?: number;
|
|
@@ -27,6 +27,8 @@ export interface StrategyConfig {
|
|
|
27
27
|
params: Record<string, number>;
|
|
28
28
|
};
|
|
29
29
|
targetRMultiples: number[];
|
|
30
|
+
/** Dedicated portfolio catalog rows are not declarative generic strategies. */
|
|
31
|
+
executionModel?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface StrategyRow {
|
|
32
34
|
id: number;
|
|
@@ -55,6 +57,7 @@ export interface StrategyEvalResult {
|
|
|
55
57
|
regime_confidence: number;
|
|
56
58
|
summary: string;
|
|
57
59
|
}
|
|
60
|
+
export declare function isDeclarativeStrategyConfig(strategy: StrategyConfig): boolean;
|
|
58
61
|
/**
|
|
59
62
|
* Evaluate a strategy against a symbol's fact object.
|
|
60
63
|
* Returns null if the strategy is regime-gated and the symbol's regime doesn't match.
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
// and optionally checks threshold-based conditions against indicator values.
|
|
5
5
|
import { CONDITION_EVALUATORS } from './condition-registry.js';
|
|
6
6
|
import { getCachedTradingParams } from '../trading-params-cache.js';
|
|
7
|
+
export function isDeclarativeStrategyConfig(strategy) {
|
|
8
|
+
return strategy.executionModel === undefined || strategy.executionModel === 'declarative';
|
|
9
|
+
}
|
|
7
10
|
// ─── Evaluator ──────────────────────────────────────────────────────────
|
|
8
11
|
/**
|
|
9
12
|
* Evaluate a strategy against a symbol's fact object.
|
|
10
13
|
* Returns null if the strategy is regime-gated and the symbol's regime doesn't match.
|
|
11
14
|
*/
|
|
12
15
|
export function evaluateStrategy(strategy, fact) {
|
|
16
|
+
if (!isDeclarativeStrategyConfig(strategy))
|
|
17
|
+
return null;
|
|
13
18
|
// Check regime gate
|
|
14
19
|
if (strategy.regimeWhitelist.length > 0) {
|
|
15
20
|
const matchesRegime = strategy.regimeWhitelist.some(r => r === fact.regime || r === 'ANY');
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
3
|
+
import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
|
|
2
4
|
interface AttachBracketsArgs {
|
|
3
5
|
symbol: string;
|
|
4
6
|
stop_price?: number;
|
|
@@ -26,9 +28,12 @@ interface AttachBracketsSuccess {
|
|
|
26
28
|
idempotent_no_op?: boolean;
|
|
27
29
|
note?: string;
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
interface AttachBracketsDeps {
|
|
30
32
|
adapter: IExchangeAdapter;
|
|
31
|
-
|
|
33
|
+
operationLock?: TradingOperationLock;
|
|
34
|
+
checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
|
|
35
|
+
}
|
|
36
|
+
export declare function attachBracketsTool(args: AttachBracketsArgs, deps: AttachBracketsDeps): Promise<AttachBracketsSuccess | {
|
|
32
37
|
error: string;
|
|
33
38
|
}>;
|
|
34
39
|
export {};
|
|
@@ -41,6 +41,42 @@ export async function attachBracketsTool(args, deps) {
|
|
|
41
41
|
if (!deps.adapter.isLive) {
|
|
42
42
|
return { error: 'attach_brackets is live-mode only (it places exchange-side STOP/TP orders). In paper mode, use modify_stop / modify_target to set or move the position\'s protective levels in place — the paper stop-watcher enforces metadata.stopPrice.' };
|
|
43
43
|
}
|
|
44
|
+
if (deps.checkWave9LiveSymbolOwnership && !deps.operationLock) {
|
|
45
|
+
return {
|
|
46
|
+
error: 'Live attach_brackets refused: the shared trading operation lock is required ' +
|
|
47
|
+
'while Wave 9 LIVE ownership checks are enabled.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const attachLive = () => attachBracketsLive(args, deps);
|
|
51
|
+
try {
|
|
52
|
+
return deps.operationLock
|
|
53
|
+
? await deps.operationLock.withAccountLock(`generic-attach-brackets-final:${args.symbol}`, attachLive)
|
|
54
|
+
: await attachLive();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return { error: formatError(err) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function attachBracketsLive(args, deps) {
|
|
61
|
+
if (deps.checkWave9LiveSymbolOwnership) {
|
|
62
|
+
let ownership;
|
|
63
|
+
try {
|
|
64
|
+
ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return {
|
|
68
|
+
error: `attach_brackets refused at mutation boundary: Wave 9 LIVE ownership check failed ` +
|
|
69
|
+
`for ${args.symbol} (${formatError(err)}).`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
|
|
73
|
+
return {
|
|
74
|
+
error: `attach_brackets refused at mutation boundary: ${args.symbol} has ` +
|
|
75
|
+
`${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
|
|
76
|
+
'Wave 9 protection is candidate-bound and cannot be replaced by the generic bracket tool.',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
44
80
|
const live = deps.adapter;
|
|
45
81
|
const mgr = live.getBracketManager?.();
|
|
46
82
|
const ledger = live.getBracketLedger?.();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
2
3
|
import type { CcxtOrder } from '../types.js';
|
|
3
4
|
export declare function cancelAllOrdersTool(args: {
|
|
4
5
|
symbol?: string;
|
|
5
6
|
}, deps: {
|
|
6
7
|
adapter: IExchangeAdapter;
|
|
8
|
+
operationLock?: TradingOperationLock;
|
|
7
9
|
}): Promise<CcxtOrder[]>;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Tool: cancel_all_orders — cancel all open orders (paper or live)
|
|
2
2
|
// NO readiness gate — emergency control (kill switch), must always work.
|
|
3
3
|
export async function cancelAllOrdersTool(args, deps) {
|
|
4
|
-
|
|
4
|
+
const cancel = () => deps.adapter.cancelAllOrders(args.symbol);
|
|
5
|
+
return deps.adapter.isLive && deps.operationLock
|
|
6
|
+
? deps.operationLock.withAccountLock(`generic-cancel-all-orders-final:${args.symbol ?? 'all'}`, cancel)
|
|
7
|
+
: cancel();
|
|
5
8
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
2
3
|
import type { CcxtOrder } from '../types.js';
|
|
4
|
+
import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
|
|
3
5
|
export declare function cancelOrderTool(args: {
|
|
4
6
|
id: string;
|
|
5
7
|
symbol?: string;
|
|
6
8
|
}, deps: {
|
|
7
9
|
adapter: IExchangeAdapter;
|
|
10
|
+
operationLock?: TradingOperationLock;
|
|
11
|
+
checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
|
|
8
12
|
}): Promise<CcxtOrder | {
|
|
9
13
|
error: string;
|
|
10
14
|
}>;
|
|
@@ -1,12 +1,58 @@
|
|
|
1
|
-
// Tool: cancel_order — cancel a single order (paper or live)
|
|
2
|
-
//
|
|
1
|
+
// Tool: cancel_order — cancel a single order (paper or live).
|
|
2
|
+
// There is no readiness gate, but LIVE cancellation cannot individually strip
|
|
3
|
+
// candidate-bound Wave 9 protection. The emergency escape is an explicit
|
|
4
|
+
// operator close, which reduces exposure instead of leaving it naked.
|
|
3
5
|
import { formatError } from '../logger.js';
|
|
4
6
|
export async function cancelOrderTool(args, deps) {
|
|
5
7
|
if (!args.id) {
|
|
6
8
|
return { error: 'Order ID is required.' };
|
|
7
9
|
}
|
|
10
|
+
if (!deps.adapter.isLive) {
|
|
11
|
+
try {
|
|
12
|
+
return await deps.adapter.cancelOrder(args.id, args.symbol);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return { error: formatError(err) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (deps.checkWave9LiveSymbolOwnership && !deps.operationLock) {
|
|
19
|
+
return {
|
|
20
|
+
error: 'Live cancel_order refused: the shared trading operation lock is required ' +
|
|
21
|
+
'while Wave 9 LIVE ownership checks are enabled.',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const cancelLive = async () => {
|
|
25
|
+
let symbol = args.symbol?.trim();
|
|
26
|
+
if (!symbol) {
|
|
27
|
+
const matches = (await deps.adapter.getOpenOrders())
|
|
28
|
+
.filter((order) => order.id === args.id);
|
|
29
|
+
if (matches.length !== 1 || !matches[0].symbol) {
|
|
30
|
+
throw new Error(`cancel_order refused: order ${args.id} could not be resolved to exactly one ` +
|
|
31
|
+
`trusted open-order symbol (matches=${matches.length}).`);
|
|
32
|
+
}
|
|
33
|
+
symbol = matches[0].symbol;
|
|
34
|
+
}
|
|
35
|
+
if (deps.checkWave9LiveSymbolOwnership) {
|
|
36
|
+
let ownership;
|
|
37
|
+
try {
|
|
38
|
+
ownership = await deps.checkWave9LiveSymbolOwnership(symbol);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new Error(`cancel_order refused at mutation boundary: Wave 9 LIVE ownership check failed ` +
|
|
42
|
+
`for ${symbol} (${formatError(err)}).`);
|
|
43
|
+
}
|
|
44
|
+
if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
|
|
45
|
+
throw new Error(`cancel_order refused at mutation boundary: ${symbol} has ` +
|
|
46
|
+
`${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
|
|
47
|
+
'Use operator_command to close the position instead of stripping candidate-bound protection.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return deps.adapter.cancelOrder(args.id, symbol);
|
|
51
|
+
};
|
|
8
52
|
try {
|
|
9
|
-
return
|
|
53
|
+
return deps.operationLock
|
|
54
|
+
? await deps.operationLock.withAccountLock(`generic-cancel-order-final:${args.id}`, cancelLive)
|
|
55
|
+
: await cancelLive();
|
|
10
56
|
}
|
|
11
57
|
catch (err) {
|
|
12
58
|
return { error: formatError(err) };
|
|
@@ -4,6 +4,11 @@ import type { CcxtOrder } from '../types.js';
|
|
|
4
4
|
import { type ClosePositionArgs } from './assessment-validation.js';
|
|
5
5
|
import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
|
|
6
6
|
import { type ExitGateMode } from '../config/position-review-config.js';
|
|
7
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
8
|
+
import type { Wave9LiveExecutionLedger } from '../wave9/live-execution-ledger.js';
|
|
9
|
+
import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
|
|
10
|
+
import { type Wave9LiveResidualProtectionResult } from '../wave9/live-residual-protection.js';
|
|
11
|
+
import { type Wave9PaperAdmissionGuard } from '../wave9/paper-admission-guard.js';
|
|
7
12
|
export declare function closePositionTool(args: ClosePositionArgs, deps: {
|
|
8
13
|
binanceApi: PublicMarketDataApi;
|
|
9
14
|
adapter: IExchangeAdapter;
|
|
@@ -11,6 +16,24 @@ export declare function closePositionTool(args: ClosePositionArgs, deps: {
|
|
|
11
16
|
/** Override for tests — production reads from plugin-config.json on each
|
|
12
17
|
* call so the flag can be flipped without a restart. */
|
|
13
18
|
exitGateMode?: ExitGateMode;
|
|
19
|
+
wave9AdmissionGuard?: Wave9PaperAdmissionGuard;
|
|
20
|
+
refreshPaperState?: () => void;
|
|
21
|
+
wave9TradingModeCheck?: () => string;
|
|
22
|
+
wave9VenueCheck?: () => string;
|
|
23
|
+
captureWave9PositionFingerprint?: (symbol: string) => Promise<{
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
position: {
|
|
26
|
+
candidateId?: string;
|
|
27
|
+
missionId?: string;
|
|
28
|
+
setupType?: string;
|
|
29
|
+
side: 'long' | 'short';
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
wave9LiveLedger?: Wave9LiveExecutionLedger;
|
|
33
|
+
checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
|
|
34
|
+
reprotectWave9LiveResidual?: (adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, candidateId: string, symbol: string) => Promise<Wave9LiveResidualProtectionResult>;
|
|
35
|
+
operationLock?: TradingOperationLock;
|
|
36
|
+
operationLockHeld?: boolean;
|
|
14
37
|
}): Promise<CcxtOrder | {
|
|
15
38
|
error: string;
|
|
16
39
|
}>;
|
|
@@ -26,7 +26,18 @@ import { onClosePositionFilled } from '../ingest/position-auto-capture.js';
|
|
|
26
26
|
import { evaluateExitGate, } from './exit-gate.js';
|
|
27
27
|
import { getRecentAuditVerdictHistory } from './audit-bracket-protection.js';
|
|
28
28
|
import { loadExitGateMode } from '../config/position-review-config.js';
|
|
29
|
+
import { confirmWave9LivePositionFlat } from '../wave9/live-position-confirmation.js';
|
|
30
|
+
import { reprotectWave9LiveResidual, } from '../wave9/live-residual-protection.js';
|
|
31
|
+
import { captureWave9PaperPositionFingerprint, isWave9ManagedPosition, WAVE9_MECHANICAL_EXIT_REASON, } from '../wave9/paper-admission-guard.js';
|
|
29
32
|
const TAG = 'close-position';
|
|
33
|
+
function currentWave9ExecutionMode(deps) {
|
|
34
|
+
const runtimeMode = deps.wave9TradingModeCheck?.() ?? deps.adapter.mode;
|
|
35
|
+
if (!deps.adapter.isLive && deps.adapter.mode === 'PAPER' && runtimeMode === 'PAPER')
|
|
36
|
+
return 'PAPER';
|
|
37
|
+
if (deps.adapter.isLive && deps.adapter.mode === 'LIVE' && runtimeMode === 'LIVE')
|
|
38
|
+
return 'LIVE';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
30
41
|
// Bracket-integrity circuit-breaker. Counts SUCCESSFUL close_position calls
|
|
31
42
|
// with reason='bracket_integrity' in a rolling 60-min window. When the 3rd
|
|
32
43
|
// such close is attempted, reject — repeated bracket-integrity flushes in
|
|
@@ -40,10 +51,131 @@ const BRACKET_INTEGRITY_LIMIT = 3;
|
|
|
40
51
|
let bracketIntegrityCloses = []; // timestamps of successful closes
|
|
41
52
|
export async function closePositionTool(args, deps) {
|
|
42
53
|
// Schema gate FIRST — reject before touching the exchange.
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const wave9ExitClaimed = args.reason !== 'operator_command' && (args.reason === WAVE9_MECHANICAL_EXIT_REASON
|
|
55
|
+
|| args.wave9_exit_token !== undefined
|
|
56
|
+
|| args.candidate_id !== undefined);
|
|
57
|
+
if (wave9ExitClaimed && deps.wave9VenueCheck?.() !== 'binance') {
|
|
58
|
+
return { error: 'Wave 9 reversal close refused: execution is pinned to Binance USD-M futures.' };
|
|
59
|
+
}
|
|
60
|
+
if (wave9ExitClaimed && !deps.operationLock) {
|
|
61
|
+
return { error: 'Wave 9 reversal close refused: process-local trading operation lock is unavailable.' };
|
|
62
|
+
}
|
|
63
|
+
const initialWave9Mode = wave9ExitClaimed ? currentWave9ExecutionMode(deps) : null;
|
|
64
|
+
let wave9Lease;
|
|
65
|
+
if (wave9ExitClaimed) {
|
|
66
|
+
if (!initialWave9Mode) {
|
|
67
|
+
return {
|
|
68
|
+
error: 'Wave 9 reversal close refused: capability execution requires an exact PAPER/paper-adapter ' +
|
|
69
|
+
'or LIVE/live-adapter runtime match.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (args.reason !== WAVE9_MECHANICAL_EXIT_REASON
|
|
73
|
+
|| !args.wave9_exit_token
|
|
74
|
+
|| !args.candidate_id
|
|
75
|
+
|| !deps.wave9AdmissionGuard) {
|
|
76
|
+
return { error: 'Wave 9 reversal close requires its exact reason, candidate identity, and short-lived exit token.' };
|
|
77
|
+
}
|
|
78
|
+
const executionCanonical = args.symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
79
|
+
if (!executionCanonical.endsWith('USDT') || executionCanonical.length <= 4) {
|
|
80
|
+
return { error: `Wave 9 reversal close refused: unsupported execution symbol ${args.symbol}.` };
|
|
81
|
+
}
|
|
82
|
+
if (initialWave9Mode === 'LIVE' && (!deps.captureWave9PositionFingerprint || !deps.wave9LiveLedger)) {
|
|
83
|
+
return {
|
|
84
|
+
error: 'Wave 9 live reversal close refused: authoritative live position capture and the durable ' +
|
|
85
|
+
'execution ledger are required.',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
args = { ...args, symbol: `${executionCanonical.slice(0, -4)}/USDT:USDT` };
|
|
89
|
+
try {
|
|
90
|
+
wave9Lease = await deps.operationLock.withAccountLock(`wave9-begin-close:${args.candidate_id ?? args.symbol}`, async () => {
|
|
91
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
92
|
+
throw new Error('adapter or trading mode changed before Wave 9 reversal began');
|
|
93
|
+
}
|
|
94
|
+
let position;
|
|
95
|
+
let positionFingerprint;
|
|
96
|
+
if (initialWave9Mode === 'LIVE') {
|
|
97
|
+
const capture = await deps.captureWave9PositionFingerprint(args.symbol);
|
|
98
|
+
position = capture.position;
|
|
99
|
+
positionFingerprint = capture.fingerprint;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
deps.refreshPaperState?.();
|
|
103
|
+
const positions = await deps.adapter.getPositionsOrNull();
|
|
104
|
+
if (positions === null)
|
|
105
|
+
throw new Error('paper position snapshot unavailable');
|
|
106
|
+
const canonical = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
107
|
+
const matches = positions.filter((candidate) => canonical(candidate.symbol) === canonical(args.symbol));
|
|
108
|
+
if (matches.length !== 1)
|
|
109
|
+
throw new Error('exactly one matching held position is required');
|
|
110
|
+
position = matches[0];
|
|
111
|
+
positionFingerprint = captureWave9PaperPositionFingerprint(deps.adapter, args.symbol).fingerprint;
|
|
112
|
+
}
|
|
113
|
+
if (!isWave9ManagedPosition(position)
|
|
114
|
+
|| position.candidateId !== args.candidate_id
|
|
115
|
+
|| !position.missionId) {
|
|
116
|
+
throw new Error('held position lacks exact Wave 9 identity/geometry');
|
|
117
|
+
}
|
|
118
|
+
return deps.wave9AdmissionGuard.beginExit({
|
|
119
|
+
token: args.wave9_exit_token,
|
|
120
|
+
candidateId: args.candidate_id,
|
|
121
|
+
missionId: position.missionId,
|
|
122
|
+
symbol: args.symbol,
|
|
123
|
+
positionSide: position.side,
|
|
124
|
+
setupType: position.setupType,
|
|
125
|
+
reason: args.reason,
|
|
126
|
+
positionFingerprint,
|
|
127
|
+
tradingMode: initialWave9Mode,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return { error: `Wave 9 reversal close refused: ${formatError(err)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Preserve the generic tool contract: malformed calls are rejected before
|
|
137
|
+
// any adapter read. Wave 9 classification is an additional check only for
|
|
138
|
+
// already-valid, non-operator risk-reducing closes.
|
|
139
|
+
const v = validateClosePosition(args);
|
|
140
|
+
if (!v.ok) {
|
|
141
|
+
logger.warn(TAG, `close_position rejected: ${v.error} (symbol=${args.symbol}, reason=${args.reason ?? 'missing'})`);
|
|
142
|
+
return { error: v.error ?? 'Invalid close_position arguments.' };
|
|
143
|
+
}
|
|
144
|
+
if (args.reason !== 'operator_command'
|
|
145
|
+
&& deps.adapter.isLive
|
|
146
|
+
&& deps.checkWave9LiveSymbolOwnership
|
|
147
|
+
&& !deps.operationLock) {
|
|
148
|
+
return {
|
|
149
|
+
error: 'Generic live close_position refused: the shared trading operation lock is required ' +
|
|
150
|
+
'while Wave 9 LIVE ownership checks are enabled.',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// When a trusted position snapshot is available, do not route a held
|
|
154
|
+
// Wave 9 position through a generic thesis/risk/bracket reason. Snapshot
|
|
155
|
+
// failure stays fail-open because an ordinary risk-reducing close must not
|
|
156
|
+
// be trapped by extra strategy-lifecycle I/O. Operator remains unconditional.
|
|
157
|
+
if (args.reason !== 'operator_command') {
|
|
158
|
+
try {
|
|
159
|
+
const getTrusted = deps.adapter.getPositionsOrNull?.bind(deps.adapter);
|
|
160
|
+
const held = getTrusted ? await getTrusted() : undefined;
|
|
161
|
+
if (held) {
|
|
162
|
+
const canonical = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
163
|
+
if (held.some((position) => canonical(position.symbol) === canonical(args.symbol)
|
|
164
|
+
&& isWave9ManagedPosition(position))) {
|
|
165
|
+
return {
|
|
166
|
+
error: `Wave 9 position close refused: call get_wave9_status and use reason=${WAVE9_MECHANICAL_EXIT_REASON} ` +
|
|
167
|
+
'with its single-use exit token. Only an explicit operator_command bypasses the strategy exit policy.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (held === null) {
|
|
172
|
+
logger.warn(TAG, 'Wave 9 generic-close classification unavailable; proceeding with legacy risk-reducing close semantics.');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logger.warn(TAG, `Wave 9 generic-close classification failed (${formatError(err)}); proceeding with legacy risk-reducing close semantics.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
47
179
|
}
|
|
48
180
|
// Exit-gate check — see docs/EXIT_GATE_DESIGN.md. The gate is orthogonal to
|
|
49
181
|
// the bracket_integrity safeguard below; both can fire on the same call.
|
|
@@ -63,7 +195,7 @@ export async function closePositionTool(args, deps) {
|
|
|
63
195
|
// snapshot was never constructed). See docs/EXIT_DISCIPLINE_BUILD_PLAN.md
|
|
64
196
|
// Step 0a + docs/EXIT_GATE_DESIGN.md §5.
|
|
65
197
|
let gateInputs = null;
|
|
66
|
-
if (gateMode !== 'off') {
|
|
198
|
+
if (!wave9ExitClaimed && gateMode !== 'off') {
|
|
67
199
|
gateInputs = await buildExitGateInputs(args, deps);
|
|
68
200
|
gateVerdict = evaluateExitGate(gateInputs);
|
|
69
201
|
logger.info(TAG, `exit_gate mode=${gateMode} ${gateVerdict.outcome} tier=${gateVerdict.tier} code=${gateVerdict.code} (symbol=${args.symbol}, reason=${args.reason})`);
|
|
@@ -80,13 +212,30 @@ export async function closePositionTool(args, deps) {
|
|
|
80
212
|
}
|
|
81
213
|
// In paper mode, fetch latest ticker + order book for realistic fills
|
|
82
214
|
if (!deps.adapter.isLive) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
215
|
+
try {
|
|
216
|
+
const paperDeps = { binanceApi: deps.binanceApi, simulator: deps.adapter.getSimulator() };
|
|
217
|
+
const [priceResult] = await Promise.all([
|
|
218
|
+
fetchCurrentPrice(args.symbol, paperDeps),
|
|
219
|
+
fetchOrderBook(args.symbol, paperDeps),
|
|
220
|
+
]);
|
|
221
|
+
if (!isError(priceResult)) {
|
|
222
|
+
// The helper updates the simulator mark used by the actual close fill.
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (wave9Lease) {
|
|
226
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'exit_market_data_unavailable', { retryable: true });
|
|
227
|
+
wave9Lease = undefined;
|
|
228
|
+
}
|
|
229
|
+
return priceResult;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
if (wave9Lease) {
|
|
234
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'exit_market_data_threw', { retryable: true });
|
|
235
|
+
wave9Lease = undefined;
|
|
236
|
+
}
|
|
237
|
+
return { error: `close_position market-data refresh failed: ${formatError(err)}` };
|
|
238
|
+
}
|
|
90
239
|
}
|
|
91
240
|
// Log the assessment so every agent close is auditable in the journal.
|
|
92
241
|
// We log after validation (we've confirmed the shape is sound) and before
|
|
@@ -133,7 +282,127 @@ export async function closePositionTool(args, deps) {
|
|
|
133
282
|
logger.info(TAG, `bracket_integrity safeguard: ${integrity.detail} — allowing close`);
|
|
134
283
|
}
|
|
135
284
|
try {
|
|
136
|
-
const
|
|
285
|
+
const close = async () => {
|
|
286
|
+
if (!wave9ExitClaimed && !deps.adapter.isLive && reason !== 'operator_command') {
|
|
287
|
+
deps.refreshPaperState?.();
|
|
288
|
+
const positions = await deps.adapter.getPositionsOrNull();
|
|
289
|
+
if (positions === null) {
|
|
290
|
+
// Preserve the existing risk-reduction contract: the extra
|
|
291
|
+
// classification read cannot trap an ordinary close.
|
|
292
|
+
logger.warn(TAG, 'Wave 9 final close classification unavailable; proceeding risk-reducing fail-open.');
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const canonical = (value) => value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
296
|
+
const target = positions.find((position) => canonical(position.symbol) === canonical(args.symbol));
|
|
297
|
+
if (target && isWave9ManagedPosition(target)) {
|
|
298
|
+
throw new Error(`Generic close_position refused at mutation boundary: ${args.symbol} is now held by Wave 9.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!wave9ExitClaimed
|
|
303
|
+
&& deps.adapter.isLive
|
|
304
|
+
&& reason !== 'operator_command'
|
|
305
|
+
&& deps.checkWave9LiveSymbolOwnership) {
|
|
306
|
+
let ownership;
|
|
307
|
+
try {
|
|
308
|
+
ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
throw new Error(`Generic close_position refused at mutation boundary: Wave 9 LIVE ownership ` +
|
|
312
|
+
`check failed for ${args.symbol} (${formatError(err)}).`);
|
|
313
|
+
}
|
|
314
|
+
if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
|
|
315
|
+
throw new Error(`Generic close_position refused at mutation boundary: ${args.symbol} has ` +
|
|
316
|
+
`${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
|
|
317
|
+
'Use get_wave9_status for a reversal authorization or operator_command for an emergency close.');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return wave9ExitClaimed
|
|
321
|
+
? deps.adapter.closePosition(args.symbol, 'wave9_signal_reversal')
|
|
322
|
+
: deps.adapter.closePosition(args.symbol);
|
|
323
|
+
};
|
|
324
|
+
const closeWave9 = async () => {
|
|
325
|
+
if (!wave9Lease)
|
|
326
|
+
throw new Error('Wave 9 exit lease is unavailable at mutation boundary.');
|
|
327
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
328
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'trading_mode_changed_before_exit_mutation', { retryable: true });
|
|
329
|
+
wave9Lease = undefined;
|
|
330
|
+
throw new Error('Wave 9 reversal close refused: adapter or trading mode changed before mutation.');
|
|
331
|
+
}
|
|
332
|
+
if (deps.wave9VenueCheck?.() !== 'binance') {
|
|
333
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'execution_venue_changed_before_exit_mutation', { retryable: true });
|
|
334
|
+
wave9Lease = undefined;
|
|
335
|
+
throw new Error('Wave 9 reversal close refused: execution venue changed away from Binance before mutation.');
|
|
336
|
+
}
|
|
337
|
+
let currentFingerprint;
|
|
338
|
+
if (initialWave9Mode === 'LIVE') {
|
|
339
|
+
const capture = await deps.captureWave9PositionFingerprint(args.symbol);
|
|
340
|
+
if (!isWave9ManagedPosition(capture.position)
|
|
341
|
+
|| capture.position.candidateId !== args.candidate_id
|
|
342
|
+
|| !capture.position.missionId) {
|
|
343
|
+
throw new Error('held live position no longer matches its exact Wave 9 identity');
|
|
344
|
+
}
|
|
345
|
+
currentFingerprint = capture.fingerprint;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
deps.refreshPaperState?.();
|
|
349
|
+
currentFingerprint = captureWave9PaperPositionFingerprint(deps.adapter, args.symbol).fingerprint;
|
|
350
|
+
}
|
|
351
|
+
deps.wave9AdmissionGuard.revalidate(wave9Lease, currentFingerprint);
|
|
352
|
+
let order;
|
|
353
|
+
let closeError;
|
|
354
|
+
try {
|
|
355
|
+
order = await close();
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
if (initialWave9Mode !== 'LIVE')
|
|
359
|
+
throw err;
|
|
360
|
+
closeError = formatError(err);
|
|
361
|
+
}
|
|
362
|
+
if (initialWave9Mode === 'LIVE') {
|
|
363
|
+
const flat = await confirmWave9LivePositionFlat(deps.adapter, args.symbol);
|
|
364
|
+
if (!flat.confirmed) {
|
|
365
|
+
let protection;
|
|
366
|
+
try {
|
|
367
|
+
protection = await (deps.reprotectWave9LiveResidual ?? reprotectWave9LiveResidual)(deps.adapter, deps.wave9LiveLedger, args.candidate_id, args.symbol);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `live_close_residual_reprotection_threw:${formatError(err)}`, { retryable: true });
|
|
371
|
+
wave9Lease = undefined;
|
|
372
|
+
throw new Error(`CRITICAL: Wave 9 LIVE reversal is not stably flat (${flat.detail}), and residual ` +
|
|
373
|
+
`re-protection threw (${formatError(err)}). Durable ownership remains OPEN. ` +
|
|
374
|
+
'IMMEDIATE MANUAL INTERVENTION REQUIRED; a fresh exit authorization may retry.');
|
|
375
|
+
}
|
|
376
|
+
if (protection.status === 'protected') {
|
|
377
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `live_close_residual_reprotected:${protection.stopClientOrderId ?? 'exact_stop'}`, { retryable: true });
|
|
378
|
+
wave9Lease = undefined;
|
|
379
|
+
throw new Error(`Wave 9 LIVE reversal close${closeError ? ` threw (${closeError}) and` : ''} left ` +
|
|
380
|
+
`${protection.residualQuantity ?? 'a'} residual ` +
|
|
381
|
+
`contract position. Durable ownership remains OPEN and the exact frozen stop ` +
|
|
382
|
+
`${protection.stopPrice} is proven live; refresh get_wave9_status and use the ` +
|
|
383
|
+
'fresh exit authorization to retry the reduce-only close.');
|
|
384
|
+
}
|
|
385
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `live_close_residual_requires_manual_intervention:${protection.detail}`, { retryable: true });
|
|
386
|
+
wave9Lease = undefined;
|
|
387
|
+
throw new Error(`CRITICAL: Wave 9 LIVE reversal close${closeError ? ` threw (${closeError}) and` : ''} ` +
|
|
388
|
+
`stable flat was not confirmed ` +
|
|
389
|
+
`(${flat.detail}). Durable ownership remains OPEN. ${protection.detail}`);
|
|
390
|
+
}
|
|
391
|
+
await deps.wave9LiveLedger.markClosed(args.candidate_id);
|
|
392
|
+
}
|
|
393
|
+
deps.wave9AdmissionGuard.commit(wave9Lease);
|
|
394
|
+
wave9Lease = undefined;
|
|
395
|
+
if (closeError) {
|
|
396
|
+
throw new Error(`Wave 9 LIVE reversal close acknowledgement failed (${closeError}), but stable flat was ` +
|
|
397
|
+
'proven and durable ownership is CLOSED. Do not retry this candidate.');
|
|
398
|
+
}
|
|
399
|
+
return order;
|
|
400
|
+
};
|
|
401
|
+
const order = wave9ExitClaimed
|
|
402
|
+
? await deps.operationLock.withAccountLock(`wave9-close-final:${args.symbol}`, closeWave9)
|
|
403
|
+
: reason !== 'operator_command' && deps.operationLock
|
|
404
|
+
? await deps.operationLock.withAccountLock(`generic-close-final:${args.symbol}`, close)
|
|
405
|
+
: await close();
|
|
137
406
|
// Successful close — record the bracket_integrity timestamp so the
|
|
138
407
|
// circuit-breaker can see the trend on the next attempt.
|
|
139
408
|
if (reason === 'bracket_integrity' && deps.adapter.isLive) {
|
|
@@ -228,6 +497,10 @@ export async function closePositionTool(args, deps) {
|
|
|
228
497
|
return order;
|
|
229
498
|
}
|
|
230
499
|
catch (err) {
|
|
500
|
+
if (wave9Lease) {
|
|
501
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `exit_execution_failed:${formatError(err)}`, { retryable: initialWave9Mode === 'LIVE' });
|
|
502
|
+
wave9Lease = undefined;
|
|
503
|
+
}
|
|
231
504
|
return { error: formatError(err) };
|
|
232
505
|
}
|
|
233
506
|
}
|