@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type VenueId = 'binance' | 'hyperliquid';
|
|
2
|
+
export declare const VENUE_IDS: readonly VenueId[];
|
|
3
|
+
export declare function isVenueId(value: unknown): value is VenueId;
|
|
4
|
+
/** Parse a raw config value into a venue.
|
|
5
|
+
* Absent/empty → 'binance' (every pre-venue config file keeps today's
|
|
6
|
+
* behavior byte-identically). An unrecognized string is surfaced to the
|
|
7
|
+
* caller instead of being silently coerced — mis-spelling a venue must be
|
|
8
|
+
* loud, never quietly become "trade on Binance". */
|
|
9
|
+
export declare function parseVenue(raw: unknown): {
|
|
10
|
+
venue: VenueId;
|
|
11
|
+
unrecognized?: string;
|
|
12
|
+
};
|
|
13
|
+
/** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
|
|
14
|
+
* audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
|
|
15
|
+
* 0042). 'binance_futures' is the historical literal on every existing row;
|
|
16
|
+
* NEVER change these strings once a venue has written rows. */
|
|
17
|
+
export declare const FILL_EXCHANGE_ID: Record<VenueId, string>;
|
|
18
|
+
export declare function fillExchangeId(venue: VenueId): string;
|
|
19
|
+
/** Quote/settle asset of the venue's linear perps. */
|
|
20
|
+
export declare const VENUE_QUOTE_ASSET: Record<VenueId, string>;
|
|
21
|
+
/** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
|
|
22
|
+
export declare const HL_INTEL_PREFIX = "HL_";
|
|
23
|
+
/** Canonical symbol → the venue's CCXT unified symbol.
|
|
24
|
+
* 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
|
|
25
|
+
* 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
|
|
26
|
+
export declare function toCcxtSymbol(venue: VenueId, canonical: string): string;
|
|
27
|
+
/** Canonical symbol → intel DB symbol.
|
|
28
|
+
* binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
|
|
29
|
+
* always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
|
|
30
|
+
* preserved). */
|
|
31
|
+
export declare function toIntelSymbol(venue: VenueId, canonical: string): string;
|
|
32
|
+
/** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
|
|
33
|
+
* the historical Binance namespace. Binance symbols that are not
|
|
34
|
+
* USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
|
|
35
|
+
* canonical — honest passthrough, not a guess. */
|
|
36
|
+
export declare function fromIntelSymbol(intelSymbol: string): {
|
|
37
|
+
venue: VenueId;
|
|
38
|
+
canonical: string;
|
|
39
|
+
};
|
|
40
|
+
/** The Hyperliquid venue-native coin name for a canonical symbol
|
|
41
|
+
* ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
|
|
42
|
+
* `meta.universe` at runtime — the coin name is the stable half. */
|
|
43
|
+
export declare function toHyperliquidCoin(canonical: string): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Canonical source of truth: shared/src/venues/symbols.ts
|
|
3
|
+
// Regenerate: node scripts/sync-shared-code.mjs (enforced by shared-code-sync.test.ts)
|
|
4
|
+
//
|
|
5
|
+
// This copy exists because this package builds with tsc and deploys as a
|
|
6
|
+
// self-contained tree that strips workspace deps, so it cannot import
|
|
7
|
+
// @reefclaw/shared runtime code across the deploy boundary.
|
|
8
|
+
export const VENUE_IDS = ['binance', 'hyperliquid'];
|
|
9
|
+
export function isVenueId(value) {
|
|
10
|
+
return value === 'binance' || value === 'hyperliquid';
|
|
11
|
+
}
|
|
12
|
+
/** Parse a raw config value into a venue.
|
|
13
|
+
* Absent/empty → 'binance' (every pre-venue config file keeps today's
|
|
14
|
+
* behavior byte-identically). An unrecognized string is surfaced to the
|
|
15
|
+
* caller instead of being silently coerced — mis-spelling a venue must be
|
|
16
|
+
* loud, never quietly become "trade on Binance". */
|
|
17
|
+
export function parseVenue(raw) {
|
|
18
|
+
if (raw === undefined || raw === null || raw === '')
|
|
19
|
+
return { venue: 'binance' };
|
|
20
|
+
if (isVenueId(raw))
|
|
21
|
+
return { venue: raw };
|
|
22
|
+
return { venue: 'binance', unrecognized: String(raw) };
|
|
23
|
+
}
|
|
24
|
+
/** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
|
|
25
|
+
* audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
|
|
26
|
+
* 0042). 'binance_futures' is the historical literal on every existing row;
|
|
27
|
+
* NEVER change these strings once a venue has written rows. */
|
|
28
|
+
export const FILL_EXCHANGE_ID = {
|
|
29
|
+
binance: 'binance_futures',
|
|
30
|
+
hyperliquid: 'hyperliquid',
|
|
31
|
+
};
|
|
32
|
+
export function fillExchangeId(venue) {
|
|
33
|
+
return FILL_EXCHANGE_ID[venue];
|
|
34
|
+
}
|
|
35
|
+
/** Quote/settle asset of the venue's linear perps. */
|
|
36
|
+
export const VENUE_QUOTE_ASSET = {
|
|
37
|
+
binance: 'USDT',
|
|
38
|
+
hyperliquid: 'USDC',
|
|
39
|
+
};
|
|
40
|
+
/** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
|
|
41
|
+
export const HL_INTEL_PREFIX = 'HL_';
|
|
42
|
+
/** Strip a CCXT settle suffix ('BTC/USDT:USDT' → 'BTC/USDT'). Same regex as
|
|
43
|
+
* webapp/src/lib/symbols.ts normalizeSymbol — kept inline so this module has
|
|
44
|
+
* zero imports and survives the sync-copy deploy boundary. */
|
|
45
|
+
function stripSettleSuffix(symbol) {
|
|
46
|
+
return symbol.replace(/:[A-Z]+$/, '');
|
|
47
|
+
}
|
|
48
|
+
function splitCanonical(canonical) {
|
|
49
|
+
const stripped = stripSettleSuffix(canonical.trim());
|
|
50
|
+
const parts = stripped.split('/');
|
|
51
|
+
if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
|
|
52
|
+
throw new Error(`Not a canonical BASE/QUOTE symbol: '${canonical}'`);
|
|
53
|
+
}
|
|
54
|
+
return { base: parts[0], quote: parts[1] };
|
|
55
|
+
}
|
|
56
|
+
/** Canonical symbol → the venue's CCXT unified symbol.
|
|
57
|
+
* 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
|
|
58
|
+
* 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
|
|
59
|
+
export function toCcxtSymbol(venue, canonical) {
|
|
60
|
+
const { base, quote } = splitCanonical(canonical);
|
|
61
|
+
if (quote !== VENUE_QUOTE_ASSET[venue]) {
|
|
62
|
+
throw new Error(`Symbol '${canonical}' is not ${VENUE_QUOTE_ASSET[venue]}-quoted — not a ${venue} linear perp`);
|
|
63
|
+
}
|
|
64
|
+
return `${base}/${quote}:${quote}`;
|
|
65
|
+
}
|
|
66
|
+
/** Canonical symbol → intel DB symbol.
|
|
67
|
+
* binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
|
|
68
|
+
* always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
|
|
69
|
+
* preserved). */
|
|
70
|
+
export function toIntelSymbol(venue, canonical) {
|
|
71
|
+
const { base, quote } = splitCanonical(canonical);
|
|
72
|
+
if (venue === 'hyperliquid') {
|
|
73
|
+
if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
|
|
74
|
+
throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
|
|
75
|
+
}
|
|
76
|
+
return `${HL_INTEL_PREFIX}${base}`;
|
|
77
|
+
}
|
|
78
|
+
return `${base}${quote}`;
|
|
79
|
+
}
|
|
80
|
+
/** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
|
|
81
|
+
* the historical Binance namespace. Binance symbols that are not
|
|
82
|
+
* USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
|
|
83
|
+
* canonical — honest passthrough, not a guess. */
|
|
84
|
+
export function fromIntelSymbol(intelSymbol) {
|
|
85
|
+
if (intelSymbol.startsWith(HL_INTEL_PREFIX)) {
|
|
86
|
+
const coin = intelSymbol.slice(HL_INTEL_PREFIX.length);
|
|
87
|
+
if (coin.length === 0) {
|
|
88
|
+
throw new Error(`Malformed intel symbol '${intelSymbol}' — empty coin after HL_ prefix`);
|
|
89
|
+
}
|
|
90
|
+
return { venue: 'hyperliquid', canonical: `${coin}/${VENUE_QUOTE_ASSET.hyperliquid}` };
|
|
91
|
+
}
|
|
92
|
+
if (intelSymbol.endsWith(VENUE_QUOTE_ASSET.binance) && intelSymbol.length > 4) {
|
|
93
|
+
const base = intelSymbol.slice(0, -VENUE_QUOTE_ASSET.binance.length);
|
|
94
|
+
return { venue: 'binance', canonical: `${base}/${VENUE_QUOTE_ASSET.binance}` };
|
|
95
|
+
}
|
|
96
|
+
return { venue: 'binance', canonical: intelSymbol };
|
|
97
|
+
}
|
|
98
|
+
/** The Hyperliquid venue-native coin name for a canonical symbol
|
|
99
|
+
* ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
|
|
100
|
+
* `meta.universe` at runtime — the coin name is the stable half. */
|
|
101
|
+
export function toHyperliquidCoin(canonical) {
|
|
102
|
+
const { base, quote } = splitCanonical(canonical);
|
|
103
|
+
if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
|
|
104
|
+
throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
|
|
105
|
+
}
|
|
106
|
+
return base;
|
|
107
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import { type Wave9ExecutionMode } from './paper-admission-guard.js';
|
|
3
|
+
import { type Wave9LiveExecutionLedger } from './live-execution-ledger.js';
|
|
4
|
+
export interface Wave9NativeStopObservation {
|
|
5
|
+
verified: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
bracketId: string | null;
|
|
8
|
+
bracketState: string | null;
|
|
9
|
+
entryClientOrderId: string | null;
|
|
10
|
+
stopClientOrderId: string | null;
|
|
11
|
+
exchangeOrderId: string | null;
|
|
12
|
+
entrySide: 'buy' | 'sell' | null;
|
|
13
|
+
stopSide: 'buy' | 'sell' | null;
|
|
14
|
+
stopPrice: number | null;
|
|
15
|
+
bracketQuantity: number | null;
|
|
16
|
+
closePosition: boolean | null;
|
|
17
|
+
liveness: 'live' | 'terminal' | 'unknown' | null;
|
|
18
|
+
}
|
|
19
|
+
export interface Wave9LiveStopConfirmationInput {
|
|
20
|
+
candidateId: string;
|
|
21
|
+
symbol: string;
|
|
22
|
+
entryClientOrderId: string;
|
|
23
|
+
entrySide: 'buy' | 'sell';
|
|
24
|
+
stopPrice: number;
|
|
25
|
+
filledQuantity: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Wave9LiveObservedPosition {
|
|
28
|
+
symbol: string;
|
|
29
|
+
side: 'long' | 'short';
|
|
30
|
+
quantity: number;
|
|
31
|
+
entryPrice: number;
|
|
32
|
+
markPrice: number;
|
|
33
|
+
stopPrice?: number;
|
|
34
|
+
missionId?: string;
|
|
35
|
+
setupType?: string;
|
|
36
|
+
candidateId?: string;
|
|
37
|
+
strategy?: 'tsmom_28d_long_flat_reversal_1d' | 'tsmom_28d_short_flat_reversal_1d';
|
|
38
|
+
accruedFundingUsd?: number;
|
|
39
|
+
openedAt?: string;
|
|
40
|
+
initialStopPrice?: number;
|
|
41
|
+
nativeStop?: Wave9NativeStopObservation;
|
|
42
|
+
positionFingerprint?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface Wave9LiveAccountCapture {
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
cashEquityUsd: number;
|
|
47
|
+
positions: Wave9LiveObservedPosition[];
|
|
48
|
+
pendingExposureOrderCount: number;
|
|
49
|
+
executedCandidateIds: string[];
|
|
50
|
+
nativeStopProtectionComplete: boolean;
|
|
51
|
+
nativeStopIssues: string[];
|
|
52
|
+
authoritative: true;
|
|
53
|
+
runtimeParity: false;
|
|
54
|
+
provenance: 'live_exchange_structural_snapshot_and_durable_wave9_ledger';
|
|
55
|
+
}
|
|
56
|
+
/** One-shot post-fill confirmation used after the bracket row becomes active. */
|
|
57
|
+
export declare function confirmWave9LiveNativeStop(adapter: IExchangeAdapter, input: Wave9LiveStopConfirmationInput): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Capture exchange-authoritative LIVE geometry without volatile mark prices in
|
|
60
|
+
* the authorization fingerprint. Wallet cash, positions, pending
|
|
61
|
+
* exposure-increasing orders, and durable Wave 9 identities are all bound.
|
|
62
|
+
*/
|
|
63
|
+
export declare function captureWave9LiveAccount(adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, mode: Wave9ExecutionMode): Promise<Wave9LiveAccountCapture>;
|
|
64
|
+
export declare function captureWave9LivePosition(adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, mode: Wave9ExecutionMode, symbol: string): Promise<{
|
|
65
|
+
fingerprint: string;
|
|
66
|
+
position: Wave9LiveObservedPosition;
|
|
67
|
+
}>;
|
|
@@ -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
|
+
}
|