perp-cli 0.4.11 → 0.5.0
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/dist/commands/account.js +151 -20
- package/dist/commands/agent.js +5 -5
- package/dist/commands/arb/index.js +191 -24
- package/dist/commands/arb-auto.d.ts +9 -0
- package/dist/commands/arb-auto.js +577 -382
- package/dist/commands/arb.d.ts +44 -0
- package/dist/commands/arb.js +236 -514
- package/dist/commands/market.js +108 -73
- package/dist/exchanges/hyperliquid.d.ts +2 -2
- package/dist/exchanges/hyperliquid.js +126 -31
- package/dist/exchanges/lighter-spot.js +4 -19
- package/dist/exchanges/lighter.js +21 -7
- package/dist/liquidity.js +3 -1
- package/package.json +1 -1
package/dist/commands/market.js
CHANGED
|
@@ -8,47 +8,55 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
|
|
|
8
8
|
market
|
|
9
9
|
.command("list")
|
|
10
10
|
.description("List all available markets")
|
|
11
|
-
.
|
|
11
|
+
.option("--hip3", "Include HIP-3 dex markets (Hyperliquid)")
|
|
12
|
+
.action(async (opts) => {
|
|
12
13
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
14
|
+
const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"];
|
|
15
|
+
const marketRow = (m) => [
|
|
16
|
+
chalk.white.bold(m.symbol),
|
|
17
|
+
`$${formatUsd(m.markPrice)}`,
|
|
18
|
+
`$${formatUsd(m.indexPrice)}`,
|
|
19
|
+
formatPercent(m.fundingRate),
|
|
20
|
+
`$${formatUsd(m.volume24h)}`,
|
|
21
|
+
`$${formatUsd(m.openInterest)}`,
|
|
22
|
+
String(m.maxLeverage) + "x",
|
|
23
|
+
];
|
|
13
24
|
// Multi-exchange mode
|
|
14
25
|
if (!explicitExchange && getAdapterForExchange) {
|
|
15
|
-
const
|
|
26
|
+
const grouped = {};
|
|
16
27
|
const errors = {};
|
|
17
28
|
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
18
29
|
try {
|
|
19
30
|
const adapter = await getAdapterForExchange(ex);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
]);
|
|
31
|
+
grouped[ex] = await adapter.getMarkets();
|
|
32
|
+
// --hip3: fetch HIP-3 dex markets
|
|
33
|
+
if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
|
|
34
|
+
const dexes = await adapter.listDeployedDexes();
|
|
35
|
+
await Promise.allSettled(dexes.map(async (d) => {
|
|
36
|
+
const dexAdapter = Object.create(adapter);
|
|
37
|
+
dexAdapter.setDex(d.name);
|
|
38
|
+
const markets = await dexAdapter.getMarkets();
|
|
39
|
+
if (markets.length > 0)
|
|
40
|
+
grouped[`hip3:${d.name}`] = markets;
|
|
41
|
+
}));
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
catch (err) {
|
|
35
45
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
36
46
|
}
|
|
37
47
|
}));
|
|
38
|
-
// Sort by symbol name
|
|
39
|
-
allRows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
40
48
|
if (isJson()) {
|
|
41
|
-
const grouped = {};
|
|
42
|
-
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
43
|
-
try {
|
|
44
|
-
const adapter = await getAdapterForExchange(ex);
|
|
45
|
-
grouped[ex] = await adapter.getMarkets();
|
|
46
|
-
}
|
|
47
|
-
catch { /* skip */ }
|
|
48
|
-
}));
|
|
49
49
|
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Per-exchange tables
|
|
52
|
+
for (const [ex, markets] of Object.entries(grouped)) {
|
|
53
|
+
if (markets.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
const isHip3 = ex.startsWith("hip3:");
|
|
56
|
+
const label = isHip3 ? chalk.magenta.bold(` HIP-3: ${ex.slice(5)}`) : chalk.cyan.bold(` ${ex.toUpperCase()}`);
|
|
57
|
+
console.log(`\n${label} ${chalk.gray(`(${markets.length} markets)`)}`);
|
|
58
|
+
console.log(makeTable(HEADERS, markets.map(marketRow)));
|
|
59
|
+
}
|
|
52
60
|
for (const [ex, msg] of Object.entries(errors)) {
|
|
53
61
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
54
62
|
}
|
|
@@ -57,58 +65,63 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
|
|
|
57
65
|
// Single exchange mode
|
|
58
66
|
const adapter = await getAdapter();
|
|
59
67
|
const markets = await adapter.getMarkets();
|
|
60
|
-
|
|
68
|
+
// --hip3: fetch HIP-3 dex markets
|
|
69
|
+
const hip3 = {};
|
|
70
|
+
if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
|
|
71
|
+
const dexes = await adapter.listDeployedDexes();
|
|
72
|
+
await Promise.allSettled(dexes.map(async (d) => {
|
|
73
|
+
const dexAdapter = Object.create(adapter);
|
|
74
|
+
dexAdapter.setDex(d.name);
|
|
75
|
+
const m = await dexAdapter.getMarkets();
|
|
76
|
+
if (m.length > 0)
|
|
77
|
+
hip3[d.name] = m;
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
if (isJson()) {
|
|
81
|
+
if (Object.keys(hip3).length > 0) {
|
|
82
|
+
return printJson(jsonOk({ main: markets, hip3 }));
|
|
83
|
+
}
|
|
61
84
|
return printJson(jsonOk(markets));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`$${formatUsd(m.openInterest)}`,
|
|
69
|
-
String(m.maxLeverage) + "x",
|
|
70
|
-
]);
|
|
71
|
-
console.log(makeTable(["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], rows));
|
|
85
|
+
}
|
|
86
|
+
console.log(makeTable(HEADERS, markets.map(marketRow)));
|
|
87
|
+
for (const [dex, m] of Object.entries(hip3)) {
|
|
88
|
+
console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`) + chalk.gray(` (${m.length} markets)`));
|
|
89
|
+
console.log(makeTable(HEADERS, m.map(marketRow)));
|
|
90
|
+
}
|
|
72
91
|
});
|
|
73
92
|
market
|
|
74
93
|
.command("prices")
|
|
75
94
|
.description("Get current prices for all markets")
|
|
76
95
|
.action(async () => {
|
|
77
96
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
97
|
+
const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
|
|
98
|
+
const priceRow = (m) => [
|
|
99
|
+
chalk.white.bold(m.symbol),
|
|
100
|
+
`$${formatUsd(m.markPrice)}`,
|
|
101
|
+
`$${formatUsd(m.indexPrice)}`,
|
|
102
|
+
formatPercent(m.fundingRate),
|
|
103
|
+
];
|
|
78
104
|
if (!explicitExchange && getAdapterForExchange) {
|
|
79
|
-
const
|
|
105
|
+
const grouped = {};
|
|
80
106
|
const errors = {};
|
|
81
107
|
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
82
108
|
try {
|
|
83
109
|
const adapter = await getAdapterForExchange(ex);
|
|
84
|
-
|
|
85
|
-
for (const m of markets) {
|
|
86
|
-
allRows.push([
|
|
87
|
-
chalk.white.bold(m.symbol),
|
|
88
|
-
chalk.gray(ex.slice(0, 3).toUpperCase()),
|
|
89
|
-
`$${formatUsd(m.markPrice)}`,
|
|
90
|
-
`$${formatUsd(m.indexPrice)}`,
|
|
91
|
-
formatPercent(m.fundingRate),
|
|
92
|
-
]);
|
|
93
|
-
}
|
|
110
|
+
grouped[ex] = await adapter.getMarkets();
|
|
94
111
|
}
|
|
95
112
|
catch (err) {
|
|
96
113
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
97
114
|
}
|
|
98
115
|
}));
|
|
99
|
-
allRows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
100
116
|
if (isJson()) {
|
|
101
|
-
const grouped = {};
|
|
102
|
-
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
103
|
-
try {
|
|
104
|
-
const adapter = await getAdapterForExchange(ex);
|
|
105
|
-
grouped[ex] = await adapter.getMarkets();
|
|
106
|
-
}
|
|
107
|
-
catch { /* skip */ }
|
|
108
|
-
}));
|
|
109
117
|
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
for (const [ex, markets] of Object.entries(grouped)) {
|
|
120
|
+
if (markets.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
console.log(chalk.cyan.bold(`\n ${ex.toUpperCase()}`) + chalk.gray(` (${markets.length} markets)`));
|
|
123
|
+
console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
|
|
124
|
+
}
|
|
112
125
|
for (const [ex, msg] of Object.entries(errors))
|
|
113
126
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
114
127
|
return;
|
|
@@ -128,17 +141,10 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
|
|
|
128
141
|
console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
|
|
129
142
|
}
|
|
130
143
|
else {
|
|
131
|
-
// Generic: use getMarkets as price source
|
|
132
144
|
const markets = await adapter.getMarkets();
|
|
133
145
|
if (isJson())
|
|
134
146
|
return printJson(jsonOk(markets));
|
|
135
|
-
|
|
136
|
-
chalk.white.bold(m.symbol),
|
|
137
|
-
`$${formatUsd(m.markPrice)}`,
|
|
138
|
-
`$${formatUsd(m.indexPrice)}`,
|
|
139
|
-
formatPercent(m.fundingRate),
|
|
140
|
-
]);
|
|
141
|
-
console.log(makeTable(["Symbol", "Mark", "Index", "Funding"], rows));
|
|
147
|
+
console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
|
|
142
148
|
}
|
|
143
149
|
});
|
|
144
150
|
market
|
|
@@ -148,7 +154,11 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
|
|
|
148
154
|
const adapter = await getAdapter();
|
|
149
155
|
const markets = await adapter.getMarkets();
|
|
150
156
|
const sym = symbol.toUpperCase();
|
|
151
|
-
const m = markets.find((mk) =>
|
|
157
|
+
const m = markets.find((mk) => {
|
|
158
|
+
const s = mk.symbol.toUpperCase();
|
|
159
|
+
return s === sym || s === `${sym}-PERP` || s.replace(/-PERP$/, "") === sym
|
|
160
|
+
|| s.split(":").pop() === sym; // HIP-3: "km:US500" matches "US500"
|
|
161
|
+
});
|
|
152
162
|
if (!m) {
|
|
153
163
|
if (isJson())
|
|
154
164
|
return printJson(jsonError("SYMBOL_NOT_FOUND", `Market ${sym} not found`));
|
|
@@ -178,15 +188,40 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
|
|
|
178
188
|
const depth = parseInt(opts.depth);
|
|
179
189
|
const asks = book.asks.slice(0, depth).reverse();
|
|
180
190
|
const bids = book.bids.slice(0, depth);
|
|
191
|
+
// Compute spread
|
|
192
|
+
const bestBid = bids[0] ? Number(bids[0][0]) : 0;
|
|
193
|
+
const bestAsk = asks[asks.length - 1] ? Number(asks[asks.length - 1][0]) : 0;
|
|
194
|
+
const midPrice = bestBid && bestAsk ? (bestBid + bestAsk) / 2 : bestBid || bestAsk;
|
|
195
|
+
const spreadAbs = bestBid && bestAsk ? bestAsk - bestBid : 0;
|
|
196
|
+
const spreadPct = midPrice > 0 ? (spreadAbs / midPrice) * 100 : 0;
|
|
197
|
+
// Compute cumulative sizes + max for bar scaling
|
|
198
|
+
let askCum = 0;
|
|
199
|
+
const askCums = asks.map(([, a]) => { askCum += Number(a); return askCum; });
|
|
200
|
+
let bidCum = 0;
|
|
201
|
+
const bidCums = bids.map(([, a]) => { bidCum += Number(a); return bidCum; });
|
|
202
|
+
const maxCum = Math.max(askCums[askCums.length - 1] || 0, bidCums[bidCums.length - 1] || 0);
|
|
203
|
+
const BAR_WIDTH = 20;
|
|
204
|
+
const bar = (cum) => {
|
|
205
|
+
const len = maxCum > 0 ? Math.round((cum / maxCum) * BAR_WIDTH) : 0;
|
|
206
|
+
return "█".repeat(len);
|
|
207
|
+
};
|
|
181
208
|
console.log(chalk.cyan.bold(`\n Orderbook: ${symbol.toUpperCase()} (${adapter.name})\n`));
|
|
182
|
-
console.log(chalk.gray(" Price Size"));
|
|
183
|
-
console.log(chalk.gray("
|
|
184
|
-
asks.forEach(([p, a]) => {
|
|
185
|
-
|
|
209
|
+
console.log(chalk.gray(" Price Size Cum$ Depth"));
|
|
210
|
+
console.log(chalk.gray(" " + "─".repeat(60)));
|
|
211
|
+
asks.forEach(([p, a], i) => {
|
|
212
|
+
const cumUsd = askCums[i] * (Number(p));
|
|
213
|
+
console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
|
|
214
|
+
chalk.red.dim(bar(askCums[i])));
|
|
186
215
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
// Spread line
|
|
217
|
+
const spreadStr = spreadAbs > 0
|
|
218
|
+
? `$${formatUsd(spreadAbs)} (${spreadPct.toFixed(3)}%)`
|
|
219
|
+
: "─";
|
|
220
|
+
console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
|
|
221
|
+
bids.forEach(([p, a], i) => {
|
|
222
|
+
const cumUsd = bidCums[i] * (Number(p));
|
|
223
|
+
console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
|
|
224
|
+
chalk.green.dim(bar(bidCums[i])));
|
|
190
225
|
});
|
|
191
226
|
console.log();
|
|
192
227
|
});
|
|
@@ -69,8 +69,8 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
69
69
|
reduceOnly?: boolean;
|
|
70
70
|
tif?: string;
|
|
71
71
|
}): Promise<unknown>;
|
|
72
|
-
cancelOrder(symbol: string, orderId: string): Promise<
|
|
73
|
-
cancelAllOrders(symbol?: string): Promise<
|
|
72
|
+
cancelOrder(symbol: string, orderId: string): Promise<unknown>;
|
|
73
|
+
cancelAllOrders(symbol?: string): Promise<unknown[]>;
|
|
74
74
|
editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
|
|
75
75
|
setLeverage(symbol: string, leverage: number, marginMode?: "cross" | "isolated"): Promise<any>;
|
|
76
76
|
stopOrder(symbol: string, side: "buy" | "sell", size: string, triggerPrice: string, opts?: {
|
|
@@ -72,17 +72,38 @@ export class HyperliquidAdapter {
|
|
|
72
72
|
async _loadAssetMap() {
|
|
73
73
|
try {
|
|
74
74
|
if (this._dex) {
|
|
75
|
-
// HIP-3 deployed dex:
|
|
76
|
-
|
|
75
|
+
// HIP-3 deployed dex: asset indices use global offset scheme
|
|
76
|
+
// offset = 110000 + dexListIndex * 10000 (per Python SDK convention)
|
|
77
|
+
const [meta, dexList] = await Promise.all([
|
|
78
|
+
this._infoPost({ type: "meta", dex: this._dex }),
|
|
79
|
+
this._infoPost({ type: "perpDexs" }),
|
|
80
|
+
]);
|
|
81
|
+
// Find this dex's position in the list (index 0 is native/null)
|
|
82
|
+
let dexOffset = 0;
|
|
83
|
+
for (let i = 1; i < dexList.length; i++) {
|
|
84
|
+
if (dexList[i]?.name === this._dex) {
|
|
85
|
+
dexOffset = 110000 + (i - 1) * 10000;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (dexOffset === 0) {
|
|
90
|
+
console.error(`[hyperliquid] Dex "${this._dex}" not found in perpDexs list`);
|
|
91
|
+
}
|
|
77
92
|
if (meta?.universe) {
|
|
78
93
|
meta.universe.forEach((asset, idx) => {
|
|
94
|
+
const globalIdx = dexOffset + idx;
|
|
79
95
|
// Store both with and without dex prefix for flexible lookup
|
|
80
96
|
// API returns "km:GOOGL" but callers may use "GOOGL" or "KM:GOOGL"
|
|
81
97
|
const fullName = asset.name; // e.g., "km:GOOGL"
|
|
82
98
|
const baseName = fullName.includes(":") ? fullName.split(":").pop() : fullName;
|
|
83
|
-
this._assetMap.set(fullName,
|
|
84
|
-
this._assetMap.set(baseName,
|
|
85
|
-
this._assetMapReverse.set(
|
|
99
|
+
this._assetMap.set(fullName, globalIdx);
|
|
100
|
+
this._assetMap.set(baseName, globalIdx);
|
|
101
|
+
this._assetMapReverse.set(globalIdx, fullName);
|
|
102
|
+
// Cache szDecimals for dex assets (critical for order sizing)
|
|
103
|
+
if (asset.szDecimals !== undefined) {
|
|
104
|
+
this._szDecimalsMap.set(fullName, asset.szDecimals);
|
|
105
|
+
this._szDecimalsMap.set(baseName, asset.szDecimals);
|
|
106
|
+
}
|
|
86
107
|
});
|
|
87
108
|
}
|
|
88
109
|
}
|
|
@@ -168,8 +189,8 @@ export class HyperliquidAdapter {
|
|
|
168
189
|
ctxs = meta[1] ?? [];
|
|
169
190
|
mids = allMids;
|
|
170
191
|
}
|
|
171
|
-
// Rebuild asset map if empty
|
|
172
|
-
if (this._assetMap.size === 0) {
|
|
192
|
+
// Rebuild asset map if empty (fallback — normally populated by init/_loadAssetMap)
|
|
193
|
+
if (this._assetMap.size === 0 && !this._dex) {
|
|
173
194
|
universe.forEach((asset, i) => {
|
|
174
195
|
const sym = String(asset.name);
|
|
175
196
|
this._assetMap.set(sym, i);
|
|
@@ -191,8 +212,24 @@ export class HyperliquidAdapter {
|
|
|
191
212
|
});
|
|
192
213
|
}
|
|
193
214
|
async getOrderbook(symbol) {
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
let levels;
|
|
216
|
+
if (this._dex) {
|
|
217
|
+
// HIP-3 dex: SDK's getL2Book() doesn't support dex parameter
|
|
218
|
+
// Resolve symbol to full dex-prefixed name (e.g. "US500" → "km:US500")
|
|
219
|
+
const resolved = this.resolveSymbol(symbol.toUpperCase());
|
|
220
|
+
// Ensure dex prefix is present (API requires it)
|
|
221
|
+
const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
|
|
222
|
+
const book = await this._infoPost({
|
|
223
|
+
type: "l2Book",
|
|
224
|
+
coin,
|
|
225
|
+
dex: this._dex,
|
|
226
|
+
});
|
|
227
|
+
levels = book?.levels ?? [[], []];
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const book = await this.sdk.info.getL2Book(symbol.toUpperCase());
|
|
231
|
+
levels = book?.levels ?? [[], []];
|
|
232
|
+
}
|
|
196
233
|
return {
|
|
197
234
|
bids: (levels[0] ?? []).map((l) => [
|
|
198
235
|
String(l.px ?? "0"),
|
|
@@ -291,6 +328,22 @@ export class HyperliquidAdapter {
|
|
|
291
328
|
}
|
|
292
329
|
async getOpenOrders() {
|
|
293
330
|
this.ensureAddress();
|
|
331
|
+
if (this._dex) {
|
|
332
|
+
// HIP-3 dex: use frontendOpenOrders with dex parameter
|
|
333
|
+
const orders = await this._infoPost({
|
|
334
|
+
type: "frontendOpenOrders", user: this._address, dex: this._dex,
|
|
335
|
+
});
|
|
336
|
+
return (orders ?? []).map((o) => ({
|
|
337
|
+
orderId: String(o.oid ?? ""),
|
|
338
|
+
symbol: String(o.coin ?? ""),
|
|
339
|
+
side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : String(o.side).toLowerCase(),
|
|
340
|
+
price: String(o.limitPx ?? "0"),
|
|
341
|
+
size: String(o.sz ?? "0"),
|
|
342
|
+
filled: "0",
|
|
343
|
+
status: "open",
|
|
344
|
+
type: "limit",
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
294
347
|
const orders = await this.sdk.info.getUserOpenOrders(this._address);
|
|
295
348
|
return (orders ?? []).map((o) => ({
|
|
296
349
|
orderId: String(o.oid ?? ""),
|
|
@@ -337,16 +390,20 @@ export class HyperliquidAdapter {
|
|
|
337
390
|
*/
|
|
338
391
|
async _dexMarketOrder(symbol, side, size) {
|
|
339
392
|
const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
|
|
340
|
-
|
|
393
|
+
const szDec = this.getSzDecimals(symbol);
|
|
394
|
+
// Get current mark price from dex meta (lookup by coin name, not array index)
|
|
395
|
+
const resolved = this.resolveSymbol(symbol.toUpperCase());
|
|
396
|
+
const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
|
|
341
397
|
const meta = await this._infoPost({
|
|
342
398
|
type: "metaAndAssetCtxs",
|
|
343
399
|
dex: this._dex,
|
|
344
400
|
});
|
|
345
|
-
|
|
401
|
+
// Find local index by matching coin name in universe
|
|
402
|
+
const localIdx = meta[0]?.universe?.findIndex(a => a.name === coin) ?? -1;
|
|
403
|
+
const ctx = localIdx >= 0 ? (meta[1] ?? [])[localIdx] : undefined;
|
|
346
404
|
const midPrice = Number(ctx?.markPx ?? 0);
|
|
347
405
|
if (midPrice <= 0)
|
|
348
406
|
throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
|
|
349
|
-
const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
|
|
350
407
|
// HL official rounding: 5 significant figures, max 6 decimals for perps
|
|
351
408
|
const MAX_DECIMALS_PERP = 6;
|
|
352
409
|
const slippage = 0.05;
|
|
@@ -419,7 +476,14 @@ export class HyperliquidAdapter {
|
|
|
419
476
|
headers: { "Content-Type": "application/json" },
|
|
420
477
|
body: JSON.stringify(payload),
|
|
421
478
|
});
|
|
422
|
-
const
|
|
479
|
+
const text = await res.text();
|
|
480
|
+
let result;
|
|
481
|
+
try {
|
|
482
|
+
result = JSON.parse(text);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
throw new Error(`HL exchange API error (${res.status}): ${text.slice(0, 200)}`);
|
|
486
|
+
}
|
|
423
487
|
if (result?.status === "err") {
|
|
424
488
|
throw new Error(result.response ?? JSON.stringify(result));
|
|
425
489
|
}
|
|
@@ -450,10 +514,7 @@ export class HyperliquidAdapter {
|
|
|
450
514
|
orders: [orderWire],
|
|
451
515
|
grouping: "na",
|
|
452
516
|
};
|
|
453
|
-
//
|
|
454
|
-
if (this._dex) {
|
|
455
|
-
action.dex = this._dex;
|
|
456
|
-
}
|
|
517
|
+
// No "dex" field needed — the global asset index (110000+) encodes the dex
|
|
457
518
|
return this._signAndSendAction(action);
|
|
458
519
|
}
|
|
459
520
|
async limitOrder(symbol, side, price, size, opts) {
|
|
@@ -480,6 +541,16 @@ export class HyperliquidAdapter {
|
|
|
480
541
|
}
|
|
481
542
|
async cancelOrder(symbol, orderId) {
|
|
482
543
|
this.ensureSigner();
|
|
544
|
+
if (this._dex) {
|
|
545
|
+
// HIP-3 dex: use raw cancel action with global asset index
|
|
546
|
+
const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
|
|
547
|
+
const result = await this._signAndSendAction({
|
|
548
|
+
type: "cancel",
|
|
549
|
+
cancels: [{ a: assetIndex, o: parseInt(orderId) }],
|
|
550
|
+
});
|
|
551
|
+
await this._invalidateAccountCache();
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
483
554
|
// HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
|
|
484
555
|
const resolved = this.resolveSymbol(symbol);
|
|
485
556
|
const result = await this.sdk.exchange.cancelOrder({
|
|
@@ -494,16 +565,25 @@ export class HyperliquidAdapter {
|
|
|
494
565
|
const orders = await this.getOpenOrders();
|
|
495
566
|
const toCancel = symbol
|
|
496
567
|
? orders.filter((o) => {
|
|
497
|
-
const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
|
|
568
|
+
const norm = (s) => s.toUpperCase().replace(/-PERP$/, "").replace(/^[^:]+:/, "");
|
|
498
569
|
return norm(o.symbol) === norm(symbol);
|
|
499
570
|
})
|
|
500
571
|
: orders;
|
|
501
572
|
const results = [];
|
|
502
573
|
for (const o of toCancel) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
574
|
+
if (this._dex) {
|
|
575
|
+
const assetIndex = await this.getAssetIndex(o.symbol);
|
|
576
|
+
results.push(await this._signAndSendAction({
|
|
577
|
+
type: "cancel",
|
|
578
|
+
cancels: [{ a: assetIndex, o: parseInt(o.orderId) }],
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
results.push(await this.sdk.exchange.cancelOrder({
|
|
583
|
+
coin: o.symbol,
|
|
584
|
+
o: parseInt(o.orderId),
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
507
587
|
}
|
|
508
588
|
return results;
|
|
509
589
|
}
|
|
@@ -608,22 +688,37 @@ export class HyperliquidAdapter {
|
|
|
608
688
|
*/
|
|
609
689
|
async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
|
|
610
690
|
this.ensureSigner();
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
691
|
+
const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
|
|
692
|
+
const szDec = this.getSzDecimals(symbol);
|
|
693
|
+
const roundedSize = Number(size).toFixed(szDec);
|
|
694
|
+
const roundedTrigger = this._hlRoundPrice(Number(triggerPrice), szDec);
|
|
695
|
+
// Remove trailing zeros (HL API requirement)
|
|
696
|
+
const trimZeros = (s) => {
|
|
697
|
+
if (!s.includes("."))
|
|
698
|
+
return s;
|
|
699
|
+
const n = s.replace(/\.?0+$/, "");
|
|
700
|
+
return n === "-0" ? "0" : n;
|
|
701
|
+
};
|
|
702
|
+
const orderWire = {
|
|
703
|
+
a: assetIndex,
|
|
704
|
+
b: side === "buy",
|
|
705
|
+
p: trimZeros(roundedTrigger),
|
|
706
|
+
s: trimZeros(roundedSize),
|
|
707
|
+
r: opts?.reduceOnly ?? true,
|
|
708
|
+
t: {
|
|
617
709
|
trigger: {
|
|
618
|
-
triggerPx:
|
|
710
|
+
triggerPx: trimZeros(roundedTrigger),
|
|
619
711
|
isMarket: opts?.isMarket ?? true,
|
|
620
712
|
tpsl,
|
|
621
713
|
},
|
|
622
714
|
},
|
|
623
|
-
|
|
715
|
+
};
|
|
716
|
+
const action = {
|
|
717
|
+
type: "order",
|
|
718
|
+
orders: [orderWire],
|
|
624
719
|
grouping: opts?.grouping ?? "positionTpsl",
|
|
625
720
|
};
|
|
626
|
-
return this.
|
|
721
|
+
return this._signAndSendAction(action);
|
|
627
722
|
}
|
|
628
723
|
/**
|
|
629
724
|
* Place a TWAP order.
|
|
@@ -140,30 +140,15 @@ export class LighterSpotAdapter {
|
|
|
140
140
|
?? res.accounts?.[0];
|
|
141
141
|
if (!acct)
|
|
142
142
|
return [];
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
token: "USDC",
|
|
146
|
-
total: String(acct.collateral ?? 0),
|
|
147
|
-
available: String(acct.available_balance ?? 0),
|
|
148
|
-
held: String(Math.max(0, Number(acct.collateral ?? 0) - Number(acct.available_balance ?? 0))),
|
|
149
|
-
});
|
|
150
|
-
// Spot token balances from the assets array
|
|
143
|
+
// Spot token balances from the assets array only.
|
|
144
|
+
// Perp USDC (collateral) is already reported by getBalance() — don't duplicate here.
|
|
151
145
|
if (acct.assets && acct.assets.length > 0) {
|
|
152
146
|
for (const asset of acct.assets) {
|
|
153
147
|
const bal = parseFloat(asset.balance || "0");
|
|
154
148
|
const locked = parseFloat(asset.locked_balance || "0");
|
|
155
|
-
if (
|
|
156
|
-
// Add spot USDC as separate entry so caller can distinguish
|
|
157
|
-
balances.push({
|
|
158
|
-
token: "USDC_SPOT",
|
|
159
|
-
total: asset.balance,
|
|
160
|
-
available: String(bal - locked),
|
|
161
|
-
held: asset.locked_balance,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
else if (bal > 0) {
|
|
149
|
+
if (bal > 0) {
|
|
165
150
|
balances.push({
|
|
166
|
-
token: asset.symbol,
|
|
151
|
+
token: asset.symbol === "USDC" ? "USDC_SPOT" : asset.symbol,
|
|
167
152
|
total: asset.balance,
|
|
168
153
|
available: String(bal - locked),
|
|
169
154
|
held: asset.locked_balance,
|
|
@@ -240,10 +240,19 @@ export class LighterAdapter {
|
|
|
240
240
|
if (!dec) {
|
|
241
241
|
throw new Error(`No market decimals loaded for ${symbol}. Market data may not be initialized.`);
|
|
242
242
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
const baseAmount = Math.round(size * Math.pow(10, dec.size));
|
|
244
|
+
const priceTicks = Math.round(price * Math.pow(10, dec.price));
|
|
245
|
+
// Validate: size too small for this market's precision → baseAmount rounds to 0
|
|
246
|
+
if (size > 0 && baseAmount <= 0) {
|
|
247
|
+
const minSize = 1 / Math.pow(10, dec.size);
|
|
248
|
+
throw new Error(`Order size ${size} too small for ${symbol} (sizeDecimals=${dec.size}, min=${minSize}). ` +
|
|
249
|
+
`Increase size or use a different market.`);
|
|
250
|
+
}
|
|
251
|
+
if (price > 0 && priceTicks <= 0) {
|
|
252
|
+
const minPrice = 1 / Math.pow(10, dec.price);
|
|
253
|
+
throw new Error(`Order price ${price} too small for ${symbol} (priceDecimals=${dec.price}, min=${minPrice}).`);
|
|
254
|
+
}
|
|
255
|
+
return { baseAmount, priceTicks };
|
|
247
256
|
}
|
|
248
257
|
async getMarkets() {
|
|
249
258
|
const markets = [];
|
|
@@ -338,9 +347,14 @@ export class LighterAdapter {
|
|
|
338
347
|
: "0"),
|
|
339
348
|
liquidationPrice: String(p.liquidation_price || "N/A"),
|
|
340
349
|
unrealizedPnl: String(p.unrealized_pnl || "0"),
|
|
341
|
-
leverage
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
// Compute actual leverage = notional / account equity (not max leverage from IMF)
|
|
351
|
+
leverage: (() => {
|
|
352
|
+
const notional = Math.abs(Number(p.position_value || 0));
|
|
353
|
+
const equity = Number(acct.total_asset_value || 0);
|
|
354
|
+
if (equity > 0 && notional > 0)
|
|
355
|
+
return Math.round(notional / equity * 10) / 10;
|
|
356
|
+
return 1;
|
|
357
|
+
})(),
|
|
344
358
|
};
|
|
345
359
|
});
|
|
346
360
|
}
|
package/dist/liquidity.js
CHANGED
|
@@ -41,13 +41,15 @@ export function computeExecutableSize(levels, requestedSizeUsd, maxSlippagePct =
|
|
|
41
41
|
const avgFillPrice = filledSize > 0 ? filledNotional / filledSize : bestPrice;
|
|
42
42
|
const slippagePct = bestPrice > 0 ? ((avgFillPrice - bestPrice) / bestPrice) * 100 : 0;
|
|
43
43
|
const canFillFull = filledNotional >= requestedSizeUsd * 0.95; // 95% fill = close enough
|
|
44
|
+
// 10% safety buffer — avoid filling at the very edge of liquidity
|
|
45
|
+
const bufferedSize = filledSize * 0.9;
|
|
44
46
|
return {
|
|
45
47
|
maxSize: filledSize,
|
|
46
48
|
avgFillPrice,
|
|
47
49
|
slippagePct: Math.abs(slippagePct),
|
|
48
50
|
depthUsd: totalDepthUsd,
|
|
49
51
|
canFillFull,
|
|
50
|
-
recommendedSize:
|
|
52
|
+
recommendedSize: bufferedSize,
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
/**
|