perp-cli 0.4.0 → 0.4.8
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/README.md +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +12 -9
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +885 -67
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +118 -43
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +181 -92
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +132 -37
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +148 -99
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +145 -0
- package/dist/trade-validator.js +2 -1
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +102 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { Hyperliquid } from "hyperliquid";
|
|
2
|
+
import { LocalEvmSigner } from "../signer/evm-local.js";
|
|
2
3
|
export class HyperliquidAdapter {
|
|
3
4
|
name = "hyperliquid";
|
|
4
5
|
sdk;
|
|
5
6
|
_address;
|
|
6
7
|
_privateKey;
|
|
7
8
|
_testnet;
|
|
9
|
+
_evmSigner;
|
|
8
10
|
_assetMap = new Map();
|
|
9
11
|
_assetMapReverse = new Map();
|
|
12
|
+
_szDecimalsMap = new Map(); // symbol → szDecimals
|
|
10
13
|
/** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
|
|
11
14
|
_dex = "";
|
|
12
15
|
// In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
|
|
13
16
|
constructor(privateKey, testnet = false) {
|
|
17
|
+
// Disable WebSocket in SDK — we use REST for all info calls and raw
|
|
18
|
+
// fetch for /exchange. This avoids the false "Please install ws" warning
|
|
19
|
+
// caused by the SDK's require('ws') failing in ESM context.
|
|
14
20
|
this.sdk = new Hyperliquid({
|
|
15
21
|
privateKey,
|
|
16
22
|
testnet,
|
|
17
23
|
walletAddress: undefined,
|
|
24
|
+
enableWs: false,
|
|
18
25
|
});
|
|
19
26
|
this._address = "";
|
|
20
|
-
this._privateKey = privateKey;
|
|
27
|
+
this._privateKey = privateKey ?? "";
|
|
21
28
|
this._testnet = testnet;
|
|
22
29
|
}
|
|
23
30
|
/** Set the HIP-3 deployed perp dex to query/trade on. */
|
|
@@ -39,6 +46,10 @@ export class HyperliquidAdapter {
|
|
|
39
46
|
get isTestnet() {
|
|
40
47
|
return this._testnet;
|
|
41
48
|
}
|
|
49
|
+
/** Inject an external EVM signer. Call before init() to skip LocalEvmSigner creation. */
|
|
50
|
+
setSigner(signer) {
|
|
51
|
+
this._evmSigner = signer;
|
|
52
|
+
}
|
|
42
53
|
async init() {
|
|
43
54
|
// Suppress "WebSocket connected" noise from hyperliquid SDK
|
|
44
55
|
const origLog = console.log;
|
|
@@ -49,14 +60,10 @@ export class HyperliquidAdapter {
|
|
|
49
60
|
finally {
|
|
50
61
|
console.log = origLog;
|
|
51
62
|
}
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this._address =
|
|
56
|
-
}
|
|
57
|
-
if (!this._address) {
|
|
58
|
-
const { ethers } = await import("ethers");
|
|
59
|
-
this._address = new ethers.Wallet(this._privateKey).address;
|
|
63
|
+
// Initialize EVM signer if not externally injected (skip if no key — read-only mode)
|
|
64
|
+
if (!this._evmSigner && this._privateKey) {
|
|
65
|
+
this._evmSigner = await LocalEvmSigner.create(this._privateKey);
|
|
66
|
+
this._address = this._evmSigner.getAddress();
|
|
60
67
|
}
|
|
61
68
|
// Build asset index map
|
|
62
69
|
await this._loadAssetMap();
|
|
@@ -85,12 +92,15 @@ export class HyperliquidAdapter {
|
|
|
85
92
|
meta.universe.forEach((asset, idx) => {
|
|
86
93
|
this._assetMap.set(asset.name, idx);
|
|
87
94
|
this._assetMapReverse.set(idx, asset.name);
|
|
95
|
+
if (asset.szDecimals !== undefined) {
|
|
96
|
+
this._szDecimalsMap.set(asset.name, asset.szDecimals);
|
|
97
|
+
}
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
catch {
|
|
93
|
-
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error("[hyperliquid] Failed to load asset map:", e instanceof Error ? e.message : e);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
/**
|
|
@@ -115,7 +125,24 @@ export class HyperliquidAdapter {
|
|
|
115
125
|
}
|
|
116
126
|
return sym; // return as-is, let downstream error
|
|
117
127
|
}
|
|
118
|
-
|
|
128
|
+
/** Get szDecimals for a symbol (cached from init/getMeta) */
|
|
129
|
+
getSzDecimals(symbol) {
|
|
130
|
+
const resolved = this.resolveSymbol(symbol.toUpperCase());
|
|
131
|
+
return this._szDecimalsMap.get(resolved) ?? 2; // 2 is safe middle ground
|
|
132
|
+
}
|
|
133
|
+
/** HL official price rounding: 5 significant figures */
|
|
134
|
+
_hlRoundPrice(px, szDecimals, maxDecimals = 6) {
|
|
135
|
+
if (px > 100_000)
|
|
136
|
+
return String(Math.round(px));
|
|
137
|
+
const sig5 = Number(px.toPrecision(5));
|
|
138
|
+
const priceDec = Math.max(0, maxDecimals - szDecimals);
|
|
139
|
+
return Number(sig5.toFixed(priceDec)).toString();
|
|
140
|
+
}
|
|
141
|
+
async getAssetIndex(symbol) {
|
|
142
|
+
// Retry once if asset map is empty (e.g. init failed silently)
|
|
143
|
+
if (this._assetMap.size === 0) {
|
|
144
|
+
await this._loadAssetMap();
|
|
145
|
+
}
|
|
119
146
|
const resolved = this.resolveSymbol(symbol);
|
|
120
147
|
const idx = this._assetMap.get(resolved);
|
|
121
148
|
if (idx !== undefined)
|
|
@@ -177,8 +204,14 @@ export class HyperliquidAdapter {
|
|
|
177
204
|
]),
|
|
178
205
|
};
|
|
179
206
|
}
|
|
207
|
+
ensureAddress() {
|
|
208
|
+
if (!this._address) {
|
|
209
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
180
212
|
/** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
|
|
181
213
|
async _getClearinghouseState() {
|
|
214
|
+
this.ensureAddress();
|
|
182
215
|
const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
183
216
|
const key = this._dex ? `acct:hl:chs:${this._address}:${this._dex}` : `acct:hl:chs:${this._address}`;
|
|
184
217
|
return fetchAndCache(key, TTL_ACCOUNT, async () => {
|
|
@@ -210,10 +243,10 @@ export class HyperliquidAdapter {
|
|
|
210
243
|
const spotState = await this.sdk.info.spot.getSpotClearinghouseState(this._address);
|
|
211
244
|
const balances = spotState?.balances ?? [];
|
|
212
245
|
const usdc = balances.find((b) => String(b.coin).startsWith("USDC"));
|
|
213
|
-
const spotTotal = Number(usdc
|
|
246
|
+
const spotTotal = usdc?.total !== undefined ? Number(usdc.total) : NaN;
|
|
214
247
|
const spotHold = Number(usdc?.hold ?? 0);
|
|
215
|
-
equity = spotTotal
|
|
216
|
-
available = spotTotal
|
|
248
|
+
equity = !isNaN(spotTotal) ? spotTotal : Number(margin.accountValue ?? cross.accountValue ?? 0);
|
|
249
|
+
available = !isNaN(spotTotal) ? spotTotal - spotHold : Number(s?.withdrawable ?? 0);
|
|
217
250
|
}
|
|
218
251
|
catch {
|
|
219
252
|
// Spot API failed — fall back to perp-only values
|
|
@@ -249,7 +282,7 @@ export class HyperliquidAdapter {
|
|
|
249
282
|
side: szi > 0 ? "long" : "short",
|
|
250
283
|
size: String(Math.abs(szi)),
|
|
251
284
|
entryPrice: String(pos.entryPx ?? "0"),
|
|
252
|
-
markPrice: String(pos.positionValue ?
|
|
285
|
+
markPrice: String(pos.positionValue ? Number(pos.positionValue) / Math.abs(szi) : "0"),
|
|
253
286
|
liquidationPrice: String(pos.liquidationPx ?? "N/A"),
|
|
254
287
|
unrealizedPnl: String(pos.unrealizedPnl ?? "0"),
|
|
255
288
|
leverage: Number(pos.leverage?.value ?? 1),
|
|
@@ -257,11 +290,13 @@ export class HyperliquidAdapter {
|
|
|
257
290
|
});
|
|
258
291
|
}
|
|
259
292
|
async getOpenOrders() {
|
|
293
|
+
this.ensureAddress();
|
|
260
294
|
const orders = await this.sdk.info.getUserOpenOrders(this._address);
|
|
261
295
|
return (orders ?? []).map((o) => ({
|
|
262
296
|
orderId: String(o.oid ?? ""),
|
|
263
297
|
symbol: String(o.coin ?? ""),
|
|
264
|
-
|
|
298
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
299
|
+
side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : o.side,
|
|
265
300
|
price: String(o.limitPx ?? "0"),
|
|
266
301
|
size: String(o.sz ?? "0"),
|
|
267
302
|
filled: "0",
|
|
@@ -277,6 +312,7 @@ export class HyperliquidAdapter {
|
|
|
277
312
|
catch { /* ignore */ }
|
|
278
313
|
}
|
|
279
314
|
async marketOrder(symbol, side, size) {
|
|
315
|
+
this.ensureSigner();
|
|
280
316
|
if (this._dex) {
|
|
281
317
|
const r = await this._dexMarketOrder(symbol, side, size);
|
|
282
318
|
await this._invalidateAccountCache();
|
|
@@ -300,8 +336,8 @@ export class HyperliquidAdapter {
|
|
|
300
336
|
* and constructs + signs the order action directly.
|
|
301
337
|
*/
|
|
302
338
|
async _dexMarketOrder(symbol, side, size) {
|
|
303
|
-
const assetIndex = this.getAssetIndex(symbol.toUpperCase());
|
|
304
|
-
// Get current mark price from dex meta
|
|
339
|
+
const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
|
|
340
|
+
// Get current mark price + szDecimals from dex meta
|
|
305
341
|
const meta = await this._infoPost({
|
|
306
342
|
type: "metaAndAssetCtxs",
|
|
307
343
|
dex: this._dex,
|
|
@@ -310,57 +346,41 @@ export class HyperliquidAdapter {
|
|
|
310
346
|
const midPrice = Number(ctx?.markPx ?? 0);
|
|
311
347
|
if (midPrice <= 0)
|
|
312
348
|
throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
|
|
313
|
-
|
|
349
|
+
const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
|
|
350
|
+
// HL official rounding: 5 significant figures, max 6 decimals for perps
|
|
351
|
+
const MAX_DECIMALS_PERP = 6;
|
|
314
352
|
const slippage = 0.05;
|
|
315
353
|
const isBuy = side === "buy";
|
|
316
354
|
const slippagePrice = isBuy ? midPrice * (1 + slippage) : midPrice * (1 - slippage);
|
|
317
|
-
const
|
|
318
|
-
|
|
355
|
+
const limitPrice = slippagePrice > 100_000
|
|
356
|
+
? String(Math.round(slippagePrice))
|
|
357
|
+
: Number(Number(slippagePrice.toPrecision(5)).toFixed(Math.max(0, MAX_DECIMALS_PERP - szDec))).toString();
|
|
319
358
|
return this._rawPlaceOrder({
|
|
320
359
|
assetIndex,
|
|
321
360
|
isBuy,
|
|
322
361
|
price: limitPrice,
|
|
323
|
-
size,
|
|
362
|
+
size: Number(size).toFixed(szDec),
|
|
324
363
|
orderType: { limit: { tif: "Ioc" } },
|
|
325
364
|
reduceOnly: false,
|
|
326
365
|
});
|
|
327
366
|
}
|
|
328
367
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
368
|
+
* Sign and send any exchange action via raw EIP-712 signing.
|
|
369
|
+
* Bypasses the SDK entirely — works for order, batchModify, twapOrder, etc.
|
|
331
370
|
*/
|
|
332
|
-
|
|
371
|
+
ensureSigner() {
|
|
372
|
+
if (!this._evmSigner) {
|
|
373
|
+
throw new Error("No private key configured. Run: perp init");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async _signAndSendAction(action) {
|
|
377
|
+
this.ensureSigner();
|
|
333
378
|
const { encode } = await import("@msgpack/msgpack");
|
|
334
379
|
const { ethers, keccak256 } = await import("ethers");
|
|
335
|
-
const wallet = new ethers.Wallet(this._privateKey);
|
|
336
380
|
const isMainnet = !this._testnet;
|
|
337
381
|
const baseUrl = isMainnet
|
|
338
382
|
? "https://api.hyperliquid.xyz"
|
|
339
383
|
: "https://api.hyperliquid-testnet.xyz";
|
|
340
|
-
// Remove trailing zeros (HL API requirement)
|
|
341
|
-
const trimZeros = (s) => {
|
|
342
|
-
if (!s.includes("."))
|
|
343
|
-
return s;
|
|
344
|
-
const n = s.replace(/\.?0+$/, "");
|
|
345
|
-
return n === "-0" ? "0" : n;
|
|
346
|
-
};
|
|
347
|
-
const orderWire = {
|
|
348
|
-
a: opts.assetIndex,
|
|
349
|
-
b: opts.isBuy,
|
|
350
|
-
p: trimZeros(opts.price),
|
|
351
|
-
s: trimZeros(opts.size),
|
|
352
|
-
r: opts.reduceOnly,
|
|
353
|
-
t: opts.orderType,
|
|
354
|
-
};
|
|
355
|
-
const action = {
|
|
356
|
-
type: "order",
|
|
357
|
-
orders: [orderWire],
|
|
358
|
-
grouping: "na",
|
|
359
|
-
};
|
|
360
|
-
// Add dex field for HIP-3 deployed perps
|
|
361
|
-
if (this._dex) {
|
|
362
|
-
action.dex = this._dex;
|
|
363
|
-
}
|
|
364
384
|
// Sign L1 action (replicates SDK's signL1Action)
|
|
365
385
|
const nonce = Date.now();
|
|
366
386
|
const msgPackBytes = encode(action);
|
|
@@ -386,7 +406,7 @@ export class HyperliquidAdapter {
|
|
|
386
406
|
source: isMainnet ? "a" : "b",
|
|
387
407
|
connectionId: hash,
|
|
388
408
|
};
|
|
389
|
-
const sig = await
|
|
409
|
+
const sig = await this._evmSigner.signTypedData(phantomDomain, agentTypes, phantomAgent);
|
|
390
410
|
const parsed = ethers.Signature.from(sig);
|
|
391
411
|
const payload = {
|
|
392
412
|
action,
|
|
@@ -405,45 +425,78 @@ export class HyperliquidAdapter {
|
|
|
405
425
|
}
|
|
406
426
|
return result;
|
|
407
427
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Place an order via raw exchange API with EIP-712 signing.
|
|
430
|
+
* This bypasses the SDK's symbolConversion and supports dex-specific orders.
|
|
431
|
+
*/
|
|
432
|
+
async _rawPlaceOrder(opts) {
|
|
433
|
+
// Remove trailing zeros (HL API requirement)
|
|
434
|
+
const trimZeros = (s) => {
|
|
435
|
+
if (!s.includes("."))
|
|
436
|
+
return s;
|
|
437
|
+
const n = s.replace(/\.?0+$/, "");
|
|
438
|
+
return n === "-0" ? "0" : n;
|
|
439
|
+
};
|
|
440
|
+
const orderWire = {
|
|
441
|
+
a: opts.assetIndex,
|
|
442
|
+
b: opts.isBuy,
|
|
443
|
+
p: trimZeros(opts.price),
|
|
444
|
+
s: trimZeros(opts.size),
|
|
445
|
+
r: opts.reduceOnly,
|
|
446
|
+
t: opts.orderType,
|
|
447
|
+
};
|
|
448
|
+
const action = {
|
|
449
|
+
type: "order",
|
|
450
|
+
orders: [orderWire],
|
|
451
|
+
grouping: "na",
|
|
452
|
+
};
|
|
453
|
+
// Add dex field for HIP-3 deployed perps
|
|
412
454
|
if (this._dex) {
|
|
413
|
-
|
|
414
|
-
assetIndex: this.getAssetIndex(symbol.toUpperCase()),
|
|
415
|
-
isBuy: side === "buy",
|
|
416
|
-
price,
|
|
417
|
-
size,
|
|
418
|
-
orderType: { limit: { tif } },
|
|
419
|
-
reduceOnly,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
result = await this.sdk.exchange.placeOrder({
|
|
424
|
-
coin: symbol.toUpperCase(),
|
|
425
|
-
is_buy: side === "buy",
|
|
426
|
-
sz: parseFloat(size),
|
|
427
|
-
limit_px: parseFloat(price),
|
|
428
|
-
order_type: { limit: { tif } },
|
|
429
|
-
reduce_only: reduceOnly,
|
|
430
|
-
});
|
|
455
|
+
action.dex = this._dex;
|
|
431
456
|
}
|
|
457
|
+
return this._signAndSendAction(action);
|
|
458
|
+
}
|
|
459
|
+
async limitOrder(symbol, side, price, size, opts) {
|
|
460
|
+
// Normalize TIF: accept "IOC"/"GTC"/"ALO" (uppercase) or native "Ioc"/"Gtc"/"Alo"
|
|
461
|
+
const rawTif = opts?.tif ?? "Gtc";
|
|
462
|
+
const tifMap = { IOC: "Ioc", GTC: "Gtc", ALO: "Alo" };
|
|
463
|
+
const tif = (tifMap[rawTif.toUpperCase()] ?? rawTif);
|
|
464
|
+
const reduceOnly = opts?.reduceOnly ?? false;
|
|
465
|
+
// Round price/size to HL-supported decimals
|
|
466
|
+
const szDec = this.getSzDecimals(symbol);
|
|
467
|
+
const roundedPrice = this._hlRoundPrice(Number(price), szDec);
|
|
468
|
+
const roundedSize = Number(size).toFixed(szDec);
|
|
469
|
+
// Use _rawPlaceOrder for both DEX and non-DEX (SDK exchange.placeOrder is unavailable)
|
|
470
|
+
const result = await this._rawPlaceOrder({
|
|
471
|
+
assetIndex: await this.getAssetIndex(symbol.toUpperCase()),
|
|
472
|
+
isBuy: side === "buy",
|
|
473
|
+
price: roundedPrice,
|
|
474
|
+
size: roundedSize,
|
|
475
|
+
orderType: { limit: { tif } },
|
|
476
|
+
reduceOnly,
|
|
477
|
+
});
|
|
432
478
|
await this._invalidateAccountCache();
|
|
433
479
|
return result;
|
|
434
480
|
}
|
|
435
481
|
async cancelOrder(symbol, orderId) {
|
|
482
|
+
this.ensureSigner();
|
|
483
|
+
// HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
|
|
484
|
+
const resolved = this.resolveSymbol(symbol);
|
|
436
485
|
const result = await this.sdk.exchange.cancelOrder({
|
|
437
|
-
coin:
|
|
486
|
+
coin: resolved,
|
|
438
487
|
o: parseInt(orderId),
|
|
439
488
|
});
|
|
440
489
|
await this._invalidateAccountCache();
|
|
441
490
|
return result;
|
|
442
491
|
}
|
|
443
492
|
async cancelAllOrders(symbol) {
|
|
493
|
+
this.ensureSigner();
|
|
444
494
|
const orders = await this.getOpenOrders();
|
|
445
495
|
const toCancel = symbol
|
|
446
|
-
? orders.filter((o) =>
|
|
496
|
+
? orders.filter((o) => {
|
|
497
|
+
const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
|
|
498
|
+
return norm(o.symbol) === norm(symbol);
|
|
499
|
+
})
|
|
447
500
|
: orders;
|
|
448
501
|
const results = [];
|
|
449
502
|
for (const o of toCancel) {
|
|
@@ -456,7 +509,11 @@ export class HyperliquidAdapter {
|
|
|
456
509
|
}
|
|
457
510
|
// ── Interface methods ──
|
|
458
511
|
async editOrder(symbol, orderId, price, size) {
|
|
459
|
-
|
|
512
|
+
// Look up the existing order's side to preserve it (avoid flipping buy↔sell)
|
|
513
|
+
const openOrders = await this.getOpenOrders();
|
|
514
|
+
const existing = openOrders.find(o => o.orderId === orderId);
|
|
515
|
+
const side = existing?.side ?? "buy";
|
|
516
|
+
return this.modifyOrder(symbol, parseInt(orderId), side, price, size);
|
|
460
517
|
}
|
|
461
518
|
async setLeverage(symbol, leverage, marginMode = "cross") {
|
|
462
519
|
return this.updateLeverage(symbol, leverage, marginMode === "cross");
|
|
@@ -493,11 +550,13 @@ export class HyperliquidAdapter {
|
|
|
493
550
|
}));
|
|
494
551
|
}
|
|
495
552
|
async getOrderHistory(limit = 30) {
|
|
553
|
+
this.ensureAddress();
|
|
496
554
|
const fills = await this.client.info.getUserFills(this._address);
|
|
497
555
|
return (fills ?? []).slice(0, limit).map((f) => ({
|
|
498
556
|
orderId: String(f.oid ?? ""),
|
|
499
557
|
symbol: String(f.coin ?? ""),
|
|
500
|
-
|
|
558
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
559
|
+
side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
|
|
501
560
|
price: String(f.px ?? "0"),
|
|
502
561
|
size: String(f.sz ?? ""),
|
|
503
562
|
filled: String(f.sz ?? ""),
|
|
@@ -506,17 +565,20 @@ export class HyperliquidAdapter {
|
|
|
506
565
|
}));
|
|
507
566
|
}
|
|
508
567
|
async getTradeHistory(limit = 30) {
|
|
568
|
+
this.ensureAddress();
|
|
509
569
|
const fills = await this.client.info.getUserFills(this._address);
|
|
510
570
|
return (fills ?? []).slice(0, limit).map((f) => ({
|
|
511
571
|
time: Number(f.time ?? 0),
|
|
512
572
|
symbol: String(f.coin ?? ""),
|
|
513
|
-
|
|
573
|
+
// SDK convertSymbolsInObject already converts "B"→"buy", "A"→"sell"
|
|
574
|
+
side: String(f.side) === "B" ? "buy" : String(f.side) === "A" ? "sell" : f.side,
|
|
514
575
|
price: String(f.px ?? "0"),
|
|
515
576
|
size: String(f.sz ?? ""),
|
|
516
577
|
fee: String(f.fee ?? "0"),
|
|
517
578
|
}));
|
|
518
579
|
}
|
|
519
580
|
async getFundingPayments(limit = 30) {
|
|
581
|
+
this.ensureAddress();
|
|
520
582
|
const now = Date.now();
|
|
521
583
|
const history = await this.client.info.perpetuals.getUserFunding(this._address, now - 7 * 24 * 60 * 60 * 1000);
|
|
522
584
|
return (history ?? []).slice(0, limit).map((h) => {
|
|
@@ -534,7 +596,7 @@ export class HyperliquidAdapter {
|
|
|
534
596
|
return (history ?? []).slice(-limit).map((h) => ({
|
|
535
597
|
time: Number(h.time ?? 0),
|
|
536
598
|
rate: String(h.fundingRate ?? "0"),
|
|
537
|
-
price:
|
|
599
|
+
price: null,
|
|
538
600
|
}));
|
|
539
601
|
}
|
|
540
602
|
// ────────────────────────────────────────────────────────────
|
|
@@ -545,6 +607,7 @@ export class HyperliquidAdapter {
|
|
|
545
607
|
* Python SDK: order() with trigger type {"trigger": {triggerPx, isMarket, tpsl: "tp"|"sl"}}
|
|
546
608
|
*/
|
|
547
609
|
async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
|
|
610
|
+
this.ensureSigner();
|
|
548
611
|
const orderParams = {
|
|
549
612
|
coin: symbol.toUpperCase(),
|
|
550
613
|
is_buy: side === "buy",
|
|
@@ -568,7 +631,7 @@ export class HyperliquidAdapter {
|
|
|
568
631
|
* nktkas TS SDK: twapOrder({twap: {a, b, s, r, m, t}})
|
|
569
632
|
*/
|
|
570
633
|
async twapOrder(symbol, side, size, durationMinutes, opts) {
|
|
571
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
634
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
572
635
|
const action = {
|
|
573
636
|
type: "twapOrder",
|
|
574
637
|
twap: {
|
|
@@ -586,7 +649,7 @@ export class HyperliquidAdapter {
|
|
|
586
649
|
* Cancel a TWAP order.
|
|
587
650
|
*/
|
|
588
651
|
async twapCancel(symbol, twapId) {
|
|
589
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
652
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
590
653
|
const action = {
|
|
591
654
|
type: "twapCancel",
|
|
592
655
|
a: assetIndex,
|
|
@@ -599,6 +662,7 @@ export class HyperliquidAdapter {
|
|
|
599
662
|
* Uses SDK's built-in updateLeverage method.
|
|
600
663
|
*/
|
|
601
664
|
async updateLeverage(symbol, leverage, isCross = true) {
|
|
665
|
+
this.ensureSigner();
|
|
602
666
|
return this.sdk.exchange.updateLeverage(this.resolveSymbol(symbol), isCross ? "cross" : "isolated", leverage);
|
|
603
667
|
}
|
|
604
668
|
/**
|
|
@@ -606,6 +670,7 @@ export class HyperliquidAdapter {
|
|
|
606
670
|
* amount > 0 to add margin, amount < 0 to remove
|
|
607
671
|
*/
|
|
608
672
|
async updateIsolatedMargin(symbol, amount) {
|
|
673
|
+
this.ensureSigner();
|
|
609
674
|
return this.sdk.exchange.updateIsolatedMargin(this.resolveSymbol(symbol), amount > 0, Math.round(Math.abs(amount) * 1e6));
|
|
610
675
|
}
|
|
611
676
|
/**
|
|
@@ -613,6 +678,7 @@ export class HyperliquidAdapter {
|
|
|
613
678
|
* Python SDK: withdraw_from_bridge(amount, destination) → action type "withdraw3"
|
|
614
679
|
*/
|
|
615
680
|
async withdraw(amount, destination) {
|
|
681
|
+
this.ensureSigner();
|
|
616
682
|
try {
|
|
617
683
|
return await this.sdk.exchange.initiateWithdrawal(destination, parseFloat(amount));
|
|
618
684
|
}
|
|
@@ -634,6 +700,7 @@ export class HyperliquidAdapter {
|
|
|
634
700
|
* Python SDK: usd_transfer(amount, destination)
|
|
635
701
|
*/
|
|
636
702
|
async usdTransfer(amount, destination) {
|
|
703
|
+
this.ensureSigner();
|
|
637
704
|
return this.sdk.exchange.usdTransfer(destination, amount);
|
|
638
705
|
}
|
|
639
706
|
/**
|
|
@@ -662,7 +729,15 @@ export class HyperliquidAdapter {
|
|
|
662
729
|
* Modify an existing order.
|
|
663
730
|
*/
|
|
664
731
|
async modifyOrder(symbol, orderId, newSide, newPrice, newSize, opts) {
|
|
665
|
-
const assetIndex = this.getAssetIndex(symbol);
|
|
732
|
+
const assetIndex = await this.getAssetIndex(symbol);
|
|
733
|
+
const szDec = this.getSzDecimals(symbol);
|
|
734
|
+
// Remove trailing zeros (HL API requirement)
|
|
735
|
+
const trimZeros = (s) => {
|
|
736
|
+
if (!s.includes("."))
|
|
737
|
+
return s;
|
|
738
|
+
const n = s.replace(/\.?0+$/, "");
|
|
739
|
+
return n === "-0" ? "0" : n;
|
|
740
|
+
};
|
|
666
741
|
const action = {
|
|
667
742
|
type: "batchModify",
|
|
668
743
|
modifies: [{
|
|
@@ -670,14 +745,14 @@ export class HyperliquidAdapter {
|
|
|
670
745
|
order: {
|
|
671
746
|
a: assetIndex,
|
|
672
747
|
b: newSide === "buy",
|
|
673
|
-
p: newPrice,
|
|
674
|
-
s: newSize,
|
|
748
|
+
p: trimZeros(this._hlRoundPrice(Number(newPrice), szDec)),
|
|
749
|
+
s: trimZeros(Number(newSize).toFixed(szDec)),
|
|
675
750
|
r: opts?.reduceOnly ?? false,
|
|
676
751
|
t: { limit: { tif: "Gtc" } },
|
|
677
752
|
},
|
|
678
753
|
}],
|
|
679
754
|
};
|
|
680
|
-
return this.
|
|
755
|
+
return this._signAndSendAction(action);
|
|
681
756
|
}
|
|
682
757
|
/**
|
|
683
758
|
* Schedule cancel: cancel all orders at a future time.
|
|
@@ -731,6 +806,7 @@ export class HyperliquidAdapter {
|
|
|
731
806
|
* Get user trade fills.
|
|
732
807
|
*/
|
|
733
808
|
async getUserFills(startTime, endTime) {
|
|
809
|
+
this.ensureAddress();
|
|
734
810
|
const baseUrl = this._testnet
|
|
735
811
|
? "https://api.hyperliquid-testnet.xyz"
|
|
736
812
|
: "https://api.hyperliquid.xyz";
|
|
@@ -753,6 +829,7 @@ export class HyperliquidAdapter {
|
|
|
753
829
|
* Get user portfolio analytics.
|
|
754
830
|
*/
|
|
755
831
|
async getPortfolio() {
|
|
832
|
+
this.ensureAddress();
|
|
756
833
|
const baseUrl = this._testnet
|
|
757
834
|
? "https://api.hyperliquid-testnet.xyz"
|
|
758
835
|
: "https://api.hyperliquid.xyz";
|
|
@@ -767,6 +844,7 @@ export class HyperliquidAdapter {
|
|
|
767
844
|
* Query a specific order by OID.
|
|
768
845
|
*/
|
|
769
846
|
async queryOrder(orderId) {
|
|
847
|
+
this.ensureAddress();
|
|
770
848
|
const baseUrl = this._testnet
|
|
771
849
|
? "https://api.hyperliquid-testnet.xyz"
|
|
772
850
|
: "https://api.hyperliquid.xyz";
|
|
@@ -856,42 +934,49 @@ export class HyperliquidAdapter {
|
|
|
856
934
|
* Get referral info.
|
|
857
935
|
*/
|
|
858
936
|
async getReferralInfo() {
|
|
937
|
+
this.ensureAddress();
|
|
859
938
|
return this._infoPost({ type: "referral", user: this._address });
|
|
860
939
|
}
|
|
861
940
|
/**
|
|
862
941
|
* Get fee info.
|
|
863
942
|
*/
|
|
864
943
|
async getUserFees() {
|
|
944
|
+
this.ensureAddress();
|
|
865
945
|
return this._infoPost({ type: "userFees", user: this._address });
|
|
866
946
|
}
|
|
867
947
|
/**
|
|
868
948
|
* Get sub-accounts.
|
|
869
949
|
*/
|
|
870
950
|
async getSubAccounts() {
|
|
951
|
+
this.ensureAddress();
|
|
871
952
|
return this._infoPost({ type: "subAccounts", user: this._address });
|
|
872
953
|
}
|
|
873
954
|
/**
|
|
874
955
|
* Get historical orders (up to 2000).
|
|
875
956
|
*/
|
|
876
957
|
async getHistoricalOrders() {
|
|
958
|
+
this.ensureAddress();
|
|
877
959
|
return this._infoPost({ type: "historicalOrders", user: this._address });
|
|
878
960
|
}
|
|
879
961
|
/**
|
|
880
962
|
* Get approved builders.
|
|
881
963
|
*/
|
|
882
964
|
async getApprovedBuilders() {
|
|
965
|
+
this.ensureAddress();
|
|
883
966
|
return this._infoPost({ type: "approvedBuilders", user: this._address });
|
|
884
967
|
}
|
|
885
968
|
/**
|
|
886
969
|
* Get vault details.
|
|
887
970
|
*/
|
|
888
971
|
async getVaultDetails(vaultAddress) {
|
|
972
|
+
this.ensureAddress();
|
|
889
973
|
return this._infoPost({ type: "vaultDetails", vaultAddress, user: this._address });
|
|
890
974
|
}
|
|
891
975
|
/**
|
|
892
976
|
* Get delegations (staking).
|
|
893
977
|
*/
|
|
894
978
|
async getDelegations() {
|
|
979
|
+
this.ensureAddress();
|
|
895
980
|
return this._infoPost({ type: "delegations", user: this._address });
|
|
896
981
|
}
|
|
897
982
|
/**
|
|
@@ -908,20 +993,24 @@ export class HyperliquidAdapter {
|
|
|
908
993
|
});
|
|
909
994
|
return res.json();
|
|
910
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Public entry point for sending signed exchange actions.
|
|
998
|
+
* Used by HyperliquidSpotAdapter to delegate signing.
|
|
999
|
+
*/
|
|
1000
|
+
async exchangeAction(action) {
|
|
1001
|
+
return this._signAndSendAction(action);
|
|
1002
|
+
}
|
|
911
1003
|
/**
|
|
912
1004
|
* Send a raw exchange action through the SDK.
|
|
913
1005
|
* Used for methods not directly exposed by the SDK.
|
|
914
1006
|
*/
|
|
915
1007
|
async _sendExchangeAction(action) {
|
|
916
1008
|
const exchange = this.sdk.exchange;
|
|
917
|
-
// Try using the SDK's internal
|
|
1009
|
+
// Try using the SDK's internal postAction if available
|
|
918
1010
|
if (typeof exchange.postAction === "function") {
|
|
919
1011
|
return exchange.postAction(action);
|
|
920
1012
|
}
|
|
921
|
-
// Fallback:
|
|
922
|
-
|
|
923
|
-
// If we get here, the SDK doesn't expose the method we need
|
|
924
|
-
throw new Error(`Hyperliquid SDK does not expose postAction(). ` +
|
|
925
|
-
`Action "${action.type}" may need a newer SDK version.`);
|
|
1013
|
+
// Fallback: sign and send directly (bypasses SDK)
|
|
1014
|
+
return this._signAndSendAction(action);
|
|
926
1015
|
}
|
|
927
1016
|
}
|
|
@@ -70,7 +70,7 @@ export interface ExchangeAdapter {
|
|
|
70
70
|
getFundingHistory(symbol: string, limit?: number): Promise<{
|
|
71
71
|
time: number;
|
|
72
72
|
rate: string;
|
|
73
|
-
price: string;
|
|
73
|
+
price: string | null;
|
|
74
74
|
}[]>;
|
|
75
75
|
getKlines(symbol: string, interval: string, startTime: number, endTime: number): Promise<ExchangeKline[]>;
|
|
76
76
|
getBalance(): Promise<ExchangeBalance>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighter Spot Adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses the same `/api/v1/sendTx` endpoint as perps — the only difference
|
|
5
|
+
* is the market_id (spot markets have different IDs from perp markets).
|
|
6
|
+
*
|
|
7
|
+
* Composes with LighterAdapter for signing and API access.
|
|
8
|
+
*/
|
|
9
|
+
import type { SpotAdapter, SpotMarketInfo, SpotBalance } from "./spot-interface.js";
|
|
10
|
+
import type { LighterAdapter } from "./lighter.js";
|
|
11
|
+
export declare class LighterSpotAdapter implements SpotAdapter {
|
|
12
|
+
readonly name = "lighter";
|
|
13
|
+
private _lt;
|
|
14
|
+
private _spotMarketMap;
|
|
15
|
+
private _spotBaseMap;
|
|
16
|
+
private _spotDecimals;
|
|
17
|
+
private _spotMinSize;
|
|
18
|
+
private _initialized;
|
|
19
|
+
constructor(ltAdapter: LighterAdapter);
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
private _loadSpotMarkets;
|
|
22
|
+
/** Resolve a symbol (e.g., "ETH" or "ETH/USDC") to the full spot symbol */
|
|
23
|
+
private resolveSymbol;
|
|
24
|
+
getSpotMarkets(): Promise<SpotMarketInfo[]>;
|
|
25
|
+
getSpotOrderbook(symbol: string): Promise<{
|
|
26
|
+
bids: [string, string][];
|
|
27
|
+
asks: [string, string][];
|
|
28
|
+
}>;
|
|
29
|
+
getSpotBalances(): Promise<SpotBalance[]>;
|
|
30
|
+
spotMarketOrder(symbol: string, side: "buy" | "sell", size: string): Promise<unknown>;
|
|
31
|
+
spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
|
|
32
|
+
tif?: string;
|
|
33
|
+
}): Promise<unknown>;
|
|
34
|
+
spotCancelOrder(symbol: string, orderId: string): Promise<unknown>;
|
|
35
|
+
/** Transfer USDC from perp account to spot account (required before spot buys) */
|
|
36
|
+
transferUsdcToSpot(amount: number): Promise<unknown>;
|
|
37
|
+
/** Transfer USDC from spot account back to perp account */
|
|
38
|
+
transferUsdcToPerp(amount: number): Promise<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Internal self-transfer using raw WASM signTransfer.
|
|
41
|
+
* TS SDK's is_spot_account mapping is buggy (sets both fromRoute and toRoute
|
|
42
|
+
* to the same value), so we call the WASM module directly with explicit routes.
|
|
43
|
+
* Route types: 0 = Perp, 1 = Spot
|
|
44
|
+
*/
|
|
45
|
+
private _selfTransfer;
|
|
46
|
+
/** Get the spot market ID for a symbol */
|
|
47
|
+
getSpotMarketId(symbol: string): number | undefined;
|
|
48
|
+
/** Check if a base token has a spot market */
|
|
49
|
+
hasSpotMarket(base: string): boolean;
|
|
50
|
+
private _placeOrder;
|
|
51
|
+
private _getNextNonce;
|
|
52
|
+
private _sendTx;
|
|
53
|
+
private _restGet;
|
|
54
|
+
private _restGetAuth;
|
|
55
|
+
}
|