@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
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/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
package/tools/create-order.js
CHANGED
|
@@ -4,10 +4,641 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { formatError } from '../logger.js';
|
|
5
5
|
import { getQuoteBalance, getQuoteWalletBalance } from '../balance-utils.js';
|
|
6
6
|
import { fetchCurrentPrice, fetchOrderBook, isError } from './helpers.js';
|
|
7
|
-
import { validateCreateOrder, sanitizeRealizationRule } from './assessment-validation.js';
|
|
7
|
+
import { validateCreateOrder, sanitizeRealizationRule, validateProtectiveGeometry } from './assessment-validation.js';
|
|
8
8
|
import { preTradeRiskCheck, getDefaultPreTradeLimits, computeConsecutiveLosses } from '../risk/pre-trade-check.js';
|
|
9
9
|
import { bracketsEnabled, loadBracketMode, loadBracketRequirements } from '../config/brackets-config.js';
|
|
10
10
|
import { onCreateOrderFilled } from '../ingest/position-auto-capture.js';
|
|
11
|
+
import { wave9ClientOrderId, } from '../wave9/live-execution-ledger.js';
|
|
12
|
+
import { inspectWave9LiveSymbolOwnership, } from '../wave9/live-symbol-ownership.js';
|
|
13
|
+
import { confirmWave9LivePositionFlat } from '../wave9/live-position-confirmation.js';
|
|
14
|
+
import { reprotectWave9LiveResidual, } from '../wave9/live-residual-protection.js';
|
|
15
|
+
import { captureWave9PaperAccountSnapshot, isWave9ManagedPosition, WAVE9_BUNDLE_SETUP_TYPE, } from '../wave9/paper-admission-guard.js';
|
|
16
|
+
function currentWave9ExecutionMode(deps) {
|
|
17
|
+
const runtimeMode = deps.wave9TradingModeCheck?.() ?? deps.adapter.mode;
|
|
18
|
+
if (!deps.adapter.isLive && deps.adapter.mode === 'PAPER' && runtimeMode === 'PAPER')
|
|
19
|
+
return 'PAPER';
|
|
20
|
+
if (deps.adapter.isLive && deps.adapter.mode === 'LIVE' && runtimeMode === 'LIVE')
|
|
21
|
+
return 'LIVE';
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function residualProtectionMessage(outcome) {
|
|
25
|
+
const protection = outcome.residualProtection;
|
|
26
|
+
if (!protection)
|
|
27
|
+
return ' Residual protection state is unavailable.';
|
|
28
|
+
return protection.status === 'protected'
|
|
29
|
+
? ` The residual is protected by the proven frozen native stop ${protection.stopPrice}` +
|
|
30
|
+
`${protection.stopClientOrderId ? ` (${protection.stopClientOrderId})` : ''}.`
|
|
31
|
+
: ` ${protection.detail}`;
|
|
32
|
+
}
|
|
33
|
+
function confirmedWave9EntryPrice(order, side, stopPrice) {
|
|
34
|
+
const average = typeof order.average === 'number'
|
|
35
|
+
&& Number.isFinite(order.average)
|
|
36
|
+
&& order.average > 0
|
|
37
|
+
? order.average
|
|
38
|
+
: undefined;
|
|
39
|
+
const limitPrice = typeof order.price === 'number'
|
|
40
|
+
&& Number.isFinite(order.price)
|
|
41
|
+
&& order.price > 0
|
|
42
|
+
? order.price
|
|
43
|
+
: undefined;
|
|
44
|
+
const responsePrice = average ?? limitPrice;
|
|
45
|
+
if (responsePrice === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
return side === 'buy'
|
|
48
|
+
? (responsePrice > stopPrice ? responsePrice : undefined)
|
|
49
|
+
: (responsePrice < stopPrice ? responsePrice : undefined);
|
|
50
|
+
}
|
|
51
|
+
function confirmedWave9ExchangeOrderId(order) {
|
|
52
|
+
return typeof order.id === 'string'
|
|
53
|
+
&& order.id.length > 0
|
|
54
|
+
&& order.id.length <= 512
|
|
55
|
+
&& order.id.trim() === order.id
|
|
56
|
+
? order.id
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
async function flattenFailedWave9LiveEntry(adapter, ledger, symbol, candidateId, identity, residualProtector = reprotectWave9LiveResidual) {
|
|
60
|
+
let observation = await observeWave9LiveResidual(adapter, symbol, identity.side);
|
|
61
|
+
if (observation.status !== 'visible') {
|
|
62
|
+
const alreadyFlat = await confirmWave9LivePositionFlat(adapter, symbol);
|
|
63
|
+
if (alreadyFlat.confirmed) {
|
|
64
|
+
let terminalPersistenceError;
|
|
65
|
+
try {
|
|
66
|
+
await ledger.markClosed(candidateId);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
terminalPersistenceError = formatError(err);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
confirmedFlat: true,
|
|
73
|
+
confirmationDetail: alreadyFlat.detail,
|
|
74
|
+
closeAttempted: false,
|
|
75
|
+
...(terminalPersistenceError ? { terminalPersistenceError } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// A terminal market fill can become visible during the stable-flat
|
|
79
|
+
// window. Re-read once as positive identity evidence; the failed flat
|
|
80
|
+
// proof itself never authorizes a symbol-wide mutation.
|
|
81
|
+
observation = await observeWave9LiveResidual(adapter, symbol, identity.side);
|
|
82
|
+
}
|
|
83
|
+
const residualQuantity = observation.status === 'visible'
|
|
84
|
+
? observation.quantity
|
|
85
|
+
: undefined;
|
|
86
|
+
if (!Number.isFinite(identity.maximumQuantity)
|
|
87
|
+
|| identity.maximumQuantity <= 0
|
|
88
|
+
|| residualQuantity === undefined
|
|
89
|
+
|| !Number.isFinite(residualQuantity)
|
|
90
|
+
|| residualQuantity <= 0
|
|
91
|
+
|| residualQuantity > identity.maximumQuantity) {
|
|
92
|
+
let stopPrice = 0;
|
|
93
|
+
try {
|
|
94
|
+
stopPrice = ledger.getByCandidateId(candidateId)?.initialStop ?? 0;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Keep the identity refusal as the primary error.
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
confirmedFlat: false,
|
|
101
|
+
confirmationDetail: `Wave 9 exposure identity fence refused symbol-wide flatten: ${observation.detail}; ` +
|
|
102
|
+
`maximum attributable quantity ${identity.maximumQuantity}`,
|
|
103
|
+
closeAttempted: false,
|
|
104
|
+
residualProtection: {
|
|
105
|
+
status: 'manual_intervention',
|
|
106
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: refusing native-stop replacement or ` +
|
|
107
|
+
`symbol-wide flatten because current exposure is not exactly candidate-bound ` +
|
|
108
|
+
`(${observation.detail}; maximum ${identity.maximumQuantity})`,
|
|
109
|
+
candidateId,
|
|
110
|
+
symbol,
|
|
111
|
+
stopPrice,
|
|
112
|
+
...(residualQuantity !== undefined ? { residualQuantity } : {}),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
let flattenError;
|
|
117
|
+
try {
|
|
118
|
+
await adapter.closePosition(symbol, 'bracket_attach_failed');
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
flattenError = formatError(err);
|
|
122
|
+
}
|
|
123
|
+
const confirmation = await confirmWave9LivePositionFlat(adapter, symbol);
|
|
124
|
+
const terminalSafeFlat = confirmation.confirmed && flattenError === undefined;
|
|
125
|
+
let terminalPersistenceError;
|
|
126
|
+
if (terminalSafeFlat) {
|
|
127
|
+
try {
|
|
128
|
+
await ledger.markClosed(candidateId);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
terminalPersistenceError = formatError(err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let residualProtection;
|
|
135
|
+
if (!confirmation.confirmed) {
|
|
136
|
+
try {
|
|
137
|
+
residualProtection = await residualProtector(adapter, ledger, candidateId, symbol);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
let stopPrice = 0;
|
|
141
|
+
try {
|
|
142
|
+
stopPrice = ledger.getByCandidateId(candidateId)?.initialStop ?? 0;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Preserve the original re-protection failure in the escalation.
|
|
146
|
+
}
|
|
147
|
+
residualProtection = {
|
|
148
|
+
status: 'manual_intervention',
|
|
149
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: residual re-protection threw: ${formatError(err)}`,
|
|
150
|
+
candidateId,
|
|
151
|
+
symbol,
|
|
152
|
+
stopPrice,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
confirmedFlat: terminalSafeFlat,
|
|
158
|
+
confirmationDetail: confirmation.confirmed && flattenError
|
|
159
|
+
? `trusted flat snapshots were observed, but the emergency close mutation was not acknowledged ` +
|
|
160
|
+
`(${flattenError}); positive-fill settlement may still be lagging`
|
|
161
|
+
: confirmation.detail,
|
|
162
|
+
closeAttempted: true,
|
|
163
|
+
...(flattenError ? { flattenError } : {}),
|
|
164
|
+
...(terminalPersistenceError ? { terminalPersistenceError } : {}),
|
|
165
|
+
...(residualProtection ? { residualProtection } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function canonicalWave9Symbol(symbol) {
|
|
169
|
+
return symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
170
|
+
}
|
|
171
|
+
async function observeWave9LiveResidual(adapter, symbol, side) {
|
|
172
|
+
let positions;
|
|
173
|
+
try {
|
|
174
|
+
positions = await adapter.getPositionsOrNull(symbol);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
return { status: 'unknown', detail: `position read threw: ${formatError(err)}` };
|
|
178
|
+
}
|
|
179
|
+
if (positions === null || !Array.isArray(positions)) {
|
|
180
|
+
return { status: 'unknown', detail: 'position state is unknown' };
|
|
181
|
+
}
|
|
182
|
+
const target = canonicalWave9Symbol(symbol);
|
|
183
|
+
const expectedPositionSide = side === 'buy' ? 'long' : 'short';
|
|
184
|
+
const visible = [];
|
|
185
|
+
for (const position of positions) {
|
|
186
|
+
if (!position
|
|
187
|
+
|| typeof position.symbol !== 'string'
|
|
188
|
+
|| typeof position.contracts !== 'number'
|
|
189
|
+
|| !Number.isFinite(position.contracts)
|
|
190
|
+
|| position.contracts < 0) {
|
|
191
|
+
return { status: 'unknown', detail: 'position snapshot contained malformed rows' };
|
|
192
|
+
}
|
|
193
|
+
if (canonicalWave9Symbol(position.symbol) !== target || position.contracts === 0)
|
|
194
|
+
continue;
|
|
195
|
+
if (position.side !== expectedPositionSide) {
|
|
196
|
+
return {
|
|
197
|
+
status: 'unknown',
|
|
198
|
+
detail: `visible residual side ${position.side} does not match ${expectedPositionSide}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
visible.push(position);
|
|
202
|
+
}
|
|
203
|
+
if (visible.length === 0)
|
|
204
|
+
return { status: 'flat', detail: 'one trusted snapshot is currently empty' };
|
|
205
|
+
if (visible.length !== 1) {
|
|
206
|
+
return { status: 'unknown', detail: 'multiple matching residual position rows were returned' };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
status: 'visible',
|
|
210
|
+
detail: `exchange reports ${visible[0].contracts} residual contracts`,
|
|
211
|
+
quantity: visible[0].contracts,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function candidateBoundFoundOrder(resolution, clientOrderId, symbol, side) {
|
|
215
|
+
if (resolution.status !== 'found')
|
|
216
|
+
return undefined;
|
|
217
|
+
const order = resolution.order;
|
|
218
|
+
if (order.clientOrderId !== clientOrderId
|
|
219
|
+
|| canonicalWave9Symbol(order.symbol) !== canonicalWave9Symbol(symbol)
|
|
220
|
+
|| order.side !== side
|
|
221
|
+
|| order.type !== 'market'
|
|
222
|
+
|| confirmedWave9ExchangeOrderId(order) === undefined
|
|
223
|
+
|| (order.status !== 'open' && order.status !== 'closed' && order.status !== 'canceled')
|
|
224
|
+
|| typeof order.amount !== 'number'
|
|
225
|
+
|| !Number.isFinite(order.amount)
|
|
226
|
+
|| order.amount <= 0
|
|
227
|
+
|| typeof order.filled !== 'number'
|
|
228
|
+
|| !Number.isFinite(order.filled)
|
|
229
|
+
|| order.filled < 0
|
|
230
|
+
|| order.filled > order.amount + 1e-9 * Math.max(1, order.amount))
|
|
231
|
+
return undefined;
|
|
232
|
+
return order;
|
|
233
|
+
}
|
|
234
|
+
async function resolveWave9EntryCid(adapter, clientOrderId, symbol) {
|
|
235
|
+
if (!adapter.resolveOrderByClientId) {
|
|
236
|
+
return { status: 'unknown', detail: 'deterministic client-order resolver is unavailable' };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
return await adapter.resolveOrderByClientId(clientOrderId, symbol);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
return { status: 'unknown', detail: `client-order resolution threw: ${formatError(err)}` };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function flattenRecoveryOutcome(flatten, context, durableOpenEstablished) {
|
|
246
|
+
if (flatten.confirmedFlat && !flatten.terminalPersistenceError) {
|
|
247
|
+
return {
|
|
248
|
+
status: 'terminal',
|
|
249
|
+
detail: flatten.closeAttempted
|
|
250
|
+
? `${context}; emergency close reached stable flat and durable ownership is CLOSED`
|
|
251
|
+
: `${context}; exchange was already stably flat and durable ownership is CLOSED`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (flatten.confirmedFlat) {
|
|
255
|
+
return {
|
|
256
|
+
status: 'manual_intervention',
|
|
257
|
+
detail: `${context}; stable flat was established, but durable CLOSED persistence failed: ` +
|
|
258
|
+
`${flatten.terminalPersistenceError}`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (flatten.residualProtection?.status === 'protected') {
|
|
262
|
+
if (!durableOpenEstablished) {
|
|
263
|
+
return {
|
|
264
|
+
status: 'manual_intervention',
|
|
265
|
+
detail: `${context}; the residual has a proven frozen native stop ` +
|
|
266
|
+
`${flatten.residualProtection.stopPrice}, but durable OPEN ownership is unavailable. ` +
|
|
267
|
+
'IMMEDIATE MANUAL INTERVENTION REQUIRED',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
status: 'protected',
|
|
272
|
+
detail: `${context}; emergency close left a residual, whose exact frozen native stop ` +
|
|
273
|
+
`${flatten.residualProtection.stopPrice} is proven live`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
status: 'manual_intervention',
|
|
278
|
+
detail: `${context}; ${flatten.closeAttempted
|
|
279
|
+
? 'emergency close did not establish stable flat'
|
|
280
|
+
: 'symbol-wide flatten was not authorized by the exposure identity fence'} ` +
|
|
281
|
+
`(${flatten.confirmationDetail}). ` +
|
|
282
|
+
`${flatten.residualProtection?.detail ?? 'IMMEDIATE MANUAL INTERVENTION REQUIRED'}`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async function recoverWave9LiveSubmitThrow(input) {
|
|
286
|
+
const recoverCandidateExposure = async (foundOrder, recoveryQuantity, observation, context, entryTerminalProven) => {
|
|
287
|
+
if (!entryTerminalProven) {
|
|
288
|
+
let openPersistenceError;
|
|
289
|
+
try {
|
|
290
|
+
// Establish ownership without freezing an underreported fill while
|
|
291
|
+
// the entry order may still add to the position.
|
|
292
|
+
await input.ledger.markOpen(input.candidateId, {
|
|
293
|
+
...(foundOrder ? { exchangeOrderId: foundOrder.id } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
openPersistenceError = formatError(err);
|
|
298
|
+
}
|
|
299
|
+
let stopConfirmed = false;
|
|
300
|
+
try {
|
|
301
|
+
stopConfirmed = await input.confirmStop({
|
|
302
|
+
candidateId: input.candidateId,
|
|
303
|
+
symbol: input.symbol,
|
|
304
|
+
entryClientOrderId: input.clientOrderId,
|
|
305
|
+
entrySide: input.side,
|
|
306
|
+
stopPrice: input.stopPrice,
|
|
307
|
+
filledQuantity: recoveryQuantity,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
stopConfirmed = false;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
status: 'manual_intervention',
|
|
315
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: ${context}; the deterministic entry order ` +
|
|
316
|
+
`is not proven terminal, so its final filled quantity is unknown and emergency flatten ` +
|
|
317
|
+
`cannot safely claim CLOSED. ${stopConfirmed
|
|
318
|
+
? 'A candidate-bound close-all stop is currently proven live.'
|
|
319
|
+
: 'Exact native stop protection is unconfirmed.'}` +
|
|
320
|
+
`${openPersistenceError ? ` Durable OPEN persistence failed (${openPersistenceError}).` : ''}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const persistTerminalOpen = async () => {
|
|
324
|
+
if (recoveryQuantity > input.authorizedQuantity) {
|
|
325
|
+
return `final fill ${recoveryQuantity} exceeds authorized quantity ${input.authorizedQuantity}`;
|
|
326
|
+
}
|
|
327
|
+
const entryPrice = foundOrder
|
|
328
|
+
? confirmedWave9EntryPrice(foundOrder, input.side, input.stopPrice)
|
|
329
|
+
: undefined;
|
|
330
|
+
try {
|
|
331
|
+
await input.ledger.markOpen(input.candidateId, {
|
|
332
|
+
filledQuantity: recoveryQuantity,
|
|
333
|
+
...(foundOrder ? { exchangeOrderId: foundOrder.id } : {}),
|
|
334
|
+
...(entryPrice !== undefined ? { entryPrice } : {}),
|
|
335
|
+
});
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const markOpenError = formatError(err);
|
|
340
|
+
if (foundOrder
|
|
341
|
+
&& typeof input.ledger.reconcileOpenFillFromExactOrder === 'function') {
|
|
342
|
+
try {
|
|
343
|
+
await input.ledger.reconcileOpenFillFromExactOrder(input.candidateId, {
|
|
344
|
+
filledQuantity: recoveryQuantity,
|
|
345
|
+
exchangeOrderId: foundOrder.id,
|
|
346
|
+
...(entryPrice !== undefined ? { entryPrice } : {}),
|
|
347
|
+
});
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
catch (reconcileError) {
|
|
351
|
+
return `${markOpenError}; exact terminal fill reconciliation failed: ${formatError(reconcileError)}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return markOpenError;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const openPersistenceError = await persistTerminalOpen();
|
|
358
|
+
const maximumRecoverableQuantity = Math.min(recoveryQuantity, input.authorizedQuantity);
|
|
359
|
+
let currentObservation = observation;
|
|
360
|
+
if (currentObservation.status !== 'visible') {
|
|
361
|
+
const flat = await confirmWave9LivePositionFlat(input.adapter, input.symbol);
|
|
362
|
+
if (flat.confirmed) {
|
|
363
|
+
if (openPersistenceError) {
|
|
364
|
+
return {
|
|
365
|
+
status: 'manual_intervention',
|
|
366
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: exact terminal fill and stable flatness ` +
|
|
367
|
+
`were proven, but durable OPEN persistence failed (${openPersistenceError})`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
await input.ledger.markClosed(input.candidateId);
|
|
372
|
+
return {
|
|
373
|
+
status: 'terminal',
|
|
374
|
+
detail: `${context}; exact terminal fill is no longer present and exchange flatness ` +
|
|
375
|
+
`remained stable (${flat.detail}); durable ownership is CLOSED`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
return {
|
|
380
|
+
status: 'manual_intervention',
|
|
381
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: exact terminal fill and stable flatness ` +
|
|
382
|
+
`were proven, but CLOSED persistence failed (${formatError(err)})`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Visibility can lag the exact order lookup. Re-read only as positive
|
|
387
|
+
// identity evidence after the stable-flat window; never interpret a
|
|
388
|
+
// failed/unknown flat proof as permission to close a symbol.
|
|
389
|
+
currentObservation = await observeWave9LiveResidual(input.adapter, input.symbol, input.side);
|
|
390
|
+
}
|
|
391
|
+
const quantity = currentObservation.status === 'visible'
|
|
392
|
+
? currentObservation.quantity
|
|
393
|
+
: undefined;
|
|
394
|
+
if (quantity === undefined
|
|
395
|
+
|| !Number.isFinite(quantity)
|
|
396
|
+
|| quantity <= 0
|
|
397
|
+
|| quantity > maximumRecoverableQuantity) {
|
|
398
|
+
return {
|
|
399
|
+
status: 'manual_intervention',
|
|
400
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: exact terminal entry fill is proven, but ` +
|
|
401
|
+
`current same-symbol exposure is not safely candidate-bound (${currentObservation.detail}; ` +
|
|
402
|
+
`maximum attributable quantity ${maximumRecoverableQuantity}). Refusing native-stop ` +
|
|
403
|
+
`replacement or symbol-wide flatten.${openPersistenceError
|
|
404
|
+
? ` Durable OPEN persistence also failed (${openPersistenceError}).`
|
|
405
|
+
: ' Durable ownership remains OPEN.'}`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (!openPersistenceError) {
|
|
409
|
+
let stopConfirmed = false;
|
|
410
|
+
try {
|
|
411
|
+
stopConfirmed = await input.confirmStop({
|
|
412
|
+
candidateId: input.candidateId,
|
|
413
|
+
symbol: input.symbol,
|
|
414
|
+
entryClientOrderId: input.clientOrderId,
|
|
415
|
+
entrySide: input.side,
|
|
416
|
+
stopPrice: input.stopPrice,
|
|
417
|
+
filledQuantity: recoveryQuantity,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
stopConfirmed = false;
|
|
422
|
+
}
|
|
423
|
+
if (stopConfirmed) {
|
|
424
|
+
return {
|
|
425
|
+
status: 'protected',
|
|
426
|
+
detail: `${context}; the deterministic entry was recovered OPEN with an exact proven native stop`,
|
|
427
|
+
...(foundOrder ? { order: foundOrder } : {}),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// A positive exact fill establishes durable ownership, but does not by
|
|
432
|
+
// itself authorize a symbol-wide mutation. The helper re-observes current
|
|
433
|
+
// exposure and runs close/re-protection only when side and quantity still
|
|
434
|
+
// prove that the residual can belong to this candidate.
|
|
435
|
+
const flatten = await flattenFailedWave9LiveEntry(input.adapter, input.ledger, input.symbol, input.candidateId, {
|
|
436
|
+
side: input.side,
|
|
437
|
+
maximumQuantity: Math.min(recoveryQuantity, input.authorizedQuantity),
|
|
438
|
+
}, input.residualProtector);
|
|
439
|
+
const outcome = flattenRecoveryOutcome(flatten, openPersistenceError
|
|
440
|
+
? `${context}; OPEN persistence failed (${openPersistenceError}); ${observation.detail}`
|
|
441
|
+
: `${context}; exact stop was unavailable; ${observation.detail}`, openPersistenceError === undefined);
|
|
442
|
+
return foundOrder && outcome.status === 'protected'
|
|
443
|
+
? { ...outcome, order: foundOrder }
|
|
444
|
+
: outcome;
|
|
445
|
+
};
|
|
446
|
+
const exactStopProven = async (quantity) => {
|
|
447
|
+
let stopConfirmed = false;
|
|
448
|
+
try {
|
|
449
|
+
stopConfirmed = await input.confirmStop({
|
|
450
|
+
candidateId: input.candidateId,
|
|
451
|
+
symbol: input.symbol,
|
|
452
|
+
entryClientOrderId: input.clientOrderId,
|
|
453
|
+
entrySide: input.side,
|
|
454
|
+
stopPrice: input.stopPrice,
|
|
455
|
+
filledQuantity: quantity,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
stopConfirmed = false;
|
|
460
|
+
}
|
|
461
|
+
return stopConfirmed;
|
|
462
|
+
};
|
|
463
|
+
const terminalizeOpenEntry = async (order) => {
|
|
464
|
+
try {
|
|
465
|
+
await input.adapter.cancelOrder(order.id, input.symbol);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
return { error: `exact open-entry cancellation failed: ${formatError(err)}` };
|
|
469
|
+
}
|
|
470
|
+
const afterCancel = await resolveWave9EntryCid(input.adapter, input.clientOrderId, input.symbol);
|
|
471
|
+
if (afterCancel.status === 'confirmed_absent') {
|
|
472
|
+
return order.filled === 0
|
|
473
|
+
? { confirmedAbsent: true }
|
|
474
|
+
: { error: 'positive-fill entry disappeared after cancellation; final fill is unknown' };
|
|
475
|
+
}
|
|
476
|
+
const resolved = candidateBoundFoundOrder(afterCancel, input.clientOrderId, input.symbol, input.side);
|
|
477
|
+
if (!resolved) {
|
|
478
|
+
return {
|
|
479
|
+
error: afterCancel.status === 'unknown'
|
|
480
|
+
? `post-cancel entry state is unknown (${afterCancel.detail})`
|
|
481
|
+
: 'post-cancel entry identity is mismatched',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return resolved.status === 'open'
|
|
485
|
+
? { error: 'exact entry order remains open after cancellation' }
|
|
486
|
+
: { order: resolved };
|
|
487
|
+
};
|
|
488
|
+
const acknowledgedOrder = input.acknowledgedOrder
|
|
489
|
+
? candidateBoundFoundOrder({ status: 'found', order: input.acknowledgedOrder }, input.clientOrderId, input.symbol, input.side)
|
|
490
|
+
: undefined;
|
|
491
|
+
let resolution = acknowledgedOrder
|
|
492
|
+
? { status: 'found', order: acknowledgedOrder }
|
|
493
|
+
: await resolveWave9EntryCid(input.adapter, input.clientOrderId, input.symbol);
|
|
494
|
+
let foundOrder = candidateBoundFoundOrder(resolution, input.clientOrderId, input.symbol, input.side);
|
|
495
|
+
if (resolution.status === 'found' && !foundOrder) {
|
|
496
|
+
return {
|
|
497
|
+
status: 'manual_intervention',
|
|
498
|
+
detail: 'IMMEDIATE MANUAL INTERVENTION REQUIRED: client-order lookup returned mismatched candidate identity',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
let residual = await observeWave9LiveResidual(input.adapter, input.symbol, input.side);
|
|
502
|
+
let foundFilled = foundOrder
|
|
503
|
+
&& Number.isFinite(foundOrder.filled)
|
|
504
|
+
&& foundOrder.filled > 0
|
|
505
|
+
? foundOrder.filled
|
|
506
|
+
: undefined;
|
|
507
|
+
if (foundFilled !== undefined) {
|
|
508
|
+
if (foundOrder.status === 'open') {
|
|
509
|
+
const terminalized = await terminalizeOpenEntry(foundOrder);
|
|
510
|
+
if (!terminalized.order || terminalized.order.filled <= 0) {
|
|
511
|
+
try {
|
|
512
|
+
await input.ledger.markOpen(input.candidateId, {
|
|
513
|
+
exchangeOrderId: foundOrder.id,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// The manual outcome remains load-bearing even if persistence fails.
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
status: 'manual_intervention',
|
|
521
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: positive partial fill has an unproven ` +
|
|
522
|
+
`final quantity (${terminalized.error ?? 'post-cancel identity unavailable'}); ` +
|
|
523
|
+
'durable ownership remains OPEN without a frozen filled quantity',
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
foundOrder = terminalized.order;
|
|
527
|
+
foundFilled = terminalized.order.filled;
|
|
528
|
+
}
|
|
529
|
+
return recoverCandidateExposure(foundOrder, foundFilled, residual, `submit threw (${formatError(input.submitError)}) and the exact CID shows a positive fill`, true);
|
|
530
|
+
}
|
|
531
|
+
if (resolution.status === 'unknown' && residual.status === 'visible') {
|
|
532
|
+
return recoverCandidateExposure(undefined, residual.quantity, residual, `submit threw and CID resolution is unknown with a same-side residual`, false);
|
|
533
|
+
}
|
|
534
|
+
// A proven candidate-bound close-all stop is itself strong evidence that
|
|
535
|
+
// LiveAdapter progressed past submission even if position visibility lags.
|
|
536
|
+
if (resolution.status === 'unknown'
|
|
537
|
+
&& await exactStopProven(input.authorizedQuantity)) {
|
|
538
|
+
return recoverCandidateExposure(undefined, input.authorizedQuantity, residual, 'submit acknowledgement is unknown, but its exact candidate-bound native stop is live', false);
|
|
539
|
+
}
|
|
540
|
+
if (foundOrder?.status === 'open' && foundOrder.filled === 0) {
|
|
541
|
+
const terminalized = await terminalizeOpenEntry(foundOrder);
|
|
542
|
+
if (terminalized.error) {
|
|
543
|
+
return {
|
|
544
|
+
status: 'manual_intervention',
|
|
545
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: the deterministic zero-fill entry remains ` +
|
|
546
|
+
`nonterminal (${terminalized.error})`,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (terminalized.confirmedAbsent) {
|
|
550
|
+
resolution = { status: 'confirmed_absent' };
|
|
551
|
+
foundOrder = undefined;
|
|
552
|
+
}
|
|
553
|
+
else if (terminalized.order) {
|
|
554
|
+
resolution = { status: 'found', order: terminalized.order };
|
|
555
|
+
foundOrder = terminalized.order;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const flat = await confirmWave9LivePositionFlat(input.adapter, input.symbol);
|
|
559
|
+
let finalResolution = await resolveWave9EntryCid(input.adapter, input.clientOrderId, input.symbol);
|
|
560
|
+
let finalOrder = candidateBoundFoundOrder(finalResolution, input.clientOrderId, input.symbol, input.side);
|
|
561
|
+
if (finalResolution.status === 'found' && !finalOrder) {
|
|
562
|
+
return {
|
|
563
|
+
status: 'manual_intervention',
|
|
564
|
+
detail: 'IMMEDIATE MANUAL INTERVENTION REQUIRED: final client-order lookup returned mismatched candidate identity',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (finalOrder?.status === 'open') {
|
|
568
|
+
const openOrder = finalOrder;
|
|
569
|
+
const terminalized = await terminalizeOpenEntry(openOrder);
|
|
570
|
+
if (terminalized.error || (!terminalized.order && !terminalized.confirmedAbsent)) {
|
|
571
|
+
if (openOrder.filled > 0) {
|
|
572
|
+
try {
|
|
573
|
+
await input.ledger.markOpen(input.candidateId, {
|
|
574
|
+
exchangeOrderId: openOrder.id,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// The explicit manual outcome below remains authoritative.
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
status: 'manual_intervention',
|
|
583
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: final exact entry order is nonterminal ` +
|
|
584
|
+
`(${terminalized.error ?? 'post-cancel state unavailable'})`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (terminalized.confirmedAbsent) {
|
|
588
|
+
finalResolution = { status: 'confirmed_absent' };
|
|
589
|
+
finalOrder = undefined;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
finalOrder = terminalized.order;
|
|
593
|
+
finalResolution = { status: 'found', order: terminalized.order };
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
residual = await observeWave9LiveResidual(input.adapter, input.symbol, input.side);
|
|
597
|
+
if (finalOrder && Number.isFinite(finalOrder.filled) && finalOrder.filled > 0) {
|
|
598
|
+
return recoverCandidateExposure(finalOrder, finalOrder.filled, residual, 'the exact CID resolved to a positive fill after the settlement window', true);
|
|
599
|
+
}
|
|
600
|
+
if (finalResolution.status === 'unknown' && residual.status === 'visible') {
|
|
601
|
+
return recoverCandidateExposure(undefined, residual.quantity, residual, 'CID resolution remained unknown and a residual appeared during the settlement window', false);
|
|
602
|
+
}
|
|
603
|
+
if (finalResolution.status === 'unknown'
|
|
604
|
+
&& await exactStopProven(input.authorizedQuantity)) {
|
|
605
|
+
return recoverCandidateExposure(undefined, input.authorizedQuantity, residual, 'CID resolution remained unknown, but an exact candidate-bound native stop appeared', false);
|
|
606
|
+
}
|
|
607
|
+
const terminalOrder = finalOrder?.status === 'canceled' && finalOrder.filled === 0;
|
|
608
|
+
let terminalFlat = flat;
|
|
609
|
+
if (finalResolution.status === 'confirmed_absent' || terminalOrder) {
|
|
610
|
+
// Bind terminalization to a fresh stable-flat window *after* exact CID
|
|
611
|
+
// terminal proof. The earlier settlement window can go stale while the
|
|
612
|
+
// final lookup/cancellation runs.
|
|
613
|
+
terminalFlat = await confirmWave9LivePositionFlat(input.adapter, input.symbol);
|
|
614
|
+
}
|
|
615
|
+
if (terminalFlat.confirmed
|
|
616
|
+
&& (finalResolution.status === 'confirmed_absent' || terminalOrder)) {
|
|
617
|
+
try {
|
|
618
|
+
await input.ledger.markClosed(input.candidateId);
|
|
619
|
+
return {
|
|
620
|
+
status: 'terminal',
|
|
621
|
+
detail: `submit threw (${formatError(input.submitError)}), but the exact client order is ` +
|
|
622
|
+
`${terminalOrder ? 'canceled with zero fill' : 'confirmed absent'} and the exchange ` +
|
|
623
|
+
`remained stably flat`,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
return {
|
|
628
|
+
status: 'manual_intervention',
|
|
629
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: entry absence and stable flat were proven, ` +
|
|
630
|
+
`but CLOSED persistence failed (${formatError(err)})`,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
status: 'manual_intervention',
|
|
636
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: submit outcome remains ambiguous; client-order ` +
|
|
637
|
+
`resolution is ${finalResolution.status} and stable flat is ${terminalFlat.confirmed
|
|
638
|
+
? 'present but insufficient without deterministic absence'
|
|
639
|
+
: `unconfirmed (${terminalFlat.detail})`}. The entry capability is consumed and must not be retried`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
11
642
|
/** Build a PortfolioSnapshot from current simulator state (paper mode). */
|
|
12
643
|
function buildPortfolioSnapshotFromSimulator(simulator) {
|
|
13
644
|
const state = simulator.getState();
|
|
@@ -33,8 +664,14 @@ function buildPortfolioSnapshotFromSimulator(simulator) {
|
|
|
33
664
|
}
|
|
34
665
|
/** Build a PortfolioSnapshot from adapter (live mode). */
|
|
35
666
|
async function buildPortfolioSnapshotFromAdapter(adapter) {
|
|
667
|
+
// Null-honest balance read where the adapter offers one (LiveAdapter) —
|
|
668
|
+
// getBalance() collapses a FAILED fetch to a phantom-zero object, which
|
|
669
|
+
// reads as walletTotal=0 against the real sessionStartNav → ~−100%
|
|
670
|
+
// drawdown → RED zone rejects the entry for the wrong reason (and sizing
|
|
671
|
+
// runs against walletAvailable=0).
|
|
672
|
+
const balanceRead = adapter.getBalanceOrNull?.() ?? adapter.getBalance();
|
|
36
673
|
const [balance, positions] = await Promise.all([
|
|
37
|
-
|
|
674
|
+
balanceRead,
|
|
38
675
|
adapter.getPositionsOrNull(),
|
|
39
676
|
]);
|
|
40
677
|
// null = positions fetch FAILED (429 / weight-paced / transient). If we
|
|
@@ -47,6 +684,11 @@ async function buildPortfolioSnapshotFromAdapter(adapter) {
|
|
|
47
684
|
throw new Error('Cannot build portfolio snapshot — position fetch failed (exchange data ' +
|
|
48
685
|
'unavailable, likely Binance 429 or transient). Order NOT placed; retry next heartbeat.');
|
|
49
686
|
}
|
|
687
|
+
// Same contract for the balance leg: null = fetch failed, NOT a zero wallet.
|
|
688
|
+
if (balance === null) {
|
|
689
|
+
throw new Error('Cannot build portfolio snapshot — balance fetch failed (exchange data ' +
|
|
690
|
+
'unavailable, likely Binance 429 or transient). Order NOT placed; retry next heartbeat.');
|
|
691
|
+
}
|
|
50
692
|
const walletTotal = getQuoteWalletBalance(balance);
|
|
51
693
|
// Use the session-start NAV captured once at initialization, not the current balance.
|
|
52
694
|
// If not available (shouldn't happen after init), fall back to current balance.
|
|
@@ -76,6 +718,65 @@ async function buildPortfolioSnapshotFromAdapter(adapter) {
|
|
|
76
718
|
};
|
|
77
719
|
}
|
|
78
720
|
export async function createOrderTool(args, deps) {
|
|
721
|
+
const wave9Claimed = isWave9ManagedPosition({
|
|
722
|
+
setupType: args.setup_type,
|
|
723
|
+
missionId: args.mission_id,
|
|
724
|
+
})
|
|
725
|
+
|| args.wave9_admission_token !== undefined
|
|
726
|
+
|| args.candidate_id !== undefined;
|
|
727
|
+
if (wave9Claimed && deps.wave9VenueCheck?.() !== 'binance') {
|
|
728
|
+
return { error: 'Wave 9 order refused: execution is pinned to Binance USD-M futures.' };
|
|
729
|
+
}
|
|
730
|
+
if (wave9Claimed && !deps.operationLock) {
|
|
731
|
+
return { error: 'Wave 9 order refused: process-local trading operation lock is unavailable.' };
|
|
732
|
+
}
|
|
733
|
+
const initialWave9Mode = wave9Claimed ? currentWave9ExecutionMode(deps) : null;
|
|
734
|
+
// Wave 9 is a dedicated targetless capability path. Never let a generic
|
|
735
|
+
// order impersonate it, and require the adapter + mutable runtime mode to
|
|
736
|
+
// agree exactly before accepting either PAPER or LIVE execution.
|
|
737
|
+
if (wave9Claimed) {
|
|
738
|
+
if (args.setup_type !== WAVE9_BUNDLE_SETUP_TYPE) {
|
|
739
|
+
return { error: `Wave 9 order refused: setup_type must be exactly ${WAVE9_BUNDLE_SETUP_TYPE}.` };
|
|
740
|
+
}
|
|
741
|
+
if (!initialWave9Mode) {
|
|
742
|
+
return {
|
|
743
|
+
error: 'Wave 9 order refused: capability execution requires an exact PAPER/paper-adapter ' +
|
|
744
|
+
'or LIVE/live-adapter runtime match.',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
if ((deps.approvalMode ?? 'off') !== 'off') {
|
|
748
|
+
return { error: 'Wave 9 order refused: approval mode must be off for immediate mechanical execution.' };
|
|
749
|
+
}
|
|
750
|
+
if (!deps.wave9AdmissionGuard || !args.wave9_admission_token || !args.candidate_id || !args.mission_id) {
|
|
751
|
+
return { error: 'Wave 9 order refused: a dedicated short-lived admission token and exact candidate/mission identity are required.' };
|
|
752
|
+
}
|
|
753
|
+
if (args.type.toLowerCase() !== 'market' || args.price !== undefined || args.target_price !== undefined) {
|
|
754
|
+
return { error: 'Wave 9 order refused: the frozen policy requires a targetless market order with no limit price.' };
|
|
755
|
+
}
|
|
756
|
+
const canonical = args.symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
757
|
+
if (!canonical.endsWith('USDT') || canonical.length <= 4) {
|
|
758
|
+
return { error: `Wave 9 order refused: unsupported execution symbol ${args.symbol}.` };
|
|
759
|
+
}
|
|
760
|
+
if (args.stopPrice === undefined || !Number.isFinite(args.stopPrice) || args.stopPrice <= 0) {
|
|
761
|
+
return { error: 'Wave 9 order refused: a positive finite initial stop is required.' };
|
|
762
|
+
}
|
|
763
|
+
if (initialWave9Mode === 'LIVE') {
|
|
764
|
+
if (!deps.captureWave9AccountFingerprint
|
|
765
|
+
|| !deps.confirmWave9LiveStop
|
|
766
|
+
|| !deps.wave9LiveLedger) {
|
|
767
|
+
return {
|
|
768
|
+
error: 'Wave 9 live order refused: authoritative live account capture, native-stop confirmation, ' +
|
|
769
|
+
'and the durable execution ledger are required.',
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (!bracketsEnabled(loadBracketMode())) {
|
|
773
|
+
return { error: 'Wave 9 live order refused: exchange-native bracket protection is not enabled.' };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Persist one unified USD-M key regardless of whether an older agent
|
|
777
|
+
// echoes BTCUSDT or BTC/USDT. Guard matching remains canonical.
|
|
778
|
+
args = { ...args, symbol: `${canonical.slice(0, -4)}/USDT:USDT` };
|
|
779
|
+
}
|
|
79
780
|
// ---- Readiness gate (live mode only) ----
|
|
80
781
|
if (deps.adapter.readiness !== 'READY') {
|
|
81
782
|
return { error: `Trading blocked: adapter is ${deps.adapter.readiness}. Wait for initialization to complete.` };
|
|
@@ -86,7 +787,7 @@ export async function createOrderTool(args, deps) {
|
|
|
86
787
|
// same data the heartbeat reassessment uses to detect scorecard reversal /
|
|
87
788
|
// regime flip — if the agent can't produce it at entry, it can't produce
|
|
88
789
|
// it mid-trade either.
|
|
89
|
-
const gateCheck = validateCreateOrder(args, deps.adapter.isLive);
|
|
790
|
+
const gateCheck = validateCreateOrder(args, deps.adapter.isLive && !wave9Claimed);
|
|
90
791
|
if (!gateCheck.ok) {
|
|
91
792
|
return { error: gateCheck.error ?? 'Invalid create_order arguments.' };
|
|
92
793
|
}
|
|
@@ -108,6 +809,38 @@ export async function createOrderTool(args, deps) {
|
|
|
108
809
|
if (type === 'limit' && (!args.price || args.price <= 0)) {
|
|
109
810
|
return { error: 'Limit orders require a positive price.' };
|
|
110
811
|
}
|
|
812
|
+
if (args.mission_id !== undefined
|
|
813
|
+
&& (typeof args.mission_id !== 'string'
|
|
814
|
+
|| !/^[A-Za-z0-9_-]{1,128}$/.test(args.mission_id))) {
|
|
815
|
+
return { error: 'mission_id must be 1-128 letters, numbers, underscores, or hyphens.' };
|
|
816
|
+
}
|
|
817
|
+
if (!wave9Claimed
|
|
818
|
+
&& deps.adapter.isLive
|
|
819
|
+
&& deps.checkWave9LiveSymbolOwnership
|
|
820
|
+
&& !deps.operationLock) {
|
|
821
|
+
return {
|
|
822
|
+
error: 'Generic live create_order refused: the shared trading operation lock is required ' +
|
|
823
|
+
'while Wave 9 LIVE ownership checks are enabled.',
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (!wave9Claimed && !deps.adapter.isLive) {
|
|
827
|
+
try {
|
|
828
|
+
deps.refreshPaperState?.();
|
|
829
|
+
const positions = await deps.adapter.getPositionsOrNull();
|
|
830
|
+
if (positions === null) {
|
|
831
|
+
return { error: 'create_order refused: paper position state unavailable for Wave 9 isolation check.' };
|
|
832
|
+
}
|
|
833
|
+
if (positions.some(isWave9ManagedPosition)) {
|
|
834
|
+
return {
|
|
835
|
+
error: 'Generic create_order refused: the paper account contains an atomic Wave 9 position. ' +
|
|
836
|
+
'The Wave 9 forward book must remain isolated until all of its positions are flat.',
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
return { error: `create_order refused: Wave 9 isolation check failed (${formatError(err)}).` };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
111
844
|
// In paper mode, fetch latest ticker + order book for realistic fills
|
|
112
845
|
let ticker = null;
|
|
113
846
|
if (!deps.adapter.isLive) {
|
|
@@ -134,211 +867,678 @@ export async function createOrderTool(args, deps) {
|
|
|
134
867
|
if (!ticker) {
|
|
135
868
|
return { error: `Failed to fetch current price for ${args.symbol}` };
|
|
136
869
|
}
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
type: type,
|
|
143
|
-
amount: args.amount,
|
|
144
|
-
price: orderPrice,
|
|
145
|
-
stopPrice: args.stopPrice,
|
|
146
|
-
targetPrice: args.target_price,
|
|
147
|
-
};
|
|
148
|
-
// Build portfolio snapshot based on mode
|
|
149
|
-
let portfolio;
|
|
150
|
-
let consecutiveLosses = 0;
|
|
151
|
-
if (!deps.adapter.isLive) {
|
|
152
|
-
const simulator = deps.adapter.getSimulator();
|
|
153
|
-
portfolio = buildPortfolioSnapshotFromSimulator(simulator);
|
|
154
|
-
consecutiveLosses = computeConsecutiveLosses(simulator.getState().tradeHistory);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
portfolio = await buildPortfolioSnapshotFromAdapter(deps.adapter);
|
|
158
|
-
// In live mode, consecutive losses would come from intelligence DB — use 0 for now
|
|
159
|
-
}
|
|
160
|
-
// Bracket enforcement only applies in live mode when the feature is enabled.
|
|
161
|
-
// Paper mode uses the stop-watcher and doesn't care about these flags.
|
|
162
|
-
let bracketEnforcement;
|
|
163
|
-
if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
|
|
164
|
-
bracketEnforcement = loadBracketRequirements();
|
|
165
|
-
}
|
|
166
|
-
const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
|
|
167
|
-
volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
|
|
168
|
-
consecutiveLosses,
|
|
169
|
-
bracketEnforcement,
|
|
170
|
-
});
|
|
171
|
-
if (!riskCheck.allowed) {
|
|
172
|
-
const reasons = riskCheck.violations.map(v => v.message).join('; ');
|
|
173
|
-
return {
|
|
174
|
-
error: `Order REJECTED by risk gate [${riskCheck.drawdownZone} zone]: ${reasons}`,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
// Build position metadata from optional args (only if any metadata provided)
|
|
178
|
-
// Sanitize numeric metadata — reject non-finite values, clamp ranges
|
|
179
|
-
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
180
|
-
const clamp = (v, min, max) => {
|
|
181
|
-
const n = num(v);
|
|
182
|
-
return n != null ? Math.max(min, Math.min(max, n)) : undefined;
|
|
183
|
-
};
|
|
184
|
-
const hasMetadata = args.setup_type || args.thesis || args.target_price != null
|
|
185
|
-
|| args.regime || args.scorecard_verdict || args.confluence_score != null
|
|
186
|
-
|| args.invalidation_price != null || args.realization_rule != null;
|
|
187
|
-
const metadata = hasMetadata ? {
|
|
188
|
-
setupType: args.setup_type,
|
|
189
|
-
thesis: args.thesis?.slice(0, 500), // Cap length
|
|
190
|
-
stopPrice: num(args.stopPrice),
|
|
191
|
-
targetPrice: num(args.target_price),
|
|
192
|
-
regime: args.regime,
|
|
193
|
-
regimeConfidence: clamp(args.regime_confidence, 0, 100),
|
|
194
|
-
scorecardVerdict: args.scorecard_verdict,
|
|
195
|
-
confluenceScore: clamp(args.confluence_score, 0, 10),
|
|
196
|
-
invalidationPrice: num(args.invalidation_price),
|
|
197
|
-
realizationRule: sanitizeRealizationRule(args.realization_rule),
|
|
198
|
-
} : undefined;
|
|
199
|
-
// ---- Approval-mode branches ----
|
|
200
|
-
// Both shadow and per_trade need full v2.10.0 metadata so the proposal is
|
|
201
|
-
// operator-meaningful. Metadata is the agent's responsibility; ingest creds
|
|
202
|
-
// are the operator's. Separate so the error messages can be precise.
|
|
203
|
-
const argsMetadataComplete = !!(args.thesis &&
|
|
204
|
-
args.setup_type &&
|
|
205
|
-
args.target_price != null &&
|
|
206
|
-
args.stopPrice != null &&
|
|
207
|
-
args.regime &&
|
|
208
|
-
typeof args.regime_confidence === 'number' &&
|
|
209
|
-
args.scorecard_verdict &&
|
|
210
|
-
typeof args.confluence_score === 'number');
|
|
211
|
-
const proposalPathWired = !!(deps.proposalManager && deps.userId);
|
|
212
|
-
const approvalMode = deps.approvalMode ?? 'off';
|
|
213
|
-
// per_trade mode: propose and EARLY-RETURN. The agent's call doesn't fire;
|
|
214
|
-
// the operator approves and ProposalDecisionListener fires via this same
|
|
215
|
-
// tool with proposalManager omitted (which takes the real path below).
|
|
216
|
-
//
|
|
217
|
-
// If proposalManager isn't wired (operator set approval.mode=per_trade in
|
|
218
|
-
// config but ingest creds aren't present), we fall through to the normal
|
|
219
|
-
// fire path — the startup warning is the operator's signal. We never trap
|
|
220
|
-
// the agent inside an unfireable per_trade branch.
|
|
221
|
-
if (approvalMode === 'per_trade' && proposalPathWired) {
|
|
222
|
-
if (!argsMetadataComplete) {
|
|
223
|
-
return {
|
|
224
|
-
error: 'approval.mode=per_trade requires the full v2.10.0 metadata block on every create_order: ' +
|
|
225
|
-
'thesis, setup_type, target_price, stopPrice, regime, regime_confidence, ' +
|
|
226
|
-
'scorecard_verdict, confluence_score. The operator approves on this metadata.',
|
|
227
|
-
};
|
|
228
|
-
}
|
|
870
|
+
// Once market data exists, consume the capability before any runtime
|
|
871
|
+
// geometry or generic-gate decision. A delayed stop that is now on the
|
|
872
|
+
// wrong side is an execution divergence and must latch the daily event.
|
|
873
|
+
let wave9Lease;
|
|
874
|
+
if (wave9Claimed) {
|
|
229
875
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
876
|
+
wave9Lease = await deps.operationLock.withAccountLock(`wave9-begin-create:${args.candidate_id ?? args.symbol}`, async () => {
|
|
877
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
878
|
+
throw new Error('adapter or trading mode changed before Wave 9 admission began');
|
|
879
|
+
}
|
|
880
|
+
let accountFingerprint;
|
|
881
|
+
if (initialWave9Mode === 'LIVE') {
|
|
882
|
+
const ownership = deps.checkWave9LiveSymbolOwnership
|
|
883
|
+
? await deps.checkWave9LiveSymbolOwnership(args.symbol)
|
|
884
|
+
: await inspectWave9LiveSymbolOwnership(deps.adapter, deps.wave9LiveLedger, args.symbol);
|
|
885
|
+
if (ownership.status !== 'unowned'
|
|
886
|
+
|| ownership.reason === 'authoritative_symbol_flat') {
|
|
887
|
+
throw new Error(`candidate-symbol ownership must be refreshed after ` +
|
|
888
|
+
`${ownership.status}:${ownership.reason}`);
|
|
889
|
+
}
|
|
890
|
+
accountFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
deps.refreshPaperState?.();
|
|
894
|
+
deps.adapter.getSimulator().getSessionStartNav();
|
|
895
|
+
accountFingerprint = captureWave9PaperAccountSnapshot(deps.adapter).fingerprint;
|
|
896
|
+
}
|
|
897
|
+
return deps.wave9AdmissionGuard.beginEntry({
|
|
898
|
+
token: args.wave9_admission_token,
|
|
899
|
+
candidateId: args.candidate_id,
|
|
900
|
+
missionId: args.mission_id,
|
|
901
|
+
symbol: args.symbol,
|
|
902
|
+
side,
|
|
903
|
+
type,
|
|
904
|
+
quantity: args.amount,
|
|
905
|
+
stopPrice: args.stopPrice,
|
|
906
|
+
setupType: args.setup_type,
|
|
907
|
+
accountFingerprint,
|
|
908
|
+
tradingMode: initialWave9Mode,
|
|
909
|
+
});
|
|
249
910
|
});
|
|
250
|
-
return {
|
|
251
|
-
status: 'pending_approval',
|
|
252
|
-
proposal_id: result.proposalUuid,
|
|
253
|
-
hard_expires_at: result.hardExpiresAt.toISOString(),
|
|
254
|
-
setup_bucket: result.setupBucket,
|
|
255
|
-
message: `Order awaits operator approval (${result.setupBucket} bucket, expires ` +
|
|
256
|
-
`${result.hardExpiresAt.toISOString()}). Do not call create_order again ` +
|
|
257
|
-
`for this setup; the listener will fire on approval. If the operator ` +
|
|
258
|
-
`requests modifications in chat, call create_order with supersedes_id=` +
|
|
259
|
-
`<id from the chat-context header>.`,
|
|
260
|
-
};
|
|
261
911
|
}
|
|
262
912
|
catch (err) {
|
|
263
|
-
return { error: `
|
|
913
|
+
return { error: `Wave 9 admission refused: ${formatError(err)}` };
|
|
264
914
|
}
|
|
265
915
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
916
|
+
const rejectWave9Divergence = (message, code) => {
|
|
917
|
+
if (wave9Lease) {
|
|
918
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, code);
|
|
919
|
+
wave9Lease = undefined;
|
|
920
|
+
}
|
|
921
|
+
return { error: `${wave9Claimed ? 'Wave 9 runtime divergence; daily event latched. ' : ''}${message}` };
|
|
922
|
+
};
|
|
923
|
+
// From beginEntry onward, every unexpected failure must release the
|
|
924
|
+
// in-flight capability. Explicit geometry/risk divergences still use the
|
|
925
|
+
// latching helper above; infrastructure/pure-computation throws are
|
|
926
|
+
// retryable because no exchange mutation has been attempted.
|
|
927
|
+
try {
|
|
928
|
+
// ---- Pre-trade risk gate ----
|
|
929
|
+
const orderPrice = type === 'limit' ? args.price : ticker.last;
|
|
930
|
+
if (wave9Claimed && args.stopPrice != null) {
|
|
931
|
+
if (!Number.isFinite(args.stopPrice) || args.stopPrice <= 0) {
|
|
932
|
+
return rejectWave9Divergence('stopPrice must be a positive finite number.', 'invalid_stop_price');
|
|
933
|
+
}
|
|
934
|
+
if ((side === 'buy' && args.stopPrice >= orderPrice) ||
|
|
935
|
+
(side === 'sell' && args.stopPrice <= orderPrice)) {
|
|
936
|
+
return rejectWave9Divergence(`stopPrice is on the wrong side of entry ${orderPrice} for a ${side} order.`, 'actual_market_stop_geometry_diverged');
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// ---- Generic-path protective geometry gate (issue #200) ----
|
|
940
|
+
// The wrong-side check above ran ONLY for wave9 orders; a generic entry with
|
|
941
|
+
// its stop/invalidation on the profit side passed validation and was closed
|
|
942
|
+
// by the stop-watcher seconds later (7 sub-5-minute kills on the 2026-07 HL
|
|
943
|
+
// soak — one spawned the −$117.86 journal fabrication of issue #199).
|
|
944
|
+
if (!wave9Claimed) {
|
|
945
|
+
const geometryError = validateProtectiveGeometry({
|
|
272
946
|
side: side,
|
|
273
|
-
|
|
274
|
-
size: args.amount,
|
|
275
|
-
proposedEntry: orderPrice,
|
|
947
|
+
refPrice: orderPrice,
|
|
276
948
|
stopPrice: args.stopPrice,
|
|
949
|
+
invalidationPrice: args.invalidation_price,
|
|
277
950
|
targetPrice: args.target_price,
|
|
278
|
-
riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
|
|
279
|
-
thesis: args.thesis,
|
|
280
|
-
setupType: args.setup_type,
|
|
281
|
-
regime: args.regime,
|
|
282
|
-
regimeConfidence: args.regime_confidence,
|
|
283
|
-
scorecardVerdict: args.scorecard_verdict,
|
|
284
|
-
confluenceScore: args.confluence_score,
|
|
285
|
-
...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
|
|
286
|
-
// Phase A telemetry — agent's create_order ALSO fires the real order
|
|
287
|
-
// synchronously below. Hidden from operator UI by origin='shadow'.
|
|
288
|
-
origin: 'shadow',
|
|
289
951
|
});
|
|
952
|
+
if (geometryError) {
|
|
953
|
+
return { error: `create_order rejected (protective geometry): ${geometryError}` };
|
|
954
|
+
}
|
|
290
955
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
956
|
+
const proposed = {
|
|
957
|
+
symbol: args.symbol,
|
|
958
|
+
side: side,
|
|
959
|
+
type: type,
|
|
960
|
+
amount: args.amount,
|
|
961
|
+
price: orderPrice,
|
|
962
|
+
stopPrice: args.stopPrice,
|
|
963
|
+
targetPrice: args.target_price,
|
|
964
|
+
};
|
|
965
|
+
// Build portfolio snapshot based on mode
|
|
966
|
+
let portfolio;
|
|
967
|
+
let consecutiveLosses = 0;
|
|
968
|
+
if (!deps.adapter.isLive) {
|
|
969
|
+
const simulator = deps.adapter.getSimulator();
|
|
970
|
+
portfolio = buildPortfolioSnapshotFromSimulator(simulator);
|
|
971
|
+
consecutiveLosses = computeConsecutiveLosses(simulator.getState().tradeHistory);
|
|
294
972
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// clientOrderId (fixed 2026-07-07; supersedes the 2026-07-05 post-return
|
|
299
|
-
// stash). For market orders the user-data WS fill routinely arrives
|
|
300
|
-
// BEFORE the REST ack resolves — observed on prod (FIL 2026-07-06
|
|
301
|
-
// 20:28:17: WS capture consumed nothing and journaled metadata=none while
|
|
302
|
-
// the post-return stash landed milliseconds later). The WS
|
|
303
|
-
// ORDER_TRADE_UPDATE carries the clientOrderId (`o.c`), so
|
|
304
|
-
// onWsFillObserved falls back to it when the exchange orderId lookup
|
|
305
|
-
// misses; promote() below adds the exchange-orderId alias once the REST
|
|
306
|
-
// ack returns for the normal (WS-after-REST) ordering. If the submission
|
|
307
|
-
// throws, the unused stash entry simply expires (24h TTL, pruned).
|
|
308
|
-
let stashCid;
|
|
309
|
-
if (deps.autoCapture?.pendingEntries && metadata) {
|
|
310
|
-
stashCid = randomUUID();
|
|
311
|
-
deps.autoCapture.pendingEntries.put({
|
|
312
|
-
orderId: stashCid,
|
|
313
|
-
clientOrderId: stashCid,
|
|
314
|
-
symbol: args.symbol,
|
|
315
|
-
side: side,
|
|
316
|
-
metadata,
|
|
317
|
-
});
|
|
973
|
+
else {
|
|
974
|
+
portfolio = await buildPortfolioSnapshotFromAdapter(deps.adapter);
|
|
975
|
+
// In live mode, consecutive losses would come from intelligence DB — use 0 for now
|
|
318
976
|
}
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
if (deps.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
977
|
+
// Bracket enforcement only applies in live mode when the feature is enabled.
|
|
978
|
+
// Paper mode uses the stop-watcher and doesn't care about these flags.
|
|
979
|
+
let bracketEnforcement;
|
|
980
|
+
if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
|
|
981
|
+
bracketEnforcement = wave9Claimed
|
|
982
|
+
? { requireStopLoss: true, requireTakeProfit: false }
|
|
983
|
+
: loadBracketRequirements();
|
|
984
|
+
}
|
|
985
|
+
const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
|
|
986
|
+
volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
|
|
987
|
+
consecutiveLosses,
|
|
988
|
+
bracketEnforcement,
|
|
989
|
+
});
|
|
990
|
+
if (!riskCheck.allowed) {
|
|
991
|
+
const reasons = riskCheck.violations.map(v => v.message).join('; ');
|
|
992
|
+
return rejectWave9Divergence(`Order REJECTED by risk gate [${riskCheck.drawdownZone} zone]: ${reasons}`, `generic_pretrade_divergence:${riskCheck.violations.map((v) => v.rule).join(',')}`);
|
|
993
|
+
}
|
|
994
|
+
// Build position metadata from optional args (only if any metadata provided)
|
|
995
|
+
// Sanitize numeric metadata — reject non-finite values, clamp ranges
|
|
996
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
997
|
+
const clamp = (v, min, max) => {
|
|
998
|
+
const n = num(v);
|
|
999
|
+
return n != null ? Math.max(min, Math.min(max, n)) : undefined;
|
|
1000
|
+
};
|
|
1001
|
+
const hasMetadata = args.mission_id || args.setup_type || args.thesis || args.target_price != null
|
|
1002
|
+
|| args.regime || args.scorecard_verdict || args.confluence_score != null
|
|
1003
|
+
|| args.invalidation_price != null || args.realization_rule != null;
|
|
1004
|
+
const metadata = hasMetadata ? {
|
|
1005
|
+
missionId: args.mission_id,
|
|
1006
|
+
setupType: args.setup_type,
|
|
1007
|
+
...(wave9Claimed ? {
|
|
1008
|
+
candidateId: args.candidate_id,
|
|
1009
|
+
strategy: args.candidate_id.split('|')[2],
|
|
1010
|
+
accruedFundingUsd: 0,
|
|
1011
|
+
fundingThroughTime: args.candidate_id.split('|')[0],
|
|
1012
|
+
} : {}),
|
|
1013
|
+
thesis: args.thesis?.slice(0, 500), // Cap length
|
|
1014
|
+
stopPrice: num(args.stopPrice),
|
|
1015
|
+
targetPrice: num(args.target_price),
|
|
1016
|
+
regime: args.regime,
|
|
1017
|
+
regimeConfidence: clamp(args.regime_confidence, 0, 100),
|
|
1018
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
1019
|
+
confluenceScore: clamp(args.confluence_score, 0, 10),
|
|
1020
|
+
invalidationPrice: num(args.invalidation_price),
|
|
1021
|
+
realizationRule: sanitizeRealizationRule(args.realization_rule),
|
|
1022
|
+
} : undefined;
|
|
1023
|
+
// ---- Approval-mode branches ----
|
|
1024
|
+
// Both shadow and per_trade need full v2.10.0 metadata so the proposal is
|
|
1025
|
+
// operator-meaningful. Metadata is the agent's responsibility; ingest creds
|
|
1026
|
+
// are the operator's. Separate so the error messages can be precise.
|
|
1027
|
+
const argsMetadataComplete = !!(args.thesis &&
|
|
1028
|
+
args.setup_type &&
|
|
1029
|
+
args.target_price != null &&
|
|
1030
|
+
args.stopPrice != null &&
|
|
1031
|
+
args.regime &&
|
|
1032
|
+
typeof args.regime_confidence === 'number' &&
|
|
1033
|
+
args.scorecard_verdict &&
|
|
1034
|
+
typeof args.confluence_score === 'number');
|
|
1035
|
+
const proposalPathWired = !!(deps.proposalManager && deps.userId);
|
|
1036
|
+
const approvalMode = deps.approvalMode ?? 'off';
|
|
1037
|
+
// per_trade mode: propose and EARLY-RETURN. The agent's call doesn't fire;
|
|
1038
|
+
// the operator approves and ProposalDecisionListener fires via this same
|
|
1039
|
+
// tool with proposalManager omitted (which takes the real path below).
|
|
1040
|
+
//
|
|
1041
|
+
// If proposalManager isn't wired (operator set approval.mode=per_trade in
|
|
1042
|
+
// config but ingest creds aren't present), we fall through to the normal
|
|
1043
|
+
// fire path — the startup warning is the operator's signal. We never trap
|
|
1044
|
+
// the agent inside an unfireable per_trade branch.
|
|
1045
|
+
if (approvalMode === 'per_trade' && proposalPathWired) {
|
|
1046
|
+
if (!argsMetadataComplete) {
|
|
1047
|
+
return {
|
|
1048
|
+
error: 'approval.mode=per_trade requires the full v2.10.0 metadata block on every create_order: ' +
|
|
1049
|
+
'thesis, setup_type, target_price, stopPrice, regime, regime_confidence, ' +
|
|
1050
|
+
'scorecard_verdict, confluence_score. The operator approves on this metadata.',
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
const result = deps.proposalManager.propose(deps.userId, {
|
|
1055
|
+
symbol: args.symbol,
|
|
1056
|
+
side: side,
|
|
1057
|
+
orderType: type,
|
|
1058
|
+
size: args.amount,
|
|
1059
|
+
proposedEntry: orderPrice,
|
|
1060
|
+
stopPrice: args.stopPrice,
|
|
1061
|
+
targetPrice: args.target_price,
|
|
1062
|
+
riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
|
|
1063
|
+
thesis: args.thesis,
|
|
1064
|
+
setupType: args.setup_type,
|
|
1065
|
+
regime: args.regime,
|
|
1066
|
+
regimeConfidence: args.regime_confidence,
|
|
1067
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
1068
|
+
confluenceScore: args.confluence_score,
|
|
1069
|
+
...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
|
|
1070
|
+
// Real, operator-actionable. Default would also resolve to 'real' but
|
|
1071
|
+
// we set it explicitly so the intent reads at the call site.
|
|
1072
|
+
origin: 'real',
|
|
336
1073
|
});
|
|
1074
|
+
return {
|
|
1075
|
+
status: 'pending_approval',
|
|
1076
|
+
proposal_id: result.proposalUuid,
|
|
1077
|
+
hard_expires_at: result.hardExpiresAt.toISOString(),
|
|
1078
|
+
setup_bucket: result.setupBucket,
|
|
1079
|
+
message: `Order awaits operator approval (${result.setupBucket} bucket, expires ` +
|
|
1080
|
+
`${result.hardExpiresAt.toISOString()}). Do not call create_order again ` +
|
|
1081
|
+
`for this setup; the listener will fire on approval. If the operator ` +
|
|
1082
|
+
`requests modifications in chat, call create_order with supersedes_id=` +
|
|
1083
|
+
`<id from the chat-context header>.`,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
catch (err) {
|
|
1087
|
+
return { error: `Proposal submission failed: ${formatError(err)}` };
|
|
337
1088
|
}
|
|
338
1089
|
}
|
|
339
|
-
|
|
1090
|
+
// shadow mode: dual-write a telemetry proposal AND continue to the real
|
|
1091
|
+
// fire below. Never blocks the hot path on a propose() failure.
|
|
1092
|
+
if (approvalMode === 'shadow' && proposalPathWired && argsMetadataComplete) {
|
|
1093
|
+
try {
|
|
1094
|
+
deps.proposalManager.propose(deps.userId, {
|
|
1095
|
+
symbol: args.symbol,
|
|
1096
|
+
side: side,
|
|
1097
|
+
orderType: type,
|
|
1098
|
+
size: args.amount,
|
|
1099
|
+
proposedEntry: orderPrice,
|
|
1100
|
+
stopPrice: args.stopPrice,
|
|
1101
|
+
targetPrice: args.target_price,
|
|
1102
|
+
riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
|
|
1103
|
+
thesis: args.thesis,
|
|
1104
|
+
setupType: args.setup_type,
|
|
1105
|
+
regime: args.regime,
|
|
1106
|
+
regimeConfidence: args.regime_confidence,
|
|
1107
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
1108
|
+
confluenceScore: args.confluence_score,
|
|
1109
|
+
...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
|
|
1110
|
+
// Phase A telemetry — agent's create_order ALSO fires the real order
|
|
1111
|
+
// synchronously below. Hidden from operator UI by origin='shadow'.
|
|
1112
|
+
origin: 'shadow',
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (err) {
|
|
1116
|
+
// eslint-disable-next-line no-console
|
|
1117
|
+
console.warn(`[approval-shadow] propose() threw: ${formatError(err)}`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
// Stash the metadata BEFORE submission, keyed by a pre-generated
|
|
1122
|
+
// clientOrderId (fixed 2026-07-07; supersedes the 2026-07-05 post-return
|
|
1123
|
+
// stash). For market orders the user-data WS fill routinely arrives
|
|
1124
|
+
// BEFORE the REST ack resolves — observed on prod (FIL 2026-07-06
|
|
1125
|
+
// 20:28:17: WS capture consumed nothing and journaled metadata=none while
|
|
1126
|
+
// the post-return stash landed milliseconds later). The WS
|
|
1127
|
+
// ORDER_TRADE_UPDATE carries the clientOrderId (`o.c`), so
|
|
1128
|
+
// onWsFillObserved falls back to it when the exchange orderId lookup
|
|
1129
|
+
// misses; promote() below adds the exchange-orderId alias once the REST
|
|
1130
|
+
// ack returns for the normal (WS-after-REST) ordering. If the submission
|
|
1131
|
+
// throws, the unused stash entry simply expires (24h TTL, pruned).
|
|
1132
|
+
let stashCid = wave9Claimed && initialWave9Mode === 'LIVE'
|
|
1133
|
+
? wave9ClientOrderId(args.candidate_id)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
if (deps.autoCapture?.pendingEntries && metadata) {
|
|
1136
|
+
stashCid ??= randomUUID();
|
|
1137
|
+
deps.autoCapture.pendingEntries.put({
|
|
1138
|
+
orderId: stashCid,
|
|
1139
|
+
clientOrderId: stashCid,
|
|
1140
|
+
symbol: args.symbol,
|
|
1141
|
+
side: side,
|
|
1142
|
+
metadata,
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
if (wave9Lease) {
|
|
1146
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
1147
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'trading_mode_changed_before_mutation', { retryable: true, latchEvent: false });
|
|
1148
|
+
wave9Lease = undefined;
|
|
1149
|
+
return { error: 'Wave 9 order refused: adapter or trading mode changed before mutation.' };
|
|
1150
|
+
}
|
|
1151
|
+
if (deps.wave9VenueCheck?.() !== 'binance') {
|
|
1152
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'execution_venue_changed_before_mutation', { retryable: true, latchEvent: false });
|
|
1153
|
+
wave9Lease = undefined;
|
|
1154
|
+
return { error: 'Wave 9 order refused: execution venue changed away from Binance before mutation.' };
|
|
1155
|
+
}
|
|
1156
|
+
let activation;
|
|
1157
|
+
try {
|
|
1158
|
+
activation = deps.wave9ActivationCheck
|
|
1159
|
+
? await deps.wave9ActivationCheck()
|
|
1160
|
+
: { available: false, active: false };
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
activation = { available: false, active: false };
|
|
1164
|
+
}
|
|
1165
|
+
if (!activation.available || !activation.active) {
|
|
1166
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'activation_unavailable_or_inactive_before_mutation', { retryable: true, latchEvent: false });
|
|
1167
|
+
wave9Lease = undefined;
|
|
1168
|
+
return { error: 'Wave 9 order refused: strategy was toggled off or activation could not be re-verified.' };
|
|
1169
|
+
}
|
|
1170
|
+
if (deps.adapter.readiness !== 'READY') {
|
|
1171
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'adapter_not_ready_before_mutation', { retryable: true, latchEvent: false });
|
|
1172
|
+
wave9Lease = undefined;
|
|
1173
|
+
return { error: `Wave 9 order refused: adapter changed to ${deps.adapter.readiness} before mutation.` };
|
|
1174
|
+
}
|
|
1175
|
+
if (initialWave9Mode === 'LIVE' && !bracketsEnabled(loadBracketMode())) {
|
|
1176
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'native_brackets_disabled_before_mutation', { retryable: true, latchEvent: false });
|
|
1177
|
+
wave9Lease = undefined;
|
|
1178
|
+
return { error: 'Wave 9 live order refused: native bracket protection was disabled before mutation.' };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const submit = async () => {
|
|
1182
|
+
if (!wave9Claimed && !deps.adapter.isLive) {
|
|
1183
|
+
deps.refreshPaperState?.();
|
|
1184
|
+
const positions = await deps.adapter.getPositionsOrNull();
|
|
1185
|
+
if (positions === null) {
|
|
1186
|
+
throw new Error('generic create_order final isolation check could not read paper positions');
|
|
1187
|
+
}
|
|
1188
|
+
if (positions.some(isWave9ManagedPosition)) {
|
|
1189
|
+
throw new Error('Generic create_order refused at mutation boundary: the paper account now contains an atomic Wave 9 position.');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (!wave9Claimed && deps.adapter.isLive && deps.checkWave9LiveSymbolOwnership) {
|
|
1193
|
+
let ownership;
|
|
1194
|
+
try {
|
|
1195
|
+
ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
|
|
1196
|
+
}
|
|
1197
|
+
catch (err) {
|
|
1198
|
+
throw new Error(`Generic create_order refused at mutation boundary: Wave 9 LIVE ownership ` +
|
|
1199
|
+
`check failed for ${args.symbol} (${formatError(err)}).`);
|
|
1200
|
+
}
|
|
1201
|
+
if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
|
|
1202
|
+
throw new Error(`Generic create_order refused at mutation boundary: ${args.symbol} has ` +
|
|
1203
|
+
`${ownership.status} Wave 9 LIVE ownership (${ownership.reason}).`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return deps.adapter.createOrder(args.symbol, side, type, args.amount, args.price, metadata, stashCid ? { clientOrderId: stashCid } : undefined);
|
|
1207
|
+
};
|
|
1208
|
+
const submitWave9 = async () => {
|
|
1209
|
+
if (!wave9Lease)
|
|
1210
|
+
throw new Error('Wave 9 admission lease is unavailable at mutation boundary.');
|
|
1211
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
1212
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'trading_mode_changed_during_activation_check', { retryable: true, latchEvent: false });
|
|
1213
|
+
wave9Lease = undefined;
|
|
1214
|
+
throw new Error('Wave 9 order refused: adapter or trading mode changed during activation verification.');
|
|
1215
|
+
}
|
|
1216
|
+
if (deps.wave9VenueCheck?.() !== 'binance') {
|
|
1217
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'execution_venue_changed_at_mutation_boundary', { retryable: true, latchEvent: false });
|
|
1218
|
+
wave9Lease = undefined;
|
|
1219
|
+
throw new Error('Wave 9 order refused: execution venue changed away from Binance at mutation boundary.');
|
|
1220
|
+
}
|
|
1221
|
+
let currentFingerprint;
|
|
1222
|
+
if (initialWave9Mode === 'LIVE') {
|
|
1223
|
+
const ownership = deps.checkWave9LiveSymbolOwnership
|
|
1224
|
+
? await deps.checkWave9LiveSymbolOwnership(args.symbol)
|
|
1225
|
+
: await inspectWave9LiveSymbolOwnership(deps.adapter, deps.wave9LiveLedger, args.symbol);
|
|
1226
|
+
if (ownership.status !== 'unowned'
|
|
1227
|
+
|| ownership.reason === 'authoritative_symbol_flat') {
|
|
1228
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `live_symbol_ownership_changed_before_mutation:${ownership.status}:${ownership.reason}`, { retryable: true, latchEvent: false });
|
|
1229
|
+
wave9Lease = undefined;
|
|
1230
|
+
throw new Error(`Wave 9 live order refused: candidate-symbol ownership changed before mutation ` +
|
|
1231
|
+
`(${ownership.status}:${ownership.reason}); refresh get_wave9_status.`);
|
|
1232
|
+
}
|
|
1233
|
+
currentFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
deps.refreshPaperState?.();
|
|
1237
|
+
currentFingerprint = captureWave9PaperAccountSnapshot(deps.adapter).fingerprint;
|
|
1238
|
+
}
|
|
1239
|
+
deps.wave9AdmissionGuard.revalidate(wave9Lease, currentFingerprint);
|
|
1240
|
+
if (initialWave9Mode === 'LIVE') {
|
|
1241
|
+
const preEntryFlat = await confirmWave9LivePositionFlat(deps.adapter, args.symbol);
|
|
1242
|
+
if (!preEntryFlat.confirmed) {
|
|
1243
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `live_candidate_symbol_not_stably_flat_before_entry:${preEntryFlat.detail}`, { retryable: true, latchEvent: false });
|
|
1244
|
+
wave9Lease = undefined;
|
|
1245
|
+
throw new Error(`Wave 9 live order refused: candidate symbol was not authoritatively stably flat ` +
|
|
1246
|
+
`immediately before entry (${preEntryFlat.detail}). No order was submitted; refresh ` +
|
|
1247
|
+
'get_wave9_status.');
|
|
1248
|
+
}
|
|
1249
|
+
const refuseAfterFlatProof = (reason, detail) => {
|
|
1250
|
+
if (wave9Lease) {
|
|
1251
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, reason, { retryable: true, latchEvent: false });
|
|
1252
|
+
}
|
|
1253
|
+
wave9Lease = undefined;
|
|
1254
|
+
throw new Error(`${detail} No order was submitted; refresh get_wave9_status.`);
|
|
1255
|
+
};
|
|
1256
|
+
let postFlatActivation;
|
|
1257
|
+
try {
|
|
1258
|
+
postFlatActivation = deps.wave9ActivationCheck
|
|
1259
|
+
? await deps.wave9ActivationCheck()
|
|
1260
|
+
: { available: false, active: false };
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
postFlatActivation = { available: false, active: false };
|
|
1264
|
+
}
|
|
1265
|
+
if (!postFlatActivation.available || !postFlatActivation.active) {
|
|
1266
|
+
refuseAfterFlatProof('activation_unavailable_or_inactive_after_flat_proof', 'Wave 9 live order refused: the strategy was toggled off or activation could not be re-verified after the stable-flat proof.');
|
|
1267
|
+
}
|
|
1268
|
+
if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
|
|
1269
|
+
refuseAfterFlatProof('trading_mode_changed_after_flat_proof', 'Wave 9 live order refused: adapter or trading mode changed after the stable-flat proof.');
|
|
1270
|
+
}
|
|
1271
|
+
if (deps.wave9VenueCheck?.() !== 'binance') {
|
|
1272
|
+
refuseAfterFlatProof('execution_venue_changed_after_flat_proof', 'Wave 9 live order refused: execution venue changed away from Binance after the stable-flat proof.');
|
|
1273
|
+
}
|
|
1274
|
+
if (deps.adapter.readiness !== 'READY') {
|
|
1275
|
+
refuseAfterFlatProof('adapter_not_ready_after_flat_proof', `Wave 9 live order refused: adapter changed to ${deps.adapter.readiness} after the stable-flat proof.`);
|
|
1276
|
+
}
|
|
1277
|
+
if (!bracketsEnabled(loadBracketMode())) {
|
|
1278
|
+
refuseAfterFlatProof('native_brackets_disabled_after_flat_proof', 'Wave 9 live order refused: native bracket protection was disabled after the stable-flat proof.');
|
|
1279
|
+
}
|
|
1280
|
+
let postFlatFingerprint;
|
|
1281
|
+
try {
|
|
1282
|
+
postFlatFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
|
|
1283
|
+
}
|
|
1284
|
+
catch (error) {
|
|
1285
|
+
refuseAfterFlatProof(`live_account_fingerprint_unavailable_after_flat_proof:${formatError(error)}`, `Wave 9 live order refused: the LIVE account fingerprint could not be refreshed ` +
|
|
1286
|
+
`after the stable-flat proof (${formatError(error)}).`);
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
deps.wave9AdmissionGuard.revalidate(wave9Lease, postFlatFingerprint);
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
refuseAfterFlatProof(`authorization_revalidation_failed_after_flat_proof:${formatError(error)}`, `Wave 9 live order refused: authorization or LIVE account identity changed during ` +
|
|
1293
|
+
`the stable-flat proof (${formatError(error)}).`);
|
|
1294
|
+
}
|
|
1295
|
+
const [eventTime, , strategy, direction] = args.candidate_id.split('|');
|
|
1296
|
+
await deps.wave9LiveLedger.recordAttempt({
|
|
1297
|
+
candidateId: args.candidate_id,
|
|
1298
|
+
missionId: args.mission_id,
|
|
1299
|
+
eventTime,
|
|
1300
|
+
symbol: args.symbol,
|
|
1301
|
+
strategy: strategy,
|
|
1302
|
+
direction: direction,
|
|
1303
|
+
side: side,
|
|
1304
|
+
quantity: args.amount,
|
|
1305
|
+
initialStop: args.stopPrice,
|
|
1306
|
+
clientOrderId: stashCid,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const finishLiveEntryRecovery = (recovery, source) => {
|
|
1310
|
+
if (recovery.status === 'protected') {
|
|
1311
|
+
deps.wave9AdmissionGuard.commit(wave9Lease);
|
|
1312
|
+
wave9Lease = undefined;
|
|
1313
|
+
if (recovery.order)
|
|
1314
|
+
return recovery.order;
|
|
1315
|
+
throw new Error(`Wave 9 LIVE ${source}, but the entry was recovered OPEN and protected. ` +
|
|
1316
|
+
`Do not retry this candidate. ${recovery.detail}`);
|
|
1317
|
+
}
|
|
1318
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, recovery.status === 'terminal'
|
|
1319
|
+
? 'live_entry_recovery_confirmed_terminal'
|
|
1320
|
+
: 'live_entry_recovery_manual_intervention');
|
|
1321
|
+
wave9Lease = undefined;
|
|
1322
|
+
throw new Error(recovery.status === 'terminal'
|
|
1323
|
+
? `Wave 9 LIVE ${source} resolved safely and is terminal. ${recovery.detail}`
|
|
1324
|
+
: `CRITICAL: ${recovery.detail}`);
|
|
1325
|
+
};
|
|
1326
|
+
let order;
|
|
1327
|
+
try {
|
|
1328
|
+
order = await submit();
|
|
1329
|
+
}
|
|
1330
|
+
catch (submitError) {
|
|
1331
|
+
if (initialWave9Mode !== 'LIVE')
|
|
1332
|
+
throw submitError;
|
|
1333
|
+
const recovery = await recoverWave9LiveSubmitThrow({
|
|
1334
|
+
adapter: deps.adapter,
|
|
1335
|
+
ledger: deps.wave9LiveLedger,
|
|
1336
|
+
candidateId: args.candidate_id,
|
|
1337
|
+
clientOrderId: stashCid,
|
|
1338
|
+
symbol: args.symbol,
|
|
1339
|
+
side: side,
|
|
1340
|
+
stopPrice: args.stopPrice,
|
|
1341
|
+
authorizedQuantity: args.amount,
|
|
1342
|
+
submitError,
|
|
1343
|
+
confirmStop: deps.confirmWave9LiveStop,
|
|
1344
|
+
residualProtector: deps.reprotectWave9LiveResidual,
|
|
1345
|
+
});
|
|
1346
|
+
return finishLiveEntryRecovery(recovery, 'submit acknowledgement failed');
|
|
1347
|
+
}
|
|
1348
|
+
const filledQuantity = typeof order.filled === 'number'
|
|
1349
|
+
&& Number.isFinite(order.filled)
|
|
1350
|
+
&& order.filled > 0
|
|
1351
|
+
? order.filled
|
|
1352
|
+
: undefined;
|
|
1353
|
+
if (filledQuantity === undefined) {
|
|
1354
|
+
if (initialWave9Mode === 'LIVE') {
|
|
1355
|
+
const recovery = await recoverWave9LiveSubmitThrow({
|
|
1356
|
+
adapter: deps.adapter,
|
|
1357
|
+
ledger: deps.wave9LiveLedger,
|
|
1358
|
+
candidateId: args.candidate_id,
|
|
1359
|
+
clientOrderId: stashCid,
|
|
1360
|
+
symbol: args.symbol,
|
|
1361
|
+
side: side,
|
|
1362
|
+
stopPrice: args.stopPrice,
|
|
1363
|
+
authorizedQuantity: args.amount,
|
|
1364
|
+
submitError: new Error('market acknowledgement lacked a positive confirmed fill quantity'),
|
|
1365
|
+
acknowledgedOrder: order,
|
|
1366
|
+
confirmStop: deps.confirmWave9LiveStop,
|
|
1367
|
+
residualProtector: deps.reprotectWave9LiveResidual,
|
|
1368
|
+
});
|
|
1369
|
+
return finishLiveEntryRecovery(recovery, 'market acknowledgement lacked a positive confirmed fill quantity');
|
|
1370
|
+
}
|
|
1371
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, 'market_order_not_filled');
|
|
1372
|
+
wave9Lease = undefined;
|
|
1373
|
+
throw new Error('Wave 9 market order lacks a positive confirmed fill quantity; admission batch latched.');
|
|
1374
|
+
}
|
|
1375
|
+
if (initialWave9Mode === 'LIVE'
|
|
1376
|
+
&& !candidateBoundFoundOrder({ status: 'found', order }, stashCid, args.symbol, side)) {
|
|
1377
|
+
const recovery = await recoverWave9LiveSubmitThrow({
|
|
1378
|
+
adapter: deps.adapter,
|
|
1379
|
+
ledger: deps.wave9LiveLedger,
|
|
1380
|
+
candidateId: args.candidate_id,
|
|
1381
|
+
clientOrderId: stashCid,
|
|
1382
|
+
symbol: args.symbol,
|
|
1383
|
+
side: side,
|
|
1384
|
+
stopPrice: args.stopPrice,
|
|
1385
|
+
authorizedQuantity: args.amount,
|
|
1386
|
+
submitError: new Error('market acknowledgement had malformed or mismatched candidate identity'),
|
|
1387
|
+
confirmStop: deps.confirmWave9LiveStop,
|
|
1388
|
+
residualProtector: deps.reprotectWave9LiveResidual,
|
|
1389
|
+
});
|
|
1390
|
+
return finishLiveEntryRecovery(recovery, 'market acknowledgement had malformed or mismatched candidate identity');
|
|
1391
|
+
}
|
|
1392
|
+
if (initialWave9Mode === 'LIVE' && order.status === 'open') {
|
|
1393
|
+
const recovery = await recoverWave9LiveSubmitThrow({
|
|
1394
|
+
adapter: deps.adapter,
|
|
1395
|
+
ledger: deps.wave9LiveLedger,
|
|
1396
|
+
candidateId: args.candidate_id,
|
|
1397
|
+
clientOrderId: stashCid,
|
|
1398
|
+
symbol: args.symbol,
|
|
1399
|
+
side: side,
|
|
1400
|
+
stopPrice: args.stopPrice,
|
|
1401
|
+
authorizedQuantity: args.amount,
|
|
1402
|
+
submitError: new Error('market acknowledgement left an open entry remainder'),
|
|
1403
|
+
acknowledgedOrder: order,
|
|
1404
|
+
confirmStop: deps.confirmWave9LiveStop,
|
|
1405
|
+
residualProtector: deps.reprotectWave9LiveResidual,
|
|
1406
|
+
});
|
|
1407
|
+
return finishLiveEntryRecovery(recovery, 'market acknowledgement left an open entry remainder');
|
|
1408
|
+
}
|
|
1409
|
+
if (initialWave9Mode === 'LIVE') {
|
|
1410
|
+
const entryPrice = confirmedWave9EntryPrice(order, side, args.stopPrice);
|
|
1411
|
+
const exchangeOrderId = confirmedWave9ExchangeOrderId(order);
|
|
1412
|
+
let openPersistenceError;
|
|
1413
|
+
try {
|
|
1414
|
+
await deps.wave9LiveLedger.markOpen(args.candidate_id, {
|
|
1415
|
+
filledQuantity,
|
|
1416
|
+
...(exchangeOrderId ? { exchangeOrderId } : {}),
|
|
1417
|
+
...(entryPrice !== undefined ? { entryPrice } : {}),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
catch (err) {
|
|
1421
|
+
openPersistenceError = formatError(err);
|
|
1422
|
+
}
|
|
1423
|
+
if (openPersistenceError) {
|
|
1424
|
+
const flatten = await flattenFailedWave9LiveEntry(deps.adapter, deps.wave9LiveLedger, args.symbol, args.candidate_id, {
|
|
1425
|
+
side: side,
|
|
1426
|
+
maximumQuantity: Math.min(filledQuantity, args.amount),
|
|
1427
|
+
}, deps.reprotectWave9LiveResidual);
|
|
1428
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, flatten.confirmedFlat
|
|
1429
|
+
? 'live_open_persistence_failed_confirmed_flat'
|
|
1430
|
+
: 'live_open_persistence_failed_residual_exposure');
|
|
1431
|
+
wave9Lease = undefined;
|
|
1432
|
+
const flattenError = flatten.flattenError
|
|
1433
|
+
? ` Close submission error: ${flatten.flattenError}.`
|
|
1434
|
+
: '';
|
|
1435
|
+
if (flatten.confirmedFlat && !flatten.terminalPersistenceError) {
|
|
1436
|
+
throw new Error(`Wave 9 confirmed fill could not be durably marked OPEN (${openPersistenceError}); ` +
|
|
1437
|
+
`${flatten.closeAttempted
|
|
1438
|
+
? 'the position was auto-flattened and confirmed flat'
|
|
1439
|
+
: 'the exchange position was already confirmed stably flat'}, and its replay tombstone was ` +
|
|
1440
|
+
`persisted terminal.${flattenError}`);
|
|
1441
|
+
}
|
|
1442
|
+
if (flatten.confirmedFlat) {
|
|
1443
|
+
throw new Error(`Wave 9 confirmed fill could not be durably marked OPEN (${openPersistenceError}). ` +
|
|
1444
|
+
`The exchange is confirmed flat, but durable CLOSED persistence also failed ` +
|
|
1445
|
+
`(${flatten.terminalPersistenceError}).${flattenError}`);
|
|
1446
|
+
}
|
|
1447
|
+
throw new Error(`CRITICAL: Wave 9 confirmed fill could not be durably marked OPEN ` +
|
|
1448
|
+
`(${openPersistenceError}), and ${flatten.closeAttempted
|
|
1449
|
+
? 'emergency flatten is not confirmed flat'
|
|
1450
|
+
: 'the exposure identity fence refused symbol-wide flatten'} ` +
|
|
1451
|
+
`(${flatten.confirmationDetail}). In-memory OPEN ownership was retained where possible; ` +
|
|
1452
|
+
`the entry capability is consumed and latched.${flattenError}${residualProtectionMessage(flatten)}`);
|
|
1453
|
+
}
|
|
1454
|
+
let stopConfirmed = false;
|
|
1455
|
+
try {
|
|
1456
|
+
stopConfirmed = await deps.confirmWave9LiveStop({
|
|
1457
|
+
candidateId: args.candidate_id,
|
|
1458
|
+
symbol: args.symbol,
|
|
1459
|
+
entryClientOrderId: stashCid,
|
|
1460
|
+
entrySide: side,
|
|
1461
|
+
stopPrice: args.stopPrice,
|
|
1462
|
+
filledQuantity,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
stopConfirmed = false;
|
|
1467
|
+
}
|
|
1468
|
+
if (!stopConfirmed) {
|
|
1469
|
+
const flatten = await flattenFailedWave9LiveEntry(deps.adapter, deps.wave9LiveLedger, args.symbol, args.candidate_id, {
|
|
1470
|
+
side: side,
|
|
1471
|
+
maximumQuantity: Math.min(filledQuantity, args.amount),
|
|
1472
|
+
}, deps.reprotectWave9LiveResidual);
|
|
1473
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, flatten.confirmedFlat
|
|
1474
|
+
? 'live_stop_unconfirmed_confirmed_flat'
|
|
1475
|
+
: 'live_stop_unconfirmed_residual_exposure');
|
|
1476
|
+
wave9Lease = undefined;
|
|
1477
|
+
const flattenError = flatten.flattenError
|
|
1478
|
+
? ` Close submission error: ${flatten.flattenError}.`
|
|
1479
|
+
: '';
|
|
1480
|
+
if (flatten.confirmedFlat && !flatten.terminalPersistenceError) {
|
|
1481
|
+
throw new Error(`Wave 9 live stop was not confirmed; ${flatten.closeAttempted
|
|
1482
|
+
? 'the filled position was auto-flattened and confirmed flat'
|
|
1483
|
+
: 'the exchange position was already confirmed stably flat'}, and its durable ` +
|
|
1484
|
+
`execution row is terminal.${flattenError}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (flatten.confirmedFlat) {
|
|
1487
|
+
throw new Error(`Wave 9 live stop was not confirmed. The exchange is confirmed flat, but durable ` +
|
|
1488
|
+
`CLOSED persistence failed (${flatten.terminalPersistenceError}).${flattenError}`);
|
|
1489
|
+
}
|
|
1490
|
+
throw new Error(`CRITICAL: Wave 9 live stop was not confirmed and ${flatten.closeAttempted
|
|
1491
|
+
? 'emergency flatten is not confirmed flat'
|
|
1492
|
+
: 'the exposure identity fence refused symbol-wide flatten'} ` +
|
|
1493
|
+
`(${flatten.confirmationDetail}). Durable ownership remains OPEN; the entry ` +
|
|
1494
|
+
`capability is consumed and latched.${flattenError}${residualProtectionMessage(flatten)}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
deps.wave9AdmissionGuard.commit(wave9Lease);
|
|
1498
|
+
wave9Lease = undefined;
|
|
1499
|
+
return order;
|
|
1500
|
+
};
|
|
1501
|
+
const order = wave9Claimed
|
|
1502
|
+
? await deps.operationLock.withAccountLock(`wave9-create-final:${args.symbol}`, submitWave9)
|
|
1503
|
+
: deps.operationLock
|
|
1504
|
+
? await deps.operationLock.withAccountLock(`generic-create-final:${args.symbol}`, submit)
|
|
1505
|
+
: await submit();
|
|
1506
|
+
// Auto-capture entry to the Position Decision Journal — fail-open, never
|
|
1507
|
+
// block the trading hot path on a webapp ingest blip.
|
|
1508
|
+
if (deps.autoCapture) {
|
|
1509
|
+
if (stashCid &&
|
|
1510
|
+
deps.autoCapture.pendingEntries &&
|
|
1511
|
+
typeof order.id === 'string' &&
|
|
1512
|
+
order.id.length > 0) {
|
|
1513
|
+
deps.autoCapture.pendingEntries.promote(stashCid, order.id);
|
|
1514
|
+
}
|
|
1515
|
+
const filledNow = typeof order.filled === 'number' && order.filled > 0;
|
|
1516
|
+
if (filledNow) {
|
|
1517
|
+
// Order filled at submit (market, or limit-that-crossed). Capture
|
|
1518
|
+
// synchronously — same as the original PR1 path.
|
|
1519
|
+
onCreateOrderFilled(deps.autoCapture, { symbol: args.symbol, side: side, metadata }, order).catch((err) => {
|
|
1520
|
+
// eslint-disable-next-line no-console
|
|
1521
|
+
console.warn(`[position-auto-capture] entry capture threw: ${formatError(err)}`);
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return order;
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
if (wave9Lease) {
|
|
1529
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `order_execution_failed:${formatError(err)}`);
|
|
1530
|
+
wave9Lease = undefined;
|
|
1531
|
+
}
|
|
1532
|
+
return { error: formatError(err) };
|
|
1533
|
+
}
|
|
340
1534
|
}
|
|
341
1535
|
catch (err) {
|
|
1536
|
+
if (!wave9Claimed)
|
|
1537
|
+
throw err;
|
|
1538
|
+
if (wave9Lease) {
|
|
1539
|
+
deps.wave9AdmissionGuard.abort(wave9Lease, `pre_mutation_processing_failed:${formatError(err)}`, { retryable: true, latchEvent: false });
|
|
1540
|
+
wave9Lease = undefined;
|
|
1541
|
+
}
|
|
342
1542
|
return { error: formatError(err) };
|
|
343
1543
|
}
|
|
344
1544
|
}
|