@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,116 @@
|
|
|
1
|
+
import type { CcxtBalance } from '../../types.js';
|
|
2
|
+
import type { CloseReason } from '../../simulator/types.js';
|
|
3
|
+
export interface HlClearinghouseState {
|
|
4
|
+
marginSummary?: {
|
|
5
|
+
accountValue?: string | number;
|
|
6
|
+
totalMarginUsed?: string | number;
|
|
7
|
+
totalNtlPos?: string | number;
|
|
8
|
+
};
|
|
9
|
+
withdrawable?: string | number;
|
|
10
|
+
assetPositions?: Array<{
|
|
11
|
+
position?: {
|
|
12
|
+
coin?: string;
|
|
13
|
+
szi?: string | number;
|
|
14
|
+
entryPx?: string | number;
|
|
15
|
+
unrealizedPnl?: string | number;
|
|
16
|
+
positionValue?: string | number;
|
|
17
|
+
liquidationPx?: string | number;
|
|
18
|
+
marginUsed?: string | number;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface HlNav {
|
|
23
|
+
/** accountValue — what the HL app's portfolio panel shows. */
|
|
24
|
+
equity: number;
|
|
25
|
+
/** equity − Σ uPnl. The Binance "wallet"/margin-balance analog. */
|
|
26
|
+
wallet: number;
|
|
27
|
+
totalUnrealizedPnl: number;
|
|
28
|
+
/** Free collateral (HL `withdrawable`). */
|
|
29
|
+
available: number;
|
|
30
|
+
}
|
|
31
|
+
/** Derive NAV/wallet from a clearinghouseState payload.
|
|
32
|
+
*
|
|
33
|
+
* ★ Returns null when the payload is UNREADABLE — never a zeroed NAV. A phantom
|
|
34
|
+
* "$0 equity" would trip risk limits and look like a catastrophic loss; null
|
|
35
|
+
* means "unknown", which the caller must refuse to act on (null ≠ empty). */
|
|
36
|
+
export declare function deriveHlNav(state: HlClearinghouseState | null | undefined): HlNav | null;
|
|
37
|
+
/** CcxtBalance shape the rest of the plugin (KPI strip, sizing, LiveBalanceEnricher)
|
|
38
|
+
* already consumes. USDC is HL's settlement asset. */
|
|
39
|
+
export declare function toCcxtBalance(nav: HlNav): CcxtBalance;
|
|
40
|
+
export interface HlFillForAnchor {
|
|
41
|
+
time: number;
|
|
42
|
+
closedPnl?: string | number;
|
|
43
|
+
fee?: string | number;
|
|
44
|
+
builderFee?: string | number;
|
|
45
|
+
}
|
|
46
|
+
export interface HlFundingForAnchor {
|
|
47
|
+
time: number;
|
|
48
|
+
/** HL funding deltas are signed USDC (negative = paid). */
|
|
49
|
+
usdc?: string | number;
|
|
50
|
+
delta?: {
|
|
51
|
+
usdc?: string | number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Σ(closedPnl − fee − builderFee) + Σ funding, over rows at/after `sinceMs`.
|
|
55
|
+
*
|
|
56
|
+
* This is the exact analog of Binance's `netNonTransfer` (REALIZED_PNL +
|
|
57
|
+
* COMMISSION + FUNDING_FEE), and — critically — it is NET of fees. The Binance
|
|
58
|
+
* lesson (the ledger was systematically gross, causing a visible KPI gap) applies
|
|
59
|
+
* verbatim: a gross anchor WILL disagree with the HL app. */
|
|
60
|
+
export declare function computeNetNonTransfer(args: {
|
|
61
|
+
fills: HlFillForAnchor[];
|
|
62
|
+
fundings: HlFundingForAnchor[];
|
|
63
|
+
sinceMs: number;
|
|
64
|
+
}): {
|
|
65
|
+
netNonTransfer: number;
|
|
66
|
+
realizedPnlGross: number;
|
|
67
|
+
fees: number;
|
|
68
|
+
funding: number;
|
|
69
|
+
};
|
|
70
|
+
/** UTC-midnight epoch ms for a given instant (the Day-P&L boundary). */
|
|
71
|
+
export declare function utcMidnightMs(now?: number): number;
|
|
72
|
+
/** sessionStartNav = wallet_now − netNonTransfer_since_midnight.
|
|
73
|
+
* dayPnl = NAV − sessionStartNav (computed by the caller each tick). */
|
|
74
|
+
export declare function deriveSessionStartNav(args: {
|
|
75
|
+
walletNow: number;
|
|
76
|
+
netNonTransfer: number;
|
|
77
|
+
}): number;
|
|
78
|
+
/** A capital flow (deposit / withdraw / internal transfer) changes the wallet
|
|
79
|
+
* WITHOUT being trading P&L — bump the anchor by the same signed amount so it
|
|
80
|
+
* never shows up as Day-P&L (the DepositTracker rule).
|
|
81
|
+
*
|
|
82
|
+
* T-3 note: the `delta.type` enum is OPEN (docs incomplete; only
|
|
83
|
+
* `internalTransfer` observed so far). We therefore treat any ledger row that
|
|
84
|
+
* carries a usdc delta as a capital flow rather than switch on a closed set — an
|
|
85
|
+
* UNKNOWN type must never be silently dropped, or the anchor drifts. */
|
|
86
|
+
export declare function applyLedgerDelta(args: {
|
|
87
|
+
sessionStartNav: number;
|
|
88
|
+
ledgerRows: Array<{
|
|
89
|
+
time: number;
|
|
90
|
+
delta?: {
|
|
91
|
+
usdc?: string | number;
|
|
92
|
+
type?: string;
|
|
93
|
+
};
|
|
94
|
+
}>;
|
|
95
|
+
sinceMs: number;
|
|
96
|
+
seenHashes?: Set<string>;
|
|
97
|
+
}): {
|
|
98
|
+
sessionStartNav: number;
|
|
99
|
+
applied: number;
|
|
100
|
+
};
|
|
101
|
+
/** ★ Close-bypass mapping (§5.9) — HL signals → journal close reasons.
|
|
102
|
+
*
|
|
103
|
+
* A position can leave our control without our close path running: a bracket
|
|
104
|
+
* triggering, a LIQUIDATION, an ADL, or a delisting. Each must land in the
|
|
105
|
+
* journal honestly (the KPI and the learning loop both read these), never as a
|
|
106
|
+
* silent disappearance. */
|
|
107
|
+
export declare function mapHlCloseReason(signal: {
|
|
108
|
+
/** Fill-level liquidation object (authoritative). */
|
|
109
|
+
liquidation?: {
|
|
110
|
+
method?: string;
|
|
111
|
+
} | null;
|
|
112
|
+
/** Order status from `orderUpdates`. */
|
|
113
|
+
orderStatus?: string;
|
|
114
|
+
/** Our own cloid role, when the closing order was one of our legs. */
|
|
115
|
+
bracketRole?: 'stop' | 'target';
|
|
116
|
+
}): CloseReason | 'liquidated' | 'adl' | 'venue_delisted' | null;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Hyperliquid balance / NAV / Day-P&L anchor (plan §5.8) + close-bypass mapping
|
|
2
|
+
// (§5.9). Pure functions — the adapter feeds them raw HL payloads.
|
|
3
|
+
//
|
|
4
|
+
// ★ THE INVARIANT THIS FILE EXISTS TO KEEP: "KPI must equal the exchange app to
|
|
5
|
+
// the cent." On Binance that meant anchoring Day-P&L to the UTC-midnight income
|
|
6
|
+
// endpoint (a multi-dollar gap there was ALWAYS a bug, never a formula quirk).
|
|
7
|
+
// HL has no income endpoint, so the anchor is RECONSTRUCTED:
|
|
8
|
+
//
|
|
9
|
+
// wallet = accountValue − Σ unrealizedPnl (accountValue INCLUDES uPnl)
|
|
10
|
+
// NAV/equity = accountValue (so equity = wallet + Σ uPnl
|
|
11
|
+
// stays literally true)
|
|
12
|
+
// netNonTransfer(since UTC midnight)
|
|
13
|
+
// = Σ closedPnl − Σ fee − Σ builderFee (fills)
|
|
14
|
+
// + Σ funding (userFunding)
|
|
15
|
+
// sessionStartNav = wallet_now − netNonTransfer
|
|
16
|
+
// dayPnl = NAV − sessionStartNav
|
|
17
|
+
//
|
|
18
|
+
// Deposits/withdrawals/transfers are NOT trading P&L: they bump the anchor
|
|
19
|
+
// instead (the DepositTracker rule), via `applyLedgerDelta`.
|
|
20
|
+
//
|
|
21
|
+
// ★ FILL-HISTORY SHALLOWNESS: only the 10,000 most recent fills exist server-side
|
|
22
|
+
// on HL (deep history is NOT queryable). The anchor must therefore be able to
|
|
23
|
+
// run off WS-captured fills persisted in our own `trades` table — which is why
|
|
24
|
+
// the WS-first audit-trail rule is a hard prerequisite here, not hygiene.
|
|
25
|
+
const num = (v) => {
|
|
26
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
27
|
+
return Number.isFinite(n) ? n : 0;
|
|
28
|
+
};
|
|
29
|
+
/** Derive NAV/wallet from a clearinghouseState payload.
|
|
30
|
+
*
|
|
31
|
+
* ★ Returns null when the payload is UNREADABLE — never a zeroed NAV. A phantom
|
|
32
|
+
* "$0 equity" would trip risk limits and look like a catastrophic loss; null
|
|
33
|
+
* means "unknown", which the caller must refuse to act on (null ≠ empty). */
|
|
34
|
+
export function deriveHlNav(state) {
|
|
35
|
+
if (!state || typeof state !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
const accountValueRaw = state.marginSummary?.accountValue;
|
|
38
|
+
if (accountValueRaw === undefined || accountValueRaw === null)
|
|
39
|
+
return null;
|
|
40
|
+
const equity = num(accountValueRaw);
|
|
41
|
+
const totalUnrealizedPnl = (state.assetPositions ?? []).reduce((sum, ap) => sum + num(ap?.position?.unrealizedPnl), 0);
|
|
42
|
+
return {
|
|
43
|
+
equity,
|
|
44
|
+
wallet: equity - totalUnrealizedPnl,
|
|
45
|
+
totalUnrealizedPnl,
|
|
46
|
+
available: num(state.withdrawable),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** CcxtBalance shape the rest of the plugin (KPI strip, sizing, LiveBalanceEnricher)
|
|
50
|
+
* already consumes. USDC is HL's settlement asset. */
|
|
51
|
+
export function toCcxtBalance(nav) {
|
|
52
|
+
const free = { USDC: nav.available };
|
|
53
|
+
const used = { USDC: Math.max(0, nav.wallet - nav.available) };
|
|
54
|
+
const total = { USDC: nav.wallet };
|
|
55
|
+
return {
|
|
56
|
+
free,
|
|
57
|
+
used,
|
|
58
|
+
total,
|
|
59
|
+
USDC: { free: nav.available, used: used.USDC, total: nav.wallet },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Σ(closedPnl − fee − builderFee) + Σ funding, over rows at/after `sinceMs`.
|
|
63
|
+
*
|
|
64
|
+
* This is the exact analog of Binance's `netNonTransfer` (REALIZED_PNL +
|
|
65
|
+
* COMMISSION + FUNDING_FEE), and — critically — it is NET of fees. The Binance
|
|
66
|
+
* lesson (the ledger was systematically gross, causing a visible KPI gap) applies
|
|
67
|
+
* verbatim: a gross anchor WILL disagree with the HL app. */
|
|
68
|
+
export function computeNetNonTransfer(args) {
|
|
69
|
+
let realizedPnlGross = 0;
|
|
70
|
+
let fees = 0;
|
|
71
|
+
for (const f of args.fills) {
|
|
72
|
+
if (!Number.isFinite(f.time) || f.time < args.sinceMs)
|
|
73
|
+
continue;
|
|
74
|
+
realizedPnlGross += num(f.closedPnl);
|
|
75
|
+
fees += num(f.fee) + num(f.builderFee);
|
|
76
|
+
}
|
|
77
|
+
let funding = 0;
|
|
78
|
+
for (const f of args.fundings) {
|
|
79
|
+
if (!Number.isFinite(f.time) || f.time < args.sinceMs)
|
|
80
|
+
continue;
|
|
81
|
+
funding += num(f.usdc ?? f.delta?.usdc);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
netNonTransfer: realizedPnlGross - fees + funding,
|
|
85
|
+
realizedPnlGross,
|
|
86
|
+
fees,
|
|
87
|
+
funding,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** UTC-midnight epoch ms for a given instant (the Day-P&L boundary). */
|
|
91
|
+
export function utcMidnightMs(now = Date.now()) {
|
|
92
|
+
const d = new Date(now);
|
|
93
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
94
|
+
}
|
|
95
|
+
/** sessionStartNav = wallet_now − netNonTransfer_since_midnight.
|
|
96
|
+
* dayPnl = NAV − sessionStartNav (computed by the caller each tick). */
|
|
97
|
+
export function deriveSessionStartNav(args) {
|
|
98
|
+
return args.walletNow - args.netNonTransfer;
|
|
99
|
+
}
|
|
100
|
+
/** A capital flow (deposit / withdraw / internal transfer) changes the wallet
|
|
101
|
+
* WITHOUT being trading P&L — bump the anchor by the same signed amount so it
|
|
102
|
+
* never shows up as Day-P&L (the DepositTracker rule).
|
|
103
|
+
*
|
|
104
|
+
* T-3 note: the `delta.type` enum is OPEN (docs incomplete; only
|
|
105
|
+
* `internalTransfer` observed so far). We therefore treat any ledger row that
|
|
106
|
+
* carries a usdc delta as a capital flow rather than switch on a closed set — an
|
|
107
|
+
* UNKNOWN type must never be silently dropped, or the anchor drifts. */
|
|
108
|
+
export function applyLedgerDelta(args) {
|
|
109
|
+
let bump = 0;
|
|
110
|
+
let applied = 0;
|
|
111
|
+
for (const row of args.ledgerRows) {
|
|
112
|
+
if (!Number.isFinite(row.time) || row.time < args.sinceMs)
|
|
113
|
+
continue;
|
|
114
|
+
const usdc = num(row.delta?.usdc);
|
|
115
|
+
if (usdc === 0)
|
|
116
|
+
continue;
|
|
117
|
+
bump += usdc;
|
|
118
|
+
applied += 1;
|
|
119
|
+
}
|
|
120
|
+
return { sessionStartNav: args.sessionStartNav + bump, applied };
|
|
121
|
+
}
|
|
122
|
+
/** ★ Close-bypass mapping (§5.9) — HL signals → journal close reasons.
|
|
123
|
+
*
|
|
124
|
+
* A position can leave our control without our close path running: a bracket
|
|
125
|
+
* triggering, a LIQUIDATION, an ADL, or a delisting. Each must land in the
|
|
126
|
+
* journal honestly (the KPI and the learning loop both read these), never as a
|
|
127
|
+
* silent disappearance. */
|
|
128
|
+
export function mapHlCloseReason(signal) {
|
|
129
|
+
if (signal.liquidation) {
|
|
130
|
+
// HL marks backstop (HLP vault) liquidations distinctly from market ones; ADL
|
|
131
|
+
// arrives as a liquidation of the OPPOSITE side hitting us.
|
|
132
|
+
const method = (signal.liquidation.method ?? '').toLowerCase();
|
|
133
|
+
return method.includes('adl') ? 'adl' : 'liquidated';
|
|
134
|
+
}
|
|
135
|
+
const status = (signal.orderStatus ?? '').toLowerCase();
|
|
136
|
+
if (status === 'liquidatedcanceled')
|
|
137
|
+
return 'liquidated';
|
|
138
|
+
if (status === 'delistedcanceled')
|
|
139
|
+
return 'venue_delisted';
|
|
140
|
+
if (signal.bracketRole === 'stop')
|
|
141
|
+
return 'exchange_stop';
|
|
142
|
+
if (signal.bracketRole === 'target')
|
|
143
|
+
return 'exchange_target';
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { BracketId, BracketRole } from '../../live/bracket-types.js';
|
|
2
|
+
import type { HlOrderRequest } from './hl-private.js';
|
|
3
|
+
export type ProtectiveRole = Extract<BracketRole, 'stop' | 'target'>;
|
|
4
|
+
export interface HlBracketLeg {
|
|
5
|
+
role: ProtectiveRole;
|
|
6
|
+
triggerPrice: number;
|
|
7
|
+
/** HL legs are FIXED SIZE (T-2) — this is why resizing exists. */
|
|
8
|
+
size: number;
|
|
9
|
+
cloid: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HlBracketPlan {
|
|
12
|
+
bracketId: BracketId;
|
|
13
|
+
symbol: string;
|
|
14
|
+
/** Side of the CLOSING orders (opposite the position). */
|
|
15
|
+
closeSide: 'buy' | 'sell';
|
|
16
|
+
legs: HlBracketLeg[];
|
|
17
|
+
}
|
|
18
|
+
/** A leg as it currently exists ON the exchange (from `frontendOpenOrders`). */
|
|
19
|
+
export interface LiveLeg {
|
|
20
|
+
role: ProtectiveRole;
|
|
21
|
+
cloid: string;
|
|
22
|
+
size: number;
|
|
23
|
+
triggerPrice: number;
|
|
24
|
+
}
|
|
25
|
+
/** Plan a fresh bracket for a filled entry. */
|
|
26
|
+
export declare function planBracket(args: {
|
|
27
|
+
bracketId: BracketId;
|
|
28
|
+
symbol: string;
|
|
29
|
+
positionSide: 'long' | 'short';
|
|
30
|
+
positionSize: number;
|
|
31
|
+
stopPrice?: number;
|
|
32
|
+
targetPrice?: number;
|
|
33
|
+
}): HlBracketPlan;
|
|
34
|
+
/** Build the submit-requests for a plan.
|
|
35
|
+
*
|
|
36
|
+
* Both legs go out as ONE `createOrders` batch: one signed action, one nonce (a
|
|
37
|
+
* same-ms nonce collision is a real HL failure mode), costing 1 IP weight unit
|
|
38
|
+
* but 2 ADDRESS actions (the rate gate counts both).
|
|
39
|
+
*
|
|
40
|
+
* Legs are reduce-only trigger-market orders. HL triggers fire on MARK price only
|
|
41
|
+
* — stop-distance validation upstream must therefore use mark, never last. */
|
|
42
|
+
export declare function buildBracketOrders(plan: HlBracketPlan): HlOrderRequest[];
|
|
43
|
+
export interface CoverageResult {
|
|
44
|
+
covered: boolean;
|
|
45
|
+
/** Un-protected contracts per role. > 0 ⇒ that much of the position is NAKED. */
|
|
46
|
+
shortfall: Record<ProtectiveRole, number>;
|
|
47
|
+
/** A role with NO live leg at all — a protection gap, not a sizing gap. */
|
|
48
|
+
missing: ProtectiveRole[];
|
|
49
|
+
}
|
|
50
|
+
/** ★ THE T-2 INVARIANT — do the live legs cover the whole position?
|
|
51
|
+
*
|
|
52
|
+
* On HL a protective leg is a FIXED SIZE, so a scale-in silently leaves the added
|
|
53
|
+
* contracts unprotected unless the legs are resized. A `covered:false` result is
|
|
54
|
+
* a safety-floor breach (naked size), not cosmetic drift. */
|
|
55
|
+
export declare function bracketCoversPosition(args: {
|
|
56
|
+
positionSize: number;
|
|
57
|
+
liveLegs: LiveLeg[];
|
|
58
|
+
}): CoverageResult;
|
|
59
|
+
export interface ResizePlan {
|
|
60
|
+
/** Cloids to cancel (the stale, wrongly-sized legs). */
|
|
61
|
+
cancelCloids: string[];
|
|
62
|
+
/** Fresh legs to submit at the correct size. */
|
|
63
|
+
submit: HlOrderRequest[];
|
|
64
|
+
/** Empty when nothing needs doing — the caller must treat this as a no-op and
|
|
65
|
+
* NOT churn the exchange (idempotent-tool contract). */
|
|
66
|
+
noop: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** ★ Resize protective legs after a scale-in (or any position-size change).
|
|
69
|
+
*
|
|
70
|
+
* HL gives us no in-place resize primitive we can trust (`modify` carries an
|
|
71
|
+
* `always_place` footgun the plan forbids, and positionTpsl auto-sizing is
|
|
72
|
+
* unreachable via ccxt — T-2). So the honest sequence is CANCEL + REPLACE, and
|
|
73
|
+
* the order matters:
|
|
74
|
+
*
|
|
75
|
+
* Cancel-then-place opens a brief unprotected window; place-then-cancel would
|
|
76
|
+
* double-protect (two reduce-only legs summing to 2× the position — the excess
|
|
77
|
+
* is harmless because reduce-only can never flip a position, and HL cancels the
|
|
78
|
+
* stragglers when flat). We therefore SUBMIT FIRST, then cancel the stale legs:
|
|
79
|
+
* over-protection for a few hundred ms beats a naked window. The caller is
|
|
80
|
+
* expected to execute `submit` before `cancelCloids`.
|
|
81
|
+
*
|
|
82
|
+
* Fresh cloids are generated for the new legs (a cloid is single-use), which is
|
|
83
|
+
* also why the ledger keys on `bracketId`, not on a specific cloid. */
|
|
84
|
+
export declare function planResize(args: {
|
|
85
|
+
bracketId: BracketId;
|
|
86
|
+
symbol: string;
|
|
87
|
+
positionSide: 'long' | 'short';
|
|
88
|
+
positionSize: number;
|
|
89
|
+
liveLegs: LiveLeg[];
|
|
90
|
+
}): ResizePlan;
|
|
91
|
+
/** Is this exchange order one of OUR protective legs?
|
|
92
|
+
*
|
|
93
|
+
* The kill-switch's second layer (`OrderData.protective`) maps from this. It is
|
|
94
|
+
* deliberately BROADER than "has one of our cloids": an externally-placed stop
|
|
95
|
+
* must also survive a Kill. Mirrors the Binance rule (`reduceOnly || closePosition
|
|
96
|
+
* || STOP/TAKE_PROFIT type`), translated to HL's fields. */
|
|
97
|
+
export declare function isProtectiveHlOrder(order: {
|
|
98
|
+
reduceOnly?: boolean;
|
|
99
|
+
isTrigger?: boolean;
|
|
100
|
+
isPositionTpsl?: boolean;
|
|
101
|
+
orderType?: string;
|
|
102
|
+
}): boolean;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Hyperliquid bracket mechanics (plan §5.7) — written against MEASURED behavior,
|
|
2
|
+
// not assumptions. The testnet run of 2026-07-12 (docs/CLAUDE/hyperliquid.md
|
|
3
|
+
// §"Testnet verification results") settled the two questions this file turns on:
|
|
4
|
+
//
|
|
5
|
+
// ★ T-1 — GOOD NEWS. Standalone (grouping `na`) reduce-only trigger legs
|
|
6
|
+
// AUTO-CANCEL when the position goes flat (~2.7s, zero residuals), and when
|
|
7
|
+
// ONE leg triggers the SIBLING SELF-CANCELS (OCO-like, ≤5.6s). HL cleans up
|
|
8
|
+
// after itself like Binance's algo orders, so BracketManager needs NO bespoke
|
|
9
|
+
// sibling-canceller. A defensive cancel stays harmless (idempotent) but is not
|
|
10
|
+
// load-bearing.
|
|
11
|
+
//
|
|
12
|
+
// ★ T-2 — THE HAZARD. `grouping:'positionTpsl'` is NOT reachable through ccxt's
|
|
13
|
+
// params passthrough (it silently produced an ordinary `isPositionTpsl:false`
|
|
14
|
+
// leg), and leg sizes DO NOT follow a scale-in: after doubling a position the
|
|
15
|
+
// stop stayed at the ORIGINAL size. Binance legs use `closePosition:true`
|
|
16
|
+
// (inherently whole-position); HL legs are FIXED SIZE. Therefore:
|
|
17
|
+
//
|
|
18
|
+
// ⇒ EVERY scale-in MUST resize the protective legs, or the added size is
|
|
19
|
+
// NAKED. `planResize()` is that requirement in code and
|
|
20
|
+
// `bracketCoversPosition()` is the invariant the auditor enforces.
|
|
21
|
+
//
|
|
22
|
+
// Everything here is PURE — it computes plans (what to submit, what to cancel);
|
|
23
|
+
// the adapter executes them. Safety logic stays unit-testable without a network.
|
|
24
|
+
import { buildHlBracketCloid } from './hl-cloid.js';
|
|
25
|
+
/** HL sizes are rounded to szDecimals, so an exact float compare would false-flag
|
|
26
|
+
* a correctly-sized leg. Below this, a gap is rounding dust, not exposure. */
|
|
27
|
+
const COVERAGE_EPSILON = 1e-8;
|
|
28
|
+
/** Plan a fresh bracket for a filled entry. */
|
|
29
|
+
export function planBracket(args) {
|
|
30
|
+
const closeSide = args.positionSide === 'long' ? 'sell' : 'buy';
|
|
31
|
+
const legs = [];
|
|
32
|
+
if (args.stopPrice !== undefined) {
|
|
33
|
+
legs.push({
|
|
34
|
+
role: 'stop',
|
|
35
|
+
triggerPrice: args.stopPrice,
|
|
36
|
+
size: args.positionSize,
|
|
37
|
+
cloid: buildHlBracketCloid(args.bracketId, 'stop'),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (args.targetPrice !== undefined) {
|
|
41
|
+
legs.push({
|
|
42
|
+
role: 'target',
|
|
43
|
+
triggerPrice: args.targetPrice,
|
|
44
|
+
size: args.positionSize,
|
|
45
|
+
cloid: buildHlBracketCloid(args.bracketId, 'target'),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return { bracketId: args.bracketId, symbol: args.symbol, closeSide, legs };
|
|
49
|
+
}
|
|
50
|
+
/** Build the submit-requests for a plan.
|
|
51
|
+
*
|
|
52
|
+
* Both legs go out as ONE `createOrders` batch: one signed action, one nonce (a
|
|
53
|
+
* same-ms nonce collision is a real HL failure mode), costing 1 IP weight unit
|
|
54
|
+
* but 2 ADDRESS actions (the rate gate counts both).
|
|
55
|
+
*
|
|
56
|
+
* Legs are reduce-only trigger-market orders. HL triggers fire on MARK price only
|
|
57
|
+
* — stop-distance validation upstream must therefore use mark, never last. */
|
|
58
|
+
export function buildBracketOrders(plan) {
|
|
59
|
+
return plan.legs.map((leg) => ({
|
|
60
|
+
symbol: plan.symbol,
|
|
61
|
+
side: plan.closeSide,
|
|
62
|
+
type: 'market',
|
|
63
|
+
amount: leg.size,
|
|
64
|
+
// A trigger-market leg still needs a price for ccxt's slippage bound; the
|
|
65
|
+
// trigger price is the natural reference (the leg only executes there).
|
|
66
|
+
price: leg.triggerPrice,
|
|
67
|
+
reduceOnly: true,
|
|
68
|
+
cloid: leg.cloid,
|
|
69
|
+
trigger: {
|
|
70
|
+
triggerPrice: leg.triggerPrice,
|
|
71
|
+
tpsl: leg.role === 'stop' ? 'sl' : 'tp',
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
/** ★ THE T-2 INVARIANT — do the live legs cover the whole position?
|
|
76
|
+
*
|
|
77
|
+
* On HL a protective leg is a FIXED SIZE, so a scale-in silently leaves the added
|
|
78
|
+
* contracts unprotected unless the legs are resized. A `covered:false` result is
|
|
79
|
+
* a safety-floor breach (naked size), not cosmetic drift. */
|
|
80
|
+
export function bracketCoversPosition(args) {
|
|
81
|
+
const shortfall = { stop: 0, target: 0 };
|
|
82
|
+
const missing = [];
|
|
83
|
+
for (const role of ['stop', 'target']) {
|
|
84
|
+
const legs = args.liveLegs.filter((l) => l.role === role);
|
|
85
|
+
if (legs.length === 0) {
|
|
86
|
+
missing.push(role);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const coveredSize = legs.reduce((sum, l) => sum + l.size, 0);
|
|
90
|
+
const gap = args.positionSize - coveredSize;
|
|
91
|
+
if (gap > COVERAGE_EPSILON)
|
|
92
|
+
shortfall[role] = gap;
|
|
93
|
+
}
|
|
94
|
+
const covered = shortfall.stop <= COVERAGE_EPSILON &&
|
|
95
|
+
shortfall.target <= COVERAGE_EPSILON &&
|
|
96
|
+
// A missing STOP is never "covered" — that is the naked-position case. A
|
|
97
|
+
// missing TARGET is a policy choice (requireTakeProfit can be off), so it is
|
|
98
|
+
// reported in `missing` but does not by itself fail coverage.
|
|
99
|
+
!missing.includes('stop');
|
|
100
|
+
return { covered, shortfall, missing };
|
|
101
|
+
}
|
|
102
|
+
/** ★ Resize protective legs after a scale-in (or any position-size change).
|
|
103
|
+
*
|
|
104
|
+
* HL gives us no in-place resize primitive we can trust (`modify` carries an
|
|
105
|
+
* `always_place` footgun the plan forbids, and positionTpsl auto-sizing is
|
|
106
|
+
* unreachable via ccxt — T-2). So the honest sequence is CANCEL + REPLACE, and
|
|
107
|
+
* the order matters:
|
|
108
|
+
*
|
|
109
|
+
* Cancel-then-place opens a brief unprotected window; place-then-cancel would
|
|
110
|
+
* double-protect (two reduce-only legs summing to 2× the position — the excess
|
|
111
|
+
* is harmless because reduce-only can never flip a position, and HL cancels the
|
|
112
|
+
* stragglers when flat). We therefore SUBMIT FIRST, then cancel the stale legs:
|
|
113
|
+
* over-protection for a few hundred ms beats a naked window. The caller is
|
|
114
|
+
* expected to execute `submit` before `cancelCloids`.
|
|
115
|
+
*
|
|
116
|
+
* Fresh cloids are generated for the new legs (a cloid is single-use), which is
|
|
117
|
+
* also why the ledger keys on `bracketId`, not on a specific cloid. */
|
|
118
|
+
export function planResize(args) {
|
|
119
|
+
const coverage = bracketCoversPosition({
|
|
120
|
+
positionSize: args.positionSize,
|
|
121
|
+
liveLegs: args.liveLegs,
|
|
122
|
+
});
|
|
123
|
+
// Nothing to do when every present leg already covers the position exactly.
|
|
124
|
+
const oversized = args.liveLegs.some((l) => l.size - args.positionSize > COVERAGE_EPSILON);
|
|
125
|
+
if (coverage.shortfall.stop <= COVERAGE_EPSILON &&
|
|
126
|
+
coverage.shortfall.target <= COVERAGE_EPSILON &&
|
|
127
|
+
!oversized) {
|
|
128
|
+
return { cancelCloids: [], submit: [], noop: true };
|
|
129
|
+
}
|
|
130
|
+
// Rebuild every PRESENT role at the correct size, preserving its trigger price.
|
|
131
|
+
const legs = [];
|
|
132
|
+
const cancelCloids = [];
|
|
133
|
+
for (const role of ['stop', 'target']) {
|
|
134
|
+
const existing = args.liveLegs.filter((l) => l.role === role);
|
|
135
|
+
if (existing.length === 0)
|
|
136
|
+
continue; // absent leg = attach path's job, not resize's
|
|
137
|
+
// Trigger price is preserved from the live leg (the agent's pinned plan) —
|
|
138
|
+
// resizing must NEVER silently move a stop.
|
|
139
|
+
const triggerPrice = existing[0].triggerPrice;
|
|
140
|
+
legs.push({
|
|
141
|
+
role,
|
|
142
|
+
triggerPrice,
|
|
143
|
+
size: args.positionSize,
|
|
144
|
+
cloid: buildHlBracketCloid(args.bracketId, role),
|
|
145
|
+
});
|
|
146
|
+
cancelCloids.push(...existing.map((l) => l.cloid));
|
|
147
|
+
}
|
|
148
|
+
const closeSide = args.positionSide === 'long' ? 'sell' : 'buy';
|
|
149
|
+
const submit = buildBracketOrders({
|
|
150
|
+
bracketId: args.bracketId,
|
|
151
|
+
symbol: args.symbol,
|
|
152
|
+
closeSide,
|
|
153
|
+
legs,
|
|
154
|
+
});
|
|
155
|
+
return { cancelCloids, submit, noop: submit.length === 0 };
|
|
156
|
+
}
|
|
157
|
+
/** Is this exchange order one of OUR protective legs?
|
|
158
|
+
*
|
|
159
|
+
* The kill-switch's second layer (`OrderData.protective`) maps from this. It is
|
|
160
|
+
* deliberately BROADER than "has one of our cloids": an externally-placed stop
|
|
161
|
+
* must also survive a Kill. Mirrors the Binance rule (`reduceOnly || closePosition
|
|
162
|
+
* || STOP/TAKE_PROFIT type`), translated to HL's fields. */
|
|
163
|
+
export function isProtectiveHlOrder(order) {
|
|
164
|
+
if (order.reduceOnly === true)
|
|
165
|
+
return true;
|
|
166
|
+
if (order.isTrigger === true)
|
|
167
|
+
return true;
|
|
168
|
+
if (order.isPositionTpsl === true)
|
|
169
|
+
return true;
|
|
170
|
+
const t = (order.orderType ?? '').toLowerCase();
|
|
171
|
+
return t.includes('stop') || t.includes('take profit') || t.includes('tp');
|
|
172
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BracketId, BracketRole } from '../../live/bracket-types.js';
|
|
2
|
+
/** Build the HL cloid for a bracket leg. Reuses the venue-neutral 16-hex
|
|
3
|
+
* bracketId (same value the Binance cid carries) so the BracketLedger,
|
|
4
|
+
* reconciler and journal stay venue-agnostic. */
|
|
5
|
+
export declare function buildHlBracketCloid(bracketId: BracketId, role: BracketRole): string;
|
|
6
|
+
/** Parse an HL cloid. Returns null for any non-bracket cloid — this is called on
|
|
7
|
+
* EVERY order HL returns during reconciliation, so a null result is normal and
|
|
8
|
+
* must never throw. */
|
|
9
|
+
export declare function parseHlBracketCloid(cloid: string): {
|
|
10
|
+
bracketId: BracketId;
|
|
11
|
+
role: BracketRole;
|
|
12
|
+
} | null;
|
|
13
|
+
/** Cheap type-guard: is this one of OUR bracket cloids? */
|
|
14
|
+
export declare function isHlBracketCloid(cloid: string): boolean;
|
|
15
|
+
/** Is this a syntactically valid HL cloid at all (128-bit hex)? Used to reject
|
|
16
|
+
* a Binance-shaped cid before it reaches the wire (HL returns a cryptic error
|
|
17
|
+
* for malformed cloids). */
|
|
18
|
+
export declare function isValidHlCloid(cloid: string): boolean;
|
|
19
|
+
/** Non-bracket cloid for a plain (entry/close) order — lets every HL order carry
|
|
20
|
+
* an idempotency key, exactly like the Binance path's clientOrderId. Distinct
|
|
21
|
+
* magic ("ord") so it never parses as a bracket leg. */
|
|
22
|
+
export declare function buildHlOrderCloid(): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Hyperliquid bracket cloid scheme (plan §5.7).
|
|
2
|
+
//
|
|
3
|
+
// HL client order ids are **128-bit hex** (`0x` + exactly 32 hex chars) — NOT
|
|
4
|
+
// free-form ASCII like Binance's `newClientOrderId`. So the Binance `bkt<16hex><role>`
|
|
5
|
+
// scheme cannot be reused verbatim; it is RE-ENCODED into the 128-bit space:
|
|
6
|
+
//
|
|
7
|
+
// 0x bc7 <role> <16hex bracketId> <12hex random>
|
|
8
|
+
// └┬┘ └┬┘ └──┬─┘ └──────┬────────┘ └─────┬──────┘
|
|
9
|
+
// │ │ │ │ └── entropy (collision + opacity)
|
|
10
|
+
// │ │ │ └── the SAME bracketId the Binance scheme uses, so the
|
|
11
|
+
// │ │ │ ledger/reconciler stay venue-agnostic
|
|
12
|
+
// │ │ └── role nibble: e=entry, 5=stop, 7=target (hex-legal; 's'/'t' are not)
|
|
13
|
+
// │ └── magic "bc7" (bracket) — short on purpose, see below
|
|
14
|
+
// └── 0x prefix
|
|
15
|
+
//
|
|
16
|
+
// 3 + 1 + 16 + 12 = 32 hex chars. Exactly a valid cloid.
|
|
17
|
+
//
|
|
18
|
+
// ★ WHY THE MAGIC IS SHORT AND THE ENTROPY IS LONG. On Binance an EXTERNAL actor
|
|
19
|
+
// with API access pattern-matched our `rc-` cids and auto-cancelled every order
|
|
20
|
+
// carrying them (docs/BRACKET_CANCEL_ROOT_CAUSE_2026-05-16.md). The lesson carries
|
|
21
|
+
// to any venue: a long, guessable, human-obvious prefix is an attack surface. Keep
|
|
22
|
+
// the recognizable part minimal (we still need to reassemble brackets from the
|
|
23
|
+
// exchange after a restart, with zero reliance on local state) and let the rest be
|
|
24
|
+
// random.
|
|
25
|
+
//
|
|
26
|
+
// ★ RECOGNITION STAYS CENTRALIZED. docs/CLAUDE/brackets.md forbids a second
|
|
27
|
+
// bracket-cid regex anywhere else in the codebase. This module is the HL half of
|
|
28
|
+
// that rule; `live/bracket-id.ts:parseBracketClientId(venue, cid)` dispatches here.
|
|
29
|
+
// Never inline `/^0xbc7/` in a caller.
|
|
30
|
+
import { randomBytes } from 'node:crypto';
|
|
31
|
+
/** 0x + bc7 + role-nibble + 16-hex bracketId + 12-hex entropy = 34 chars total. */
|
|
32
|
+
const HL_CLOID_REGEX = /^0xbc7([e57])([0-9a-f]{16})([0-9a-f]{12})$/;
|
|
33
|
+
/** Any well-formed HL cloid (128-bit hex), bracket or not. */
|
|
34
|
+
const HL_CLOID_SHAPE = /^0x[0-9a-f]{32}$/;
|
|
35
|
+
const ROLE_TO_NIBBLE = {
|
|
36
|
+
entry: 'e',
|
|
37
|
+
stop: '5',
|
|
38
|
+
target: '7',
|
|
39
|
+
};
|
|
40
|
+
const NIBBLE_TO_ROLE = {
|
|
41
|
+
e: 'entry',
|
|
42
|
+
'5': 'stop',
|
|
43
|
+
'7': 'target',
|
|
44
|
+
};
|
|
45
|
+
/** Build the HL cloid for a bracket leg. Reuses the venue-neutral 16-hex
|
|
46
|
+
* bracketId (same value the Binance cid carries) so the BracketLedger,
|
|
47
|
+
* reconciler and journal stay venue-agnostic. */
|
|
48
|
+
export function buildHlBracketCloid(bracketId, role) {
|
|
49
|
+
if (!/^[0-9a-f]{16}$/.test(bracketId)) {
|
|
50
|
+
throw new Error(`Invalid bracketId: ${bracketId} — expected 16 hex chars`);
|
|
51
|
+
}
|
|
52
|
+
const entropy = randomBytes(6).toString('hex'); // 12 hex chars
|
|
53
|
+
return `0xbc7${ROLE_TO_NIBBLE[role]}${bracketId}${entropy}`;
|
|
54
|
+
}
|
|
55
|
+
/** Parse an HL cloid. Returns null for any non-bracket cloid — this is called on
|
|
56
|
+
* EVERY order HL returns during reconciliation, so a null result is normal and
|
|
57
|
+
* must never throw. */
|
|
58
|
+
export function parseHlBracketCloid(cloid) {
|
|
59
|
+
if (typeof cloid !== 'string')
|
|
60
|
+
return null;
|
|
61
|
+
const m = HL_CLOID_REGEX.exec(cloid.toLowerCase());
|
|
62
|
+
if (!m)
|
|
63
|
+
return null;
|
|
64
|
+
return { bracketId: m[2], role: NIBBLE_TO_ROLE[m[1]] };
|
|
65
|
+
}
|
|
66
|
+
/** Cheap type-guard: is this one of OUR bracket cloids? */
|
|
67
|
+
export function isHlBracketCloid(cloid) {
|
|
68
|
+
return typeof cloid === 'string' && HL_CLOID_REGEX.test(cloid.toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
/** Is this a syntactically valid HL cloid at all (128-bit hex)? Used to reject
|
|
71
|
+
* a Binance-shaped cid before it reaches the wire (HL returns a cryptic error
|
|
72
|
+
* for malformed cloids). */
|
|
73
|
+
export function isValidHlCloid(cloid) {
|
|
74
|
+
return typeof cloid === 'string' && HL_CLOID_SHAPE.test(cloid.toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
/** Non-bracket cloid for a plain (entry/close) order — lets every HL order carry
|
|
77
|
+
* an idempotency key, exactly like the Binance path's clientOrderId. Distinct
|
|
78
|
+
* magic ("ord") so it never parses as a bracket leg. */
|
|
79
|
+
export function buildHlOrderCloid() {
|
|
80
|
+
// 0x + "0d" (order) + 30 hex random = 32 hex chars.
|
|
81
|
+
return `0x0d${randomBytes(15).toString('hex')}`;
|
|
82
|
+
}
|