@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,498 @@
|
|
|
1
|
+
// Durable Wave 9 live-execution identity and replay ledger.
|
|
2
|
+
//
|
|
3
|
+
// A live market-order outcome can become ambiguous across a process crash: the
|
|
4
|
+
// exchange may have accepted the order even when the local caller never saw the
|
|
5
|
+
// acknowledgement. Therefore an attempt is persisted before submission and is
|
|
6
|
+
// permanently treated as executed for same-event replay prevention. Terminal
|
|
7
|
+
// rows are retained so a restart cannot forget a candidate that already ran.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { DAY_MS, WAVE9_LONG_STRATEGY, WAVE9_SHORT_STRATEGY, WAVE9_SYMBOL_PRIORITY, } from '../portfolio/wave9-policy.js';
|
|
12
|
+
import { resolvePluginsBaseDir } from '../util/plugin-paths.js';
|
|
13
|
+
const DEFAULT_PLUGIN_ID = 'reefclaw-paper-trading';
|
|
14
|
+
const LEDGER_FILENAME = 'wave9-live-execution-ledger.json';
|
|
15
|
+
const SCHEMA_VERSION = 1;
|
|
16
|
+
const MAX_TEXT_LENGTH = 512;
|
|
17
|
+
const CLIENT_ORDER_ID_PATTERN = /^w9e[a-f0-9]{32}$/;
|
|
18
|
+
/** Stable Binance-compatible client order id (35 chars; Binance max is 36). */
|
|
19
|
+
export function wave9ClientOrderId(candidateId) {
|
|
20
|
+
requireText(candidateId, 'candidateId');
|
|
21
|
+
return `w9e${createHash('sha256').update(candidateId).digest('hex').slice(0, 32)}`;
|
|
22
|
+
}
|
|
23
|
+
/** Backwards-compatible descriptive alias. */
|
|
24
|
+
export const wave9LiveClientOrderId = wave9ClientOrderId;
|
|
25
|
+
/** Stable Wave 9 mission identity, mirrored from the status authorization path. */
|
|
26
|
+
function expectedMissionId(candidateId) {
|
|
27
|
+
return `wave9_${createHash('sha256').update(candidateId).digest('hex').slice(0, 32)}`;
|
|
28
|
+
}
|
|
29
|
+
function canonicalSymbol(value) {
|
|
30
|
+
requireText(value, 'symbol');
|
|
31
|
+
return value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
32
|
+
}
|
|
33
|
+
function requireText(value, label) {
|
|
34
|
+
if (typeof value !== 'string'
|
|
35
|
+
|| value.length === 0
|
|
36
|
+
|| value.length > MAX_TEXT_LENGTH
|
|
37
|
+
|| value.trim() !== value) {
|
|
38
|
+
throw new Error(`wave9-live-execution-ledger: ${label} is malformed`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function requirePositiveFinite(value, label) {
|
|
42
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
43
|
+
throw new Error(`wave9-live-execution-ledger: ${label} must be positive and finite`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function requireTimestamp(value, label) {
|
|
47
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
48
|
+
throw new Error(`wave9-live-execution-ledger: ${label} must be a non-negative safe integer`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isRecord(value) {
|
|
52
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
function assertExactKeys(value, required, optional, label) {
|
|
55
|
+
const allowed = new Set([...required, ...optional]);
|
|
56
|
+
for (const key of required) {
|
|
57
|
+
if (!(key in value))
|
|
58
|
+
throw new Error(`wave9-live-execution-ledger: ${label} missing ${key}`);
|
|
59
|
+
}
|
|
60
|
+
for (const key of Object.keys(value)) {
|
|
61
|
+
if (!allowed.has(key))
|
|
62
|
+
throw new Error(`wave9-live-execution-ledger: ${label} has unknown field ${key}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function cloneRecord(record) {
|
|
66
|
+
return { ...record };
|
|
67
|
+
}
|
|
68
|
+
function sameOptional(left, right) {
|
|
69
|
+
return left === right;
|
|
70
|
+
}
|
|
71
|
+
export class Wave9LiveExecutionLedger {
|
|
72
|
+
filePath;
|
|
73
|
+
dir;
|
|
74
|
+
now;
|
|
75
|
+
entries = new Map();
|
|
76
|
+
constructor(options = {}) {
|
|
77
|
+
const base = resolvePluginsBaseDir(options.basePath);
|
|
78
|
+
this.dir = join(base, options.pluginId ?? DEFAULT_PLUGIN_ID);
|
|
79
|
+
this.filePath = join(this.dir, LEDGER_FILENAME);
|
|
80
|
+
this.now = options.now ?? Date.now;
|
|
81
|
+
if (!existsSync(this.dir))
|
|
82
|
+
mkdirSync(this.dir, { recursive: true });
|
|
83
|
+
this.loadOrInit();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Persist an execution attempt before exchange submission. Repeating the
|
|
87
|
+
* exact immutable authorization is idempotent; any mismatch fails closed.
|
|
88
|
+
*/
|
|
89
|
+
recordAttempt(input) {
|
|
90
|
+
const normalized = this.validateAttemptInput(input);
|
|
91
|
+
const existing = this.entries.get(normalized.candidateId);
|
|
92
|
+
if (existing) {
|
|
93
|
+
if (!this.sameAttempt(existing, normalized)) {
|
|
94
|
+
throw new Error(`wave9-live-execution-ledger: candidate ${normalized.candidateId} already exists with different authorization`);
|
|
95
|
+
}
|
|
96
|
+
return cloneRecord(existing);
|
|
97
|
+
}
|
|
98
|
+
const attemptedAt = this.currentTimestamp('attemptedAt');
|
|
99
|
+
const record = {
|
|
100
|
+
...normalized,
|
|
101
|
+
state: 'attempted',
|
|
102
|
+
attemptedAt,
|
|
103
|
+
updatedAt: attemptedAt,
|
|
104
|
+
};
|
|
105
|
+
if ([...this.entries.values()].some((row) => row.clientOrderId === record.clientOrderId)) {
|
|
106
|
+
throw new Error('wave9-live-execution-ledger: deterministic clientOrderId collision');
|
|
107
|
+
}
|
|
108
|
+
this.entries.set(record.candidateId, record);
|
|
109
|
+
this.persist();
|
|
110
|
+
return cloneRecord(record);
|
|
111
|
+
}
|
|
112
|
+
/** Mark a confirmed fill. Repeating the exact confirmation is idempotent. */
|
|
113
|
+
markOpen(candidateId, input = {}) {
|
|
114
|
+
requireText(candidateId, 'candidateId');
|
|
115
|
+
const existing = this.requireCandidate(candidateId);
|
|
116
|
+
const normalized = this.validateOpenInput(existing, input);
|
|
117
|
+
if (existing.state === 'closed') {
|
|
118
|
+
throw new Error(`wave9-live-execution-ledger: closed candidate ${candidateId} cannot reopen`);
|
|
119
|
+
}
|
|
120
|
+
if (existing.state === 'open') {
|
|
121
|
+
if (!sameOptional(existing.filledQuantity, normalized.filledQuantity)
|
|
122
|
+
|| !sameOptional(existing.entryPrice, normalized.entryPrice)
|
|
123
|
+
|| !sameOptional(existing.exchangeOrderId, normalized.exchangeOrderId)) {
|
|
124
|
+
throw new Error(`wave9-live-execution-ledger: candidate ${candidateId} already open with different fill`);
|
|
125
|
+
}
|
|
126
|
+
return cloneRecord(existing);
|
|
127
|
+
}
|
|
128
|
+
const openedAt = this.currentTimestamp('openedAt');
|
|
129
|
+
if (openedAt < existing.attemptedAt) {
|
|
130
|
+
throw new Error('wave9-live-execution-ledger: openedAt precedes attemptedAt');
|
|
131
|
+
}
|
|
132
|
+
const updated = {
|
|
133
|
+
...existing,
|
|
134
|
+
...normalized,
|
|
135
|
+
state: 'open',
|
|
136
|
+
openedAt,
|
|
137
|
+
updatedAt: openedAt,
|
|
138
|
+
};
|
|
139
|
+
this.entries.set(candidateId, updated);
|
|
140
|
+
this.persist();
|
|
141
|
+
return cloneRecord(updated);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Enrich an already-OPEN row after an exact deterministic entry lookup has
|
|
145
|
+
* proven the order terminal. This is deliberately narrower than markOpen:
|
|
146
|
+
* the final fill may populate a previously missing value or increase a
|
|
147
|
+
* crash-time partial fill, but it can never decrease, exceed authorization,
|
|
148
|
+
* or replace an already-bound exchange order identity.
|
|
149
|
+
*/
|
|
150
|
+
reconcileOpenFillFromExactOrder(candidateId, input) {
|
|
151
|
+
requireText(candidateId, 'candidateId');
|
|
152
|
+
const existing = this.requireCandidate(candidateId);
|
|
153
|
+
if (existing.state !== 'open') {
|
|
154
|
+
throw new Error(`wave9-live-execution-ledger: exact-order fill reconciliation requires OPEN candidate ${candidateId}`);
|
|
155
|
+
}
|
|
156
|
+
if (!isRecord(input)) {
|
|
157
|
+
throw new Error('wave9-live-execution-ledger: exact-order fill is not an object');
|
|
158
|
+
}
|
|
159
|
+
assertExactKeys(input, ['filledQuantity', 'exchangeOrderId'], ['entryPrice'], 'exact-order fill');
|
|
160
|
+
requirePositiveFinite(input.filledQuantity, 'filledQuantity');
|
|
161
|
+
if (input.filledQuantity > existing.quantity) {
|
|
162
|
+
throw new Error('wave9-live-execution-ledger: exact-order fill exceeds authorized quantity');
|
|
163
|
+
}
|
|
164
|
+
requireText(input.exchangeOrderId, 'exchangeOrderId');
|
|
165
|
+
if (existing.exchangeOrderId !== undefined
|
|
166
|
+
&& existing.exchangeOrderId !== input.exchangeOrderId) {
|
|
167
|
+
throw new Error('wave9-live-execution-ledger: exact-order exchangeOrderId disagrees with durable identity');
|
|
168
|
+
}
|
|
169
|
+
if (existing.filledQuantity !== undefined
|
|
170
|
+
&& input.filledQuantity < existing.filledQuantity) {
|
|
171
|
+
throw new Error('wave9-live-execution-ledger: exact-order fill cannot decrease');
|
|
172
|
+
}
|
|
173
|
+
if (input.entryPrice !== undefined) {
|
|
174
|
+
this.validateOpenInput(existing, { entryPrice: input.entryPrice });
|
|
175
|
+
if (existing.filledQuantity === input.filledQuantity
|
|
176
|
+
&& existing.entryPrice !== undefined
|
|
177
|
+
&& existing.entryPrice !== input.entryPrice) {
|
|
178
|
+
throw new Error('wave9-live-execution-ledger: exact-order entryPrice changed without a larger final fill');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const nextEntryPrice = input.entryPrice ?? existing.entryPrice;
|
|
182
|
+
const unchanged = existing.filledQuantity === input.filledQuantity
|
|
183
|
+
&& existing.exchangeOrderId === input.exchangeOrderId
|
|
184
|
+
&& existing.entryPrice === nextEntryPrice;
|
|
185
|
+
if (unchanged)
|
|
186
|
+
return cloneRecord(existing);
|
|
187
|
+
const updatedAt = this.currentTimestamp('updatedAt');
|
|
188
|
+
if (updatedAt < (existing.openedAt ?? existing.attemptedAt)) {
|
|
189
|
+
throw new Error('wave9-live-execution-ledger: updatedAt precedes the execution lifecycle');
|
|
190
|
+
}
|
|
191
|
+
const updated = {
|
|
192
|
+
...existing,
|
|
193
|
+
filledQuantity: input.filledQuantity,
|
|
194
|
+
exchangeOrderId: input.exchangeOrderId,
|
|
195
|
+
...(nextEntryPrice !== undefined ? { entryPrice: nextEntryPrice } : {}),
|
|
196
|
+
updatedAt,
|
|
197
|
+
};
|
|
198
|
+
this.entries.set(candidateId, updated);
|
|
199
|
+
try {
|
|
200
|
+
this.persist();
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
// Do not let a failed disk replace make this process believe a final
|
|
204
|
+
// fill that the next process cannot load.
|
|
205
|
+
this.entries.set(candidateId, existing);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
return cloneRecord(updated);
|
|
209
|
+
}
|
|
210
|
+
/** Mark an attempted/open record terminal. Terminal rows are never deleted. */
|
|
211
|
+
markClosed(candidateId) {
|
|
212
|
+
requireText(candidateId, 'candidateId');
|
|
213
|
+
const existing = this.requireCandidate(candidateId);
|
|
214
|
+
if (existing.state === 'closed')
|
|
215
|
+
return cloneRecord(existing);
|
|
216
|
+
const closedAt = this.currentTimestamp('closedAt');
|
|
217
|
+
if (closedAt < existing.attemptedAt || (existing.openedAt !== undefined && closedAt < existing.openedAt)) {
|
|
218
|
+
throw new Error('wave9-live-execution-ledger: closedAt precedes the execution lifecycle');
|
|
219
|
+
}
|
|
220
|
+
const updated = {
|
|
221
|
+
...existing,
|
|
222
|
+
state: 'closed',
|
|
223
|
+
closedAt,
|
|
224
|
+
updatedAt: closedAt,
|
|
225
|
+
};
|
|
226
|
+
this.entries.set(candidateId, updated);
|
|
227
|
+
this.persist();
|
|
228
|
+
return cloneRecord(updated);
|
|
229
|
+
}
|
|
230
|
+
getByCandidateId(candidateId) {
|
|
231
|
+
const record = this.entries.get(candidateId);
|
|
232
|
+
return record ? cloneRecord(record) : undefined;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Latest confirmed-open row matching an exchange position.
|
|
236
|
+
*
|
|
237
|
+
* An `attempted` row is a durable replay tombstone: the exchange outcome
|
|
238
|
+
* may be ambiguous, so the candidate must never execute again, but the row
|
|
239
|
+
* is not proof that a later same-symbol/same-side position belongs to Wave 9.
|
|
240
|
+
* Only `markOpen()` establishes durable position ownership.
|
|
241
|
+
*/
|
|
242
|
+
findLatestForPosition(symbol, side) {
|
|
243
|
+
const canonical = canonicalSymbol(symbol);
|
|
244
|
+
if (side !== 'long' && side !== 'short' && side !== 'buy' && side !== 'sell') {
|
|
245
|
+
throw new Error('wave9-live-execution-ledger: position side is malformed');
|
|
246
|
+
}
|
|
247
|
+
const orderSide = side === 'long' || side === 'buy' ? 'buy' : 'sell';
|
|
248
|
+
const matches = [...this.entries.values()].filter((row) => row.state === 'open' && row.symbol === canonical && row.side === orderSide);
|
|
249
|
+
matches.sort((left, right) => right.attemptedAt - left.attemptedAt
|
|
250
|
+
|| right.updatedAt - left.updatedAt
|
|
251
|
+
|| right.candidateId.localeCompare(left.candidateId));
|
|
252
|
+
return matches[0] ? cloneRecord(matches[0]) : undefined;
|
|
253
|
+
}
|
|
254
|
+
/** Attempts count as executed because exchange outcome can be crash-ambiguous. */
|
|
255
|
+
getExecutedCandidateIds() {
|
|
256
|
+
return [...this.entries.keys()].sort((left, right) => left.localeCompare(right));
|
|
257
|
+
}
|
|
258
|
+
getAll() {
|
|
259
|
+
return [...this.entries.values()]
|
|
260
|
+
.sort((left, right) => left.attemptedAt - right.attemptedAt
|
|
261
|
+
|| left.candidateId.localeCompare(right.candidateId))
|
|
262
|
+
.map(cloneRecord);
|
|
263
|
+
}
|
|
264
|
+
getFilePath() {
|
|
265
|
+
return this.filePath;
|
|
266
|
+
}
|
|
267
|
+
currentTimestamp(label) {
|
|
268
|
+
const value = this.now();
|
|
269
|
+
requireTimestamp(value, label);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
requireCandidate(candidateId) {
|
|
273
|
+
const record = this.entries.get(candidateId);
|
|
274
|
+
if (!record)
|
|
275
|
+
throw new Error(`wave9-live-execution-ledger: unknown candidate ${candidateId}`);
|
|
276
|
+
return record;
|
|
277
|
+
}
|
|
278
|
+
validateAttemptInput(input) {
|
|
279
|
+
if (!isRecord(input))
|
|
280
|
+
throw new Error('wave9-live-execution-ledger: attempt is not an object');
|
|
281
|
+
assertExactKeys(input, [
|
|
282
|
+
'candidateId', 'missionId', 'eventTime', 'symbol', 'strategy',
|
|
283
|
+
'direction', 'side', 'quantity', 'initialStop', 'clientOrderId',
|
|
284
|
+
], [], 'attempt');
|
|
285
|
+
requireText(input.candidateId, 'candidateId');
|
|
286
|
+
requireText(input.missionId, 'missionId');
|
|
287
|
+
requireText(input.eventTime, 'eventTime');
|
|
288
|
+
const eventMs = Date.parse(input.eventTime);
|
|
289
|
+
if (!Number.isFinite(eventMs)
|
|
290
|
+
|| new Date(eventMs).toISOString() !== input.eventTime
|
|
291
|
+
|| eventMs % DAY_MS !== 0) {
|
|
292
|
+
throw new Error('wave9-live-execution-ledger: eventTime must be a UTC daily boundary');
|
|
293
|
+
}
|
|
294
|
+
const symbol = canonicalSymbol(input.symbol);
|
|
295
|
+
if (!WAVE9_SYMBOL_PRIORITY.includes(symbol)) {
|
|
296
|
+
throw new Error(`wave9-live-execution-ledger: unsupported Wave 9 symbol ${symbol}`);
|
|
297
|
+
}
|
|
298
|
+
if (input.strategy !== WAVE9_LONG_STRATEGY && input.strategy !== WAVE9_SHORT_STRATEGY) {
|
|
299
|
+
throw new Error('wave9-live-execution-ledger: unsupported Wave 9 strategy');
|
|
300
|
+
}
|
|
301
|
+
if (input.direction !== 'LONG' && input.direction !== 'SHORT') {
|
|
302
|
+
throw new Error('wave9-live-execution-ledger: direction is malformed');
|
|
303
|
+
}
|
|
304
|
+
if (input.side !== 'buy' && input.side !== 'sell') {
|
|
305
|
+
throw new Error('wave9-live-execution-ledger: side is malformed');
|
|
306
|
+
}
|
|
307
|
+
const expectedDirection = input.strategy === WAVE9_LONG_STRATEGY ? 'LONG' : 'SHORT';
|
|
308
|
+
const expectedSide = expectedDirection === 'LONG' ? 'buy' : 'sell';
|
|
309
|
+
if (input.direction !== expectedDirection || input.side !== expectedSide) {
|
|
310
|
+
throw new Error('wave9-live-execution-ledger: strategy, direction, and side disagree');
|
|
311
|
+
}
|
|
312
|
+
const expectedCandidateId = `${input.eventTime}|${symbol}|${input.strategy}|${input.direction}`;
|
|
313
|
+
if (input.candidateId !== expectedCandidateId) {
|
|
314
|
+
throw new Error('wave9-live-execution-ledger: candidateId does not match its frozen identity');
|
|
315
|
+
}
|
|
316
|
+
if (input.missionId !== expectedMissionId(input.candidateId)) {
|
|
317
|
+
throw new Error('wave9-live-execution-ledger: missionId does not match candidateId');
|
|
318
|
+
}
|
|
319
|
+
requirePositiveFinite(input.quantity, 'quantity');
|
|
320
|
+
requirePositiveFinite(input.initialStop, 'initialStop');
|
|
321
|
+
requireText(input.clientOrderId, 'clientOrderId');
|
|
322
|
+
if (!CLIENT_ORDER_ID_PATTERN.test(input.clientOrderId)
|
|
323
|
+
|| input.clientOrderId !== wave9ClientOrderId(input.candidateId)) {
|
|
324
|
+
throw new Error('wave9-live-execution-ledger: clientOrderId is not deterministic');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...input,
|
|
328
|
+
symbol,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
validateOpenInput(record, input) {
|
|
332
|
+
if (!isRecord(input))
|
|
333
|
+
throw new Error('wave9-live-execution-ledger: open confirmation is not an object');
|
|
334
|
+
assertExactKeys(input, [], ['filledQuantity', 'entryPrice', 'exchangeOrderId'], 'open confirmation');
|
|
335
|
+
if (input.filledQuantity !== undefined) {
|
|
336
|
+
requirePositiveFinite(input.filledQuantity, 'filledQuantity');
|
|
337
|
+
if (input.filledQuantity > record.quantity * (1 + 1e-9)) {
|
|
338
|
+
throw new Error('wave9-live-execution-ledger: filledQuantity exceeds authorized quantity');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (input.entryPrice !== undefined) {
|
|
342
|
+
requirePositiveFinite(input.entryPrice, 'entryPrice');
|
|
343
|
+
if ((record.direction === 'LONG' && record.initialStop >= input.entryPrice)
|
|
344
|
+
|| (record.direction === 'SHORT' && record.initialStop <= input.entryPrice)) {
|
|
345
|
+
throw new Error('wave9-live-execution-ledger: fill/initial-stop geometry is invalid');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (input.exchangeOrderId !== undefined)
|
|
349
|
+
requireText(input.exchangeOrderId, 'exchangeOrderId');
|
|
350
|
+
return { ...input };
|
|
351
|
+
}
|
|
352
|
+
sameAttempt(existing, input) {
|
|
353
|
+
return existing.candidateId === input.candidateId
|
|
354
|
+
&& existing.missionId === input.missionId
|
|
355
|
+
&& existing.eventTime === input.eventTime
|
|
356
|
+
&& existing.symbol === input.symbol
|
|
357
|
+
&& existing.strategy === input.strategy
|
|
358
|
+
&& existing.direction === input.direction
|
|
359
|
+
&& existing.side === input.side
|
|
360
|
+
&& existing.quantity === input.quantity
|
|
361
|
+
&& existing.initialStop === input.initialStop
|
|
362
|
+
&& existing.clientOrderId === input.clientOrderId
|
|
363
|
+
&& existing.clientOrderId === wave9ClientOrderId(input.candidateId);
|
|
364
|
+
}
|
|
365
|
+
loadOrInit() {
|
|
366
|
+
if (!existsSync(this.filePath))
|
|
367
|
+
return;
|
|
368
|
+
let parsed;
|
|
369
|
+
try {
|
|
370
|
+
parsed = JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
throw new Error(`wave9-live-execution-ledger: failed to read valid JSON from ${this.filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
374
|
+
}
|
|
375
|
+
const file = this.validateFile(parsed);
|
|
376
|
+
const clientOrderIds = new Set();
|
|
377
|
+
for (const entry of file.entries) {
|
|
378
|
+
if (this.entries.has(entry.candidateId)) {
|
|
379
|
+
throw new Error(`wave9-live-execution-ledger: duplicate candidate ${entry.candidateId}`);
|
|
380
|
+
}
|
|
381
|
+
if (clientOrderIds.has(entry.clientOrderId)) {
|
|
382
|
+
throw new Error(`wave9-live-execution-ledger: duplicate clientOrderId ${entry.clientOrderId}`);
|
|
383
|
+
}
|
|
384
|
+
this.entries.set(entry.candidateId, entry);
|
|
385
|
+
clientOrderIds.add(entry.clientOrderId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
persist() {
|
|
389
|
+
const file = {
|
|
390
|
+
schemaVersion: SCHEMA_VERSION,
|
|
391
|
+
entries: this.getAll(),
|
|
392
|
+
};
|
|
393
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
394
|
+
writeFileSync(tempPath, JSON.stringify(file, null, 2), { mode: 0o600 });
|
|
395
|
+
renameSync(tempPath, this.filePath);
|
|
396
|
+
}
|
|
397
|
+
validateFile(raw) {
|
|
398
|
+
if (!isRecord(raw))
|
|
399
|
+
throw new Error('wave9-live-execution-ledger: file root is not an object');
|
|
400
|
+
assertExactKeys(raw, ['schemaVersion', 'entries'], [], 'file');
|
|
401
|
+
if (raw.schemaVersion !== SCHEMA_VERSION) {
|
|
402
|
+
throw new Error(`wave9-live-execution-ledger: unsupported schemaVersion ${String(raw.schemaVersion)} (expected ${SCHEMA_VERSION})`);
|
|
403
|
+
}
|
|
404
|
+
if (!Array.isArray(raw.entries)) {
|
|
405
|
+
throw new Error('wave9-live-execution-ledger: entries is not an array');
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
schemaVersion: SCHEMA_VERSION,
|
|
409
|
+
entries: raw.entries.map((entry) => this.validateStoredRecord(entry)),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
validateStoredRecord(raw) {
|
|
413
|
+
if (!isRecord(raw))
|
|
414
|
+
throw new Error('wave9-live-execution-ledger: stored entry is not an object');
|
|
415
|
+
assertExactKeys(raw, [
|
|
416
|
+
'candidateId', 'missionId', 'eventTime', 'symbol', 'strategy', 'direction',
|
|
417
|
+
'side', 'quantity', 'initialStop', 'clientOrderId', 'state',
|
|
418
|
+
'attemptedAt', 'updatedAt',
|
|
419
|
+
], ['openedAt', 'closedAt', 'filledQuantity', 'entryPrice', 'exchangeOrderId'], 'stored entry');
|
|
420
|
+
const attempt = this.validateAttemptInput({
|
|
421
|
+
candidateId: raw.candidateId,
|
|
422
|
+
missionId: raw.missionId,
|
|
423
|
+
eventTime: raw.eventTime,
|
|
424
|
+
symbol: raw.symbol,
|
|
425
|
+
strategy: raw.strategy,
|
|
426
|
+
direction: raw.direction,
|
|
427
|
+
side: raw.side,
|
|
428
|
+
quantity: raw.quantity,
|
|
429
|
+
initialStop: raw.initialStop,
|
|
430
|
+
clientOrderId: raw.clientOrderId,
|
|
431
|
+
});
|
|
432
|
+
requireText(raw.clientOrderId, 'clientOrderId');
|
|
433
|
+
if (!CLIENT_ORDER_ID_PATTERN.test(raw.clientOrderId)
|
|
434
|
+
|| raw.clientOrderId !== wave9ClientOrderId(attempt.candidateId)) {
|
|
435
|
+
throw new Error('wave9-live-execution-ledger: clientOrderId is not deterministic');
|
|
436
|
+
}
|
|
437
|
+
if (raw.state !== 'attempted' && raw.state !== 'open' && raw.state !== 'closed') {
|
|
438
|
+
throw new Error('wave9-live-execution-ledger: state is malformed');
|
|
439
|
+
}
|
|
440
|
+
requireTimestamp(raw.attemptedAt, 'attemptedAt');
|
|
441
|
+
requireTimestamp(raw.updatedAt, 'updatedAt');
|
|
442
|
+
if (raw.updatedAt < raw.attemptedAt) {
|
|
443
|
+
throw new Error('wave9-live-execution-ledger: updatedAt precedes attemptedAt');
|
|
444
|
+
}
|
|
445
|
+
const openInput = this.validateOpenInput(attempt, {
|
|
446
|
+
...(raw.filledQuantity !== undefined ? { filledQuantity: raw.filledQuantity } : {}),
|
|
447
|
+
...(raw.entryPrice !== undefined ? { entryPrice: raw.entryPrice } : {}),
|
|
448
|
+
...(raw.exchangeOrderId !== undefined ? { exchangeOrderId: raw.exchangeOrderId } : {}),
|
|
449
|
+
});
|
|
450
|
+
if (raw.openedAt !== undefined)
|
|
451
|
+
requireTimestamp(raw.openedAt, 'openedAt');
|
|
452
|
+
if (raw.closedAt !== undefined)
|
|
453
|
+
requireTimestamp(raw.closedAt, 'closedAt');
|
|
454
|
+
if (raw.state === 'attempted'
|
|
455
|
+
&& (raw.openedAt !== undefined || raw.closedAt !== undefined || Object.keys(openInput).length > 0)) {
|
|
456
|
+
throw new Error('wave9-live-execution-ledger: attempted entry contains fill/terminal fields');
|
|
457
|
+
}
|
|
458
|
+
if (raw.state === 'open' && raw.openedAt === undefined) {
|
|
459
|
+
throw new Error('wave9-live-execution-ledger: open entry is missing openedAt');
|
|
460
|
+
}
|
|
461
|
+
if (raw.state === 'open' && raw.closedAt !== undefined) {
|
|
462
|
+
throw new Error('wave9-live-execution-ledger: open entry contains closedAt');
|
|
463
|
+
}
|
|
464
|
+
if (raw.state === 'closed' && raw.closedAt === undefined) {
|
|
465
|
+
throw new Error('wave9-live-execution-ledger: closed entry is missing closedAt');
|
|
466
|
+
}
|
|
467
|
+
if (raw.openedAt !== undefined && raw.openedAt < raw.attemptedAt) {
|
|
468
|
+
throw new Error('wave9-live-execution-ledger: openedAt precedes attemptedAt');
|
|
469
|
+
}
|
|
470
|
+
if (raw.closedAt !== undefined
|
|
471
|
+
&& (raw.closedAt < raw.attemptedAt
|
|
472
|
+
|| (raw.openedAt !== undefined && raw.closedAt < raw.openedAt))) {
|
|
473
|
+
throw new Error('wave9-live-execution-ledger: closedAt precedes the execution lifecycle');
|
|
474
|
+
}
|
|
475
|
+
if (Object.keys(openInput).length > 0 && raw.openedAt === undefined) {
|
|
476
|
+
throw new Error('wave9-live-execution-ledger: fill fields require openedAt');
|
|
477
|
+
}
|
|
478
|
+
if (raw.state === 'attempted' && raw.updatedAt !== raw.attemptedAt) {
|
|
479
|
+
throw new Error('wave9-live-execution-ledger: updatedAt does not match attemptedAt');
|
|
480
|
+
}
|
|
481
|
+
if (raw.state === 'open' && raw.updatedAt < raw.openedAt) {
|
|
482
|
+
throw new Error('wave9-live-execution-ledger: OPEN updatedAt precedes openedAt');
|
|
483
|
+
}
|
|
484
|
+
if (raw.state === 'closed' && raw.updatedAt !== raw.closedAt) {
|
|
485
|
+
throw new Error('wave9-live-execution-ledger: CLOSED updatedAt does not match closedAt');
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
...attempt,
|
|
489
|
+
clientOrderId: raw.clientOrderId,
|
|
490
|
+
state: raw.state,
|
|
491
|
+
attemptedAt: raw.attemptedAt,
|
|
492
|
+
updatedAt: raw.updatedAt,
|
|
493
|
+
...(raw.openedAt !== undefined ? { openedAt: raw.openedAt } : {}),
|
|
494
|
+
...(raw.closedAt !== undefined ? { closedAt: raw.closedAt } : {}),
|
|
495
|
+
...openInput,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
export interface Wave9LiveFlatConfirmation {
|
|
3
|
+
confirmed: boolean;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Wave9LiveFlatConfirmationOptions {
|
|
7
|
+
attempts?: number;
|
|
8
|
+
delayMs?: number;
|
|
9
|
+
requiredFlatSnapshots?: number;
|
|
10
|
+
minStableMs?: number;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Confirm a symbol is absent from a trusted exchange position snapshot.
|
|
15
|
+
* A close-order response is not sufficient: partial fills, asynchronous
|
|
16
|
+
* settlement, malformed rows, and failed reads all remain non-terminal.
|
|
17
|
+
*/
|
|
18
|
+
export declare function confirmWave9LivePositionFlat(adapter: IExchangeAdapter, symbol: string, options?: Wave9LiveFlatConfirmationOptions): Promise<Wave9LiveFlatConfirmation>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { formatError } from '../logger.js';
|
|
2
|
+
const DEFAULT_ATTEMPTS = 16;
|
|
3
|
+
const DEFAULT_DELAY_MS = 250;
|
|
4
|
+
const DEFAULT_REQUIRED_FLAT_SNAPSHOTS = 3;
|
|
5
|
+
const DEFAULT_MIN_STABLE_MS = 1_000;
|
|
6
|
+
function canonicalSymbol(value) {
|
|
7
|
+
return value.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/USDTUSDT$/, 'USDT');
|
|
8
|
+
}
|
|
9
|
+
function wait(delayMs) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Confirm a symbol is absent from a trusted exchange position snapshot.
|
|
14
|
+
* A close-order response is not sufficient: partial fills, asynchronous
|
|
15
|
+
* settlement, malformed rows, and failed reads all remain non-terminal.
|
|
16
|
+
*/
|
|
17
|
+
export async function confirmWave9LivePositionFlat(adapter, symbol, options = {}) {
|
|
18
|
+
const target = canonicalSymbol(symbol);
|
|
19
|
+
const attempts = options.attempts ?? DEFAULT_ATTEMPTS;
|
|
20
|
+
const delayMs = options.delayMs ?? DEFAULT_DELAY_MS;
|
|
21
|
+
const requiredFlatSnapshots = options.requiredFlatSnapshots ?? DEFAULT_REQUIRED_FLAT_SNAPSHOTS;
|
|
22
|
+
const minStableMs = options.minStableMs ?? DEFAULT_MIN_STABLE_MS;
|
|
23
|
+
const now = options.now ?? Date.now;
|
|
24
|
+
if (!Number.isInteger(attempts) || attempts <= 0) {
|
|
25
|
+
throw new Error('Wave 9 LIVE flat confirmation attempts must be a positive integer');
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
|
28
|
+
throw new Error('Wave 9 LIVE flat confirmation delay must be finite and non-negative');
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(requiredFlatSnapshots) || requiredFlatSnapshots < 2) {
|
|
31
|
+
throw new Error('Wave 9 LIVE flat confirmation requires at least two stable snapshots');
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(minStableMs) || minStableMs < 0) {
|
|
34
|
+
throw new Error('Wave 9 LIVE flat confirmation stable window must be finite and non-negative');
|
|
35
|
+
}
|
|
36
|
+
if (attempts < requiredFlatSnapshots) {
|
|
37
|
+
throw new Error('Wave 9 LIVE flat confirmation attempts cannot be shorter than its stable streak');
|
|
38
|
+
}
|
|
39
|
+
let detail = 'no trusted position snapshot was returned';
|
|
40
|
+
let consecutiveFlatSnapshots = 0;
|
|
41
|
+
let flatSince;
|
|
42
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
43
|
+
let trustedFlat = false;
|
|
44
|
+
try {
|
|
45
|
+
const positions = await adapter.getPositionsOrNull(symbol);
|
|
46
|
+
if (Array.isArray(positions)) {
|
|
47
|
+
let malformed = false;
|
|
48
|
+
let residualContracts = 0;
|
|
49
|
+
for (const position of positions) {
|
|
50
|
+
if (!position
|
|
51
|
+
|| typeof position.symbol !== 'string'
|
|
52
|
+
|| typeof position.contracts !== 'number'
|
|
53
|
+
|| !Number.isFinite(position.contracts)) {
|
|
54
|
+
malformed = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (canonicalSymbol(position.symbol) === target) {
|
|
58
|
+
residualContracts += Math.abs(position.contracts);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!malformed && residualContracts > 0) {
|
|
62
|
+
return {
|
|
63
|
+
confirmed: false,
|
|
64
|
+
detail: `exchange still reports ${residualContracts} contracts`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (!malformed && residualContracts === 0) {
|
|
68
|
+
const observedAt = now();
|
|
69
|
+
if (!Number.isFinite(observedAt)) {
|
|
70
|
+
throw new Error('flat confirmation clock returned a non-finite timestamp');
|
|
71
|
+
}
|
|
72
|
+
if (flatSince !== undefined && observedAt < flatSince) {
|
|
73
|
+
throw new Error('flat confirmation clock moved backwards');
|
|
74
|
+
}
|
|
75
|
+
trustedFlat = true;
|
|
76
|
+
if (consecutiveFlatSnapshots === 0)
|
|
77
|
+
flatSince = observedAt;
|
|
78
|
+
consecutiveFlatSnapshots += 1;
|
|
79
|
+
const stableForMs = observedAt - (flatSince ?? observedAt);
|
|
80
|
+
detail = `trusted flat streak ${consecutiveFlatSnapshots}/${requiredFlatSnapshots} ` +
|
|
81
|
+
`stable for ${stableForMs}/${minStableMs}ms`;
|
|
82
|
+
if (consecutiveFlatSnapshots >= requiredFlatSnapshots && stableForMs >= minStableMs) {
|
|
83
|
+
return {
|
|
84
|
+
confirmed: true,
|
|
85
|
+
detail: `trusted exchange position snapshot remained flat across ` +
|
|
86
|
+
`${consecutiveFlatSnapshots} reads for ${stableForMs}ms`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!trustedFlat) {
|
|
91
|
+
detail = malformed
|
|
92
|
+
? 'exchange position snapshot contained malformed rows'
|
|
93
|
+
: `exchange still reports ${residualContracts} contracts`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
detail = 'exchange position state is unknown';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
detail = `position confirmation failed: ${formatError(err)}`;
|
|
102
|
+
}
|
|
103
|
+
if (!trustedFlat) {
|
|
104
|
+
consecutiveFlatSnapshots = 0;
|
|
105
|
+
flatSince = undefined;
|
|
106
|
+
}
|
|
107
|
+
if (attempt + 1 < attempts)
|
|
108
|
+
await wait(delayMs);
|
|
109
|
+
}
|
|
110
|
+
return { confirmed: false, detail };
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { Wave9LiveExecutionLedger } from './live-execution-ledger.js';
|
|
3
|
+
export interface Wave9LiveResidualProtectionResult {
|
|
4
|
+
status: 'protected' | 'manual_intervention';
|
|
5
|
+
detail: string;
|
|
6
|
+
candidateId: string;
|
|
7
|
+
symbol: string;
|
|
8
|
+
stopPrice: number;
|
|
9
|
+
residualQuantity?: number;
|
|
10
|
+
stopClientOrderId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Re-arm the exact frozen Wave 9 stop after a reduce-only close leaves a
|
|
14
|
+
* residual. Success means one current residual was observed and its newly
|
|
15
|
+
* candidate-bound native close-all STOP_MARKET was proven by exact CID.
|
|
16
|
+
* Every unverifiable state remains OPEN and escalates manual intervention.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reprotectWave9LiveResidual(adapter: IExchangeAdapter, executionLedger: Wave9LiveExecutionLedger, candidateId: string, symbol: string): Promise<Wave9LiveResidualProtectionResult>;
|