@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,357 @@
|
|
|
1
|
+
// Hyperliquid signed-action client — the HL mirror of `ccxt/binance-private.ts`.
|
|
2
|
+
//
|
|
3
|
+
// CONTRACTS THIS FILE INHERITS FROM THE BINANCE SIDE (non-negotiable — the shared
|
|
4
|
+
// adapter-contract suite runs against both):
|
|
5
|
+
// ★ null ≠ empty. EVERY read returns `null` on a FAILED fetch, never `[]`.
|
|
6
|
+
// `[]` means "the exchange said: nothing". Callers make destructive decisions
|
|
7
|
+
// (cancel a bracket, close a position, trust a snapshot) on that difference.
|
|
8
|
+
// ★ Reads are pre-gated (`assertNotLimited`), order/cancel paths are NOT (an
|
|
9
|
+
// exit must never be blocked by our own pacer) but every path arms the gate
|
|
10
|
+
// via `noteError`.
|
|
11
|
+
//
|
|
12
|
+
// HYPERLIQUID-SPECIFIC LANDMINES (all verified live on testnet 2026-07-12):
|
|
13
|
+
// ★ ccxt AUTO-MONETIZES an authed HL client — `initializeClient()` enrolls
|
|
14
|
+
// CCXT's own builder fee (1bp/order) + referral code on the first signed call.
|
|
15
|
+
// `options.{builderFee:false, refSet:true}` disables both. A test pins this.
|
|
16
|
+
// ★ Market orders REQUIRE a reference price (ccxt derives the slippage cap from
|
|
17
|
+
// it) — `createOrder(sym,'market',side,size)` with no price THROWS client-side.
|
|
18
|
+
// There is no native market order on HL: it is an IOC limit at a bounded price.
|
|
19
|
+
// ★ Signed actions carry a NONCE (epoch ms) and HL keeps only the 100 highest
|
|
20
|
+
// per signer. Two actions signed in the same millisecond can collide → every
|
|
21
|
+
// signed action goes through a monotonic-nonce MUTEX (one in-flight at a time,
|
|
22
|
+
// strictly increasing ms). This is why `submitOrders` batches a bracket pair
|
|
23
|
+
// into ONE action rather than firing two.
|
|
24
|
+
// ★ Agent (API) wallets can only TRADE. `usdClassTransfer`/withdraw are
|
|
25
|
+
// user-signed actions and fail with "Must deposit before performing actions"
|
|
26
|
+
// when signed by an agent. We never call them.
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { logger } from '../../logger.js';
|
|
29
|
+
import { toCcxtSymbol } from '../symbols.js';
|
|
30
|
+
import { assertNotLimited, noteError, noteSuccess, exchangeIpWeight, updateAddressBudget, } from './hl-rate-gate.js';
|
|
31
|
+
import { isValidHlCloid } from './hl-cloid.js';
|
|
32
|
+
// ccxt via CJS require — OpenClaw's ESM loader yields the wrong module shape
|
|
33
|
+
// (same rationale as binance-private.ts / hl-public.ts).
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
let ccxtCjs;
|
|
36
|
+
try {
|
|
37
|
+
const _require = createRequire(import.meta.url);
|
|
38
|
+
ccxtCjs = _require('ccxt');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
ccxtCjs = require('ccxt');
|
|
43
|
+
}
|
|
44
|
+
const TAG = 'hl-private';
|
|
45
|
+
/** The CCXT options that MUST be present on every authed HL client. Exported so
|
|
46
|
+
* `hl-private.test.ts` can assert them — if a future ccxt bump changes the
|
|
47
|
+
* option names, the test fails rather than the users paying a silent 1bp. */
|
|
48
|
+
export const HL_REQUIRED_OPTIONS = {
|
|
49
|
+
builderFee: false,
|
|
50
|
+
refSet: true,
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Serializes every signed action behind a monotonic nonce.
|
|
54
|
+
*
|
|
55
|
+
* HL keeps the 100 highest nonces per signer and rejects repeats; two actions
|
|
56
|
+
* signed inside the same millisecond can therefore collide and one is silently
|
|
57
|
+
* lost. Rather than hand-rolling nonces (ccxt owns that), we guarantee that no
|
|
58
|
+
* two signed actions are ever *issued* in the same millisecond from this process:
|
|
59
|
+
* each waits for the previous to settle AND for the clock to advance.
|
|
60
|
+
*/
|
|
61
|
+
class NonceMutex {
|
|
62
|
+
chain = Promise.resolve();
|
|
63
|
+
lastIssuedMs = 0;
|
|
64
|
+
run(fn) {
|
|
65
|
+
const next = this.chain.then(async () => {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (now <= this.lastIssuedMs) {
|
|
68
|
+
// Same-ms collision guard: wait out the millisecond.
|
|
69
|
+
await new Promise((r) => setTimeout(r, this.lastIssuedMs - now + 1));
|
|
70
|
+
}
|
|
71
|
+
this.lastIssuedMs = Date.now();
|
|
72
|
+
return fn();
|
|
73
|
+
});
|
|
74
|
+
// Keep the chain alive even when a link rejects (otherwise one failed action
|
|
75
|
+
// would poison every later one).
|
|
76
|
+
this.chain = next.then(() => undefined, () => undefined);
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class HyperliquidPrivateApi {
|
|
81
|
+
creds;
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
exchange;
|
|
84
|
+
mutex = new NonceMutex();
|
|
85
|
+
marketsLoaded = false;
|
|
86
|
+
constructor(creds) {
|
|
87
|
+
this.creds = creds;
|
|
88
|
+
if (!creds.walletAddress || !creds.agentPrivateKey) {
|
|
89
|
+
throw new Error('HyperliquidPrivateApi requires walletAddress + agentPrivateKey');
|
|
90
|
+
}
|
|
91
|
+
this.exchange = new ccxtCjs.hyperliquid({
|
|
92
|
+
walletAddress: creds.walletAddress,
|
|
93
|
+
privateKey: creds.agentPrivateKey,
|
|
94
|
+
options: { ...HL_REQUIRED_OPTIONS },
|
|
95
|
+
enableRateLimit: true,
|
|
96
|
+
});
|
|
97
|
+
if (creds.testnet) {
|
|
98
|
+
this.exchange.setSandboxMode(true);
|
|
99
|
+
const url = JSON.stringify(this.exchange.urls?.api ?? '');
|
|
100
|
+
if (!url.includes('testnet')) {
|
|
101
|
+
throw new Error(`HL testnet requested but sandbox URL is not testnet: ${url.slice(0, 80)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
logger.info(TAG, `Hyperliquid private API initialized (${creds.testnet ? 'TESTNET' : 'MAINNET'}, master ${creds.walletAddress.slice(0, 8)}…)`);
|
|
105
|
+
}
|
|
106
|
+
/** Exposed for the options-pin test + diagnostics. */
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
getExchange() {
|
|
109
|
+
return this.exchange;
|
|
110
|
+
}
|
|
111
|
+
async loadMarkets() {
|
|
112
|
+
try {
|
|
113
|
+
assertNotLimited('meta');
|
|
114
|
+
await this.exchange.loadMarkets();
|
|
115
|
+
noteSuccess('meta');
|
|
116
|
+
this.marketsLoaded = true;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
noteError(err, 'loadMarkets');
|
|
121
|
+
logger.error(TAG, `loadMarkets failed: ${msg(err)}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---- Reads (null on failure — NEVER []) ----
|
|
126
|
+
/** Positions for the MASTER account. `null` = fetch failed (state unknown);
|
|
127
|
+
* `[]` = the exchange confirmed flat. */
|
|
128
|
+
async fetchPositions(symbol) {
|
|
129
|
+
try {
|
|
130
|
+
assertNotLimited('clearinghouseState');
|
|
131
|
+
const symbols = symbol ? [toCcxtSymbol('hyperliquid', symbol)] : undefined;
|
|
132
|
+
const raw = await this.exchange.fetchPositions(symbols);
|
|
133
|
+
noteSuccess('clearinghouseState');
|
|
134
|
+
if (!Array.isArray(raw))
|
|
135
|
+
return null;
|
|
136
|
+
return raw.filter((p) => Math.abs(Number(p.contracts ?? 0)) > 0);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
noteError(err, 'fetchPositions');
|
|
140
|
+
logger.error(TAG, `fetchPositions failed: ${msg(err)}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Open orders INCLUDING trigger/TPSL legs. CCXT's HL `fetchOpenOrders`
|
|
145
|
+
* defaults to `frontendOpenOrders`, which is the only endpoint that returns
|
|
146
|
+
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints. */
|
|
147
|
+
async fetchOpenOrders(symbol) {
|
|
148
|
+
try {
|
|
149
|
+
assertNotLimited('frontendOpenOrders');
|
|
150
|
+
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
151
|
+
const raw = await this.exchange.fetchOpenOrders(s);
|
|
152
|
+
noteSuccess('frontendOpenOrders');
|
|
153
|
+
return Array.isArray(raw) ? raw : null;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
noteError(err, 'fetchOpenOrders');
|
|
157
|
+
logger.error(TAG, `fetchOpenOrders failed: ${msg(err)}`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async fetchBalance() {
|
|
162
|
+
try {
|
|
163
|
+
assertNotLimited('clearinghouseState');
|
|
164
|
+
const raw = await this.exchange.fetchBalance();
|
|
165
|
+
noteSuccess('clearinghouseState');
|
|
166
|
+
return raw ?? null;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
noteError(err, 'fetchBalance');
|
|
170
|
+
logger.error(TAG, `fetchBalance failed: ${msg(err)}`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Per-order status — the liveness resolver's REST tier (Tier 2 of the
|
|
175
|
+
* 3-tier rule). Weight 2. `null` = lookup FAILED (unknown), which callers
|
|
176
|
+
* must treat as "do not act", NOT as "gone". */
|
|
177
|
+
async fetchOrder(orderId, symbol) {
|
|
178
|
+
try {
|
|
179
|
+
assertNotLimited('orderStatus');
|
|
180
|
+
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
181
|
+
const raw = await this.exchange.fetchOrder(orderId, s);
|
|
182
|
+
noteSuccess('orderStatus');
|
|
183
|
+
return raw ?? null;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
noteError(err, 'fetchOrder');
|
|
187
|
+
logger.warn(TAG, `fetchOrder(${orderId}) failed: ${msg(err)}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Own fills. WS is the authoritative ingress (plan + the audit-trail rule);
|
|
192
|
+
* this is the gap-fill/truth-check path. NOTE: only the 10,000 most recent
|
|
193
|
+
* fills exist server-side — deep history is NOT queryable on HL, which is why
|
|
194
|
+
* the `trades` table must be WS-first. */
|
|
195
|
+
async fetchMyTrades(symbol, since, limit = 100) {
|
|
196
|
+
try {
|
|
197
|
+
assertNotLimited('userFills');
|
|
198
|
+
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
199
|
+
const raw = await this.exchange.fetchMyTrades(s, since, limit);
|
|
200
|
+
noteSuccess('userFills');
|
|
201
|
+
return Array.isArray(raw) ? raw : null;
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
noteError(err, 'fetchMyTrades');
|
|
205
|
+
logger.error(TAG, `fetchMyTrades failed: ${msg(err)}`);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Refresh the ADDRESS action budget (the starvation guard). Weight 20 — call
|
|
210
|
+
* every ~5 min, never per-heartbeat. */
|
|
211
|
+
async refreshAddressBudget() {
|
|
212
|
+
try {
|
|
213
|
+
assertNotLimited('userRateLimit');
|
|
214
|
+
const res = await this.rawInfo({ type: 'userRateLimit', user: this.creds.walletAddress });
|
|
215
|
+
noteSuccess('userRateLimit');
|
|
216
|
+
const used = Number(res?.nRequestsUsed);
|
|
217
|
+
const cap = Number(res?.nRequestsCap);
|
|
218
|
+
if (Number.isFinite(used) && Number.isFinite(cap)) {
|
|
219
|
+
updateAddressBudget({ nRequestsUsed: used, nRequestsCap: cap });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
noteError(err, 'userRateLimit');
|
|
224
|
+
logger.warn(TAG, `refreshAddressBudget failed: ${msg(err)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Raw `POST /info` — for the handful of reads CCXT doesn't expose
|
|
228
|
+
* (userRateLimit, userNonFundingLedgerUpdates, portfolio). Never used for
|
|
229
|
+
* signed actions. */
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
231
|
+
async rawInfo(body) {
|
|
232
|
+
const base = this.creds.testnet
|
|
233
|
+
? 'https://api.hyperliquid-testnet.xyz'
|
|
234
|
+
: 'https://api.hyperliquid.xyz';
|
|
235
|
+
const res = await fetch(`${base}/info`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'content-type': 'application/json' },
|
|
238
|
+
body: JSON.stringify(body),
|
|
239
|
+
signal: AbortSignal.timeout(15_000),
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
throw new Error(`POST /info ${String(body.type)} → ${res.status}`);
|
|
243
|
+
return res.json();
|
|
244
|
+
}
|
|
245
|
+
// ---- Signed actions (NEVER pre-gated; always nonce-serialized) ----
|
|
246
|
+
/** Submit one order. Market orders are IOC-limit emulated — `price` is
|
|
247
|
+
* MANDATORY (ccxt throws without it) and the caller must already have applied
|
|
248
|
+
* the slippage bound + HL rounding (hl-precision). */
|
|
249
|
+
async submitOrder(req) {
|
|
250
|
+
if (req.cloid && !isValidHlCloid(req.cloid)) {
|
|
251
|
+
throw new Error(`Invalid HL cloid ${req.cloid} — must be 0x + 32 hex chars`);
|
|
252
|
+
}
|
|
253
|
+
return this.mutex.run(async () => {
|
|
254
|
+
try {
|
|
255
|
+
const params = this.buildParams(req);
|
|
256
|
+
const order = await this.exchange.createOrder(toCcxtSymbol('hyperliquid', req.symbol), req.type === 'market' ? 'market' : 'limit', req.side, req.amount, req.price, // ★ always passed — ccxt needs it even for 'market'
|
|
257
|
+
params);
|
|
258
|
+
noteSuccess('exchange', { ipWeight: exchangeIpWeight(1), addressActions: 1 });
|
|
259
|
+
return order ?? null;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
noteError(err, 'submitOrder');
|
|
263
|
+
// Order paths RETHROW (the caller must see a rejection) — unlike reads,
|
|
264
|
+
// which collapse to null.
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Submit N orders as ONE signed action (one nonce, one `/exchange` call).
|
|
270
|
+
* This is how a bracket pair (SL+TP) is placed: atomic-ish, and it costs 1 IP
|
|
271
|
+
* weight unit but N address actions. */
|
|
272
|
+
async submitOrders(reqs) {
|
|
273
|
+
if (reqs.length === 0)
|
|
274
|
+
return [];
|
|
275
|
+
for (const r of reqs) {
|
|
276
|
+
if (r.cloid && !isValidHlCloid(r.cloid)) {
|
|
277
|
+
throw new Error(`Invalid HL cloid ${r.cloid} — must be 0x + 32 hex chars`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return this.mutex.run(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const ccxtReqs = reqs.map((r) => ({
|
|
283
|
+
symbol: toCcxtSymbol('hyperliquid', r.symbol),
|
|
284
|
+
type: r.type === 'market' ? 'market' : 'limit',
|
|
285
|
+
side: r.side,
|
|
286
|
+
amount: r.amount,
|
|
287
|
+
price: r.price,
|
|
288
|
+
params: this.buildParams(r),
|
|
289
|
+
}));
|
|
290
|
+
const orders = await this.exchange.createOrders(ccxtReqs);
|
|
291
|
+
noteSuccess('exchange', {
|
|
292
|
+
ipWeight: exchangeIpWeight(reqs.length),
|
|
293
|
+
addressActions: reqs.length, // ★ batches cost n against the ADDRESS budget
|
|
294
|
+
});
|
|
295
|
+
return Array.isArray(orders) ? orders : null;
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
noteError(err, 'submitOrders');
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async cancelOrder(orderId, symbol) {
|
|
304
|
+
return this.mutex.run(async () => {
|
|
305
|
+
try {
|
|
306
|
+
const res = await this.exchange.cancelOrder(orderId, toCcxtSymbol('hyperliquid', symbol));
|
|
307
|
+
noteSuccess('exchange', { ipWeight: exchangeIpWeight(1), addressActions: 1 });
|
|
308
|
+
return res ?? null;
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
noteError(err, 'cancelOrder');
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** Cancel by cloid — the bracket path's cancel (we always know our own cloid,
|
|
317
|
+
* and it survives a restart because it is derived, not stored). */
|
|
318
|
+
async cancelOrderByCloid(cloid, symbol) {
|
|
319
|
+
return this.mutex.run(async () => {
|
|
320
|
+
try {
|
|
321
|
+
const res = await this.exchange.cancelOrder(cloid, toCcxtSymbol('hyperliquid', symbol), { clientOrderId: cloid });
|
|
322
|
+
noteSuccess('exchange', { ipWeight: exchangeIpWeight(1), addressActions: 1 });
|
|
323
|
+
return res ?? null;
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
noteError(err, 'cancelOrderByCloid');
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
332
|
+
buildParams(req) {
|
|
333
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
334
|
+
const params = {};
|
|
335
|
+
if (req.reduceOnly)
|
|
336
|
+
params.reduceOnly = true;
|
|
337
|
+
if (req.postOnly)
|
|
338
|
+
params.postOnly = true; // → ALO
|
|
339
|
+
if (req.cloid)
|
|
340
|
+
params.clientOrderId = req.cloid;
|
|
341
|
+
if (req.trigger) {
|
|
342
|
+
// HL trigger orders fire on MARK price only (no workingType choice).
|
|
343
|
+
params.triggerPrice = req.trigger.triggerPrice;
|
|
344
|
+
if (req.trigger.tpsl === 'tp')
|
|
345
|
+
params.takeProfitPrice = req.trigger.triggerPrice;
|
|
346
|
+
else
|
|
347
|
+
params.stopLossPrice = req.trigger.triggerPrice;
|
|
348
|
+
}
|
|
349
|
+
return params;
|
|
350
|
+
}
|
|
351
|
+
get isMarketsLoaded() {
|
|
352
|
+
return this.marketsLoaded;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function msg(err) {
|
|
356
|
+
return err instanceof Error ? err.message : String(err);
|
|
357
|
+
}
|
|
@@ -15,6 +15,9 @@ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
|
|
|
15
15
|
/** One upstream fetchTickers call serves every symbol within the TTL. */
|
|
16
16
|
private tickersCache;
|
|
17
17
|
private tickersInflight;
|
|
18
|
+
/** Coin → mid from one weight-2 allMids call (fetchTicker's price source). */
|
|
19
|
+
private midsCache;
|
|
20
|
+
private midsInflight;
|
|
18
21
|
constructor(opts?: HyperliquidPublicApiOptions);
|
|
19
22
|
private baseUrl;
|
|
20
23
|
/** Canonical/ccxt symbol → this venue's ccxt symbol, or null (logged) when
|
|
@@ -24,11 +27,24 @@ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
|
|
|
24
27
|
/** Fetch-all-tickers with a short TTL + inflight dedup. Returns a map keyed
|
|
25
28
|
* by ccxt symbol, or null on failure. */
|
|
26
29
|
private getTickers;
|
|
30
|
+
/** Coin → mid price from one weight-2 `allMids` call (TTL-cached, inflight-
|
|
31
|
+
* deduped; docs: hyperliquid.gitbook.io → Info endpoint → "Retrieve mids
|
|
32
|
+
* for all coins"). Null on ANY failure — callers fall back to the full
|
|
33
|
+
* snapshot path. Spot entries (`@<idx>` keys) parse fine and are simply
|
|
34
|
+
* never looked up (we key by perp coin name). */
|
|
35
|
+
private getMids;
|
|
36
|
+
/** Fire-and-forget full-snapshot refresh once it ages past
|
|
37
|
+
* FULL_SNAPSHOT_REFRESH_MS — fetchTicker must never block on the heavy
|
|
38
|
+
* fetchTickers call when a fresh mid is available. getTickers' own
|
|
39
|
+
* inflight dedup + error handling make this safe to kick repeatedly. */
|
|
40
|
+
private maybeRefreshSnapshot;
|
|
27
41
|
fetchTickerRaw(symbol: string): Promise<Record<string, any> | null>;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
42
|
+
/** Price from the weight-2 allMids fast path; volume/change context from
|
|
43
|
+
* the last full snapshot (background-refreshed). Hyperliquid's asset
|
|
44
|
+
* contexts carry mark/mid rather than a trade-tape bid/ask; absent fields
|
|
45
|
+
* fall back to `last` with zero modeled spread — the paper fill engine
|
|
46
|
+
* models slippage itself (same convention as IntelPublicApi). Falls back
|
|
47
|
+
* to the original blocking full-snapshot path when allMids fails. */
|
|
32
48
|
fetchTicker(symbol: string): Promise<CcxtTicker | null>;
|
|
33
49
|
fetchFundingRate(symbol: string): Promise<Record<string, any> | null>;
|
|
34
50
|
fetchOpenInterest(symbol: string): Promise<Record<string, any> | null>;
|
|
@@ -49,4 +65,15 @@ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
|
|
|
49
65
|
outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
|
|
50
66
|
driftMs: number | null;
|
|
51
67
|
}>;
|
|
68
|
+
/** `meta` — the asset universe (szDecimals, maxLeverage, positional assetIndex).
|
|
69
|
+
* Keyless info read, weight 20. Feeds HyperliquidInfoCache. Returns null on any
|
|
70
|
+
* failure (never a partial universe — a half-loaded rules table would silently
|
|
71
|
+
* reject orders). */
|
|
72
|
+
fetchMeta(): Promise<any | null>;
|
|
73
|
+
/** ★ MARK price — the price HL trigger orders fire on (there is no workingType
|
|
74
|
+
* choice on this venue). Stop-distance validation and every trigger MUST use
|
|
75
|
+
* this, never `last`: the mark is a CEX-composite median that can diverge from
|
|
76
|
+
* HL's own last trade. Sourced from `metaAndAssetCtxs` (one weight-20 call
|
|
77
|
+
* covers every asset). Returns null on failure — never a stale guess. */
|
|
78
|
+
fetchMarkPrice(symbol: string): Promise<number | null>;
|
|
52
79
|
}
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
// api.hyperliquid-testnet.xyz). CCXT@4.5.37 wraps everything we need.
|
|
10
10
|
// - `exchangeStatus` (weight 2) returns `{specialStatuses, time}` — the
|
|
11
11
|
// reachability probe AND a clock-drift source in one call.
|
|
12
|
-
// -
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
12
|
+
// - `allMids` (weight 2) returns the mid price for EVERY coin in one call —
|
|
13
|
+
// the docs' recommended price read (plan §3: ccxt fetchTicker is
|
|
14
|
+
// 'emulated' — prefer allMids). fetchTicker serves its PRICE from one
|
|
15
|
+
// TTL-cached allMids call; volume/change context rides the last full
|
|
16
|
+
// fetchTickers snapshot, refreshed in the background at most every
|
|
17
|
+
// FULL_SNAPSHOT_REFRESH_MS. Why the split: ccxt@4.5.37's hyperliquid
|
|
18
|
+
// fetchTickers() literally calls fetchMarkets() — the full perp+spot
|
|
19
|
+
// meta/ctx pipeline — measured ~13s on the first HL soak box, which
|
|
20
|
+
// starved every 10s-timeout skill-poller call 100% of the time. allMids
|
|
21
|
+
// measured ~1.4s on the same box. The full address/IP rate gate is a
|
|
22
|
+
// Phase 3 concern (live cadences); paper-mode cadence here is bounded by
|
|
23
|
+
// the caches: ≤ (60s/TTL) weight-2 calls/min + ≤ weight-20 every 5 min.
|
|
18
24
|
// - CCXT auto-monetization landmine (plan §3.8): initializeClient() only
|
|
19
25
|
// fires on AUTHENTICATED clients, and this class is keyless — but we pin
|
|
20
26
|
// `builderFee:false, refSet:true` in options anyway so a future
|
|
@@ -25,8 +31,8 @@
|
|
|
25
31
|
// never throw to consumers. Binance-shaped symbols (…/USDT) are a caller bug
|
|
26
32
|
// on this venue — logged clearly, null returned (never silently translated).
|
|
27
33
|
import { createRequire } from 'node:module';
|
|
28
|
-
import { toCcxtSymbol } from '@reefclaw/shared';
|
|
29
34
|
import { logger } from '../../logger.js';
|
|
35
|
+
import { toCcxtSymbol, toHyperliquidCoin } from '../symbols.js';
|
|
30
36
|
const TAG = 'hl-public';
|
|
31
37
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
32
38
|
// (same pattern as binance-public.ts / binance-private.ts).
|
|
@@ -44,6 +50,11 @@ catch {
|
|
|
44
50
|
const HL_MAINNET_API = 'https://api.hyperliquid.xyz';
|
|
45
51
|
const HL_TESTNET_API = 'https://api.hyperliquid-testnet.xyz';
|
|
46
52
|
const DEFAULT_TICKER_TTL_MS = 4_000;
|
|
53
|
+
/** How stale the full fetchTickers snapshot (volume/change context) may get
|
|
54
|
+
* before fetchTicker kicks a background refresh. Deliberately long: the
|
|
55
|
+
* snapshot's ccxt call re-runs the whole fetchMarkets pipeline (weight ~60,
|
|
56
|
+
* ~13s on a slow host) and nothing price-critical reads these fields. */
|
|
57
|
+
const FULL_SNAPSHOT_REFRESH_MS = 300_000;
|
|
47
58
|
function resolveTickerTtlMs() {
|
|
48
59
|
const raw = Number(process.env.RC_HL_TICKER_TTL_MS);
|
|
49
60
|
if (!Number.isFinite(raw) || raw < 500)
|
|
@@ -59,6 +70,9 @@ export class HyperliquidPublicApi {
|
|
|
59
70
|
/** One upstream fetchTickers call serves every symbol within the TTL. */
|
|
60
71
|
tickersCache = null;
|
|
61
72
|
tickersInflight = null;
|
|
73
|
+
/** Coin → mid from one weight-2 allMids call (fetchTicker's price source). */
|
|
74
|
+
midsCache = null;
|
|
75
|
+
midsInflight = null;
|
|
62
76
|
constructor(opts = {}) {
|
|
63
77
|
this.testnet = opts.testnet === true;
|
|
64
78
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
@@ -127,6 +141,56 @@ export class HyperliquidPublicApi {
|
|
|
127
141
|
})();
|
|
128
142
|
return this.tickersInflight;
|
|
129
143
|
}
|
|
144
|
+
/** Coin → mid price from one weight-2 `allMids` call (TTL-cached, inflight-
|
|
145
|
+
* deduped; docs: hyperliquid.gitbook.io → Info endpoint → "Retrieve mids
|
|
146
|
+
* for all coins"). Null on ANY failure — callers fall back to the full
|
|
147
|
+
* snapshot path. Spot entries (`@<idx>` keys) parse fine and are simply
|
|
148
|
+
* never looked up (we key by perp coin name). */
|
|
149
|
+
async getMids() {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (this.midsCache && now - this.midsCache.at < this.tickerTtlMs) {
|
|
152
|
+
return this.midsCache.byCoin;
|
|
153
|
+
}
|
|
154
|
+
if (this.midsInflight)
|
|
155
|
+
return this.midsInflight;
|
|
156
|
+
this.midsInflight = (async () => {
|
|
157
|
+
try {
|
|
158
|
+
const raw = await this.exchange.publicPostInfo({ type: 'allMids' });
|
|
159
|
+
const byCoin = new Map();
|
|
160
|
+
for (const [coin, mid] of Object.entries(raw ?? {})) {
|
|
161
|
+
const n = Number(mid);
|
|
162
|
+
if (Number.isFinite(n) && n > 0)
|
|
163
|
+
byCoin.set(coin, n);
|
|
164
|
+
}
|
|
165
|
+
if (byCoin.size === 0) {
|
|
166
|
+
logger.warn(TAG, 'allMids returned no parseable mids');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
this.midsCache = { at: Date.now(), byCoin };
|
|
170
|
+
return byCoin;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
logger.error(TAG, `allMids failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.midsInflight = null;
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
return this.midsInflight;
|
|
181
|
+
}
|
|
182
|
+
/** Fire-and-forget full-snapshot refresh once it ages past
|
|
183
|
+
* FULL_SNAPSHOT_REFRESH_MS — fetchTicker must never block on the heavy
|
|
184
|
+
* fetchTickers call when a fresh mid is available. getTickers' own
|
|
185
|
+
* inflight dedup + error handling make this safe to kick repeatedly. */
|
|
186
|
+
maybeRefreshSnapshot() {
|
|
187
|
+
const at = this.tickersCache?.at ?? 0;
|
|
188
|
+
if (Date.now() - at < FULL_SNAPSHOT_REFRESH_MS)
|
|
189
|
+
return;
|
|
190
|
+
if (this.tickersInflight)
|
|
191
|
+
return;
|
|
192
|
+
void this.getTickers();
|
|
193
|
+
}
|
|
130
194
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
195
|
async fetchTickerRaw(symbol) {
|
|
132
196
|
const vs = this.venueSymbol(symbol, 'fetchTickerRaw');
|
|
@@ -135,14 +199,36 @@ export class HyperliquidPublicApi {
|
|
|
135
199
|
const tickers = await this.getTickers();
|
|
136
200
|
return tickers?.get(vs) ?? null;
|
|
137
201
|
}
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
202
|
+
/** Price from the weight-2 allMids fast path; volume/change context from
|
|
203
|
+
* the last full snapshot (background-refreshed). Hyperliquid's asset
|
|
204
|
+
* contexts carry mark/mid rather than a trade-tape bid/ask; absent fields
|
|
205
|
+
* fall back to `last` with zero modeled spread — the paper fill engine
|
|
206
|
+
* models slippage itself (same convention as IntelPublicApi). Falls back
|
|
207
|
+
* to the original blocking full-snapshot path when allMids fails. */
|
|
142
208
|
async fetchTicker(symbol) {
|
|
143
209
|
const vs = this.venueSymbol(symbol, 'fetchTicker');
|
|
144
210
|
if (!vs)
|
|
145
211
|
return null;
|
|
212
|
+
const coin = toHyperliquidCoin(symbol);
|
|
213
|
+
const mids = await this.getMids();
|
|
214
|
+
const mid = mids?.get(coin);
|
|
215
|
+
if (mid !== undefined) {
|
|
216
|
+
this.maybeRefreshSnapshot();
|
|
217
|
+
const base = this.tickersCache?.bySymbol.get(vs);
|
|
218
|
+
const timestamp = this.midsCache?.at ?? Date.now();
|
|
219
|
+
return {
|
|
220
|
+
symbol,
|
|
221
|
+
last: mid,
|
|
222
|
+
bid: mid,
|
|
223
|
+
ask: mid,
|
|
224
|
+
baseVolume: Number(base?.baseVolume ?? 0) || 0,
|
|
225
|
+
quoteVolume: Number(base?.quoteVolume ?? 0) || 0,
|
|
226
|
+
change: Number(base?.change ?? 0) || 0,
|
|
227
|
+
percentage: Number(base?.percentage ?? 0) || 0,
|
|
228
|
+
timestamp,
|
|
229
|
+
datetime: new Date(timestamp).toISOString(),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
146
232
|
const tickers = await this.getTickers();
|
|
147
233
|
const raw = tickers?.get(vs);
|
|
148
234
|
if (!raw) {
|
|
@@ -155,7 +241,14 @@ export class HyperliquidPublicApi {
|
|
|
155
241
|
logger.warn(TAG, `fetchTicker(${symbol}) — no usable price on ticker`);
|
|
156
242
|
return null;
|
|
157
243
|
}
|
|
158
|
-
|
|
244
|
+
// Timestamp honesty (issue #202): this fallback serves the background-
|
|
245
|
+
// refreshed FULL SNAPSHOT price, which can be minutes old. When the raw
|
|
246
|
+
// ticker carries no timestamp, stamp the snapshot's fetch time — never
|
|
247
|
+
// Date.now(), which would disguise a stale price as fresh and defeat the
|
|
248
|
+
// paper engine's stale-quote fill guard.
|
|
249
|
+
const timestamp = Number.isFinite(raw.timestamp)
|
|
250
|
+
? Number(raw.timestamp)
|
|
251
|
+
: this.tickersCache?.at ?? Date.now();
|
|
159
252
|
return {
|
|
160
253
|
// Key on the symbol the CALLER used so per-symbol maps line up.
|
|
161
254
|
symbol,
|
|
@@ -282,4 +375,62 @@ export class HyperliquidPublicApi {
|
|
|
282
375
|
clearTimeout(tid);
|
|
283
376
|
}
|
|
284
377
|
}
|
|
378
|
+
/** `meta` — the asset universe (szDecimals, maxLeverage, positional assetIndex).
|
|
379
|
+
* Keyless info read, weight 20. Feeds HyperliquidInfoCache. Returns null on any
|
|
380
|
+
* failure (never a partial universe — a half-loaded rules table would silently
|
|
381
|
+
* reject orders). */
|
|
382
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
383
|
+
async fetchMeta() {
|
|
384
|
+
try {
|
|
385
|
+
const res = await this.fetchImpl(`${this.baseUrl()}/info`, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { 'content-type': 'application/json' },
|
|
388
|
+
body: JSON.stringify({ type: 'meta' }),
|
|
389
|
+
signal: AbortSignal.timeout(15_000),
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok) {
|
|
392
|
+
logger.warn(TAG, `fetchMeta: HTTP ${res.status}`);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
return await res.json();
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
logger.warn(TAG, `fetchMeta failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/** ★ MARK price — the price HL trigger orders fire on (there is no workingType
|
|
403
|
+
* choice on this venue). Stop-distance validation and every trigger MUST use
|
|
404
|
+
* this, never `last`: the mark is a CEX-composite median that can diverge from
|
|
405
|
+
* HL's own last trade. Sourced from `metaAndAssetCtxs` (one weight-20 call
|
|
406
|
+
* covers every asset). Returns null on failure — never a stale guess. */
|
|
407
|
+
async fetchMarkPrice(symbol) {
|
|
408
|
+
try {
|
|
409
|
+
const res = await this.fetchImpl(`${this.baseUrl()}/info`, {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: { 'content-type': 'application/json' },
|
|
412
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
|
|
413
|
+
signal: AbortSignal.timeout(15_000),
|
|
414
|
+
});
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
logger.warn(TAG, `fetchMarkPrice: HTTP ${res.status}`);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const body = (await res.json());
|
|
420
|
+
const universe = body?.[0]?.universe ?? [];
|
|
421
|
+
const ctxs = body?.[1] ?? [];
|
|
422
|
+
const coin = toHyperliquidCoin(symbol);
|
|
423
|
+
const idx = universe.findIndex((u) => u?.name === coin);
|
|
424
|
+
if (idx < 0) {
|
|
425
|
+
logger.warn(TAG, `fetchMarkPrice: ${coin} not in meta.universe`);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const mark = Number(ctxs[idx]?.markPx);
|
|
429
|
+
return Number.isFinite(mark) && mark > 0 ? mark : null;
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
logger.warn(TAG, `fetchMarkPrice failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
285
436
|
}
|