@reefclaw/openclaw-plugin 0.1.12 → 0.1.14
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 +19 -0
- package/bridge/gateway/event-parser.js +52 -0
- package/bridge/gateway/gateway-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/heartbeat-cron.d.ts +1 -0
- package/bridge/gateway/heartbeat-cron.js +22 -1
- package/bridge/gateway/poller.js +18 -8
- package/bridge/providers/emergency-commands.d.ts +9 -1
- package/bridge/providers/emergency-commands.js +38 -1
- package/bridge/providers/gateway.d.ts +72 -1
- package/bridge/providers/gateway.js +241 -29
- package/bridge/providers/onboarding-commands.d.ts +8 -0
- package/bridge/providers/onboarding-commands.js +4 -4
- package/bridge/types.d.ts +30 -0
- package/bridge/utils/identity-name.d.ts +24 -0
- package/bridge/utils/identity-name.js +54 -0
- package/ccxt/binance-public.d.ts +21 -7
- package/ccxt/binance-public.js +70 -6
- package/config/operator-provenance.d.ts +6 -0
- package/config/operator-provenance.js +50 -0
- package/config/plugin-config-io.d.ts +15 -1
- package/config/plugin-config-io.js +24 -0
- package/index.js +216 -173
- package/ingest/event-loop-monitor.d.ts +11 -0
- package/ingest/event-loop-monitor.js +113 -0
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +14 -5
- package/ingest/readiness-reporter.d.ts +17 -6
- package/ingest/readiness-reporter.js +88 -9
- package/ingest/skill-version-reader.d.ts +16 -0
- package/ingest/skill-version-reader.js +64 -0
- package/live/approval-lifecycle.d.ts +30 -0
- package/live/approval-lifecycle.js +80 -0
- package/live/bracket-types.d.ts +9 -0
- package/live/live-adapter.d.ts +0 -1
- package/live/proposal-decision-listener.d.ts +20 -0
- package/live/proposal-decision-listener.js +211 -48
- package/onboarding/runtime.d.ts +34 -1
- package/onboarding/runtime.js +56 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/simulator/exchange-simulator.d.ts +45 -2
- package/simulator/exchange-simulator.js +96 -4
- package/simulator/types.d.ts +17 -0
- package/tools/attach-brackets.js +50 -1
- package/tools/create-order.d.ts +11 -0
- package/tools/create-order.js +23 -2
- package/tools/get-risk-summary.d.ts +4 -0
- package/tools/get-risk-summary.js +62 -23
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +29 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +59 -2
- package/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/venues/hyperliquid/hl-brackets.js +45 -13
- package/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/venues/hyperliquid/hl-fill-ingest.js +69 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/venues/hyperliquid/hl-live-adapter.js +155 -12
- package/venues/hyperliquid/hl-order.d.ts +35 -0
- package/venues/hyperliquid/hl-order.js +123 -0
- package/venues/hyperliquid/hl-position.d.ts +36 -0
- package/venues/hyperliquid/hl-position.js +127 -0
- package/venues/hyperliquid/hl-private.d.ts +20 -3
- package/venues/hyperliquid/hl-private.js +37 -6
- package/venues/hyperliquid/hl-public.d.ts +12 -5
- package/venues/hyperliquid/hl-public.js +24 -3
- package/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/venues/hyperliquid/hl-user-stream.js +4 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CcxtOrder } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Collapse HL/ccxt order-type strings into the declared `'market' | 'limit'`
|
|
4
|
+
* union. HL order types are all execution-styles of one of the two: anything
|
|
5
|
+
* containing "market" ('Market', 'Stop Market', 'Take Profit Market') executes
|
|
6
|
+
* as a market when it goes; everything else — including a missing type, which
|
|
7
|
+
* only occurs on RESTING rows (a market order never rests) — is a limit.
|
|
8
|
+
* The raw subtype stays readable at `info.orderType`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeHlOrderType(raw: unknown): 'market' | 'limit';
|
|
11
|
+
/**
|
|
12
|
+
* Collapse order statuses into `'open' | 'closed' | 'canceled'`.
|
|
13
|
+
*
|
|
14
|
+
* ccxt's parseOrderStatus already maps filled→closed, triggered→open,
|
|
15
|
+
* marginCanceled/liquidatedCanceled→canceled — but lets 'rejected' (and any
|
|
16
|
+
* future HL status) through verbatim. Directional rule for the residue:
|
|
17
|
+
* - 'rejected' and any '*canceled' variant → 'canceled' (positively terminal,
|
|
18
|
+
* nothing was or will be executed);
|
|
19
|
+
* - 'filled' → 'closed' (in case a raw status bypasses ccxt's map);
|
|
20
|
+
* - anything UNKNOWN → 'open' + WARN. 'open' is the conservative direction on
|
|
21
|
+
* both consumer classes: destructive bracket decisions treat the leg as
|
|
22
|
+
* still live (never "gone" on a guess — the null≠empty doctrine), and the
|
|
23
|
+
* entry-order poller just keeps polling into its own timeout.
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeHlOrderStatus(raw: unknown): 'open' | 'closed' | 'canceled';
|
|
26
|
+
/**
|
|
27
|
+
* Normalize one ccxt HL order into a contract-compliant `CcxtOrder`.
|
|
28
|
+
*
|
|
29
|
+
* Unlike positions (where a fabricated number is a risk-limit hazard and an
|
|
30
|
+
* unpriceable row nulls the snapshot), an order row is never dropped: the
|
|
31
|
+
* fields we default (fee 0, cost from average×filled) are display/accounting
|
|
32
|
+
* conveniences, while the fields decisions hang on (id, cloid, status, info)
|
|
33
|
+
* are passed through or conservatively mapped.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeHlOrder(raw: CcxtOrder, nowMs?: number): CcxtOrder;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Hyperliquid order normalization — brings ccxt's HL order rows up to the
|
|
2
|
+
// `CcxtOrder` contract, the sibling of hl-position.ts (see its header for the
|
|
3
|
+
// class of bug: ccxt emits fields our types declare required as undefined, and
|
|
4
|
+
// TypeScript never checks runtime shapes).
|
|
5
|
+
//
|
|
6
|
+
// What ccxt@4.5.x's hyperliquid `parseOrder` actually emits (verified offline
|
|
7
|
+
// against the real parser, 2026-07-26, surfaced by hl-venue-conformance.test.ts):
|
|
8
|
+
// - `cost: undefined`, `fee: undefined` (HL carries fees on FILLS only, never
|
|
9
|
+
// on order objects) — our contract requires both.
|
|
10
|
+
// - `type: 'take profit market'` / `'take profit limit'` pass straight through
|
|
11
|
+
// `parseOrderType` (only 'stop limit'/'stop market' are mapped) — outside
|
|
12
|
+
// our `'market' | 'limit'` union. A resting limit with no `orderType` field
|
|
13
|
+
// yields `type: undefined`.
|
|
14
|
+
// - `status: 'rejected'` survives `parseOrderStatus` — outside our
|
|
15
|
+
// `'open' | 'closed' | 'canceled'` union.
|
|
16
|
+
// - `average: undefined`, `timeInForce: undefined` on trigger legs.
|
|
17
|
+
//
|
|
18
|
+
// ★ SCOPE: READ paths only (`fetchOpenOrders`, `fetchOrder`). Submit-path
|
|
19
|
+
// responses (`submitOrder`/`submitOrders`) are deliberately NOT normalized —
|
|
20
|
+
// `HlBracketCoordinator.isRejectedOrder` keys on the raw `status: 'rejected'`
|
|
21
|
+
// sentinel there, and blurring it into 'canceled' would break per-item batch
|
|
22
|
+
// rejection detection.
|
|
23
|
+
//
|
|
24
|
+
// ★ WHAT MUST SURVIVE VERBATIM: `info` (skill-side protective classification
|
|
25
|
+
// reads `info.orderType` — 'Take Profit Market' etc — via isProtectiveOrder/
|
|
26
|
+
// classifyProtectiveRole) and `clientOrderId` (bracket reconciliation parses
|
|
27
|
+
// the cloid). The conformance suite pins both.
|
|
28
|
+
import { logger } from '../../logger.js';
|
|
29
|
+
const TAG = 'hl-order';
|
|
30
|
+
/** HL's settlement asset — the only fee currency on this venue. */
|
|
31
|
+
const HL_FEE_CURRENCY = 'USDC';
|
|
32
|
+
/** Finite number or undefined — never NaN, never a silent 0. */
|
|
33
|
+
function fin(v) {
|
|
34
|
+
if (v === undefined || v === null || v === '')
|
|
35
|
+
return undefined;
|
|
36
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
37
|
+
return Number.isFinite(n) ? n : undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Collapse HL/ccxt order-type strings into the declared `'market' | 'limit'`
|
|
41
|
+
* union. HL order types are all execution-styles of one of the two: anything
|
|
42
|
+
* containing "market" ('Market', 'Stop Market', 'Take Profit Market') executes
|
|
43
|
+
* as a market when it goes; everything else — including a missing type, which
|
|
44
|
+
* only occurs on RESTING rows (a market order never rests) — is a limit.
|
|
45
|
+
* The raw subtype stays readable at `info.orderType`.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeHlOrderType(raw) {
|
|
48
|
+
const t = String(raw ?? '').toLowerCase();
|
|
49
|
+
return t.includes('market') ? 'market' : 'limit';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collapse order statuses into `'open' | 'closed' | 'canceled'`.
|
|
53
|
+
*
|
|
54
|
+
* ccxt's parseOrderStatus already maps filled→closed, triggered→open,
|
|
55
|
+
* marginCanceled/liquidatedCanceled→canceled — but lets 'rejected' (and any
|
|
56
|
+
* future HL status) through verbatim. Directional rule for the residue:
|
|
57
|
+
* - 'rejected' and any '*canceled' variant → 'canceled' (positively terminal,
|
|
58
|
+
* nothing was or will be executed);
|
|
59
|
+
* - 'filled' → 'closed' (in case a raw status bypasses ccxt's map);
|
|
60
|
+
* - anything UNKNOWN → 'open' + WARN. 'open' is the conservative direction on
|
|
61
|
+
* both consumer classes: destructive bracket decisions treat the leg as
|
|
62
|
+
* still live (never "gone" on a guess — the null≠empty doctrine), and the
|
|
63
|
+
* entry-order poller just keeps polling into its own timeout.
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeHlOrderStatus(raw) {
|
|
66
|
+
const s = String(raw ?? '').trim();
|
|
67
|
+
if (s === 'open' || s === 'closed' || s === 'canceled')
|
|
68
|
+
return s;
|
|
69
|
+
const lower = s.toLowerCase();
|
|
70
|
+
if (lower === 'rejected' || lower.endsWith('canceled') || lower.endsWith('cancelled')) {
|
|
71
|
+
return 'canceled';
|
|
72
|
+
}
|
|
73
|
+
if (lower === 'filled')
|
|
74
|
+
return 'closed';
|
|
75
|
+
logger.warn(TAG, `unknown HL order status '${s}' — treating as 'open' (not confirmed terminal)`);
|
|
76
|
+
return 'open';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Normalize one ccxt HL order into a contract-compliant `CcxtOrder`.
|
|
80
|
+
*
|
|
81
|
+
* Unlike positions (where a fabricated number is a risk-limit hazard and an
|
|
82
|
+
* unpriceable row nulls the snapshot), an order row is never dropped: the
|
|
83
|
+
* fields we default (fee 0, cost from average×filled) are display/accounting
|
|
84
|
+
* conveniences, while the fields decisions hang on (id, cloid, status, info)
|
|
85
|
+
* are passed through or conservatively mapped.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeHlOrder(raw, nowMs = Date.now()) {
|
|
88
|
+
const r = raw;
|
|
89
|
+
const amount = fin(r.amount) ?? 0;
|
|
90
|
+
const filledRaw = fin(r.filled);
|
|
91
|
+
const remainingRaw = fin(r.remaining);
|
|
92
|
+
const filled = filledRaw ?? (remainingRaw !== undefined ? Math.max(0, amount - remainingRaw) : 0);
|
|
93
|
+
const remaining = remainingRaw ?? Math.max(0, amount - filled);
|
|
94
|
+
const average = fin(r.average) ?? null;
|
|
95
|
+
const price = fin(r.price) ?? null;
|
|
96
|
+
const cost = fin(r.cost) ?? (average !== null ? average * filled : 0);
|
|
97
|
+
const fee = r.fee && typeof r.fee === 'object' && Number.isFinite(r.fee.cost)
|
|
98
|
+
? r.fee
|
|
99
|
+
: { cost: 0, currency: HL_FEE_CURRENCY };
|
|
100
|
+
const timestamp = fin(r.timestamp) ?? nowMs;
|
|
101
|
+
return {
|
|
102
|
+
...raw,
|
|
103
|
+
id: String(r.id ?? ''),
|
|
104
|
+
type: normalizeHlOrderType(
|
|
105
|
+
// Prefer the raw venue subtype — ccxt's unified type may already be
|
|
106
|
+
// collapsed ('stop market'→'market') or missing; info.orderType is the
|
|
107
|
+
// authoritative HL string when present.
|
|
108
|
+
r.info?.orderType ?? r.type),
|
|
109
|
+
status: normalizeHlOrderStatus(r.status),
|
|
110
|
+
amount,
|
|
111
|
+
filled,
|
|
112
|
+
remaining,
|
|
113
|
+
average,
|
|
114
|
+
price,
|
|
115
|
+
cost,
|
|
116
|
+
fee,
|
|
117
|
+
timeInForce: typeof r.timeInForce === 'string' && r.timeInForce.length > 0 ? r.timeInForce : 'GTC',
|
|
118
|
+
timestamp,
|
|
119
|
+
datetime: typeof r.datetime === 'string' && r.datetime.length > 0
|
|
120
|
+
? r.datetime
|
|
121
|
+
: new Date(timestamp).toISOString(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CcxtPosition } from '../../types.js';
|
|
2
|
+
/** The raw `assetPositions[].position` payload ccxt passes through at
|
|
3
|
+
* `position.info.position` (fields per the info-endpoint docs, §3.6 of the
|
|
4
|
+
* integration plan). */
|
|
5
|
+
export interface HlRawPositionInfo {
|
|
6
|
+
coin?: string;
|
|
7
|
+
/** Signed size — negative for a short. */
|
|
8
|
+
szi?: string | number;
|
|
9
|
+
entryPx?: string | number;
|
|
10
|
+
unrealizedPnl?: string | number;
|
|
11
|
+
/** |szi| × mark. */
|
|
12
|
+
positionValue?: string | number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mark price for an HL position, derived from the position payload alone.
|
|
16
|
+
*
|
|
17
|
+
* Primary route inverts the documented uPnl formula; secondary route divides
|
|
18
|
+
* `positionValue` by |szi|. Returns undefined rather than a guess when neither
|
|
19
|
+
* route yields a positive price.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveHlMarkPrice(args: {
|
|
22
|
+
entryPrice?: number;
|
|
23
|
+
unrealizedPnl?: number;
|
|
24
|
+
/** Signed size (negative = short). */
|
|
25
|
+
signedSize?: number;
|
|
26
|
+
positionValue?: number;
|
|
27
|
+
}): number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Normalize one ccxt HL position into a contract-compliant `CcxtPosition`.
|
|
30
|
+
*
|
|
31
|
+
* Returns **null** when the row cannot be priced or sized — the caller must then
|
|
32
|
+
* treat the whole snapshot as UNKNOWN (`null`), not drop the row: a silently
|
|
33
|
+
* missing position is exactly the "empty ≠ unknown" collapse that drove the
|
|
34
|
+
* naked-bracket incidents on the Binance side.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeHlPosition(raw: CcxtPosition | null | undefined, nowMs?: number): CcxtPosition | null;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Hyperliquid position normalization — brings ccxt's HL position rows up to the
|
|
2
|
+
// `CcxtPosition` contract the rest of the plugin treats as REQUIRED numbers.
|
|
3
|
+
//
|
|
4
|
+
// ★ WHY THIS FILE EXISTS (the bug it fixes, 2026-07-26): ccxt@4.5.x's hyperliquid
|
|
5
|
+
// `parsePosition` sets `markPrice: undefined` (hyperliquid.js:3603), plus
|
|
6
|
+
// `contractSize`/`timestamp`/`datetime: undefined` — and `safePosition()` fills
|
|
7
|
+
// none of them. `CcxtPosition` declares all four as non-optional numbers, so
|
|
8
|
+
// every consumer that trusts the type broke on an HL LIVE position:
|
|
9
|
+
// - `get_risk_summary` threw `Cannot read properties of undefined (reading
|
|
10
|
+
// 'toFixed')` on `+pos.markPrice.toFixed(2)` (the reported symptom),
|
|
11
|
+
// - `get_wave9_status` / `live-account-capture` reject the position as
|
|
12
|
+
// non-finite, `stop-watcher` / `modify_stop` / `modify_target` read a
|
|
13
|
+
// `undefined` reference price, and `pre-trade-check` / `create-order`
|
|
14
|
+
// silently fall back to entryPrice (stale exposure + stop-distance math).
|
|
15
|
+
// Fixing it at this boundary fixes all of them at once — which is why the
|
|
16
|
+
// normalization lives here (the single choke point every HL position read
|
|
17
|
+
// passes through) and not in the individual tools.
|
|
18
|
+
//
|
|
19
|
+
// ★ THE MARK DERIVATION IS DOC-EXACT AND COSTS NO NETWORK CALL.
|
|
20
|
+
// `clearinghouseState` carries NO per-position mark price — markPx lives only in
|
|
21
|
+
// `metaAndAssetCtxs` (weight 20), so ccxt has nothing to parse and a naive fix
|
|
22
|
+
// would put a heavy call on a per-tick read path (the rule from
|
|
23
|
+
// memory/project_hl_venue_scoped_scan_jul13: never put a heavy HL info call on a
|
|
24
|
+
// poll path). Instead we invert Hyperliquid's own documented formula:
|
|
25
|
+
//
|
|
26
|
+
// "Unrealized pnl is defined as `side * (mark_price - entry_price) *
|
|
27
|
+
// position_size`" — hyperliquid.gitbook.io → Trading → Entry price and pnl
|
|
28
|
+
// (verified 2026-07-26)
|
|
29
|
+
//
|
|
30
|
+
// With `szi` the SIGNED size (side × size) that inverts exactly to
|
|
31
|
+
// mark = entryPx + unrealizedPnl / szi
|
|
32
|
+
// and the docs' own example confirms both this and the secondary route:
|
|
33
|
+
// szi 0.0335, entryPx 2986.3, uPnl −0.0134 → mark 2985.9
|
|
34
|
+
// positionValue 100.02765 / 0.0335 → mark 2985.9 ✓ (|szi| × mark)
|
|
35
|
+
//
|
|
36
|
+
// ★ NEVER FABRICATE. A row we cannot price returns null and the CALLER turns the
|
|
37
|
+
// whole snapshot into null ("unknown"), never a partial list and never a $0
|
|
38
|
+
// mark — a phantom zero mark reads as a total loss and would trip risk limits.
|
|
39
|
+
/** Finite number or undefined — never NaN, never a silent 0. */
|
|
40
|
+
function fin(v) {
|
|
41
|
+
if (v === undefined || v === null || v === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
44
|
+
return Number.isFinite(n) ? n : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Mark price for an HL position, derived from the position payload alone.
|
|
48
|
+
*
|
|
49
|
+
* Primary route inverts the documented uPnl formula; secondary route divides
|
|
50
|
+
* `positionValue` by |szi|. Returns undefined rather than a guess when neither
|
|
51
|
+
* route yields a positive price.
|
|
52
|
+
*/
|
|
53
|
+
export function deriveHlMarkPrice(args) {
|
|
54
|
+
const { entryPrice, unrealizedPnl, signedSize, positionValue } = args;
|
|
55
|
+
// Primary: mark = entry + uPnl / szi (exact inverse of the documented
|
|
56
|
+
// uPnl = side * (mark - entry) * size). Also correct when uPnl is 0 — a
|
|
57
|
+
// freshly-filled position marks at its entry.
|
|
58
|
+
if (entryPrice !== undefined &&
|
|
59
|
+
entryPrice > 0 &&
|
|
60
|
+
unrealizedPnl !== undefined &&
|
|
61
|
+
signedSize !== undefined &&
|
|
62
|
+
signedSize !== 0) {
|
|
63
|
+
const mark = entryPrice + unrealizedPnl / signedSize;
|
|
64
|
+
if (Number.isFinite(mark) && mark > 0)
|
|
65
|
+
return mark;
|
|
66
|
+
}
|
|
67
|
+
// Secondary: positionValue = |szi| × mark.
|
|
68
|
+
if (positionValue !== undefined &&
|
|
69
|
+
positionValue > 0 &&
|
|
70
|
+
signedSize !== undefined &&
|
|
71
|
+
signedSize !== 0) {
|
|
72
|
+
const mark = positionValue / Math.abs(signedSize);
|
|
73
|
+
if (Number.isFinite(mark) && mark > 0)
|
|
74
|
+
return mark;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Normalize one ccxt HL position into a contract-compliant `CcxtPosition`.
|
|
80
|
+
*
|
|
81
|
+
* Returns **null** when the row cannot be priced or sized — the caller must then
|
|
82
|
+
* treat the whole snapshot as UNKNOWN (`null`), not drop the row: a silently
|
|
83
|
+
* missing position is exactly the "empty ≠ unknown" collapse that drove the
|
|
84
|
+
* naked-bracket incidents on the Binance side.
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeHlPosition(raw, nowMs = Date.now()) {
|
|
87
|
+
if (!raw || typeof raw !== 'object')
|
|
88
|
+
return null;
|
|
89
|
+
const info = raw.info?.position;
|
|
90
|
+
const signedFromInfo = fin(info?.szi);
|
|
91
|
+
const contracts = fin(raw.contracts) ?? (signedFromInfo === undefined ? undefined : Math.abs(signedFromInfo));
|
|
92
|
+
if (contracts === undefined || contracts <= 0)
|
|
93
|
+
return null;
|
|
94
|
+
const side = raw.side === 'short' || (raw.side === undefined && signedFromInfo !== undefined && signedFromInfo < 0)
|
|
95
|
+
? 'short'
|
|
96
|
+
: 'long';
|
|
97
|
+
const signedSize = signedFromInfo ?? (side === 'short' ? -contracts : contracts);
|
|
98
|
+
const entryPrice = fin(raw.entryPrice) ?? fin(info?.entryPx);
|
|
99
|
+
if (entryPrice === undefined || entryPrice <= 0)
|
|
100
|
+
return null;
|
|
101
|
+
// HL always sends unrealizedPnl for an open position; 0 is the neutral value
|
|
102
|
+
// for the impossible branch (it makes the primary mark route degrade to
|
|
103
|
+
// "marks at entry" rather than inventing a price).
|
|
104
|
+
const unrealizedPnl = fin(raw.unrealizedPnl) ?? fin(info?.unrealizedPnl) ?? 0;
|
|
105
|
+
const positionValue = fin(raw.notional) ?? fin(info?.positionValue);
|
|
106
|
+
const markPrice = fin(raw.markPrice) ??
|
|
107
|
+
deriveHlMarkPrice({ entryPrice, unrealizedPnl, signedSize, positionValue });
|
|
108
|
+
if (markPrice === undefined || markPrice <= 0)
|
|
109
|
+
return null;
|
|
110
|
+
const notional = positionValue ?? Math.abs(contracts * markPrice);
|
|
111
|
+
const timestamp = fin(raw.timestamp) ?? nowMs;
|
|
112
|
+
return {
|
|
113
|
+
...raw,
|
|
114
|
+
side,
|
|
115
|
+
contracts,
|
|
116
|
+
contractSize: fin(raw.contractSize) ?? 1, // HL perps: 1 coin per contract
|
|
117
|
+
entryPrice,
|
|
118
|
+
markPrice,
|
|
119
|
+
notional,
|
|
120
|
+
unrealizedPnl,
|
|
121
|
+
percentage: fin(raw.percentage) ?? 0,
|
|
122
|
+
timestamp,
|
|
123
|
+
datetime: typeof raw.datetime === 'string' && raw.datetime.length > 0
|
|
124
|
+
? raw.datetime
|
|
125
|
+
: new Date(timestamp).toISOString(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -49,16 +49,33 @@ export declare class HyperliquidPrivateApi {
|
|
|
49
49
|
getExchange(): any;
|
|
50
50
|
loadMarkets(): Promise<boolean>;
|
|
51
51
|
/** Positions for the MASTER account. `null` = fetch failed (state unknown);
|
|
52
|
-
* `[]` = the exchange confirmed flat.
|
|
52
|
+
* `[]` = the exchange confirmed flat.
|
|
53
|
+
*
|
|
54
|
+
* ★ Rows are normalized through `normalizeHlPosition` — ccxt leaves
|
|
55
|
+
* `markPrice`/`contractSize`/`timestamp`/`datetime` UNDEFINED on this venue
|
|
56
|
+
* (clearinghouseState carries no markPx) while `CcxtPosition` declares them
|
|
57
|
+
* required, which crashed `get_risk_summary` and degraded every other
|
|
58
|
+
* mark-reading consumer. See hl-position.ts for the doc-exact derivation.
|
|
59
|
+
* A row that cannot be priced makes the WHOLE snapshot null (unknown) — never
|
|
60
|
+
* a partial list, never a fabricated zero. */
|
|
53
61
|
fetchPositions(symbol?: string): Promise<CcxtPosition[] | null>;
|
|
54
62
|
/** Open orders INCLUDING trigger/TPSL legs. CCXT's HL `fetchOpenOrders`
|
|
55
63
|
* defaults to `frontendOpenOrders`, which is the only endpoint that returns
|
|
56
|
-
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
64
|
+
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
65
|
+
*
|
|
66
|
+
* ★ Rows are normalized through `normalizeHlOrder` (ccxt leaves cost/fee/
|
|
67
|
+
* average/timeInForce undefined and leaks 'take profit market' past the
|
|
68
|
+
* type union — see hl-order.ts); `info` + `clientOrderId` survive verbatim
|
|
69
|
+
* (protective classification + bracket reconciliation read them). */
|
|
57
70
|
fetchOpenOrders(symbol?: string): Promise<CcxtOrder[] | null>;
|
|
58
71
|
fetchBalance(): Promise<CcxtBalance | null>;
|
|
59
72
|
/** Per-order status — the liveness resolver's REST tier (Tier 2 of the
|
|
60
73
|
* 3-tier rule). Weight 2. `null` = lookup FAILED (unknown), which callers
|
|
61
|
-
* must treat as "do not act", NOT as "gone".
|
|
74
|
+
* must treat as "do not act", NOT as "gone".
|
|
75
|
+
*
|
|
76
|
+
* Normalized like fetchOpenOrders. Unknown statuses collapse to 'open'
|
|
77
|
+
* (= not confirmed terminal — the conservative direction for both the
|
|
78
|
+
* liveness resolver and the entry poller); raw status stays in `info`. */
|
|
62
79
|
fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
|
|
63
80
|
/** Own fills. WS is the authoritative ingress (plan + the audit-trail rule);
|
|
64
81
|
* this is the gap-fill/truth-check path. NOTE: only the 10,000 most recent
|
|
@@ -29,6 +29,8 @@ import { logger } from '../../logger.js';
|
|
|
29
29
|
import { toCcxtSymbol } from '../symbols.js';
|
|
30
30
|
import { assertNotLimited, noteError, noteSuccess, exchangeIpWeight, updateAddressBudget, } from './hl-rate-gate.js';
|
|
31
31
|
import { isValidHlCloid } from './hl-cloid.js';
|
|
32
|
+
import { normalizeHlPosition } from './hl-position.js';
|
|
33
|
+
import { normalizeHlOrder } from './hl-order.js';
|
|
32
34
|
// ccxt via CJS require — OpenClaw's ESM loader yields the wrong module shape
|
|
33
35
|
// (same rationale as binance-private.ts / hl-public.ts).
|
|
34
36
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -124,7 +126,15 @@ export class HyperliquidPrivateApi {
|
|
|
124
126
|
}
|
|
125
127
|
// ---- Reads (null on failure — NEVER []) ----
|
|
126
128
|
/** Positions for the MASTER account. `null` = fetch failed (state unknown);
|
|
127
|
-
* `[]` = the exchange confirmed flat.
|
|
129
|
+
* `[]` = the exchange confirmed flat.
|
|
130
|
+
*
|
|
131
|
+
* ★ Rows are normalized through `normalizeHlPosition` — ccxt leaves
|
|
132
|
+
* `markPrice`/`contractSize`/`timestamp`/`datetime` UNDEFINED on this venue
|
|
133
|
+
* (clearinghouseState carries no markPx) while `CcxtPosition` declares them
|
|
134
|
+
* required, which crashed `get_risk_summary` and degraded every other
|
|
135
|
+
* mark-reading consumer. See hl-position.ts for the doc-exact derivation.
|
|
136
|
+
* A row that cannot be priced makes the WHOLE snapshot null (unknown) — never
|
|
137
|
+
* a partial list, never a fabricated zero. */
|
|
128
138
|
async fetchPositions(symbol) {
|
|
129
139
|
try {
|
|
130
140
|
assertNotLimited('clearinghouseState');
|
|
@@ -133,7 +143,19 @@ export class HyperliquidPrivateApi {
|
|
|
133
143
|
noteSuccess('clearinghouseState');
|
|
134
144
|
if (!Array.isArray(raw))
|
|
135
145
|
return null;
|
|
136
|
-
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const p of raw) {
|
|
148
|
+
if (Math.abs(Number(p?.contracts ?? 0)) <= 0)
|
|
149
|
+
continue; // flat row — information, not failure
|
|
150
|
+
const normalized = normalizeHlPosition(p);
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
logger.error(TAG, `fetchPositions: unparseable position row for ${String(p?.symbol ?? 'unknown symbol')} ` +
|
|
153
|
+
'— returning null (snapshot UNTRUSTED; a dropped row would read as "flat")');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
out.push(normalized);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
137
159
|
}
|
|
138
160
|
catch (err) {
|
|
139
161
|
noteError(err, 'fetchPositions');
|
|
@@ -143,14 +165,19 @@ export class HyperliquidPrivateApi {
|
|
|
143
165
|
}
|
|
144
166
|
/** Open orders INCLUDING trigger/TPSL legs. CCXT's HL `fetchOpenOrders`
|
|
145
167
|
* defaults to `frontendOpenOrders`, which is the only endpoint that returns
|
|
146
|
-
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
168
|
+
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
169
|
+
*
|
|
170
|
+
* ★ Rows are normalized through `normalizeHlOrder` (ccxt leaves cost/fee/
|
|
171
|
+
* average/timeInForce undefined and leaks 'take profit market' past the
|
|
172
|
+
* type union — see hl-order.ts); `info` + `clientOrderId` survive verbatim
|
|
173
|
+
* (protective classification + bracket reconciliation read them). */
|
|
147
174
|
async fetchOpenOrders(symbol) {
|
|
148
175
|
try {
|
|
149
176
|
assertNotLimited('frontendOpenOrders');
|
|
150
177
|
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
151
178
|
const raw = await this.exchange.fetchOpenOrders(s);
|
|
152
179
|
noteSuccess('frontendOpenOrders');
|
|
153
|
-
return Array.isArray(raw) ? raw : null;
|
|
180
|
+
return Array.isArray(raw) ? raw.map((o) => normalizeHlOrder(o)) : null;
|
|
154
181
|
}
|
|
155
182
|
catch (err) {
|
|
156
183
|
noteError(err, 'fetchOpenOrders');
|
|
@@ -173,14 +200,18 @@ export class HyperliquidPrivateApi {
|
|
|
173
200
|
}
|
|
174
201
|
/** Per-order status — the liveness resolver's REST tier (Tier 2 of the
|
|
175
202
|
* 3-tier rule). Weight 2. `null` = lookup FAILED (unknown), which callers
|
|
176
|
-
* must treat as "do not act", NOT as "gone".
|
|
203
|
+
* must treat as "do not act", NOT as "gone".
|
|
204
|
+
*
|
|
205
|
+
* Normalized like fetchOpenOrders. Unknown statuses collapse to 'open'
|
|
206
|
+
* (= not confirmed terminal — the conservative direction for both the
|
|
207
|
+
* liveness resolver and the entry poller); raw status stays in `info`. */
|
|
177
208
|
async fetchOrder(orderId, symbol) {
|
|
178
209
|
try {
|
|
179
210
|
assertNotLimited('orderStatus');
|
|
180
211
|
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
181
212
|
const raw = await this.exchange.fetchOrder(orderId, s);
|
|
182
213
|
noteSuccess('orderStatus');
|
|
183
|
-
return raw
|
|
214
|
+
return raw ? normalizeHlOrder(raw) : null;
|
|
184
215
|
}
|
|
185
216
|
catch (err) {
|
|
186
217
|
noteError(err, 'fetchOrder');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CcxtTicker, CcxtOHLCV } from '../../types.js';
|
|
2
2
|
import type { OrderBookDepth } from '../../simulator/types.js';
|
|
3
3
|
import type { PublicMarketDataApi } from '../../ccxt/public-market-data-api.js';
|
|
4
|
+
import { type VenueReachabilityResult } from '@reefclaw/shared';
|
|
4
5
|
export interface HyperliquidPublicApiOptions {
|
|
5
6
|
testnet?: boolean;
|
|
6
7
|
/** Test seam — injected ccxt exchange instance. */
|
|
@@ -60,11 +61,17 @@ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
|
|
|
60
61
|
* (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
|
|
61
62
|
* as the clock-drift source. Geo classification is best-effort — HL's
|
|
62
63
|
* API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
|
|
63
|
-
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
65
|
+
*
|
|
66
|
+
* ★ Self-stall detection (issue #265): the failure path is wall-clock timed.
|
|
67
|
+
* A throw is only evidence about the VENUE if our own abort timer fired
|
|
68
|
+
* roughly when it was set for. On a starved host the loop stops running —
|
|
69
|
+
* observed live on the HL rig: this 10s timer landed 86s late while the
|
|
70
|
+
* agent was trading on HL perfectly well — and blaming the network then
|
|
71
|
+
* produces a confident, wrong "check DNS/firewall/region" banner. Past
|
|
72
|
+
* STALL_FACTOR× the budget we report `stalled`, which the reporter renders
|
|
73
|
+
* as `unknown`: we genuinely did not learn whether HL is reachable. */
|
|
74
|
+
probeReachability(): Promise<VenueReachabilityResult>;
|
|
68
75
|
/** `meta` — the asset universe (szDecimals, maxLeverage, positional assetIndex).
|
|
69
76
|
* Keyless info read, weight 20. Feeds HyperliquidInfoCache. Returns null on any
|
|
70
77
|
* failure (never a partial universe — a half-loaded rules table would silently
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// on this venue — logged clearly, null returned (never silently translated).
|
|
33
33
|
import { createRequire } from 'node:module';
|
|
34
34
|
import { logger } from '../../logger.js';
|
|
35
|
+
import { REACHABILITY_STALL_FACTOR } from '@reefclaw/shared';
|
|
35
36
|
import { toCcxtSymbol, toHyperliquidCoin } from '../symbols.js';
|
|
36
37
|
const TAG = 'hl-public';
|
|
37
38
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
@@ -55,6 +56,10 @@ const DEFAULT_TICKER_TTL_MS = 4_000;
|
|
|
55
56
|
* snapshot's ccxt call re-runs the whole fetchMarkets pipeline (weight ~60,
|
|
56
57
|
* ~13s on a slow host) and nothing price-critical reads these fields. */
|
|
57
58
|
const FULL_SNAPSHOT_REFRESH_MS = 300_000;
|
|
59
|
+
/** Reachability-probe request budget. Also the yardstick for self-stall
|
|
60
|
+
* detection — see probeReachability. */
|
|
61
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
62
|
+
const STALL_FACTOR = REACHABILITY_STALL_FACTOR;
|
|
58
63
|
function resolveTickerTtlMs() {
|
|
59
64
|
const raw = Number(process.env.RC_HL_TICKER_TTL_MS);
|
|
60
65
|
if (!Number.isFinite(raw) || raw < 500)
|
|
@@ -343,10 +348,20 @@ export class HyperliquidPublicApi {
|
|
|
343
348
|
* (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
|
|
344
349
|
* as the clock-drift source. Geo classification is best-effort — HL's
|
|
345
350
|
* API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
|
|
346
|
-
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
351
|
+
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
352
|
+
*
|
|
353
|
+
* ★ Self-stall detection (issue #265): the failure path is wall-clock timed.
|
|
354
|
+
* A throw is only evidence about the VENUE if our own abort timer fired
|
|
355
|
+
* roughly when it was set for. On a starved host the loop stops running —
|
|
356
|
+
* observed live on the HL rig: this 10s timer landed 86s late while the
|
|
357
|
+
* agent was trading on HL perfectly well — and blaming the network then
|
|
358
|
+
* produces a confident, wrong "check DNS/firewall/region" banner. Past
|
|
359
|
+
* STALL_FACTOR× the budget we report `stalled`, which the reporter renders
|
|
360
|
+
* as `unknown`: we genuinely did not learn whether HL is reachable. */
|
|
347
361
|
async probeReachability() {
|
|
348
362
|
const ac = new AbortController();
|
|
349
|
-
const tid = setTimeout(() => ac.abort(),
|
|
363
|
+
const tid = setTimeout(() => ac.abort(), PROBE_TIMEOUT_MS);
|
|
364
|
+
const startedAt = Date.now();
|
|
350
365
|
try {
|
|
351
366
|
const res = await this.fetchImpl(`${this.baseUrl()}/info`, {
|
|
352
367
|
method: 'POST',
|
|
@@ -368,7 +383,13 @@ export class HyperliquidPublicApi {
|
|
|
368
383
|
return { outcome: 'reachable', driftMs };
|
|
369
384
|
}
|
|
370
385
|
catch (err) {
|
|
371
|
-
|
|
386
|
+
const elapsedMs = Date.now() - startedAt;
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
if (elapsedMs > PROBE_TIMEOUT_MS * STALL_FACTOR) {
|
|
389
|
+
logger.warn(TAG, `probeReachability inconclusive: this process was starved — a ${PROBE_TIMEOUT_MS}ms probe took ${Math.round(elapsedMs / 1000)}s (${msg}). Reporting reachability as unknown, NOT as a Hyperliquid failure.`);
|
|
390
|
+
return { outcome: 'stalled', driftMs: null, stallMs: elapsedMs };
|
|
391
|
+
}
|
|
392
|
+
logger.warn(TAG, `probeReachability failed: ${msg}`);
|
|
372
393
|
return { outcome: 'unreachable', driftMs: null };
|
|
373
394
|
}
|
|
374
395
|
finally {
|
|
@@ -41,7 +41,19 @@ export interface HlOrderUpdateEvent {
|
|
|
41
41
|
statusTimestamp: number;
|
|
42
42
|
}
|
|
43
43
|
export interface HlUserStreamCallbacks {
|
|
44
|
-
|
|
44
|
+
/** ★ `meta.isSnapshot` marks a fill from the subscription SNAPSHOT — the batch
|
|
45
|
+
* of recent history HL sends on every (re)subscribe — not a live execution.
|
|
46
|
+
*
|
|
47
|
+
* Do NOT confuse this with the T-5 gap rule below: HL not replaying the fills
|
|
48
|
+
* you MISSED while disconnected is true and unrelated. It still ships a
|
|
49
|
+
* snapshot of recent history on subscribe, and consumers must tell them apart.
|
|
50
|
+
* Idempotent consumers (the audit-trail ingest, keyed on exchange trade id)
|
|
51
|
+
* should take snapshot fills; state MUTATORS (bracket resize/attach) must not
|
|
52
|
+
* — replaying an old fill's `startPosition + sz` resizes live protective legs
|
|
53
|
+
* to a stale historical size (audit 2026-07-27 F44). */
|
|
54
|
+
onFill: (fill: HlFillEvent, meta: {
|
|
55
|
+
isSnapshot: boolean;
|
|
56
|
+
}) => void;
|
|
45
57
|
onOrderUpdate: (update: HlOrderUpdateEvent) => void;
|
|
46
58
|
/** userEvents: liquidation / funding / non-user-cancel — the close-bypass feed. */
|
|
47
59
|
onUserEvent: (event: Record<string, unknown>) => void;
|
|
@@ -137,9 +137,12 @@ export class HyperliquidUserStream {
|
|
|
137
137
|
switch (channel) {
|
|
138
138
|
case 'userFills': {
|
|
139
139
|
const payload = data;
|
|
140
|
+
// `isSnapshot` was typed here from the start but never read — so every
|
|
141
|
+
// (re)connect replayed recent history as if it were live (F44).
|
|
142
|
+
const isSnapshot = payload?.isSnapshot === true;
|
|
140
143
|
for (const fill of payload?.fills ?? []) {
|
|
141
144
|
this.lastEventAt = Math.max(this.lastEventAt, fill.time ?? Date.now());
|
|
142
|
-
this.opts.callbacks.onFill(fill);
|
|
145
|
+
this.opts.callbacks.onFill(fill, { isSnapshot });
|
|
143
146
|
}
|
|
144
147
|
break;
|
|
145
148
|
}
|