perp-cli 0.4.8 → 0.4.10
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 +44 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +366 -62
- package/dist/commands/agent.js +9 -5
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +67 -19
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +5 -3
- package/dist/commands/market.js +34 -1
- package/dist/commands/pnl.js +1 -1
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +241 -259
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +10 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +11 -3
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +13 -21
- package/dist/exchanges/lighter-spot.js +13 -15
- package/dist/exchanges/lighter.js +82 -21
- package/dist/exchanges/pacifica.js +12 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/index.js +39 -123
- package/dist/mcp-server.js +43 -96
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +13 -26
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +38 -31
- package/skills/perp-cli/references/strategies.md +26 -16
- package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
- package/skills/perp-cli/scripts/preflight.sh +1 -1
- package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
|
@@ -27,6 +27,7 @@ export class HyperliquidSpotAdapter {
|
|
|
27
27
|
_spotAssetReverse = new Map(); // spotIndex → "ETH"
|
|
28
28
|
_spotDecimals = new Map();
|
|
29
29
|
_initialized = false;
|
|
30
|
+
_cachedMetaCtx = null;
|
|
30
31
|
constructor(hlAdapter) {
|
|
31
32
|
this._hl = hlAdapter;
|
|
32
33
|
}
|
|
@@ -39,9 +40,12 @@ export class HyperliquidSpotAdapter {
|
|
|
39
40
|
}
|
|
40
41
|
async _loadSpotMeta() {
|
|
41
42
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// Single API call — spotMetaAndAssetCtxs includes all data from spotMeta
|
|
44
|
+
const metaCtx = await this._infoPost({ type: "spotMetaAndAssetCtxs" });
|
|
45
|
+
if (!metaCtx?.[0]?.universe)
|
|
44
46
|
return;
|
|
47
|
+
this._cachedMetaCtx = metaCtx;
|
|
48
|
+
const meta = metaCtx[0];
|
|
45
49
|
// Token index → name map
|
|
46
50
|
const tokenNames = new Map();
|
|
47
51
|
for (const t of meta.tokens ?? []) {
|
|
@@ -54,29 +58,15 @@ export class HyperliquidSpotAdapter {
|
|
|
54
58
|
if (!baseToken)
|
|
55
59
|
continue;
|
|
56
60
|
const key = baseToken.toUpperCase();
|
|
57
|
-
// Prefer /USDC pairs (quote token = USDC). Skip non-USDC pairs if we already have one.
|
|
58
61
|
const isUsdcPair = u.tokens[1] === usdcTokenIndex;
|
|
59
62
|
if (this._spotAssetMap.has(key) && !isUsdcPair)
|
|
60
63
|
continue;
|
|
61
64
|
this._spotAssetMap.set(key, u.index);
|
|
62
65
|
this._spotAssetReverse.set(u.index, key);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const u of metaCtx[0].universe) {
|
|
68
|
-
const baseToken = tokenNames.get(u.tokens[0]) ?? "";
|
|
69
|
-
if (!baseToken)
|
|
70
|
-
continue;
|
|
71
|
-
const key = baseToken.toUpperCase();
|
|
72
|
-
const isUsdcPair = u.tokens[1] === usdcTokenIndex;
|
|
73
|
-
if (this._spotDecimals.has(key) && !isUsdcPair)
|
|
74
|
-
continue;
|
|
75
|
-
this._spotDecimals.set(key, {
|
|
76
|
-
size: u.szDecimals ?? 2,
|
|
77
|
-
price: 6, // HL spot prices use up to 6 significant digits
|
|
78
|
-
});
|
|
79
|
-
}
|
|
66
|
+
this._spotDecimals.set(key, {
|
|
67
|
+
size: u.szDecimals ?? 2,
|
|
68
|
+
price: 6,
|
|
69
|
+
});
|
|
80
70
|
}
|
|
81
71
|
}
|
|
82
72
|
catch (e) {
|
|
@@ -86,7 +76,9 @@ export class HyperliquidSpotAdapter {
|
|
|
86
76
|
async getSpotMarkets() {
|
|
87
77
|
await this.init();
|
|
88
78
|
try {
|
|
89
|
-
|
|
79
|
+
// Reuse cached metaCtx from init, or fetch fresh if cache expired
|
|
80
|
+
const metaCtx = this._cachedMetaCtx
|
|
81
|
+
?? await this._infoPost({ type: "spotMetaAndAssetCtxs" });
|
|
90
82
|
if (!metaCtx?.[0]?.universe)
|
|
91
83
|
return [];
|
|
92
84
|
const tokenNames = new Map();
|
|
@@ -83,28 +83,26 @@ export class LighterSpotAdapter {
|
|
|
83
83
|
}
|
|
84
84
|
async getSpotMarkets() {
|
|
85
85
|
await this.init();
|
|
86
|
-
const
|
|
87
|
-
|
|
86
|
+
const entries = Array.from(this._spotMarketMap.entries());
|
|
87
|
+
// Fetch all orderbooks in parallel (instead of sequential per-market)
|
|
88
|
+
const obResults = await Promise.allSettled(entries.map(([symbol]) => this.getSpotOrderbook(symbol).then(ob => {
|
|
89
|
+
if (ob.bids.length > 0 && ob.asks.length > 0) {
|
|
90
|
+
return String((Number(ob.bids[0][0]) + Number(ob.asks[0][0])) / 2);
|
|
91
|
+
}
|
|
92
|
+
return "0";
|
|
93
|
+
})));
|
|
94
|
+
return entries.map(([symbol], i) => {
|
|
88
95
|
const parts = symbol.split("/");
|
|
89
96
|
const base = parts[0] ?? "";
|
|
90
97
|
const quote = parts[1] ?? "USDC";
|
|
91
98
|
const dec = this._spotDecimals.get(symbol) ?? { size: 4, price: 2 };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const ob = await this.getSpotOrderbook(symbol);
|
|
96
|
-
if (ob.bids.length > 0 && ob.asks.length > 0) {
|
|
97
|
-
markPrice = String((Number(ob.bids[0][0]) + Number(ob.asks[0][0])) / 2);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch { /* skip */ }
|
|
101
|
-
markets.push({
|
|
99
|
+
const markPrice = obResults[i].status === "fulfilled" ? obResults[i].value : "0";
|
|
100
|
+
return {
|
|
102
101
|
symbol, baseToken: base, quoteToken: quote,
|
|
103
102
|
markPrice, volume24h: "0",
|
|
104
103
|
sizeDecimals: dec.size, priceDecimals: dec.price,
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return markets;
|
|
104
|
+
};
|
|
105
|
+
});
|
|
108
106
|
}
|
|
109
107
|
async getSpotOrderbook(symbol) {
|
|
110
108
|
await this.init();
|
|
@@ -73,12 +73,28 @@ export class LighterAdapter {
|
|
|
73
73
|
await this._refreshMarketMap();
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
// Retry account lookup on 429 (rate limit) — critical for _accountIndex
|
|
77
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`${this._baseUrl}/api/v1/account?by=l1_address&value=${this._address}`);
|
|
80
|
+
if (res.status === 429 && attempt < 2) {
|
|
81
|
+
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const json = await res.json();
|
|
85
|
+
if (json.accounts && json.accounts.length > 0) {
|
|
86
|
+
this._accountIndex = this._accountIndexInit >= 0
|
|
87
|
+
? this._accountIndexInit
|
|
88
|
+
: json.accounts[0].account_index;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
if (attempt < 2) {
|
|
94
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
82
98
|
}
|
|
83
99
|
// Auto-generate API key if we have PK but no API key and account exists
|
|
84
100
|
let signerReady = false;
|
|
@@ -286,6 +302,8 @@ export class LighterAdapter {
|
|
|
286
302
|
}
|
|
287
303
|
}
|
|
288
304
|
async getBalance() {
|
|
305
|
+
if (!this._address)
|
|
306
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
289
307
|
const acct = await this.fetchAccount();
|
|
290
308
|
if (!acct)
|
|
291
309
|
return { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
|
|
@@ -301,6 +319,8 @@ export class LighterAdapter {
|
|
|
301
319
|
};
|
|
302
320
|
}
|
|
303
321
|
async getPositions() {
|
|
322
|
+
if (!this._address)
|
|
323
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
304
324
|
const acct = await this.fetchAccount();
|
|
305
325
|
if (!acct)
|
|
306
326
|
return [];
|
|
@@ -336,6 +356,8 @@ export class LighterAdapter {
|
|
|
336
356
|
return this.restGet(path, params);
|
|
337
357
|
}
|
|
338
358
|
async getOpenOrders() {
|
|
359
|
+
if (!this._address)
|
|
360
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
339
361
|
if (this._accountIndex < 0 || this._readOnly)
|
|
340
362
|
return [];
|
|
341
363
|
try {
|
|
@@ -344,13 +366,26 @@ export class LighterAdapter {
|
|
|
344
366
|
const totalOrders = Number(acct?.total_order_count ?? 0);
|
|
345
367
|
if (totalOrders === 0)
|
|
346
368
|
return [];
|
|
347
|
-
//
|
|
369
|
+
// Only query markets that have open orders (avoids 156-market fan-out)
|
|
370
|
+
const activeMarketIds = new Set();
|
|
371
|
+
for (const p of acct?.positions ?? []) {
|
|
372
|
+
if (Number(p.open_order_count ?? 0) > 0 || Number(p.pending_order_count ?? 0) > 0) {
|
|
373
|
+
activeMarketIds.add(Number(p.market_id));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (activeMarketIds.size === 0)
|
|
377
|
+
return [];
|
|
378
|
+
const reverseMap = new Map();
|
|
379
|
+
for (const [sym, id] of this._marketMap)
|
|
380
|
+
reverseMap.set(id, sym);
|
|
348
381
|
const allOrders = [];
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
382
|
+
const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
|
|
383
|
+
const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
|
|
384
|
+
return this.restGetAuth("/accountActiveOrders", {
|
|
385
|
+
account_index: String(this._accountIndex),
|
|
386
|
+
market_id: String(marketId),
|
|
387
|
+
}).then(res => ({ sym, orders: (res.orders ?? []) }));
|
|
388
|
+
}));
|
|
354
389
|
for (const r of results) {
|
|
355
390
|
if (r.status !== "fulfilled" || !r.value.orders.length)
|
|
356
391
|
continue;
|
|
@@ -575,6 +610,8 @@ export class LighterAdapter {
|
|
|
575
610
|
}));
|
|
576
611
|
}
|
|
577
612
|
async getOrderHistory(limit = 30) {
|
|
613
|
+
if (!this._address)
|
|
614
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
578
615
|
const raw = await this._getOrderHistoryRaw();
|
|
579
616
|
const orders = (raw.orders ?? []);
|
|
580
617
|
return orders.slice(0, limit).map((o) => ({
|
|
@@ -589,6 +626,8 @@ export class LighterAdapter {
|
|
|
589
626
|
}));
|
|
590
627
|
}
|
|
591
628
|
async getTradeHistory(limit = 30) {
|
|
629
|
+
if (!this._address)
|
|
630
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
592
631
|
const raw = await this._getTradeHistoryRaw(limit);
|
|
593
632
|
const trades = (raw.trades ?? []);
|
|
594
633
|
return trades.slice(0, limit).map((t) => ({
|
|
@@ -601,6 +640,8 @@ export class LighterAdapter {
|
|
|
601
640
|
}));
|
|
602
641
|
}
|
|
603
642
|
async getFundingPayments(limit = 30) {
|
|
643
|
+
if (!this._address)
|
|
644
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
604
645
|
const raw = await this._getPositionFundingRaw();
|
|
605
646
|
const items = (raw.funding ?? []);
|
|
606
647
|
return items.slice(0, limit).map((f) => ({
|
|
@@ -704,15 +745,35 @@ export class LighterAdapter {
|
|
|
704
745
|
return { funding: [] };
|
|
705
746
|
if (this._marketMap.size === 0)
|
|
706
747
|
await this._refreshMarketMap();
|
|
748
|
+
// Only query markets where the account has/had positions (avoids 156-market fan-out)
|
|
749
|
+
const acct = await this.fetchAccount();
|
|
750
|
+
const activeMarketIds = new Set();
|
|
751
|
+
if (acct) {
|
|
752
|
+
for (const p of acct.positions ?? []) {
|
|
753
|
+
const pos = Number(p.position || 0);
|
|
754
|
+
const rpnl = Number(p.realized_pnl || 0);
|
|
755
|
+
if (pos !== 0 || rpnl !== 0) {
|
|
756
|
+
activeMarketIds.add(Number(p.market_id));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (activeMarketIds.size === 0)
|
|
761
|
+
return { funding: [] };
|
|
762
|
+
// Resolve market_id → symbol from marketMap
|
|
763
|
+
const reverseMap = new Map();
|
|
764
|
+
for (const [sym, id] of this._marketMap)
|
|
765
|
+
reverseMap.set(id, sym);
|
|
707
766
|
const allFunding = [];
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
767
|
+
const results = await Promise.allSettled(Array.from(activeMarketIds).map((marketId) => {
|
|
768
|
+
const sym = reverseMap.get(marketId) ?? `Market-${marketId}`;
|
|
769
|
+
return this.restGetAuth("/positionFunding", {
|
|
770
|
+
account_index: String(this._accountIndex),
|
|
771
|
+
market_id: String(marketId),
|
|
772
|
+
}).then((res) => {
|
|
773
|
+
const items = (res.funding ?? res.data ?? []);
|
|
774
|
+
return items.map((f) => ({ ...f, symbol: f.symbol ?? sym }));
|
|
775
|
+
});
|
|
776
|
+
}));
|
|
716
777
|
for (const r of results) {
|
|
717
778
|
if (r.status === "fulfilled")
|
|
718
779
|
allFunding.push(...r.value);
|
|
@@ -872,7 +933,7 @@ export class LighterAdapter {
|
|
|
872
933
|
return res.json();
|
|
873
934
|
}
|
|
874
935
|
ensureSigner() {
|
|
875
|
-
if (this._readOnly) {
|
|
936
|
+
if (this._readOnly || this._accountIndex < 0 || !this._signer) {
|
|
876
937
|
throw new Error("This command requires a Lighter API key. Run `perp -e lighter manage setup-api-key` first, " +
|
|
877
938
|
"then set LIGHTER_API_KEY in your .env");
|
|
878
939
|
}
|
|
@@ -77,6 +77,8 @@ export class PacificaAdapter {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
async getBalance() {
|
|
80
|
+
if (!this._hasRealKey)
|
|
81
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
80
82
|
const [info, positions] = await Promise.all([
|
|
81
83
|
this.client.getAccount(this.account),
|
|
82
84
|
this._getPositions(),
|
|
@@ -92,6 +94,8 @@ export class PacificaAdapter {
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
async getPositions() {
|
|
97
|
+
if (!this._hasRealKey)
|
|
98
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
95
99
|
const [positions, prices] = await Promise.all([
|
|
96
100
|
this._getPositions(),
|
|
97
101
|
this._getPrices(),
|
|
@@ -123,6 +127,8 @@ export class PacificaAdapter {
|
|
|
123
127
|
});
|
|
124
128
|
}
|
|
125
129
|
async getOpenOrders() {
|
|
130
|
+
if (!this._hasRealKey)
|
|
131
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
126
132
|
const orders = await this.client.getOrders(this.account);
|
|
127
133
|
return orders.map((o) => {
|
|
128
134
|
const raw = o;
|
|
@@ -220,6 +226,8 @@ export class PacificaAdapter {
|
|
|
220
226
|
}));
|
|
221
227
|
}
|
|
222
228
|
async getOrderHistory(limit = 30) {
|
|
229
|
+
if (!this._hasRealKey)
|
|
230
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
223
231
|
const result = await this.client.getOrderHistory(this.account);
|
|
224
232
|
const data = (result.data ?? result.orders ?? []);
|
|
225
233
|
if (!Array.isArray(data))
|
|
@@ -236,6 +244,8 @@ export class PacificaAdapter {
|
|
|
236
244
|
}));
|
|
237
245
|
}
|
|
238
246
|
async getTradeHistory(limit = 30) {
|
|
247
|
+
if (!this._hasRealKey)
|
|
248
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
239
249
|
const raw = await this.client.getTradeHistory(this.account);
|
|
240
250
|
const trades = (raw.data ?? raw);
|
|
241
251
|
if (!Array.isArray(trades))
|
|
@@ -250,6 +260,8 @@ export class PacificaAdapter {
|
|
|
250
260
|
}));
|
|
251
261
|
}
|
|
252
262
|
async getFundingPayments(limit = 30) {
|
|
263
|
+
if (!this._hasRealKey)
|
|
264
|
+
throw new Error("No private key configured — account data unavailable. Run: perp init");
|
|
253
265
|
const raw = await this.client.getFundingAccountHistory(this.account);
|
|
254
266
|
const history = (raw.data ?? raw);
|
|
255
267
|
if (!Array.isArray(history))
|
package/dist/execution-log.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ExecutionRecord {
|
|
2
2
|
id: string;
|
|
3
3
|
timestamp: string;
|
|
4
|
-
type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback";
|
|
4
|
+
type: "market_order" | "limit_order" | "cancel" | "stop_order" | "edit_order" | "rebalance" | "arb_entry" | "arb_close" | "bridge" | "multi_leg" | "multi_leg_rollback" | "split_order";
|
|
5
5
|
exchange: string;
|
|
6
6
|
symbol: string;
|
|
7
7
|
side: string;
|
package/dist/index.js
CHANGED
|
@@ -15,34 +15,27 @@ import { registerMarketCommands } from "./commands/market.js";
|
|
|
15
15
|
import { registerAccountCommands } from "./commands/account.js";
|
|
16
16
|
import { registerTradeCommands } from "./commands/trade.js";
|
|
17
17
|
import { registerManageCommands } from "./commands/manage.js";
|
|
18
|
-
|
|
18
|
+
// stream commands removed — WS feeds still used by dashboard/event-stream internally
|
|
19
19
|
import { registerArbCommands } from "./commands/arb.js";
|
|
20
20
|
import { registerWalletCommands } from "./commands/wallet.js";
|
|
21
21
|
import { registerBridgeCommands } from "./commands/bridge.js";
|
|
22
|
-
|
|
23
|
-
import {
|
|
22
|
+
// deposit + withdraw merged into funds
|
|
23
|
+
import { registerFundsCommands } from "./commands/funds.js";
|
|
24
|
+
// alert commands removed
|
|
24
25
|
import { registerArbAutoCommands } from "./commands/arb-auto.js";
|
|
25
|
-
import { registerArbManageCommands } from "./commands/arb
|
|
26
|
-
import { registerGapCommands } from "./commands/gap.js";
|
|
26
|
+
import { registerArbManageCommands } from "./commands/arb/index.js";
|
|
27
27
|
import { registerAgentCommands } from "./commands/agent.js";
|
|
28
|
-
import { registerWithdrawCommands } from "./commands/withdraw.js";
|
|
29
28
|
import { registerRebalanceCommands } from "./commands/rebalance.js";
|
|
30
29
|
import { registerBotCommands } from "./commands/bot.js";
|
|
31
|
-
import { registerHealthCommands } from "./commands/health.js";
|
|
32
|
-
import { registerPortfolioCommands } from "./commands/portfolio.js";
|
|
33
30
|
import { registerRiskCommands } from "./commands/risk.js";
|
|
34
31
|
import { registerHistoryCommands } from "./commands/history.js";
|
|
35
|
-
import { registerAnalyticsCommands } from "./commands/analytics.js";
|
|
36
|
-
import { registerPnlCommands } from "./commands/pnl.js";
|
|
37
|
-
import { registerMultilegCommands } from "./commands/multileg.js";
|
|
38
32
|
import { registerSettingsCommands } from "./commands/settings.js";
|
|
39
|
-
|
|
33
|
+
// dex commands merged into market (hip3) — use --dex flag for markets/balance
|
|
40
34
|
import { registerPlanCommands } from "./commands/plan.js";
|
|
41
|
-
|
|
35
|
+
// funding merged into arb.ts
|
|
42
36
|
import { registerBacktestCommands } from "./commands/backtest.js";
|
|
43
37
|
import { registerDashboardCommands } from "./commands/dashboard.js";
|
|
44
38
|
import { registerInitCommand, EXCHANGE_ENV_MAP, validateKey } from "./commands/init.js";
|
|
45
|
-
import { registerEnvCommands } from "./commands/env.js";
|
|
46
39
|
import { loadSettings, saveSettings } from "./settings.js";
|
|
47
40
|
import { setSharedApiNetwork } from "./shared-api.js";
|
|
48
41
|
const _require = createRequire(import.meta.url);
|
|
@@ -62,6 +55,7 @@ program
|
|
|
62
55
|
.option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
|
|
63
56
|
.option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
|
|
64
57
|
.option("--dry-run", "Simulate trades without executing (log as simulated)")
|
|
58
|
+
.option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
|
|
65
59
|
.option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
|
|
66
60
|
.configureOutput({
|
|
67
61
|
writeErr: (str) => {
|
|
@@ -96,14 +90,16 @@ function getExchange() {
|
|
|
96
90
|
return resolveExchangeAlias(program.opts().exchange);
|
|
97
91
|
}
|
|
98
92
|
async function getAdapter() {
|
|
99
|
-
if (_adapter)
|
|
100
|
-
return _adapter;
|
|
101
93
|
const opts = program.opts();
|
|
94
|
+
const walletName = opts.wallet;
|
|
95
|
+
// Skip cache when --wallet is specified (different wallet = different account)
|
|
96
|
+
if (!walletName && _adapter)
|
|
97
|
+
return _adapter;
|
|
102
98
|
const exchange = resolveExchangeAlias(opts.exchange);
|
|
103
99
|
const network = opts.network;
|
|
104
100
|
const isTestnet = network === "testnet";
|
|
105
101
|
// Try to load key — null means no key configured (read-only mode)
|
|
106
|
-
const pk = await tryLoadPrivateKey(exchange, opts.privateKey);
|
|
102
|
+
const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
|
|
107
103
|
switch (exchange) {
|
|
108
104
|
case "pacifica": {
|
|
109
105
|
const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
|
|
@@ -193,22 +189,23 @@ function getHLAdapter() {
|
|
|
193
189
|
}
|
|
194
190
|
// Register command groups with async adapter getter
|
|
195
191
|
registerMarketCommands(program, getAdapter, isJson);
|
|
196
|
-
registerAccountCommands(program, getAdapter, isJson);
|
|
197
|
-
registerTradeCommands(program, getAdapter, isJson, isDryRun);
|
|
192
|
+
registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
193
|
+
registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
|
|
198
194
|
registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
|
|
199
|
-
|
|
200
|
-
registerArbCommands(program, isJson);
|
|
195
|
+
// stream commands removed
|
|
196
|
+
registerArbCommands(program, isJson, getAdapterForExchange);
|
|
201
197
|
registerWalletCommands(program, isJson);
|
|
202
198
|
registerBridgeCommands(program, isJson);
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
|
|
200
|
+
// alert commands removed
|
|
205
201
|
// Helper to get adapter for a specific exchange (used by arb-auto)
|
|
206
202
|
async function getAdapterForExchange(rawExchange) {
|
|
207
203
|
const exchange = resolveExchangeAlias(rawExchange);
|
|
208
204
|
const opts = program.opts();
|
|
209
205
|
const network = opts.network;
|
|
210
206
|
const isTestnet = network === "testnet";
|
|
211
|
-
const
|
|
207
|
+
const walletName = opts.wallet;
|
|
208
|
+
const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
|
|
212
209
|
switch (exchange) {
|
|
213
210
|
case "pacifica": {
|
|
214
211
|
if (_pacificaAdapter)
|
|
@@ -287,7 +284,8 @@ async function getHLAdapterForDex(dex) {
|
|
|
287
284
|
if (_dexAdapters.has(dex))
|
|
288
285
|
return _dexAdapters.get(dex);
|
|
289
286
|
const opts = program.opts();
|
|
290
|
-
const
|
|
287
|
+
const walletName = opts.wallet;
|
|
288
|
+
const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey, walletName);
|
|
291
289
|
const adapter = new HyperliquidAdapter(pk ?? undefined, opts.network === "testnet");
|
|
292
290
|
if (dex !== "hl")
|
|
293
291
|
adapter.setDex(dex);
|
|
@@ -297,125 +295,43 @@ async function getHLAdapterForDex(dex) {
|
|
|
297
295
|
}
|
|
298
296
|
registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
|
|
299
297
|
registerArbManageCommands(program, getAdapterForExchange, isJson);
|
|
300
|
-
registerGapCommands(program, isJson);
|
|
301
298
|
registerAgentCommands(program, getAdapter, isJson);
|
|
302
|
-
|
|
299
|
+
// withdraw merged into funds
|
|
303
300
|
registerRebalanceCommands(program, getAdapterForExchange, isJson);
|
|
304
301
|
// Jobs & strategies
|
|
305
302
|
import { registerJobsCommands } from "./commands/jobs.js";
|
|
306
|
-
import { registerRunCommands } from "./commands/run.js";
|
|
307
303
|
registerJobsCommands(program, isJson);
|
|
308
|
-
registerRunCommands(program, getAdapter, getAdapterForExchange, isJson);
|
|
309
304
|
registerBotCommands(program, getAdapter, getAdapterForExchange, isJson);
|
|
310
305
|
// Agent-friendly commands
|
|
311
|
-
registerHealthCommands(program, isJson);
|
|
312
|
-
registerPortfolioCommands(program, getAdapterForExchange, isJson);
|
|
313
306
|
registerRiskCommands(program, getAdapterForExchange, isJson);
|
|
314
|
-
registerHistoryCommands(program, isJson);
|
|
315
|
-
registerAnalyticsCommands(program, getAdapterForExchange, isJson);
|
|
316
|
-
registerPnlCommands(program, getAdapterForExchange, isJson);
|
|
317
|
-
registerMultilegCommands(program, getAdapterForExchange, isJson);
|
|
307
|
+
registerHistoryCommands(program, isJson, getAdapterForExchange);
|
|
318
308
|
registerSettingsCommands(program, isJson, getAdapterForExchange);
|
|
319
|
-
|
|
309
|
+
// dex commands removed — use 'market hip3' + --dex flag
|
|
320
310
|
registerPlanCommands(program, getAdapter, isJson);
|
|
321
|
-
registerFundingCommands
|
|
311
|
+
// funding merged into arb — registerFundingCommands removed
|
|
322
312
|
registerBacktestCommands(program, isJson);
|
|
323
313
|
registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
|
|
324
314
|
registerInitCommand(program);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
program
|
|
315
|
+
// Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
|
|
316
|
+
const apiSpecCmd = program
|
|
328
317
|
.command("api-spec")
|
|
329
|
-
.description("
|
|
318
|
+
.description("Use 'perp agent schema'")
|
|
330
319
|
.action(async () => {
|
|
331
320
|
const { jsonOk, printJson } = await import("./utils.js");
|
|
332
321
|
const { getCliSpec } = await import("./cli-spec.js");
|
|
333
322
|
printJson(jsonOk(getCliSpec(program)));
|
|
334
323
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
// Use allSettled to handle unfunded/404 accounts gracefully
|
|
344
|
-
const [balanceResult, positionsResult, ordersResult] = await Promise.allSettled([
|
|
345
|
-
adapter.getBalance(),
|
|
346
|
-
adapter.getPositions(),
|
|
347
|
-
adapter.getOpenOrders(),
|
|
348
|
-
]);
|
|
349
|
-
const balance = balanceResult.status === "fulfilled" ? balanceResult.value : { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
|
|
350
|
-
const positions = positionsResult.status === "fulfilled" ? positionsResult.value : [];
|
|
351
|
-
const orders = ordersResult.status === "fulfilled" ? ordersResult.value : [];
|
|
352
|
-
// Collect errors for reporting
|
|
353
|
-
const errors = [];
|
|
354
|
-
if (balanceResult.status === "rejected")
|
|
355
|
-
errors.push(`balance: ${balanceResult.reason instanceof Error ? balanceResult.reason.message : String(balanceResult.reason)}`);
|
|
356
|
-
if (positionsResult.status === "rejected")
|
|
357
|
-
errors.push(`positions: ${positionsResult.reason instanceof Error ? positionsResult.reason.message : String(positionsResult.reason)}`);
|
|
358
|
-
if (ordersResult.status === "rejected")
|
|
359
|
-
errors.push(`orders: ${ordersResult.reason instanceof Error ? ordersResult.reason.message : String(ordersResult.reason)}`);
|
|
360
|
-
// Detect unfunded account (all calls failed, likely 404)
|
|
361
|
-
const allFailed = errors.length === 3;
|
|
362
|
-
const isUnfunded = allFailed && errors.some(e => e.includes("404") || e.includes("not found") || e.includes("does not exist"));
|
|
363
|
-
if (json) {
|
|
364
|
-
const { jsonOk, printJson } = await import("./utils.js");
|
|
365
|
-
return printJson(jsonOk({
|
|
366
|
-
exchange: adapter.name,
|
|
367
|
-
balance,
|
|
368
|
-
positions,
|
|
369
|
-
orders,
|
|
370
|
-
...(isUnfunded ? { warning: "Account not yet initialized on this exchange. Deposit funds to get started." } : {}),
|
|
371
|
-
...(errors.length > 0 && !isUnfunded ? { errors } : {}),
|
|
372
|
-
}));
|
|
373
|
-
}
|
|
374
|
-
console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Status\n`));
|
|
375
|
-
if (isUnfunded) {
|
|
376
|
-
console.log(chalk.yellow(" Account not yet initialized on this exchange."));
|
|
377
|
-
console.log(chalk.gray(" Deposit funds to get started: perp deposit <exchange> <amount>\n"));
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
console.log(` Equity: $${Number(balance.equity).toFixed(2)}`);
|
|
381
|
-
console.log(` Available: $${Number(balance.available).toFixed(2)}`);
|
|
382
|
-
console.log(` Margin Used: $${Number(balance.marginUsed).toFixed(2)}`);
|
|
383
|
-
console.log(` Positions: ${positions.length}`);
|
|
384
|
-
console.log(` Open Orders: ${orders.length}`);
|
|
385
|
-
if (positions.length > 0) {
|
|
386
|
-
console.log(chalk.cyan.bold("\n Positions:"));
|
|
387
|
-
positions.forEach((p) => {
|
|
388
|
-
const color = p.side === "long" ? chalk.green : chalk.red;
|
|
389
|
-
const pnlNum = Number(p.unrealizedPnl);
|
|
390
|
-
const pnlColor = pnlNum >= 0 ? chalk.green : chalk.red;
|
|
391
|
-
console.log(` ${color(p.side.toUpperCase().padEnd(5))} ${chalk.white(p.symbol.padEnd(12))} ${p.size.padEnd(10)} entry: $${Number(p.entryPrice).toFixed(2)} pnl: ${pnlColor(pnlNum >= 0 ? "+" : "")}$${pnlNum.toFixed(2)}`);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
if (orders.length > 0) {
|
|
395
|
-
console.log(chalk.cyan.bold("\n Open Orders:"));
|
|
396
|
-
orders.forEach((o) => {
|
|
397
|
-
const color = o.side === "buy" ? chalk.green : chalk.red;
|
|
398
|
-
console.log(` ${color(o.side.toUpperCase().padEnd(4))} ${chalk.white(o.symbol.padEnd(12))} ${o.type.padEnd(8)} $${Number(o.price).toFixed(2)} x ${o.size}`);
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
if (errors.length > 0) {
|
|
402
|
-
console.log(chalk.yellow(`\n Warnings: ${errors.join(", ")}`));
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
console.log();
|
|
406
|
-
}
|
|
407
|
-
catch (err) {
|
|
408
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
409
|
-
if (isJson()) {
|
|
410
|
-
const { jsonError } = await import("./utils.js");
|
|
411
|
-
console.log(JSON.stringify(jsonError("COMMAND_ERROR", msg)));
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
console.error(chalk.red(`Error: ${msg}`));
|
|
415
|
-
}
|
|
416
|
-
process.exit(1);
|
|
324
|
+
apiSpecCmd._hidden = true;
|
|
325
|
+
// Deprecated: perp status → use 'perp portfolio' or 'perp account'
|
|
326
|
+
const statusCmd = program.command("status").description("Use 'perp portfolio'")
|
|
327
|
+
.option("--health", "Check connectivity").action(async (opts) => {
|
|
328
|
+
if (opts.health) {
|
|
329
|
+
const { runHealthCheck } = await import("./commands/risk.js");
|
|
330
|
+
return runHealthCheck(isJson);
|
|
417
331
|
}
|
|
332
|
+
console.log("Use 'perp portfolio' or 'perp account' instead.");
|
|
418
333
|
});
|
|
334
|
+
statusCmd._hidden = true;
|
|
419
335
|
// Switch shared API URLs if --network testnet is used
|
|
420
336
|
program.hook("preAction", () => {
|
|
421
337
|
const network = program.opts().network;
|