@reefclaw/openclaw-plugin 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/ccxt/binance-private.d.ts +21 -0
  2. package/ccxt/binance-private.js +132 -22
  3. package/config/plugin-config-io.d.ts +6 -0
  4. package/config/tool-gate.js +3 -0
  5. package/exchange-adapter.d.ts +16 -0
  6. package/index.js +567 -57
  7. package/lifecycle/trading-operation-lock.d.ts +17 -0
  8. package/lifecycle/trading-operation-lock.js +14 -0
  9. package/live/bracket-id.d.ts +2 -3
  10. package/live/bracket-id.js +22 -9
  11. package/live/live-adapter.d.ts +24 -1
  12. package/live/live-adapter.js +114 -2
  13. package/live/local-signal-service.js +11 -6
  14. package/live/local-strategy-evaluator.js +4 -0
  15. package/live/proposal-decision-listener.d.ts +6 -0
  16. package/live/proposal-decision-listener.js +4 -0
  17. package/live/stop-watcher.d.ts +27 -1
  18. package/live/stop-watcher.js +59 -2
  19. package/onboarding/runtime.d.ts +13 -0
  20. package/onboarding/runtime.js +22 -2
  21. package/openclaw.plugin.json +1 -0
  22. package/package.json +1 -1
  23. package/persistence/state-manager.d.ts +24 -0
  24. package/persistence/state-manager.js +62 -4
  25. package/portfolio/wave9-admission.d.ts +67 -0
  26. package/portfolio/wave9-admission.js +262 -0
  27. package/portfolio/wave9-policy.d.ts +36 -0
  28. package/portfolio/wave9-policy.js +183 -0
  29. package/signals/conditions/registry.js +50 -0
  30. package/simulator/exchange-simulator.js +5 -0
  31. package/simulator/fill-engine.js +5 -1
  32. package/simulator/types.d.ts +6 -1
  33. package/strategy/evaluator.d.ts +3 -0
  34. package/strategy/evaluator.js +5 -0
  35. package/tools/assessment-validation.d.ts +2 -0
  36. package/tools/attach-brackets.d.ts +7 -2
  37. package/tools/attach-brackets.js +36 -0
  38. package/tools/cancel-all-orders.d.ts +2 -0
  39. package/tools/cancel-all-orders.js +4 -1
  40. package/tools/cancel-order.d.ts +4 -0
  41. package/tools/cancel-order.js +49 -3
  42. package/tools/close-position.d.ts +23 -0
  43. package/tools/close-position.js +286 -13
  44. package/tools/create-order.d.ts +28 -0
  45. package/tools/create-order.js +1364 -192
  46. package/tools/get-analytics.js +2 -2
  47. package/tools/get-basis.js +2 -2
  48. package/tools/get-cascade-risk.js +2 -2
  49. package/tools/get-crypto-metrics.js +20 -3
  50. package/tools/get-cvd.js +2 -2
  51. package/tools/get-divergences.js +2 -2
  52. package/tools/get-funding-context.js +2 -2
  53. package/tools/get-liquidation-levels.js +2 -2
  54. package/tools/get-liquidation-pulse.js +2 -2
  55. package/tools/get-pattern-scan.js +2 -2
  56. package/tools/get-regime.js +2 -2
  57. package/tools/get-resting-liquidity.js +2 -2
  58. package/tools/get-risk-scenario.js +2 -2
  59. package/tools/get-session-review.js +2 -2
  60. package/tools/get-setup-detail.js +9 -1
  61. package/tools/get-signals.js +2 -2
  62. package/tools/get-sizing.js +2 -2
  63. package/tools/get-trade-feedback.js +2 -2
  64. package/tools/get-trade-flow.js +2 -2
  65. package/tools/get-volume-profile.js +2 -2
  66. package/tools/get-wave9-status.d.ts +127 -0
  67. package/tools/get-wave9-status.js +796 -0
  68. package/tools/intel-api.d.ts +20 -0
  69. package/tools/intel-api.js +67 -0
  70. package/tools/intel-cache.d.ts +1 -1
  71. package/tools/intel-cache.js +20 -5
  72. package/tools/list-strategies.d.ts +11 -1
  73. package/tools/list-strategies.js +17 -0
  74. package/tools/modify-stop.d.ts +4 -0
  75. package/tools/modify-stop.js +63 -24
  76. package/tools/modify-target.d.ts +4 -0
  77. package/tools/modify-target.js +62 -23
  78. package/tools/scan-pairs.js +19 -8
  79. package/tools/toggle-strategy.js +7 -0
  80. package/types.d.ts +5 -0
  81. package/venues/hyperliquid/hl-balance.d.ts +116 -0
  82. package/venues/hyperliquid/hl-balance.js +145 -0
  83. package/venues/hyperliquid/hl-brackets.d.ts +102 -0
  84. package/venues/hyperliquid/hl-brackets.js +172 -0
  85. package/venues/hyperliquid/hl-cloid.d.ts +22 -0
  86. package/venues/hyperliquid/hl-cloid.js +82 -0
  87. package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
  88. package/venues/hyperliquid/hl-info-cache.js +125 -0
  89. package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
  90. package/venues/hyperliquid/hl-live-adapter.js +353 -0
  91. package/venues/hyperliquid/hl-precision.d.ts +61 -0
  92. package/venues/hyperliquid/hl-precision.js +176 -0
  93. package/venues/hyperliquid/hl-private.d.ts +88 -0
  94. package/venues/hyperliquid/hl-private.js +357 -0
  95. package/venues/hyperliquid/hl-public.d.ts +31 -4
  96. package/venues/hyperliquid/hl-public.js +155 -11
  97. package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
  98. package/venues/hyperliquid/hl-rate-gate.js +220 -0
  99. package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
  100. package/venues/hyperliquid/hl-user-stream.js +220 -0
  101. package/venues/registry.d.ts +28 -9
  102. package/venues/registry.js +19 -13
  103. package/venues/symbols.d.ts +43 -0
  104. package/venues/symbols.js +107 -0
  105. package/wave9/live-account-capture.d.ts +67 -0
  106. package/wave9/live-account-capture.js +435 -0
  107. package/wave9/live-autonomous-protection.d.ts +39 -0
  108. package/wave9/live-autonomous-protection.js +112 -0
  109. package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
  110. package/wave9/live-durable-reconciliation-scheduler.js +115 -0
  111. package/wave9/live-execution-ledger.d.ts +107 -0
  112. package/wave9/live-execution-ledger.js +498 -0
  113. package/wave9/live-position-confirmation.d.ts +18 -0
  114. package/wave9/live-position-confirmation.js +111 -0
  115. package/wave9/live-residual-protection.d.ts +18 -0
  116. package/wave9/live-residual-protection.js +250 -0
  117. package/wave9/live-startup-reconciliation.d.ts +38 -0
  118. package/wave9/live-startup-reconciliation.js +454 -0
  119. package/wave9/live-symbol-ownership.d.ts +20 -0
  120. package/wave9/live-symbol-ownership.js +132 -0
  121. package/wave9/paper-admission-guard.d.ts +199 -0
  122. package/wave9/paper-admission-guard.js +650 -0
  123. package/wave9/usdm-evidence-provider.d.ts +42 -0
  124. package/wave9/usdm-evidence-provider.js +133 -0
@@ -8,6 +8,637 @@ import { validateCreateOrder, sanitizeRealizationRule } from './assessment-valid
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();
@@ -76,6 +707,65 @@ async function buildPortfolioSnapshotFromAdapter(adapter) {
76
707
  };
77
708
  }
78
709
  export async function createOrderTool(args, deps) {
710
+ const wave9Claimed = isWave9ManagedPosition({
711
+ setupType: args.setup_type,
712
+ missionId: args.mission_id,
713
+ })
714
+ || args.wave9_admission_token !== undefined
715
+ || args.candidate_id !== undefined;
716
+ if (wave9Claimed && deps.wave9VenueCheck?.() !== 'binance') {
717
+ return { error: 'Wave 9 order refused: execution is pinned to Binance USD-M futures.' };
718
+ }
719
+ if (wave9Claimed && !deps.operationLock) {
720
+ return { error: 'Wave 9 order refused: process-local trading operation lock is unavailable.' };
721
+ }
722
+ const initialWave9Mode = wave9Claimed ? currentWave9ExecutionMode(deps) : null;
723
+ // Wave 9 is a dedicated targetless capability path. Never let a generic
724
+ // order impersonate it, and require the adapter + mutable runtime mode to
725
+ // agree exactly before accepting either PAPER or LIVE execution.
726
+ if (wave9Claimed) {
727
+ if (args.setup_type !== WAVE9_BUNDLE_SETUP_TYPE) {
728
+ return { error: `Wave 9 order refused: setup_type must be exactly ${WAVE9_BUNDLE_SETUP_TYPE}.` };
729
+ }
730
+ if (!initialWave9Mode) {
731
+ return {
732
+ error: 'Wave 9 order refused: capability execution requires an exact PAPER/paper-adapter ' +
733
+ 'or LIVE/live-adapter runtime match.',
734
+ };
735
+ }
736
+ if ((deps.approvalMode ?? 'off') !== 'off') {
737
+ return { error: 'Wave 9 order refused: approval mode must be off for immediate mechanical execution.' };
738
+ }
739
+ if (!deps.wave9AdmissionGuard || !args.wave9_admission_token || !args.candidate_id || !args.mission_id) {
740
+ return { error: 'Wave 9 order refused: a dedicated short-lived admission token and exact candidate/mission identity are required.' };
741
+ }
742
+ if (args.type.toLowerCase() !== 'market' || args.price !== undefined || args.target_price !== undefined) {
743
+ return { error: 'Wave 9 order refused: the frozen policy requires a targetless market order with no limit price.' };
744
+ }
745
+ const canonical = args.symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
746
+ if (!canonical.endsWith('USDT') || canonical.length <= 4) {
747
+ return { error: `Wave 9 order refused: unsupported execution symbol ${args.symbol}.` };
748
+ }
749
+ if (args.stopPrice === undefined || !Number.isFinite(args.stopPrice) || args.stopPrice <= 0) {
750
+ return { error: 'Wave 9 order refused: a positive finite initial stop is required.' };
751
+ }
752
+ if (initialWave9Mode === 'LIVE') {
753
+ if (!deps.captureWave9AccountFingerprint
754
+ || !deps.confirmWave9LiveStop
755
+ || !deps.wave9LiveLedger) {
756
+ return {
757
+ error: 'Wave 9 live order refused: authoritative live account capture, native-stop confirmation, ' +
758
+ 'and the durable execution ledger are required.',
759
+ };
760
+ }
761
+ if (!bracketsEnabled(loadBracketMode())) {
762
+ return { error: 'Wave 9 live order refused: exchange-native bracket protection is not enabled.' };
763
+ }
764
+ }
765
+ // Persist one unified USD-M key regardless of whether an older agent
766
+ // echoes BTCUSDT or BTC/USDT. Guard matching remains canonical.
767
+ args = { ...args, symbol: `${canonical.slice(0, -4)}/USDT:USDT` };
768
+ }
79
769
  // ---- Readiness gate (live mode only) ----
80
770
  if (deps.adapter.readiness !== 'READY') {
81
771
  return { error: `Trading blocked: adapter is ${deps.adapter.readiness}. Wait for initialization to complete.` };
@@ -86,7 +776,7 @@ export async function createOrderTool(args, deps) {
86
776
  // same data the heartbeat reassessment uses to detect scorecard reversal /
87
777
  // regime flip — if the agent can't produce it at entry, it can't produce
88
778
  // it mid-trade either.
89
- const gateCheck = validateCreateOrder(args, deps.adapter.isLive);
779
+ const gateCheck = validateCreateOrder(args, deps.adapter.isLive && !wave9Claimed);
90
780
  if (!gateCheck.ok) {
91
781
  return { error: gateCheck.error ?? 'Invalid create_order arguments.' };
92
782
  }
@@ -108,6 +798,38 @@ export async function createOrderTool(args, deps) {
108
798
  if (type === 'limit' && (!args.price || args.price <= 0)) {
109
799
  return { error: 'Limit orders require a positive price.' };
110
800
  }
801
+ if (args.mission_id !== undefined
802
+ && (typeof args.mission_id !== 'string'
803
+ || !/^[A-Za-z0-9_-]{1,128}$/.test(args.mission_id))) {
804
+ return { error: 'mission_id must be 1-128 letters, numbers, underscores, or hyphens.' };
805
+ }
806
+ if (!wave9Claimed
807
+ && deps.adapter.isLive
808
+ && deps.checkWave9LiveSymbolOwnership
809
+ && !deps.operationLock) {
810
+ return {
811
+ error: 'Generic live create_order refused: the shared trading operation lock is required ' +
812
+ 'while Wave 9 LIVE ownership checks are enabled.',
813
+ };
814
+ }
815
+ if (!wave9Claimed && !deps.adapter.isLive) {
816
+ try {
817
+ deps.refreshPaperState?.();
818
+ const positions = await deps.adapter.getPositionsOrNull();
819
+ if (positions === null) {
820
+ return { error: 'create_order refused: paper position state unavailable for Wave 9 isolation check.' };
821
+ }
822
+ if (positions.some(isWave9ManagedPosition)) {
823
+ return {
824
+ error: 'Generic create_order refused: the paper account contains an atomic Wave 9 position. ' +
825
+ 'The Wave 9 forward book must remain isolated until all of its positions are flat.',
826
+ };
827
+ }
828
+ }
829
+ catch (err) {
830
+ return { error: `create_order refused: Wave 9 isolation check failed (${formatError(err)}).` };
831
+ }
832
+ }
111
833
  // In paper mode, fetch latest ticker + order book for realistic fills
112
834
  let ticker = null;
113
835
  if (!deps.adapter.isLive) {
@@ -134,211 +856,661 @@ export async function createOrderTool(args, deps) {
134
856
  if (!ticker) {
135
857
  return { error: `Failed to fetch current price for ${args.symbol}` };
136
858
  }
137
- // ---- Pre-trade risk gate ----
138
- const orderPrice = type === 'limit' ? args.price : ticker.last;
139
- const proposed = {
140
- symbol: args.symbol,
141
- side: side,
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
- }
859
+ // Once market data exists, consume the capability before any runtime
860
+ // geometry or generic-gate decision. A delayed stop that is now on the
861
+ // wrong side is an execution divergence and must latch the daily event.
862
+ let wave9Lease;
863
+ if (wave9Claimed) {
229
864
  try {
230
- const result = deps.proposalManager.propose(deps.userId, {
231
- symbol: args.symbol,
232
- side: side,
233
- orderType: type,
234
- size: args.amount,
235
- proposedEntry: orderPrice,
236
- stopPrice: args.stopPrice,
237
- targetPrice: args.target_price,
238
- riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
239
- thesis: args.thesis,
240
- setupType: args.setup_type,
241
- regime: args.regime,
242
- regimeConfidence: args.regime_confidence,
243
- scorecardVerdict: args.scorecard_verdict,
244
- confluenceScore: args.confluence_score,
245
- ...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
246
- // Real, operator-actionable. Default would also resolve to 'real' but
247
- // we set it explicitly so the intent reads at the call site.
248
- origin: 'real',
865
+ wave9Lease = await deps.operationLock.withAccountLock(`wave9-begin-create:${args.candidate_id ?? args.symbol}`, async () => {
866
+ if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
867
+ throw new Error('adapter or trading mode changed before Wave 9 admission began');
868
+ }
869
+ let accountFingerprint;
870
+ if (initialWave9Mode === 'LIVE') {
871
+ const ownership = deps.checkWave9LiveSymbolOwnership
872
+ ? await deps.checkWave9LiveSymbolOwnership(args.symbol)
873
+ : await inspectWave9LiveSymbolOwnership(deps.adapter, deps.wave9LiveLedger, args.symbol);
874
+ if (ownership.status !== 'unowned'
875
+ || ownership.reason === 'authoritative_symbol_flat') {
876
+ throw new Error(`candidate-symbol ownership must be refreshed after ` +
877
+ `${ownership.status}:${ownership.reason}`);
878
+ }
879
+ accountFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
880
+ }
881
+ else {
882
+ deps.refreshPaperState?.();
883
+ deps.adapter.getSimulator().getSessionStartNav();
884
+ accountFingerprint = captureWave9PaperAccountSnapshot(deps.adapter).fingerprint;
885
+ }
886
+ return deps.wave9AdmissionGuard.beginEntry({
887
+ token: args.wave9_admission_token,
888
+ candidateId: args.candidate_id,
889
+ missionId: args.mission_id,
890
+ symbol: args.symbol,
891
+ side,
892
+ type,
893
+ quantity: args.amount,
894
+ stopPrice: args.stopPrice,
895
+ setupType: args.setup_type,
896
+ accountFingerprint,
897
+ tradingMode: initialWave9Mode,
898
+ });
249
899
  });
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
900
  }
262
901
  catch (err) {
263
- return { error: `Proposal submission failed: ${formatError(err)}` };
902
+ return { error: `Wave 9 admission refused: ${formatError(err)}` };
264
903
  }
265
904
  }
266
- // shadow mode: dual-write a telemetry proposal AND continue to the real
267
- // fire below. Never blocks the hot path on a propose() failure.
268
- if (approvalMode === 'shadow' && proposalPathWired && argsMetadataComplete) {
269
- try {
270
- deps.proposalManager.propose(deps.userId, {
271
- symbol: args.symbol,
272
- side: side,
273
- orderType: type,
274
- size: args.amount,
275
- proposedEntry: orderPrice,
276
- stopPrice: args.stopPrice,
277
- 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
- });
905
+ const rejectWave9Divergence = (message, code) => {
906
+ if (wave9Lease) {
907
+ deps.wave9AdmissionGuard.abort(wave9Lease, code);
908
+ wave9Lease = undefined;
290
909
  }
291
- catch (err) {
292
- // eslint-disable-next-line no-console
293
- console.warn(`[approval-shadow] propose() threw: ${formatError(err)}`);
294
- }
295
- }
910
+ return { error: `${wave9Claimed ? 'Wave 9 runtime divergence; daily event latched. ' : ''}${message}` };
911
+ };
912
+ // From beginEntry onward, every unexpected failure must release the
913
+ // in-flight capability. Explicit geometry/risk divergences still use the
914
+ // latching helper above; infrastructure/pure-computation throws are
915
+ // retryable because no exchange mutation has been attempted.
296
916
  try {
297
- // Stash the metadata BEFORE submission, keyed by a pre-generated
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
- });
917
+ // ---- Pre-trade risk gate ----
918
+ const orderPrice = type === 'limit' ? args.price : ticker.last;
919
+ if (wave9Claimed && args.stopPrice != null) {
920
+ if (!Number.isFinite(args.stopPrice) || args.stopPrice <= 0) {
921
+ return rejectWave9Divergence('stopPrice must be a positive finite number.', 'invalid_stop_price');
922
+ }
923
+ if ((side === 'buy' && args.stopPrice >= orderPrice) ||
924
+ (side === 'sell' && args.stopPrice <= orderPrice)) {
925
+ return rejectWave9Divergence(`stopPrice is on the wrong side of entry ${orderPrice} for a ${side} order.`, 'actual_market_stop_geometry_diverged');
926
+ }
927
+ }
928
+ const proposed = {
929
+ symbol: args.symbol,
930
+ side: side,
931
+ type: type,
932
+ amount: args.amount,
933
+ price: orderPrice,
934
+ stopPrice: args.stopPrice,
935
+ targetPrice: args.target_price,
936
+ };
937
+ // Build portfolio snapshot based on mode
938
+ let portfolio;
939
+ let consecutiveLosses = 0;
940
+ if (!deps.adapter.isLive) {
941
+ const simulator = deps.adapter.getSimulator();
942
+ portfolio = buildPortfolioSnapshotFromSimulator(simulator);
943
+ consecutiveLosses = computeConsecutiveLosses(simulator.getState().tradeHistory);
944
+ }
945
+ else {
946
+ portfolio = await buildPortfolioSnapshotFromAdapter(deps.adapter);
947
+ // In live mode, consecutive losses would come from intelligence DB — use 0 for now
948
+ }
949
+ // Bracket enforcement only applies in live mode when the feature is enabled.
950
+ // Paper mode uses the stop-watcher and doesn't care about these flags.
951
+ let bracketEnforcement;
952
+ if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
953
+ bracketEnforcement = wave9Claimed
954
+ ? { requireStopLoss: true, requireTakeProfit: false }
955
+ : loadBracketRequirements();
956
+ }
957
+ const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
958
+ volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
959
+ consecutiveLosses,
960
+ bracketEnforcement,
961
+ });
962
+ if (!riskCheck.allowed) {
963
+ const reasons = riskCheck.violations.map(v => v.message).join('; ');
964
+ return rejectWave9Divergence(`Order REJECTED by risk gate [${riskCheck.drawdownZone} zone]: ${reasons}`, `generic_pretrade_divergence:${riskCheck.violations.map((v) => v.rule).join(',')}`);
965
+ }
966
+ // Build position metadata from optional args (only if any metadata provided)
967
+ // Sanitize numeric metadata — reject non-finite values, clamp ranges
968
+ const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
969
+ const clamp = (v, min, max) => {
970
+ const n = num(v);
971
+ return n != null ? Math.max(min, Math.min(max, n)) : undefined;
972
+ };
973
+ const hasMetadata = args.mission_id || args.setup_type || args.thesis || args.target_price != null
974
+ || args.regime || args.scorecard_verdict || args.confluence_score != null
975
+ || args.invalidation_price != null || args.realization_rule != null;
976
+ const metadata = hasMetadata ? {
977
+ missionId: args.mission_id,
978
+ setupType: args.setup_type,
979
+ ...(wave9Claimed ? {
980
+ candidateId: args.candidate_id,
981
+ strategy: args.candidate_id.split('|')[2],
982
+ accruedFundingUsd: 0,
983
+ fundingThroughTime: args.candidate_id.split('|')[0],
984
+ } : {}),
985
+ thesis: args.thesis?.slice(0, 500), // Cap length
986
+ stopPrice: num(args.stopPrice),
987
+ targetPrice: num(args.target_price),
988
+ regime: args.regime,
989
+ regimeConfidence: clamp(args.regime_confidence, 0, 100),
990
+ scorecardVerdict: args.scorecard_verdict,
991
+ confluenceScore: clamp(args.confluence_score, 0, 10),
992
+ invalidationPrice: num(args.invalidation_price),
993
+ realizationRule: sanitizeRealizationRule(args.realization_rule),
994
+ } : undefined;
995
+ // ---- Approval-mode branches ----
996
+ // Both shadow and per_trade need full v2.10.0 metadata so the proposal is
997
+ // operator-meaningful. Metadata is the agent's responsibility; ingest creds
998
+ // are the operator's. Separate so the error messages can be precise.
999
+ const argsMetadataComplete = !!(args.thesis &&
1000
+ args.setup_type &&
1001
+ args.target_price != null &&
1002
+ args.stopPrice != null &&
1003
+ args.regime &&
1004
+ typeof args.regime_confidence === 'number' &&
1005
+ args.scorecard_verdict &&
1006
+ typeof args.confluence_score === 'number');
1007
+ const proposalPathWired = !!(deps.proposalManager && deps.userId);
1008
+ const approvalMode = deps.approvalMode ?? 'off';
1009
+ // per_trade mode: propose and EARLY-RETURN. The agent's call doesn't fire;
1010
+ // the operator approves and ProposalDecisionListener fires via this same
1011
+ // tool with proposalManager omitted (which takes the real path below).
1012
+ //
1013
+ // If proposalManager isn't wired (operator set approval.mode=per_trade in
1014
+ // config but ingest creds aren't present), we fall through to the normal
1015
+ // fire path — the startup warning is the operator's signal. We never trap
1016
+ // the agent inside an unfireable per_trade branch.
1017
+ if (approvalMode === 'per_trade' && proposalPathWired) {
1018
+ if (!argsMetadataComplete) {
1019
+ return {
1020
+ error: 'approval.mode=per_trade requires the full v2.10.0 metadata block on every create_order: ' +
1021
+ 'thesis, setup_type, target_price, stopPrice, regime, regime_confidence, ' +
1022
+ 'scorecard_verdict, confluence_score. The operator approves on this metadata.',
1023
+ };
1024
+ }
1025
+ try {
1026
+ const result = deps.proposalManager.propose(deps.userId, {
1027
+ symbol: args.symbol,
1028
+ side: side,
1029
+ orderType: type,
1030
+ size: args.amount,
1031
+ proposedEntry: orderPrice,
1032
+ stopPrice: args.stopPrice,
1033
+ targetPrice: args.target_price,
1034
+ riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
1035
+ thesis: args.thesis,
1036
+ setupType: args.setup_type,
1037
+ regime: args.regime,
1038
+ regimeConfidence: args.regime_confidence,
1039
+ scorecardVerdict: args.scorecard_verdict,
1040
+ confluenceScore: args.confluence_score,
1041
+ ...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
1042
+ // Real, operator-actionable. Default would also resolve to 'real' but
1043
+ // we set it explicitly so the intent reads at the call site.
1044
+ origin: 'real',
1045
+ });
1046
+ return {
1047
+ status: 'pending_approval',
1048
+ proposal_id: result.proposalUuid,
1049
+ hard_expires_at: result.hardExpiresAt.toISOString(),
1050
+ setup_bucket: result.setupBucket,
1051
+ message: `Order awaits operator approval (${result.setupBucket} bucket, expires ` +
1052
+ `${result.hardExpiresAt.toISOString()}). Do not call create_order again ` +
1053
+ `for this setup; the listener will fire on approval. If the operator ` +
1054
+ `requests modifications in chat, call create_order with supersedes_id=` +
1055
+ `<id from the chat-context header>.`,
1056
+ };
1057
+ }
1058
+ catch (err) {
1059
+ return { error: `Proposal submission failed: ${formatError(err)}` };
1060
+ }
318
1061
  }
319
- const order = await deps.adapter.createOrder(args.symbol, side, type, args.amount, args.price, metadata, stashCid ? { clientOrderId: stashCid } : undefined);
320
- // Auto-capture entry to the Position Decision Journal fail-open, never
321
- // block the trading hot path on a webapp ingest blip.
322
- if (deps.autoCapture) {
323
- if (stashCid &&
324
- deps.autoCapture.pendingEntries &&
325
- typeof order.id === 'string' &&
326
- order.id.length > 0) {
327
- deps.autoCapture.pendingEntries.promote(stashCid, order.id);
328
- }
329
- const filledNow = typeof order.filled === 'number' && order.filled > 0;
330
- if (filledNow) {
331
- // Order filled at submit (market, or limit-that-crossed). Capture
332
- // synchronously — same as the original PR1 path.
333
- onCreateOrderFilled(deps.autoCapture, { symbol: args.symbol, side: side, metadata }, order).catch((err) => {
334
- // eslint-disable-next-line no-console
335
- console.warn(`[position-auto-capture] entry capture threw: ${formatError(err)}`);
1062
+ // shadow mode: dual-write a telemetry proposal AND continue to the real
1063
+ // fire below. Never blocks the hot path on a propose() failure.
1064
+ if (approvalMode === 'shadow' && proposalPathWired && argsMetadataComplete) {
1065
+ try {
1066
+ deps.proposalManager.propose(deps.userId, {
1067
+ symbol: args.symbol,
1068
+ side: side,
1069
+ orderType: type,
1070
+ size: args.amount,
1071
+ proposedEntry: orderPrice,
1072
+ stopPrice: args.stopPrice,
1073
+ targetPrice: args.target_price,
1074
+ riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
1075
+ thesis: args.thesis,
1076
+ setupType: args.setup_type,
1077
+ regime: args.regime,
1078
+ regimeConfidence: args.regime_confidence,
1079
+ scorecardVerdict: args.scorecard_verdict,
1080
+ confluenceScore: args.confluence_score,
1081
+ ...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
1082
+ // Phase A telemetry — agent's create_order ALSO fires the real order
1083
+ // synchronously below. Hidden from operator UI by origin='shadow'.
1084
+ origin: 'shadow',
336
1085
  });
337
1086
  }
1087
+ catch (err) {
1088
+ // eslint-disable-next-line no-console
1089
+ console.warn(`[approval-shadow] propose() threw: ${formatError(err)}`);
1090
+ }
1091
+ }
1092
+ try {
1093
+ // Stash the metadata BEFORE submission, keyed by a pre-generated
1094
+ // clientOrderId (fixed 2026-07-07; supersedes the 2026-07-05 post-return
1095
+ // stash). For market orders the user-data WS fill routinely arrives
1096
+ // BEFORE the REST ack resolves — observed on prod (FIL 2026-07-06
1097
+ // 20:28:17: WS capture consumed nothing and journaled metadata=none while
1098
+ // the post-return stash landed milliseconds later). The WS
1099
+ // ORDER_TRADE_UPDATE carries the clientOrderId (`o.c`), so
1100
+ // onWsFillObserved falls back to it when the exchange orderId lookup
1101
+ // misses; promote() below adds the exchange-orderId alias once the REST
1102
+ // ack returns for the normal (WS-after-REST) ordering. If the submission
1103
+ // throws, the unused stash entry simply expires (24h TTL, pruned).
1104
+ let stashCid = wave9Claimed && initialWave9Mode === 'LIVE'
1105
+ ? wave9ClientOrderId(args.candidate_id)
1106
+ : undefined;
1107
+ if (deps.autoCapture?.pendingEntries && metadata) {
1108
+ stashCid ??= randomUUID();
1109
+ deps.autoCapture.pendingEntries.put({
1110
+ orderId: stashCid,
1111
+ clientOrderId: stashCid,
1112
+ symbol: args.symbol,
1113
+ side: side,
1114
+ metadata,
1115
+ });
1116
+ }
1117
+ if (wave9Lease) {
1118
+ if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
1119
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'trading_mode_changed_before_mutation', { retryable: true, latchEvent: false });
1120
+ wave9Lease = undefined;
1121
+ return { error: 'Wave 9 order refused: adapter or trading mode changed before mutation.' };
1122
+ }
1123
+ if (deps.wave9VenueCheck?.() !== 'binance') {
1124
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'execution_venue_changed_before_mutation', { retryable: true, latchEvent: false });
1125
+ wave9Lease = undefined;
1126
+ return { error: 'Wave 9 order refused: execution venue changed away from Binance before mutation.' };
1127
+ }
1128
+ let activation;
1129
+ try {
1130
+ activation = deps.wave9ActivationCheck
1131
+ ? await deps.wave9ActivationCheck()
1132
+ : { available: false, active: false };
1133
+ }
1134
+ catch {
1135
+ activation = { available: false, active: false };
1136
+ }
1137
+ if (!activation.available || !activation.active) {
1138
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'activation_unavailable_or_inactive_before_mutation', { retryable: true, latchEvent: false });
1139
+ wave9Lease = undefined;
1140
+ return { error: 'Wave 9 order refused: strategy was toggled off or activation could not be re-verified.' };
1141
+ }
1142
+ if (deps.adapter.readiness !== 'READY') {
1143
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'adapter_not_ready_before_mutation', { retryable: true, latchEvent: false });
1144
+ wave9Lease = undefined;
1145
+ return { error: `Wave 9 order refused: adapter changed to ${deps.adapter.readiness} before mutation.` };
1146
+ }
1147
+ if (initialWave9Mode === 'LIVE' && !bracketsEnabled(loadBracketMode())) {
1148
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'native_brackets_disabled_before_mutation', { retryable: true, latchEvent: false });
1149
+ wave9Lease = undefined;
1150
+ return { error: 'Wave 9 live order refused: native bracket protection was disabled before mutation.' };
1151
+ }
1152
+ }
1153
+ const submit = async () => {
1154
+ if (!wave9Claimed && !deps.adapter.isLive) {
1155
+ deps.refreshPaperState?.();
1156
+ const positions = await deps.adapter.getPositionsOrNull();
1157
+ if (positions === null) {
1158
+ throw new Error('generic create_order final isolation check could not read paper positions');
1159
+ }
1160
+ if (positions.some(isWave9ManagedPosition)) {
1161
+ throw new Error('Generic create_order refused at mutation boundary: the paper account now contains an atomic Wave 9 position.');
1162
+ }
1163
+ }
1164
+ if (!wave9Claimed && deps.adapter.isLive && deps.checkWave9LiveSymbolOwnership) {
1165
+ let ownership;
1166
+ try {
1167
+ ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
1168
+ }
1169
+ catch (err) {
1170
+ throw new Error(`Generic create_order refused at mutation boundary: Wave 9 LIVE ownership ` +
1171
+ `check failed for ${args.symbol} (${formatError(err)}).`);
1172
+ }
1173
+ if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
1174
+ throw new Error(`Generic create_order refused at mutation boundary: ${args.symbol} has ` +
1175
+ `${ownership.status} Wave 9 LIVE ownership (${ownership.reason}).`);
1176
+ }
1177
+ }
1178
+ return deps.adapter.createOrder(args.symbol, side, type, args.amount, args.price, metadata, stashCid ? { clientOrderId: stashCid } : undefined);
1179
+ };
1180
+ const submitWave9 = async () => {
1181
+ if (!wave9Lease)
1182
+ throw new Error('Wave 9 admission lease is unavailable at mutation boundary.');
1183
+ if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
1184
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'trading_mode_changed_during_activation_check', { retryable: true, latchEvent: false });
1185
+ wave9Lease = undefined;
1186
+ throw new Error('Wave 9 order refused: adapter or trading mode changed during activation verification.');
1187
+ }
1188
+ if (deps.wave9VenueCheck?.() !== 'binance') {
1189
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'execution_venue_changed_at_mutation_boundary', { retryable: true, latchEvent: false });
1190
+ wave9Lease = undefined;
1191
+ throw new Error('Wave 9 order refused: execution venue changed away from Binance at mutation boundary.');
1192
+ }
1193
+ let currentFingerprint;
1194
+ if (initialWave9Mode === 'LIVE') {
1195
+ const ownership = deps.checkWave9LiveSymbolOwnership
1196
+ ? await deps.checkWave9LiveSymbolOwnership(args.symbol)
1197
+ : await inspectWave9LiveSymbolOwnership(deps.adapter, deps.wave9LiveLedger, args.symbol);
1198
+ if (ownership.status !== 'unowned'
1199
+ || ownership.reason === 'authoritative_symbol_flat') {
1200
+ deps.wave9AdmissionGuard.abort(wave9Lease, `live_symbol_ownership_changed_before_mutation:${ownership.status}:${ownership.reason}`, { retryable: true, latchEvent: false });
1201
+ wave9Lease = undefined;
1202
+ throw new Error(`Wave 9 live order refused: candidate-symbol ownership changed before mutation ` +
1203
+ `(${ownership.status}:${ownership.reason}); refresh get_wave9_status.`);
1204
+ }
1205
+ currentFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
1206
+ }
1207
+ else {
1208
+ deps.refreshPaperState?.();
1209
+ currentFingerprint = captureWave9PaperAccountSnapshot(deps.adapter).fingerprint;
1210
+ }
1211
+ deps.wave9AdmissionGuard.revalidate(wave9Lease, currentFingerprint);
1212
+ if (initialWave9Mode === 'LIVE') {
1213
+ const preEntryFlat = await confirmWave9LivePositionFlat(deps.adapter, args.symbol);
1214
+ if (!preEntryFlat.confirmed) {
1215
+ deps.wave9AdmissionGuard.abort(wave9Lease, `live_candidate_symbol_not_stably_flat_before_entry:${preEntryFlat.detail}`, { retryable: true, latchEvent: false });
1216
+ wave9Lease = undefined;
1217
+ throw new Error(`Wave 9 live order refused: candidate symbol was not authoritatively stably flat ` +
1218
+ `immediately before entry (${preEntryFlat.detail}). No order was submitted; refresh ` +
1219
+ 'get_wave9_status.');
1220
+ }
1221
+ const refuseAfterFlatProof = (reason, detail) => {
1222
+ if (wave9Lease) {
1223
+ deps.wave9AdmissionGuard.abort(wave9Lease, reason, { retryable: true, latchEvent: false });
1224
+ }
1225
+ wave9Lease = undefined;
1226
+ throw new Error(`${detail} No order was submitted; refresh get_wave9_status.`);
1227
+ };
1228
+ let postFlatActivation;
1229
+ try {
1230
+ postFlatActivation = deps.wave9ActivationCheck
1231
+ ? await deps.wave9ActivationCheck()
1232
+ : { available: false, active: false };
1233
+ }
1234
+ catch {
1235
+ postFlatActivation = { available: false, active: false };
1236
+ }
1237
+ if (!postFlatActivation.available || !postFlatActivation.active) {
1238
+ 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.');
1239
+ }
1240
+ if (currentWave9ExecutionMode(deps) !== initialWave9Mode) {
1241
+ refuseAfterFlatProof('trading_mode_changed_after_flat_proof', 'Wave 9 live order refused: adapter or trading mode changed after the stable-flat proof.');
1242
+ }
1243
+ if (deps.wave9VenueCheck?.() !== 'binance') {
1244
+ refuseAfterFlatProof('execution_venue_changed_after_flat_proof', 'Wave 9 live order refused: execution venue changed away from Binance after the stable-flat proof.');
1245
+ }
1246
+ if (deps.adapter.readiness !== 'READY') {
1247
+ refuseAfterFlatProof('adapter_not_ready_after_flat_proof', `Wave 9 live order refused: adapter changed to ${deps.adapter.readiness} after the stable-flat proof.`);
1248
+ }
1249
+ if (!bracketsEnabled(loadBracketMode())) {
1250
+ refuseAfterFlatProof('native_brackets_disabled_after_flat_proof', 'Wave 9 live order refused: native bracket protection was disabled after the stable-flat proof.');
1251
+ }
1252
+ let postFlatFingerprint;
1253
+ try {
1254
+ postFlatFingerprint = (await deps.captureWave9AccountFingerprint()).fingerprint;
1255
+ }
1256
+ catch (error) {
1257
+ refuseAfterFlatProof(`live_account_fingerprint_unavailable_after_flat_proof:${formatError(error)}`, `Wave 9 live order refused: the LIVE account fingerprint could not be refreshed ` +
1258
+ `after the stable-flat proof (${formatError(error)}).`);
1259
+ }
1260
+ try {
1261
+ deps.wave9AdmissionGuard.revalidate(wave9Lease, postFlatFingerprint);
1262
+ }
1263
+ catch (error) {
1264
+ refuseAfterFlatProof(`authorization_revalidation_failed_after_flat_proof:${formatError(error)}`, `Wave 9 live order refused: authorization or LIVE account identity changed during ` +
1265
+ `the stable-flat proof (${formatError(error)}).`);
1266
+ }
1267
+ const [eventTime, , strategy, direction] = args.candidate_id.split('|');
1268
+ await deps.wave9LiveLedger.recordAttempt({
1269
+ candidateId: args.candidate_id,
1270
+ missionId: args.mission_id,
1271
+ eventTime,
1272
+ symbol: args.symbol,
1273
+ strategy: strategy,
1274
+ direction: direction,
1275
+ side: side,
1276
+ quantity: args.amount,
1277
+ initialStop: args.stopPrice,
1278
+ clientOrderId: stashCid,
1279
+ });
1280
+ }
1281
+ const finishLiveEntryRecovery = (recovery, source) => {
1282
+ if (recovery.status === 'protected') {
1283
+ deps.wave9AdmissionGuard.commit(wave9Lease);
1284
+ wave9Lease = undefined;
1285
+ if (recovery.order)
1286
+ return recovery.order;
1287
+ throw new Error(`Wave 9 LIVE ${source}, but the entry was recovered OPEN and protected. ` +
1288
+ `Do not retry this candidate. ${recovery.detail}`);
1289
+ }
1290
+ deps.wave9AdmissionGuard.abort(wave9Lease, recovery.status === 'terminal'
1291
+ ? 'live_entry_recovery_confirmed_terminal'
1292
+ : 'live_entry_recovery_manual_intervention');
1293
+ wave9Lease = undefined;
1294
+ throw new Error(recovery.status === 'terminal'
1295
+ ? `Wave 9 LIVE ${source} resolved safely and is terminal. ${recovery.detail}`
1296
+ : `CRITICAL: ${recovery.detail}`);
1297
+ };
1298
+ let order;
1299
+ try {
1300
+ order = await submit();
1301
+ }
1302
+ catch (submitError) {
1303
+ if (initialWave9Mode !== 'LIVE')
1304
+ throw submitError;
1305
+ const recovery = await recoverWave9LiveSubmitThrow({
1306
+ adapter: deps.adapter,
1307
+ ledger: deps.wave9LiveLedger,
1308
+ candidateId: args.candidate_id,
1309
+ clientOrderId: stashCid,
1310
+ symbol: args.symbol,
1311
+ side: side,
1312
+ stopPrice: args.stopPrice,
1313
+ authorizedQuantity: args.amount,
1314
+ submitError,
1315
+ confirmStop: deps.confirmWave9LiveStop,
1316
+ residualProtector: deps.reprotectWave9LiveResidual,
1317
+ });
1318
+ return finishLiveEntryRecovery(recovery, 'submit acknowledgement failed');
1319
+ }
1320
+ const filledQuantity = typeof order.filled === 'number'
1321
+ && Number.isFinite(order.filled)
1322
+ && order.filled > 0
1323
+ ? order.filled
1324
+ : undefined;
1325
+ if (filledQuantity === undefined) {
1326
+ if (initialWave9Mode === 'LIVE') {
1327
+ const recovery = await recoverWave9LiveSubmitThrow({
1328
+ adapter: deps.adapter,
1329
+ ledger: deps.wave9LiveLedger,
1330
+ candidateId: args.candidate_id,
1331
+ clientOrderId: stashCid,
1332
+ symbol: args.symbol,
1333
+ side: side,
1334
+ stopPrice: args.stopPrice,
1335
+ authorizedQuantity: args.amount,
1336
+ submitError: new Error('market acknowledgement lacked a positive confirmed fill quantity'),
1337
+ acknowledgedOrder: order,
1338
+ confirmStop: deps.confirmWave9LiveStop,
1339
+ residualProtector: deps.reprotectWave9LiveResidual,
1340
+ });
1341
+ return finishLiveEntryRecovery(recovery, 'market acknowledgement lacked a positive confirmed fill quantity');
1342
+ }
1343
+ deps.wave9AdmissionGuard.abort(wave9Lease, 'market_order_not_filled');
1344
+ wave9Lease = undefined;
1345
+ throw new Error('Wave 9 market order lacks a positive confirmed fill quantity; admission batch latched.');
1346
+ }
1347
+ if (initialWave9Mode === 'LIVE'
1348
+ && !candidateBoundFoundOrder({ status: 'found', order }, stashCid, args.symbol, side)) {
1349
+ const recovery = await recoverWave9LiveSubmitThrow({
1350
+ adapter: deps.adapter,
1351
+ ledger: deps.wave9LiveLedger,
1352
+ candidateId: args.candidate_id,
1353
+ clientOrderId: stashCid,
1354
+ symbol: args.symbol,
1355
+ side: side,
1356
+ stopPrice: args.stopPrice,
1357
+ authorizedQuantity: args.amount,
1358
+ submitError: new Error('market acknowledgement had malformed or mismatched candidate identity'),
1359
+ confirmStop: deps.confirmWave9LiveStop,
1360
+ residualProtector: deps.reprotectWave9LiveResidual,
1361
+ });
1362
+ return finishLiveEntryRecovery(recovery, 'market acknowledgement had malformed or mismatched candidate identity');
1363
+ }
1364
+ if (initialWave9Mode === 'LIVE' && order.status === 'open') {
1365
+ const recovery = await recoverWave9LiveSubmitThrow({
1366
+ adapter: deps.adapter,
1367
+ ledger: deps.wave9LiveLedger,
1368
+ candidateId: args.candidate_id,
1369
+ clientOrderId: stashCid,
1370
+ symbol: args.symbol,
1371
+ side: side,
1372
+ stopPrice: args.stopPrice,
1373
+ authorizedQuantity: args.amount,
1374
+ submitError: new Error('market acknowledgement left an open entry remainder'),
1375
+ acknowledgedOrder: order,
1376
+ confirmStop: deps.confirmWave9LiveStop,
1377
+ residualProtector: deps.reprotectWave9LiveResidual,
1378
+ });
1379
+ return finishLiveEntryRecovery(recovery, 'market acknowledgement left an open entry remainder');
1380
+ }
1381
+ if (initialWave9Mode === 'LIVE') {
1382
+ const entryPrice = confirmedWave9EntryPrice(order, side, args.stopPrice);
1383
+ const exchangeOrderId = confirmedWave9ExchangeOrderId(order);
1384
+ let openPersistenceError;
1385
+ try {
1386
+ await deps.wave9LiveLedger.markOpen(args.candidate_id, {
1387
+ filledQuantity,
1388
+ ...(exchangeOrderId ? { exchangeOrderId } : {}),
1389
+ ...(entryPrice !== undefined ? { entryPrice } : {}),
1390
+ });
1391
+ }
1392
+ catch (err) {
1393
+ openPersistenceError = formatError(err);
1394
+ }
1395
+ if (openPersistenceError) {
1396
+ const flatten = await flattenFailedWave9LiveEntry(deps.adapter, deps.wave9LiveLedger, args.symbol, args.candidate_id, {
1397
+ side: side,
1398
+ maximumQuantity: Math.min(filledQuantity, args.amount),
1399
+ }, deps.reprotectWave9LiveResidual);
1400
+ deps.wave9AdmissionGuard.abort(wave9Lease, flatten.confirmedFlat
1401
+ ? 'live_open_persistence_failed_confirmed_flat'
1402
+ : 'live_open_persistence_failed_residual_exposure');
1403
+ wave9Lease = undefined;
1404
+ const flattenError = flatten.flattenError
1405
+ ? ` Close submission error: ${flatten.flattenError}.`
1406
+ : '';
1407
+ if (flatten.confirmedFlat && !flatten.terminalPersistenceError) {
1408
+ throw new Error(`Wave 9 confirmed fill could not be durably marked OPEN (${openPersistenceError}); ` +
1409
+ `${flatten.closeAttempted
1410
+ ? 'the position was auto-flattened and confirmed flat'
1411
+ : 'the exchange position was already confirmed stably flat'}, and its replay tombstone was ` +
1412
+ `persisted terminal.${flattenError}`);
1413
+ }
1414
+ if (flatten.confirmedFlat) {
1415
+ throw new Error(`Wave 9 confirmed fill could not be durably marked OPEN (${openPersistenceError}). ` +
1416
+ `The exchange is confirmed flat, but durable CLOSED persistence also failed ` +
1417
+ `(${flatten.terminalPersistenceError}).${flattenError}`);
1418
+ }
1419
+ throw new Error(`CRITICAL: Wave 9 confirmed fill could not be durably marked OPEN ` +
1420
+ `(${openPersistenceError}), and ${flatten.closeAttempted
1421
+ ? 'emergency flatten is not confirmed flat'
1422
+ : 'the exposure identity fence refused symbol-wide flatten'} ` +
1423
+ `(${flatten.confirmationDetail}). In-memory OPEN ownership was retained where possible; ` +
1424
+ `the entry capability is consumed and latched.${flattenError}${residualProtectionMessage(flatten)}`);
1425
+ }
1426
+ let stopConfirmed = false;
1427
+ try {
1428
+ stopConfirmed = await deps.confirmWave9LiveStop({
1429
+ candidateId: args.candidate_id,
1430
+ symbol: args.symbol,
1431
+ entryClientOrderId: stashCid,
1432
+ entrySide: side,
1433
+ stopPrice: args.stopPrice,
1434
+ filledQuantity,
1435
+ });
1436
+ }
1437
+ catch {
1438
+ stopConfirmed = false;
1439
+ }
1440
+ if (!stopConfirmed) {
1441
+ const flatten = await flattenFailedWave9LiveEntry(deps.adapter, deps.wave9LiveLedger, args.symbol, args.candidate_id, {
1442
+ side: side,
1443
+ maximumQuantity: Math.min(filledQuantity, args.amount),
1444
+ }, deps.reprotectWave9LiveResidual);
1445
+ deps.wave9AdmissionGuard.abort(wave9Lease, flatten.confirmedFlat
1446
+ ? 'live_stop_unconfirmed_confirmed_flat'
1447
+ : 'live_stop_unconfirmed_residual_exposure');
1448
+ wave9Lease = undefined;
1449
+ const flattenError = flatten.flattenError
1450
+ ? ` Close submission error: ${flatten.flattenError}.`
1451
+ : '';
1452
+ if (flatten.confirmedFlat && !flatten.terminalPersistenceError) {
1453
+ throw new Error(`Wave 9 live stop was not confirmed; ${flatten.closeAttempted
1454
+ ? 'the filled position was auto-flattened and confirmed flat'
1455
+ : 'the exchange position was already confirmed stably flat'}, and its durable ` +
1456
+ `execution row is terminal.${flattenError}`);
1457
+ }
1458
+ if (flatten.confirmedFlat) {
1459
+ throw new Error(`Wave 9 live stop was not confirmed. The exchange is confirmed flat, but durable ` +
1460
+ `CLOSED persistence failed (${flatten.terminalPersistenceError}).${flattenError}`);
1461
+ }
1462
+ throw new Error(`CRITICAL: Wave 9 live stop was not confirmed and ${flatten.closeAttempted
1463
+ ? 'emergency flatten is not confirmed flat'
1464
+ : 'the exposure identity fence refused symbol-wide flatten'} ` +
1465
+ `(${flatten.confirmationDetail}). Durable ownership remains OPEN; the entry ` +
1466
+ `capability is consumed and latched.${flattenError}${residualProtectionMessage(flatten)}`);
1467
+ }
1468
+ }
1469
+ deps.wave9AdmissionGuard.commit(wave9Lease);
1470
+ wave9Lease = undefined;
1471
+ return order;
1472
+ };
1473
+ const order = wave9Claimed
1474
+ ? await deps.operationLock.withAccountLock(`wave9-create-final:${args.symbol}`, submitWave9)
1475
+ : deps.operationLock
1476
+ ? await deps.operationLock.withAccountLock(`generic-create-final:${args.symbol}`, submit)
1477
+ : await submit();
1478
+ // Auto-capture entry to the Position Decision Journal — fail-open, never
1479
+ // block the trading hot path on a webapp ingest blip.
1480
+ if (deps.autoCapture) {
1481
+ if (stashCid &&
1482
+ deps.autoCapture.pendingEntries &&
1483
+ typeof order.id === 'string' &&
1484
+ order.id.length > 0) {
1485
+ deps.autoCapture.pendingEntries.promote(stashCid, order.id);
1486
+ }
1487
+ const filledNow = typeof order.filled === 'number' && order.filled > 0;
1488
+ if (filledNow) {
1489
+ // Order filled at submit (market, or limit-that-crossed). Capture
1490
+ // synchronously — same as the original PR1 path.
1491
+ onCreateOrderFilled(deps.autoCapture, { symbol: args.symbol, side: side, metadata }, order).catch((err) => {
1492
+ // eslint-disable-next-line no-console
1493
+ console.warn(`[position-auto-capture] entry capture threw: ${formatError(err)}`);
1494
+ });
1495
+ }
1496
+ }
1497
+ return order;
1498
+ }
1499
+ catch (err) {
1500
+ if (wave9Lease) {
1501
+ deps.wave9AdmissionGuard.abort(wave9Lease, `order_execution_failed:${formatError(err)}`);
1502
+ wave9Lease = undefined;
1503
+ }
1504
+ return { error: formatError(err) };
338
1505
  }
339
- return order;
340
1506
  }
341
1507
  catch (err) {
1508
+ if (!wave9Claimed)
1509
+ throw err;
1510
+ if (wave9Lease) {
1511
+ deps.wave9AdmissionGuard.abort(wave9Lease, `pre_mutation_processing_failed:${formatError(err)}`, { retryable: true, latchEvent: false });
1512
+ wave9Lease = undefined;
1513
+ }
342
1514
  return { error: formatError(err) };
343
1515
  }
344
1516
  }