@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
@@ -0,0 +1,435 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { getQuoteWalletBalance } from '../balance-utils.js';
3
+ import { parseBracketCid } from '../live/bracket-id.js';
4
+ import { WAVE9_BUNDLE_SETUP_TYPE, } from './paper-admission-guard.js';
5
+ import { wave9ClientOrderId, } from './live-execution-ledger.js';
6
+ function canonicalSymbol(symbol) {
7
+ return symbol.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
8
+ }
9
+ function finitePositive(value, label) {
10
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
11
+ throw new Error(`Wave 9 live snapshot requires positive ${label}`);
12
+ }
13
+ return value;
14
+ }
15
+ function nearlyEqual(left, right) {
16
+ return Math.abs(left - right) <= 1e-9 * Math.max(1, Math.abs(left), Math.abs(right));
17
+ }
18
+ /**
19
+ * Binance may tick-normalize the submitted trigger while the local bracket
20
+ * row retains the requested ATR-derived value. One basis point matches the
21
+ * existing bracket idempotency tolerance, admits normal tick rounding, and
22
+ * still rejects any materially different stop.
23
+ */
24
+ function exchangeStopPriceMatches(actual, requested) {
25
+ return Math.abs(actual - requested)
26
+ <= Math.max(1e-12, Math.abs(requested) * 1e-4);
27
+ }
28
+ function asNativeStopAdapter(adapter) {
29
+ const candidate = adapter;
30
+ return typeof candidate.getBracketLedger === 'function'
31
+ && typeof candidate.resolveBracketLegLiveness === 'function'
32
+ ? candidate
33
+ : null;
34
+ }
35
+ function openWave9Record(ledger, symbol, side) {
36
+ const canonical = canonicalSymbol(symbol);
37
+ const orderSide = side === 'long' ? 'buy' : 'sell';
38
+ const matches = ledger.getAll().filter((record) => record.state === 'open'
39
+ && record.symbol === canonical
40
+ && record.side === orderSide);
41
+ matches.sort((left, right) => right.updatedAt - left.updatedAt || right.candidateId.localeCompare(left.candidateId));
42
+ return matches[0];
43
+ }
44
+ function emptyStopObservation(reason) {
45
+ return {
46
+ verified: false,
47
+ reason,
48
+ bracketId: null,
49
+ bracketState: null,
50
+ entryClientOrderId: null,
51
+ stopClientOrderId: null,
52
+ exchangeOrderId: null,
53
+ entrySide: null,
54
+ stopSide: null,
55
+ stopPrice: null,
56
+ bracketQuantity: null,
57
+ closePosition: null,
58
+ liveness: null,
59
+ };
60
+ }
61
+ function rawOrderField(order, key) {
62
+ const direct = order.info?.[key];
63
+ if (direct !== undefined)
64
+ return direct;
65
+ const nested = order.info?.info;
66
+ return typeof nested === 'object' && nested !== null && !Array.isArray(nested)
67
+ ? nested[key]
68
+ : undefined;
69
+ }
70
+ function boolField(value) {
71
+ if (value === true || value === 'true' || value === 'TRUE')
72
+ return true;
73
+ if (value === false || value === 'false' || value === 'FALSE')
74
+ return false;
75
+ return null;
76
+ }
77
+ function finiteOrderStopPrice(order) {
78
+ if (!order)
79
+ return null;
80
+ const candidates = [order.price, rawOrderField(order, 'triggerPrice'), rawOrderField(order, 'stopPrice')];
81
+ for (const value of candidates) {
82
+ const parsed = typeof value === 'number' ? value : Number(value);
83
+ if (Number.isFinite(parsed) && parsed > 0)
84
+ return parsed;
85
+ }
86
+ return null;
87
+ }
88
+ function proofLiveness(proof) {
89
+ if (!proof)
90
+ return 'unknown';
91
+ if (proof.algoStatus === 'NEW'
92
+ || proof.algoStatus === 'WORKING'
93
+ || proof.algoStatus === 'TRIGGERING')
94
+ return 'live';
95
+ if (proof.algoStatus === 'TRIGGERED'
96
+ || proof.algoStatus === 'CANCELED'
97
+ || proof.algoStatus === 'FINISHED'
98
+ || proof.algoStatus === 'EXPIRED'
99
+ || proof.algoStatus === 'REJECTED')
100
+ return 'terminal';
101
+ return 'unknown';
102
+ }
103
+ /**
104
+ * Bind one durable Wave 9 execution identity to the exact exchange-native
105
+ * stop currently protecting it. A failed observation remains structural data
106
+ * (so ownership/reversal handling survives) but can never authorize admission.
107
+ */
108
+ async function observeNativeStop(adapter, openOrders, expectation) {
109
+ const native = asNativeStopAdapter(adapter);
110
+ if (!native)
111
+ return emptyStopObservation('native_stop_adapter_unavailable');
112
+ const bracket = native.getBracketLedger()?.getBySymbol(expectation.symbol);
113
+ if (!bracket)
114
+ return emptyStopObservation('native_stop_ledger_row_missing');
115
+ const stopOrders = bracket.slCid
116
+ ? openOrders.filter((order) => order.clientOrderId === bracket.slCid)
117
+ : [];
118
+ const stopOrder = stopOrders.length === 1 ? stopOrders[0] : undefined;
119
+ const directProofRequired = !expectation.requireExchangeOrderDetails;
120
+ let directProof = null;
121
+ let liveness = null;
122
+ if (bracket.slCid) {
123
+ if (directProofRequired) {
124
+ try {
125
+ directProof = await native.resolveBracketLegProof?.(bracket.slCid) ?? null;
126
+ }
127
+ catch {
128
+ directProof = null;
129
+ }
130
+ liveness = proofLiveness(directProof);
131
+ }
132
+ else {
133
+ try {
134
+ liveness = await native.resolveBracketLegLiveness(bracket.slCid);
135
+ }
136
+ catch {
137
+ liveness = 'unknown';
138
+ }
139
+ }
140
+ }
141
+ const exchangeStopPrice = directProofRequired
142
+ ? directProof?.triggerPrice ?? null
143
+ : finiteOrderStopPrice(stopOrder);
144
+ const closePosition = directProofRequired
145
+ ? directProof?.closePosition ?? null
146
+ : stopOrder ? boolField(rawOrderField(stopOrder, 'closePosition')) : null;
147
+ const exchangeSymbol = directProofRequired ? directProof?.symbol : stopOrder?.symbol;
148
+ const exchangeSide = directProofRequired ? directProof?.side : stopOrder?.side;
149
+ const orderType = directProofRequired
150
+ ? directProof?.orderType ?? ''
151
+ : stopOrder ? String(rawOrderField(stopOrder, 'orderType') ?? '').toUpperCase() : '';
152
+ const observation = {
153
+ verified: false,
154
+ reason: 'native_stop_unverified',
155
+ bracketId: bracket.bracketId,
156
+ bracketState: bracket.state,
157
+ entryClientOrderId: bracket.entryCid,
158
+ stopClientOrderId: bracket.slCid ?? null,
159
+ exchangeOrderId: directProofRequired ? directProof?.algoId ?? null : stopOrder?.id ?? null,
160
+ entrySide: bracket.entrySide,
161
+ stopSide: exchangeSide ?? null,
162
+ stopPrice: exchangeStopPrice ?? (Number.isFinite(bracket.stopPrice) ? bracket.stopPrice : null),
163
+ bracketQuantity: Number.isFinite(bracket.qty) ? bracket.qty : null,
164
+ closePosition,
165
+ liveness,
166
+ };
167
+ const parsedStopCid = bracket.slCid ? parseBracketCid(bracket.slCid) : null;
168
+ const expectedStopSide = expectation.entrySide === 'buy' ? 'sell' : 'buy';
169
+ const expectedEntryCid = wave9ClientOrderId(expectation.candidateId);
170
+ const quantityTolerance = 1e-9 * Math.max(1, expectation.currentPositionQuantity, expectation.filledQuantity, typeof bracket.qty === 'number' ? Math.abs(bracket.qty) : 0);
171
+ const exactCurrentQuantity = nearlyEqual(expectation.currentPositionQuantity, expectation.filledQuantity);
172
+ const bracketQuantityValid = typeof bracket.qty === 'number'
173
+ && Number.isFinite(bracket.qty)
174
+ && bracket.qty > 0
175
+ && (expectation.requireExactPositionQuantity
176
+ ? nearlyEqual(bracket.qty, expectation.filledQuantity)
177
+ : bracket.qty + quantityTolerance >= expectation.currentPositionQuantity
178
+ && bracket.qty <= expectation.filledQuantity + quantityTolerance);
179
+ if (expectation.entryClientOrderId !== expectedEntryCid) {
180
+ observation.reason = 'expected_entry_client_order_id_is_not_candidate_bound';
181
+ }
182
+ else if (bracket.state !== 'active' && bracket.state !== 'partial') {
183
+ observation.reason = 'native_stop_bracket_not_active';
184
+ }
185
+ else if (bracket.entryCid !== expectation.entryClientOrderId) {
186
+ observation.reason = 'native_stop_entry_client_order_id_mismatch';
187
+ }
188
+ else if (bracket.entrySide !== expectation.entrySide) {
189
+ observation.reason = 'native_stop_entry_side_mismatch';
190
+ }
191
+ else if (!bracket.slCid || parsedStopCid?.role !== 'stop'
192
+ || parsedStopCid.bracketId !== bracket.bracketId) {
193
+ observation.reason = 'native_stop_client_order_id_invalid';
194
+ }
195
+ else if (typeof bracket.stopPrice !== 'number'
196
+ || !Number.isFinite(bracket.stopPrice)
197
+ || !nearlyEqual(bracket.stopPrice, expectation.stopPrice)) {
198
+ observation.reason = 'native_stop_ledger_price_mismatch';
199
+ }
200
+ else if (expectation.currentPositionQuantity
201
+ > expectation.filledQuantity + quantityTolerance
202
+ || (expectation.requireExactPositionQuantity && !exactCurrentQuantity)) {
203
+ observation.reason = 'native_stop_position_quantity_not_covered';
204
+ }
205
+ else if (!bracketQuantityValid) {
206
+ observation.reason = 'native_stop_bracket_quantity_mismatch';
207
+ }
208
+ else if (directProofRequired && !directProof) {
209
+ observation.reason = 'native_stop_exact_exchange_proof_unavailable';
210
+ }
211
+ else if (directProofRequired && directProof?.clientAlgoId !== bracket.slCid) {
212
+ observation.reason = 'native_stop_exact_exchange_identity_mismatch';
213
+ }
214
+ else if (liveness !== 'live') {
215
+ observation.reason = 'native_stop_exchange_liveness_unverified';
216
+ }
217
+ else if (!directProofRequired
218
+ && (stopOrders.length !== 1 || !stopOrder || stopOrder.status !== 'open')) {
219
+ observation.reason = 'native_stop_exchange_order_missing';
220
+ }
221
+ else if (!exchangeSymbol
222
+ || canonicalSymbol(exchangeSymbol) !== canonicalSymbol(expectation.symbol)) {
223
+ observation.reason = 'native_stop_exchange_symbol_mismatch';
224
+ }
225
+ else if (exchangeSide !== expectedStopSide) {
226
+ observation.reason = 'native_stop_exchange_side_mismatch';
227
+ }
228
+ else if (orderType !== 'STOP_MARKET') {
229
+ observation.reason = 'native_stop_exchange_type_mismatch';
230
+ }
231
+ else if (exchangeStopPrice === null
232
+ || !exchangeStopPriceMatches(exchangeStopPrice, expectation.stopPrice)) {
233
+ observation.reason = 'native_stop_exchange_price_mismatch';
234
+ }
235
+ else if (closePosition !== true) {
236
+ observation.reason = 'native_stop_is_not_close_all_position';
237
+ }
238
+ else {
239
+ observation.verified = true;
240
+ observation.reason = 'verified_exchange_native_close_all_stop';
241
+ }
242
+ return observation;
243
+ }
244
+ /** One-shot post-fill confirmation used after the bracket row becomes active. */
245
+ export async function confirmWave9LiveNativeStop(adapter, input) {
246
+ if (!adapter.isLive || adapter.mode !== 'LIVE' || adapter.readiness !== 'READY')
247
+ return false;
248
+ const observation = await observeNativeStop(adapter, [], {
249
+ ...input,
250
+ currentPositionQuantity: input.filledQuantity,
251
+ requireExactPositionQuantity: true,
252
+ requireExchangeOrderDetails: false,
253
+ });
254
+ return observation.verified;
255
+ }
256
+ function validatePositions(positions) {
257
+ for (const position of positions) {
258
+ if (typeof position.symbol !== 'string' || position.symbol.length === 0) {
259
+ throw new Error('Wave 9 live snapshot contains a malformed position symbol');
260
+ }
261
+ if (position.side !== 'long' && position.side !== 'short') {
262
+ throw new Error(`Wave 9 live snapshot contains a malformed ${position.symbol} side`);
263
+ }
264
+ if (typeof position.contracts !== 'number'
265
+ || !Number.isFinite(position.contracts)
266
+ || position.contracts < 0) {
267
+ throw new Error(`Wave 9 live snapshot contains malformed ${position.symbol} contracts`);
268
+ }
269
+ }
270
+ }
271
+ function isProtectiveOrder(order) {
272
+ if (order.clientOrderId) {
273
+ const parsed = parseBracketCid(order.clientOrderId);
274
+ if (parsed?.role === 'stop' || parsed?.role === 'target')
275
+ return true;
276
+ }
277
+ const info = order.info ?? {};
278
+ if (info.reduceOnly === true || info.closePosition === true)
279
+ return true;
280
+ const orderType = String(info.orderType ?? info.type ?? '').toUpperCase();
281
+ return Boolean((orderType === 'STOP_MARKET' || orderType === 'TAKE_PROFIT_MARKET')
282
+ && (info.closePosition === true || info.reduceOnly === true));
283
+ }
284
+ function structuralPosition(position) {
285
+ return {
286
+ symbol: canonicalSymbol(position.symbol),
287
+ side: position.side,
288
+ contracts: position.contracts,
289
+ entryPrice: position.entryPrice,
290
+ };
291
+ }
292
+ function structuralOrder(order) {
293
+ return {
294
+ id: order.id,
295
+ clientOrderId: order.clientOrderId ?? null,
296
+ symbol: canonicalSymbol(order.symbol),
297
+ side: order.side,
298
+ type: order.type,
299
+ amount: order.amount,
300
+ filled: order.filled,
301
+ price: order.price,
302
+ };
303
+ }
304
+ function positionFingerprint(position) {
305
+ const structural = {
306
+ symbol: canonicalSymbol(position.symbol),
307
+ side: position.side,
308
+ quantity: position.quantity,
309
+ entryPrice: position.entryPrice,
310
+ stopPrice: position.stopPrice ?? null,
311
+ missionId: position.missionId ?? null,
312
+ setupType: position.setupType ?? null,
313
+ candidateId: position.candidateId ?? null,
314
+ strategy: position.strategy ?? null,
315
+ openedAt: position.openedAt ?? null,
316
+ nativeStop: position.nativeStop ?? null,
317
+ };
318
+ return createHash('sha256').update(JSON.stringify(structural)).digest('hex');
319
+ }
320
+ /**
321
+ * Capture exchange-authoritative LIVE geometry without volatile mark prices in
322
+ * the authorization fingerprint. Wallet cash, positions, pending
323
+ * exposure-increasing orders, and durable Wave 9 identities are all bound.
324
+ */
325
+ export async function captureWave9LiveAccount(adapter, ledger, mode) {
326
+ if (!adapter.isLive || mode !== 'LIVE' || adapter.mode !== 'LIVE') {
327
+ throw new Error('Wave 9 live snapshot requires a LIVE adapter in LIVE mode');
328
+ }
329
+ if (adapter.readiness !== 'READY') {
330
+ throw new Error(`Wave 9 live snapshot requires adapter READY (got ${adapter.readiness})`);
331
+ }
332
+ const [balance, positions, openOrders] = await Promise.all([
333
+ adapter.getBalance(),
334
+ adapter.getPositionsOrNull(),
335
+ adapter.getOpenOrders(),
336
+ ]);
337
+ if (positions === null) {
338
+ throw new Error('Wave 9 live position snapshot is unavailable');
339
+ }
340
+ validatePositions(positions);
341
+ const cashEquityUsd = finitePositive(getQuoteWalletBalance(balance), 'wallet cash');
342
+ const pendingExposureOrders = openOrders
343
+ .filter((order) => order.status === 'open' && !isProtectiveOrder(order))
344
+ .sort((left, right) => left.id.localeCompare(right.id));
345
+ const observed = await Promise.all(positions
346
+ .filter((position) => Number.isFinite(position.contracts) && position.contracts > 0)
347
+ .map(async (position) => {
348
+ const record = openWave9Record(ledger, position.symbol, position.side);
349
+ const row = {
350
+ symbol: position.symbol,
351
+ side: position.side,
352
+ quantity: finitePositive(position.contracts, `${position.symbol} contracts`),
353
+ entryPrice: finitePositive(position.entryPrice, `${position.symbol} entry price`),
354
+ markPrice: finitePositive(position.markPrice, `${position.symbol} mark price`),
355
+ };
356
+ if (!record)
357
+ return row;
358
+ const nativeStop = await observeNativeStop(adapter, openOrders, {
359
+ candidateId: record.candidateId,
360
+ symbol: position.symbol,
361
+ entryClientOrderId: record.clientOrderId,
362
+ entrySide: record.side,
363
+ stopPrice: record.initialStop,
364
+ filledQuantity: record.filledQuantity ?? record.quantity,
365
+ currentPositionQuantity: position.contracts,
366
+ requireExactPositionQuantity: false,
367
+ requireExchangeOrderDetails: true,
368
+ });
369
+ row.initialStopPrice = record.initialStop;
370
+ if (nativeStop.stopPrice !== null)
371
+ row.stopPrice = nativeStop.stopPrice;
372
+ row.missionId = record.missionId;
373
+ row.setupType = WAVE9_BUNDLE_SETUP_TYPE;
374
+ row.candidateId = record.candidateId;
375
+ row.strategy = record.strategy;
376
+ row.accruedFundingUsd = 0;
377
+ row.openedAt = new Date(record.openedAt ?? record.attemptedAt).toISOString();
378
+ row.nativeStop = nativeStop;
379
+ row.positionFingerprint = positionFingerprint(row);
380
+ return row;
381
+ }));
382
+ observed
383
+ .sort((left, right) => canonicalSymbol(left.symbol).localeCompare(canonicalSymbol(right.symbol)));
384
+ const executedCandidateIds = ledger.getExecutedCandidateIds().sort((a, b) => a.localeCompare(b));
385
+ const ownedPositions = observed.filter((position) => position.candidateId);
386
+ const nativeStopIssues = ownedPositions
387
+ .filter((position) => position.nativeStop?.verified !== true)
388
+ .map((position) => `${position.candidateId}:${position.nativeStop?.reason ?? 'native_stop_missing'}`)
389
+ .sort((left, right) => left.localeCompare(right));
390
+ const structural = {
391
+ schema: 'wave9-live-account-v1',
392
+ mode,
393
+ walletCashUsd: cashEquityUsd,
394
+ positions: positions
395
+ .filter((position) => Number.isFinite(position.contracts) && position.contracts > 0)
396
+ .map(structuralPosition)
397
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))),
398
+ pendingExposureOrders: pendingExposureOrders.map(structuralOrder),
399
+ wave9Identities: ledger.getAll().map((record) => ({
400
+ candidateId: record.candidateId,
401
+ missionId: record.missionId,
402
+ symbol: record.symbol,
403
+ side: record.side,
404
+ quantity: record.quantity,
405
+ initialStop: record.initialStop,
406
+ state: record.state,
407
+ })),
408
+ wave9NativeStops: ownedPositions.map((position) => ({
409
+ candidateId: position.candidateId,
410
+ nativeStop: position.nativeStop ?? null,
411
+ })),
412
+ executedCandidateIds,
413
+ };
414
+ return {
415
+ fingerprint: createHash('sha256').update(JSON.stringify(structural)).digest('hex'),
416
+ cashEquityUsd,
417
+ positions: observed,
418
+ pendingExposureOrderCount: pendingExposureOrders.length,
419
+ executedCandidateIds,
420
+ nativeStopProtectionComplete: nativeStopIssues.length === 0,
421
+ nativeStopIssues,
422
+ authoritative: true,
423
+ runtimeParity: false,
424
+ provenance: 'live_exchange_structural_snapshot_and_durable_wave9_ledger',
425
+ };
426
+ }
427
+ export async function captureWave9LivePosition(adapter, ledger, mode, symbol) {
428
+ const account = await captureWave9LiveAccount(adapter, ledger, mode);
429
+ const canonical = canonicalSymbol(symbol);
430
+ const matches = account.positions.filter((position) => canonicalSymbol(position.symbol) === canonical && position.candidateId);
431
+ if (matches.length !== 1 || !matches[0].positionFingerprint) {
432
+ throw new Error(`Wave 9 live position snapshot requires exactly one durable ${canonical} position`);
433
+ }
434
+ return { fingerprint: matches[0].positionFingerprint, position: matches[0] };
435
+ }
@@ -0,0 +1,39 @@
1
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { Wave9LiveExecutionLedger } from './live-execution-ledger.js';
3
+ import { type Wave9LiveFlatConfirmation, type Wave9LiveFlatConfirmationOptions } from './live-position-confirmation.js';
4
+ import { reprotectWave9LiveResidual, type Wave9LiveResidualProtectionResult } from './live-residual-protection.js';
5
+ export interface Wave9LiveFlatRecoveryResult {
6
+ status: 'flat';
7
+ detail: string;
8
+ candidateId: string;
9
+ symbol: string;
10
+ }
11
+ export type Wave9LiveAutonomousProtectionResult = Wave9LiveFlatRecoveryResult | Wave9LiveResidualProtectionResult;
12
+ /** Test seams are optional; production callers always use the exact defaults. */
13
+ export interface Wave9LiveAutonomousProtectionOptions {
14
+ flatConfirmation?: Wave9LiveFlatConfirmationOptions;
15
+ confirmFlat?: (adapter: IExchangeAdapter, symbol: string, options?: Wave9LiveFlatConfirmationOptions) => Promise<Wave9LiveFlatConfirmation>;
16
+ reprotect?: typeof reprotectWave9LiveResidual;
17
+ /**
18
+ * Optional crash-recovery identity fence. Startup reconciliation supplies
19
+ * this after binding the durable candidate to its exact exchange entry CID.
20
+ * It is re-evaluated immediately before every operation that may cancel,
21
+ * replace, or close same-symbol exposure. A failed/unknown fence leaves the
22
+ * durable row unresolved and performs no exchange mutation.
23
+ */
24
+ authorizeExposureMutation?: () => Promise<{
25
+ authorized: boolean;
26
+ detail: string;
27
+ }>;
28
+ }
29
+ /**
30
+ * Resolve a Wave 9 LIVE exposure after a close acknowledgement or a native
31
+ * stop-loss event. The only successful terminal state is stable exchange
32
+ * flatness persisted to the durable execution ledger. Otherwise the exact
33
+ * frozen candidate stop is proven/re-armed. If that cannot be done, issue one
34
+ * emergency reduce-only close, prove flatness again, and make one final
35
+ * re-protection attempt for any residual. Every unresolved state stays OPEN.
36
+ *
37
+ * The caller must hold the shared trading-operation lock for the whole call.
38
+ */
39
+ export declare function recoverWave9LiveExposure(adapter: IExchangeAdapter, executionLedger: Wave9LiveExecutionLedger, candidateId: string, symbol: string, options?: Wave9LiveAutonomousProtectionOptions): Promise<Wave9LiveAutonomousProtectionResult>;
@@ -0,0 +1,112 @@
1
+ import { confirmWave9LivePositionFlat, } from './live-position-confirmation.js';
2
+ import { reprotectWave9LiveResidual, } from './live-residual-protection.js';
3
+ function manualResult(ledger, candidateId, symbol, detail) {
4
+ return {
5
+ status: 'manual_intervention',
6
+ detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: ${detail}`,
7
+ candidateId,
8
+ symbol,
9
+ stopPrice: ledger.getByCandidateId(candidateId)?.initialStop ?? Number.NaN,
10
+ };
11
+ }
12
+ function combineManualDetail(result, prefix) {
13
+ return {
14
+ ...result,
15
+ detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: ${prefix}; ${result.detail}`,
16
+ };
17
+ }
18
+ async function authorizeMutation(ledger, candidateId, symbol, action, authorize) {
19
+ if (!authorize)
20
+ return undefined;
21
+ try {
22
+ const result = await authorize();
23
+ if (result.authorized)
24
+ return undefined;
25
+ return manualResult(ledger, candidateId, symbol, `${action} was refused by the exact entry/position identity fence: ${result.detail}`);
26
+ }
27
+ catch (error) {
28
+ return manualResult(ledger, candidateId, symbol, `${action} identity verification threw: ${String(error)}`);
29
+ }
30
+ }
31
+ async function markConfirmedFlat(ledger, candidateId, symbol, confirmation) {
32
+ try {
33
+ ledger.markClosed(candidateId);
34
+ return {
35
+ status: 'flat',
36
+ detail: confirmation.detail,
37
+ candidateId,
38
+ symbol,
39
+ };
40
+ }
41
+ catch (error) {
42
+ return manualResult(ledger, candidateId, symbol, `exchange flatness was proven but durable closure could not be persisted: ${String(error)}`);
43
+ }
44
+ }
45
+ /**
46
+ * Resolve a Wave 9 LIVE exposure after a close acknowledgement or a native
47
+ * stop-loss event. The only successful terminal state is stable exchange
48
+ * flatness persisted to the durable execution ledger. Otherwise the exact
49
+ * frozen candidate stop is proven/re-armed. If that cannot be done, issue one
50
+ * emergency reduce-only close, prove flatness again, and make one final
51
+ * re-protection attempt for any residual. Every unresolved state stays OPEN.
52
+ *
53
+ * The caller must hold the shared trading-operation lock for the whole call.
54
+ */
55
+ export async function recoverWave9LiveExposure(adapter, executionLedger, candidateId, symbol, options = {}) {
56
+ const confirmFlat = options.confirmFlat ?? confirmWave9LivePositionFlat;
57
+ const reprotect = options.reprotect ?? reprotectWave9LiveResidual;
58
+ const firstFlat = await confirmFlat(adapter, symbol, options.flatConfirmation);
59
+ if (firstFlat.confirmed) {
60
+ return markConfirmedFlat(executionLedger, candidateId, symbol, firstFlat);
61
+ }
62
+ const initialMutationRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'initial residual re-protection', options.authorizeExposureMutation);
63
+ if (initialMutationRefusal)
64
+ return initialMutationRefusal;
65
+ let firstProtection;
66
+ try {
67
+ firstProtection = await reprotect(adapter, executionLedger, candidateId, symbol);
68
+ }
69
+ catch (error) {
70
+ firstProtection = manualResult(executionLedger, candidateId, symbol, `initial residual re-protection threw: ${String(error)}`);
71
+ }
72
+ if (firstProtection.status === 'protected')
73
+ return firstProtection;
74
+ const closeRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'emergency close', options.authorizeExposureMutation);
75
+ if (closeRefusal) {
76
+ return combineManualDetail(closeRefusal, firstProtection.detail);
77
+ }
78
+ let emergencyCloseError;
79
+ try {
80
+ await adapter.closePosition(symbol, 'emergency');
81
+ }
82
+ catch (error) {
83
+ // A post-submit exception is not proof the exchange rejected the close.
84
+ // Always inspect stable exchange position state before deciding what is
85
+ // left to protect.
86
+ emergencyCloseError = String(error);
87
+ }
88
+ const afterEmergencyFlat = await confirmFlat(adapter, symbol, options.flatConfirmation);
89
+ if (afterEmergencyFlat.confirmed) {
90
+ return markConfirmedFlat(executionLedger, candidateId, symbol, afterEmergencyFlat);
91
+ }
92
+ const finalMutationRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'final residual re-protection', options.authorizeExposureMutation);
93
+ if (finalMutationRefusal) {
94
+ const closeDetail = emergencyCloseError
95
+ ? `emergency close threw (${emergencyCloseError}) and stable flatness was not proven`
96
+ : 'emergency close was acknowledged but stable flatness was not proven';
97
+ return combineManualDetail(finalMutationRefusal, `${firstProtection.detail}; ${closeDetail}`);
98
+ }
99
+ let finalProtection;
100
+ try {
101
+ finalProtection = await reprotect(adapter, executionLedger, candidateId, symbol);
102
+ }
103
+ catch (error) {
104
+ finalProtection = manualResult(executionLedger, candidateId, symbol, `final residual re-protection threw: ${String(error)}`);
105
+ }
106
+ if (finalProtection.status === 'protected')
107
+ return finalProtection;
108
+ const closeDetail = emergencyCloseError
109
+ ? `emergency close threw (${emergencyCloseError}) and stable flatness was not proven`
110
+ : 'emergency close was acknowledged but stable flatness was not proven';
111
+ return combineManualDetail(finalProtection, `${firstProtection.detail}; ${closeDetail}; final residual remains unverified`);
112
+ }
@@ -0,0 +1,33 @@
1
+ import type { AdapterReadiness, IExchangeAdapter } from '../exchange-adapter.js';
2
+ import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
3
+ import type { Wave9LiveStartupReconciliationResult } from './live-startup-reconciliation.js';
4
+ export interface Wave9LiveReadySource extends IExchangeAdapter {
5
+ on(event: 'readiness_changed', listener: (readiness: AdapterReadiness, detail?: string) => void): unknown;
6
+ }
7
+ export interface Wave9LiveDurableReconciliationSchedulerOptions {
8
+ operationLock: TradingOperationLock;
9
+ isCurrentSource(source: Wave9LiveReadySource): boolean;
10
+ reconcile(source: Wave9LiveReadySource): Promise<Wave9LiveStartupReconciliationResult>;
11
+ onResult?(trigger: string, result: Wave9LiveStartupReconciliationResult): void;
12
+ onError?(trigger: string, error: unknown): void;
13
+ retryDelaysMs?: readonly number[];
14
+ periodicMs?: number;
15
+ }
16
+ /**
17
+ * Source-bound READY/periodic driver for durable Wave 9 recovery. At most one
18
+ * timer and one lock-serialized pass exist for the active adapter. A replaced
19
+ * adapter is stopped synchronously and an in-flight old pass cannot reschedule
20
+ * itself. Timers are unref'ed so this safety loop never keeps Node alive.
21
+ */
22
+ export declare class Wave9LiveDurableReconciliationScheduler {
23
+ private readonly options;
24
+ private readonly retryDelaysMs;
25
+ private readonly periodicMs;
26
+ private active?;
27
+ constructor(options: Wave9LiveDurableReconciliationSchedulerOptions);
28
+ attach(source: Wave9LiveReadySource): void;
29
+ stopIfSourceChanged(current: IExchangeAdapter): void;
30
+ stop(): void;
31
+ private isCurrentReady;
32
+ private schedule;
33
+ }