@reefclaw/openclaw-plugin 0.1.5 → 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.
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/config/plugin-config-io.d.ts +6 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +551 -54
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/live-adapter.d.ts +24 -1
- package/live/live-adapter.js +114 -2
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +27 -1
- package/live/stop-watcher.js +59 -2
- package/onboarding/runtime.d.ts +13 -0
- package/onboarding/runtime.js +22 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +50 -0
- package/simulator/exchange-simulator.js +5 -0
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +6 -1
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +2 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +36 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1364 -192
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +9 -1
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.js +19 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
- package/venues/hyperliquid/hl-live-adapter.js +353 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +155 -11
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
|
@@ -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
|
+
}
|