perp-cli 0.4.2 → 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.
Files changed (68) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/cli-spec.d.ts +0 -4
  6. package/dist/cli-spec.js +8 -1
  7. package/dist/commands/agent.js +7 -4
  8. package/dist/commands/arb-auto.js +643 -4
  9. package/dist/commands/dex.js +7 -1
  10. package/dist/commands/funding.d.ts +2 -1
  11. package/dist/commands/funding.js +211 -4
  12. package/dist/commands/manage.js +1 -1
  13. package/dist/commands/market.js +1 -1
  14. package/dist/commands/multileg.d.ts +16 -0
  15. package/dist/commands/multileg.js +174 -0
  16. package/dist/commands/pnl.d.ts +12 -0
  17. package/dist/commands/pnl.js +243 -0
  18. package/dist/commands/stream.js +132 -55
  19. package/dist/commands/trade.js +29 -8
  20. package/dist/commands/wallet.js +1 -1
  21. package/dist/config.d.ts +5 -0
  22. package/dist/config.js +12 -0
  23. package/dist/dashboard/ws-feeds.js +1 -1
  24. package/dist/equity-tracker.d.ts +57 -0
  25. package/dist/equity-tracker.js +209 -0
  26. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  27. package/dist/exchanges/hyperliquid-spot.js +317 -0
  28. package/dist/exchanges/hyperliquid.d.ts +26 -5
  29. package/dist/exchanges/hyperliquid.js +171 -73
  30. package/dist/exchanges/interface.d.ts +1 -1
  31. package/dist/exchanges/lighter-spot.d.ts +55 -0
  32. package/dist/exchanges/lighter-spot.js +417 -0
  33. package/dist/exchanges/lighter.d.ts +10 -2
  34. package/dist/exchanges/lighter.js +128 -35
  35. package/dist/exchanges/pacifica.d.ts +11 -4
  36. package/dist/exchanges/pacifica.js +52 -37
  37. package/dist/exchanges/spot-interface.d.ts +45 -0
  38. package/dist/exchanges/spot-interface.js +22 -0
  39. package/dist/execution-log.d.ts +2 -2
  40. package/dist/funding/rates.d.ts +36 -0
  41. package/dist/funding/rates.js +146 -0
  42. package/dist/index.js +147 -98
  43. package/dist/liquidity.d.ts +12 -0
  44. package/dist/liquidity.js +43 -0
  45. package/dist/signer/evm-local.d.ts +12 -0
  46. package/dist/signer/evm-local.js +19 -0
  47. package/dist/signer/index.d.ts +3 -0
  48. package/dist/signer/index.js +2 -0
  49. package/dist/signer/interface.d.ts +31 -0
  50. package/dist/signer/interface.js +7 -0
  51. package/dist/signer/solana-local.d.ts +13 -0
  52. package/dist/signer/solana-local.js +19 -0
  53. package/dist/smart-order.js +14 -3
  54. package/dist/utils.js +7 -1
  55. package/dist/ws/market-feed.d.ts +74 -0
  56. package/dist/ws/market-feed.js +227 -0
  57. package/package.json +1 -1
  58. package/skills/perp-cli/SKILL.md +93 -4
  59. package/skills/perp-cli/references/commands.md +16 -1
  60. package/skills/perp-cli/references/strategies.md +1 -1
  61. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  62. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  63. package/skills/perp-cli/scripts/preflight.sh +98 -0
  64. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  65. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  66. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  67. package/skills/perp-cli/templates/open-position.sh +0 -38
  68. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -43,3 +43,39 @@ export declare function fetchAllFundingRates(opts?: {
43
43
  * Fetch rates for a single symbol across all exchanges.
44
44
  */
45
45
  export declare function fetchSymbolFundingRates(symbol: string): Promise<SymbolFundingComparison | null>;
46
+ export interface SpotPerpSpread {
47
+ symbol: string;
48
+ /** Perp exchange with the highest absolute funding rate */
49
+ perpExchange: string;
50
+ /** Spot exchange(s) available for the hedge leg */
51
+ spotExchanges: string[];
52
+ /** Perp funding rate (raw) */
53
+ perpFundingRate: number;
54
+ /** Perp funding hourly rate */
55
+ perpHourlyRate: number;
56
+ /** Annualized spread % (spot funding = 0, so spread = |perp annual rate|) */
57
+ annualSpreadPct: number;
58
+ /** Best mark price */
59
+ bestMarkPrice: number;
60
+ /** Direction: "long-spot-short-perp" when perp funding > 0, else "sell-spot-long-perp" */
61
+ direction: "long-spot-short-perp" | "sell-spot-long-perp";
62
+ /** Estimated hourly income for $1000 notional */
63
+ estHourlyIncomeUsd: number;
64
+ }
65
+ /**
66
+ * Fetch spot+perp funding rate spreads.
67
+ * Since spot has 0 funding cost, the spread is simply |perp funding rate|.
68
+ * Only includes symbols available on at least one spot exchange (HL or LT).
69
+ *
70
+ * Key safety checks:
71
+ * - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
72
+ * - Price validation: spot mid price must be within 5% of perp mark price
73
+ * (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
74
+ */
75
+ export declare function fetchSpotPerpSpreads(opts?: {
76
+ symbols?: string[];
77
+ minSpread?: number;
78
+ }): Promise<{
79
+ timestamp: string;
80
+ spreads: SpotPerpSpread[];
81
+ }>;
@@ -8,6 +8,7 @@
8
8
  import { toHourlyRate, annualizeRate, computeAnnualSpread, estimateHourlyFunding } from "./normalize.js";
9
9
  import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates as fetchLtFundingRates, } from "../shared-api.js";
10
10
  import { getHistoricalAverages } from "./history.js";
11
+ import { SPOT_PERP_TOKEN_MAP, MAX_PRICE_DEVIATION_PCT } from "../exchanges/spot-interface.js";
11
12
  // ── Default top symbols to track ──
12
13
  export const TOP_SYMBOLS = [
13
14
  "BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
@@ -170,3 +171,148 @@ export async function fetchSymbolFundingRates(symbol) {
170
171
  const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()] });
171
172
  return snapshot.symbols[0] ?? null;
172
173
  }
174
+ /**
175
+ * Fetch spot+perp funding rate spreads.
176
+ * Since spot has 0 funding cost, the spread is simply |perp funding rate|.
177
+ * Only includes symbols available on at least one spot exchange (HL or LT).
178
+ *
179
+ * Key safety checks:
180
+ * - U-token mapping: UBTC→BTC, UETH→ETH, USOL→SOL, UFART→FARTCOIN
181
+ * - Price validation: spot mid price must be within 5% of perp mark price
182
+ * (filters out same-ticker-different-token issues like HIP-1 TRUMP ≠ perp TRUMP)
183
+ */
184
+ export async function fetchSpotPerpSpreads(opts) {
185
+ // Fetch all perp funding rates
186
+ const [hlRates, ltRates, pacRates] = await Promise.all([
187
+ fetchHyperliquidRates(),
188
+ fetchLighterRates(),
189
+ fetchPacificaRates(),
190
+ ]);
191
+ // Build actual spot availability maps + spot prices for cross-validation
192
+ // hlSpotByPerp: perpSymbol → { spotToken, exchange }
193
+ const hlSpotByPerp = new Map(); // perpSymbol → spotTokenName
194
+ const hlSpotPrices = new Map(); // perpSymbol → spot mid price
195
+ const ltSpotSymbols = new Set();
196
+ const ltSpotPrices = new Map(); // symbol → spot mid price
197
+ try {
198
+ // HL: spotMetaAndAssetCtxs → universe + prices in one call
199
+ const hlMetaCtx = await fetch("https://api.hyperliquid.xyz/info", {
200
+ method: "POST", headers: { "Content-Type": "application/json" },
201
+ body: JSON.stringify({ type: "spotMetaAndAssetCtxs" }),
202
+ }).then(r => r.json());
203
+ const meta = hlMetaCtx[0];
204
+ const ctxs = hlMetaCtx[1] ?? [];
205
+ const tokenNames = new Map();
206
+ for (const t of meta.tokens ?? [])
207
+ tokenNames.set(t.index, t.name);
208
+ const usdcIdx = meta.tokens?.find(t => t.name === "USDC")?.index ?? 0;
209
+ // We need to match universe entries to ctxs by index.
210
+ // API returns ctxs in same order as universe entries.
211
+ for (let i = 0; i < (meta.universe ?? []).length; i++) {
212
+ const u = meta.universe[i];
213
+ if (u.tokens[1] !== usdcIdx)
214
+ continue; // only /USDC pairs
215
+ const base = tokenNames.get(u.tokens[0])?.toUpperCase();
216
+ if (!base)
217
+ continue;
218
+ const ctx = ctxs[i];
219
+ const spotPrice = Number(ctx?.midPx ?? ctx?.markPx ?? 0);
220
+ // Map to perp symbol: UBTC→BTC, or direct if no mapping
221
+ const perpSymbol = SPOT_PERP_TOKEN_MAP[base] ?? base;
222
+ // Only keep the best-priced entry per perp symbol (prefer higher-volume)
223
+ if (!hlSpotByPerp.has(perpSymbol) || spotPrice > 0) {
224
+ hlSpotByPerp.set(perpSymbol, base);
225
+ if (spotPrice > 0)
226
+ hlSpotPrices.set(perpSymbol, spotPrice);
227
+ }
228
+ }
229
+ }
230
+ catch { /* non-critical */ }
231
+ try {
232
+ // LT: explorer API → symbols with "/"
233
+ const ltMarkets = await fetch("https://explorer.elliot.ai/api/markets")
234
+ .then(r => r.json());
235
+ for (const m of ltMarkets) {
236
+ if (m.symbol.includes("/")) {
237
+ ltSpotSymbols.add(m.symbol.split("/")[0].toUpperCase());
238
+ }
239
+ }
240
+ // Fetch LT spot prices from orderbook for price validation
241
+ for (const sym of ltSpotSymbols) {
242
+ try {
243
+ const book = await fetch(`https://mainnet.zklighter.elliot.ai/api/v1/orderBookOrders?market_id=${
244
+ // We don't have market_id here, skip detailed price fetch for LT
245
+ // LT tokens (ETH, LINK, etc.) are direct names, low risk of mismatch
246
+ ""}`);
247
+ // Skip — LT uses direct token names, not indices
248
+ }
249
+ catch { /* skip */ }
250
+ }
251
+ }
252
+ catch { /* non-critical */ }
253
+ // Merge all perp rates by symbol
254
+ const perpRateMap = new Map();
255
+ for (const r of [...hlRates, ...ltRates, ...pacRates]) {
256
+ if (!r.symbol)
257
+ continue;
258
+ if (opts?.symbols && !opts.symbols.includes(r.symbol.toUpperCase()))
259
+ continue;
260
+ const key = r.symbol.toUpperCase();
261
+ if (!perpRateMap.has(key))
262
+ perpRateMap.set(key, []);
263
+ perpRateMap.get(key).push(r);
264
+ }
265
+ const spreads = [];
266
+ const minSpread = opts?.minSpread ?? 0;
267
+ for (const [symbol, rates] of perpRateMap) {
268
+ rates.sort((a, b) => Math.abs(b.hourlyRate) - Math.abs(a.hourlyRate));
269
+ const bestPerp = rates[0];
270
+ // Spot exchanges: only include if symbol has a verified spot market
271
+ const uniqueSpot = [];
272
+ // HL spot: check U-token mapped availability
273
+ if (hlSpotByPerp.has(symbol)) {
274
+ // Price cross-validation: spot price must be close to perp mark price
275
+ const spotPrice = hlSpotPrices.get(symbol);
276
+ const perpPrice = bestPerp.markPrice;
277
+ if (spotPrice && perpPrice > 0) {
278
+ const deviation = Math.abs(spotPrice - perpPrice) / perpPrice * 100;
279
+ if (deviation <= MAX_PRICE_DEVIATION_PCT) {
280
+ uniqueSpot.push("hyperliquid");
281
+ }
282
+ // else: same ticker, different token (e.g., HIP-1 TRUMP ≠ perp TRUMP) → skip
283
+ }
284
+ else if (spotPrice === undefined) {
285
+ // No price data (empty orderbook) → skip, can't verify
286
+ }
287
+ }
288
+ // LT spot: direct token names, low mismatch risk
289
+ if (ltSpotSymbols.has(symbol))
290
+ uniqueSpot.push("lighter");
291
+ if (uniqueSpot.length === 0)
292
+ continue;
293
+ const annualSpreadPct = Math.abs(bestPerp.annualizedPct);
294
+ if (annualSpreadPct < minSpread)
295
+ continue;
296
+ const direction = bestPerp.hourlyRate > 0
297
+ ? "long-spot-short-perp"
298
+ : "sell-spot-long-perp";
299
+ const notional = 1000;
300
+ const estHourlyIncomeUsd = Math.abs(bestPerp.hourlyRate) * notional;
301
+ spreads.push({
302
+ symbol,
303
+ perpExchange: bestPerp.exchange,
304
+ spotExchanges: uniqueSpot,
305
+ perpFundingRate: bestPerp.fundingRate,
306
+ perpHourlyRate: bestPerp.hourlyRate,
307
+ annualSpreadPct,
308
+ bestMarkPrice: bestPerp.markPrice,
309
+ direction,
310
+ estHourlyIncomeUsd,
311
+ });
312
+ }
313
+ spreads.sort((a, b) => b.annualSpreadPct - a.annualSpreadPct);
314
+ return {
315
+ timestamp: new Date().toISOString(),
316
+ spreads,
317
+ };
318
+ }
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
7
7
  config();
8
8
  import { Command } from "commander";
9
9
  import chalk from "chalk";
10
- import { loadPrivateKey, parseSolanaKeypair } from "./config.js";
10
+ import { Keypair } from "@solana/web3.js";
11
+ import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
11
12
  import { PacificaAdapter } from "./exchanges/pacifica.js";
12
13
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
13
14
  import { registerMarketCommands } from "./commands/market.js";
@@ -32,6 +33,8 @@ import { registerPortfolioCommands } from "./commands/portfolio.js";
32
33
  import { registerRiskCommands } from "./commands/risk.js";
33
34
  import { registerHistoryCommands } from "./commands/history.js";
34
35
  import { registerAnalyticsCommands } from "./commands/analytics.js";
36
+ import { registerPnlCommands } from "./commands/pnl.js";
37
+ import { registerMultilegCommands } from "./commands/multileg.js";
35
38
  import { registerSettingsCommands } from "./commands/settings.js";
36
39
  import { registerDexCommands } from "./commands/dex.js";
37
40
  import { registerPlanCommands } from "./commands/plan.js";
@@ -80,71 +83,81 @@ let _adapter = null;
80
83
  let _pacificaAdapter = null;
81
84
  let _hlAdapter = null;
82
85
  let _lighterAdapter = null;
86
+ /** Map short aliases to canonical exchange names. Full names pass through. */
87
+ function resolveExchangeAlias(name) {
88
+ const aliases = {
89
+ hl: "hyperliquid",
90
+ lt: "lighter",
91
+ pac: "pacifica",
92
+ };
93
+ return aliases[name.toLowerCase()] ?? name.toLowerCase();
94
+ }
83
95
  function getExchange() {
84
- return program.opts().exchange;
96
+ return resolveExchangeAlias(program.opts().exchange);
85
97
  }
86
98
  async function getAdapter() {
87
99
  if (_adapter)
88
100
  return _adapter;
89
101
  const opts = program.opts();
90
- const exchange = opts.exchange;
102
+ const exchange = resolveExchangeAlias(opts.exchange);
91
103
  const network = opts.network;
92
104
  const isTestnet = network === "testnet";
105
+ // Try to load key — null means no key configured (read-only mode)
106
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey);
93
107
  switch (exchange) {
94
108
  case "pacifica": {
95
- const pk = await loadPrivateKey("pacifica", opts.privateKey);
96
- const keypair = parseSolanaKeypair(pk);
109
+ const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
97
110
  const pacNetwork = (isTestnet ? "testnet" : "mainnet");
98
111
  const settings = loadSettings();
99
112
  const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
100
- _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode);
113
+ _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
101
114
  _adapter = _pacificaAdapter;
102
115
  break;
103
116
  }
104
117
  case "hyperliquid": {
105
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
106
- _hlAdapter = new HyperliquidAdapter(pk, isTestnet);
118
+ _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
107
119
  if (opts.dex)
108
120
  _hlAdapter.setDex(opts.dex);
109
121
  await _hlAdapter.init();
110
- const hlSettings = loadSettings();
111
- if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
112
- const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
113
- if (hlRef) {
114
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
115
- const s = loadSettings();
116
- s.referralApplied.hyperliquid = true;
117
- saveSettings(s);
118
- }).catch(() => {
119
- // Already referred or API error — mark as done either way
120
- const s = loadSettings();
121
- s.referralApplied.hyperliquid = true;
122
- saveSettings(s);
123
- });
122
+ if (pk) {
123
+ const hlSettings = loadSettings();
124
+ if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
125
+ const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
126
+ if (hlRef) {
127
+ _hlAdapter.autoSetReferrer(hlRef).then(() => {
128
+ const s = loadSettings();
129
+ s.referralApplied.hyperliquid = true;
130
+ saveSettings(s);
131
+ }).catch(() => {
132
+ const s = loadSettings();
133
+ s.referralApplied.hyperliquid = true;
134
+ saveSettings(s);
135
+ });
136
+ }
124
137
  }
125
138
  }
126
139
  _adapter = _hlAdapter;
127
140
  break;
128
141
  }
129
142
  case "lighter": {
130
- const pk = await loadPrivateKey("lighter", opts.privateKey);
131
143
  const { LighterAdapter } = await import("./exchanges/lighter.js");
132
- _lighterAdapter = new LighterAdapter(pk, isTestnet);
144
+ _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
133
145
  await _lighterAdapter.init();
134
- const ltSettings = loadSettings();
135
- if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
136
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
137
- if (ltRef) {
138
- _lighterAdapter.useReferralCode(ltRef).then(() => {
139
- const s = loadSettings();
140
- s.referralApplied.lighter = true;
141
- saveSettings(s);
142
- }).catch(() => {
143
- // Already referred or API error — mark as done either way
144
- const s = loadSettings();
145
- s.referralApplied.lighter = true;
146
- saveSettings(s);
147
- });
146
+ if (pk) {
147
+ const ltSettings = loadSettings();
148
+ if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
149
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
150
+ if (ltRef) {
151
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
152
+ const s = loadSettings();
153
+ s.referralApplied.lighter = true;
154
+ saveSettings(s);
155
+ }).catch(() => {
156
+ const s = loadSettings();
157
+ s.referralApplied.lighter = true;
158
+ saveSettings(s);
159
+ });
160
+ }
148
161
  }
149
162
  }
150
163
  _adapter = _lighterAdapter;
@@ -162,7 +175,7 @@ function getAdapterSync() {
162
175
  return _adapter;
163
176
  }
164
177
  function isJson() {
165
- return !!program.opts().json;
178
+ return !!program.opts().json || process.argv.includes("--ndjson");
166
179
  }
167
180
  function isDryRun() {
168
181
  return !!program.opts().dryRun;
@@ -190,20 +203,21 @@ registerBridgeCommands(program, isJson);
190
203
  registerDepositCommands(program, getAdapter, isJson, () => program.opts().network);
191
204
  registerAlertCommands(program, isJson, getAdapterForExchange);
192
205
  // Helper to get adapter for a specific exchange (used by arb-auto)
193
- async function getAdapterForExchange(exchange) {
206
+ async function getAdapterForExchange(rawExchange) {
207
+ const exchange = resolveExchangeAlias(rawExchange);
194
208
  const opts = program.opts();
195
209
  const network = opts.network;
196
210
  const isTestnet = network === "testnet";
211
+ const pk = await tryLoadPrivateKey(exchange, opts.privateKey);
197
212
  switch (exchange) {
198
213
  case "pacifica": {
199
214
  if (_pacificaAdapter)
200
215
  return _pacificaAdapter;
201
- const pk = await loadPrivateKey("pacifica", opts.privateKey);
202
- const keypair = parseSolanaKeypair(pk);
216
+ const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
203
217
  const pacNetwork = (isTestnet ? "testnet" : "mainnet");
204
218
  const s1 = loadSettings();
205
219
  const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
206
- _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode);
220
+ _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
207
221
  if (!_adapter)
208
222
  _adapter = _pacificaAdapter;
209
223
  return _pacificaAdapter;
@@ -211,24 +225,25 @@ async function getAdapterForExchange(exchange) {
211
225
  case "hyperliquid": {
212
226
  if (_hlAdapter)
213
227
  return _hlAdapter;
214
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
215
- _hlAdapter = new HyperliquidAdapter(pk, isTestnet);
228
+ _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
216
229
  if (opts.dex)
217
230
  _hlAdapter.setDex(opts.dex);
218
231
  await _hlAdapter.init();
219
- const s2 = loadSettings();
220
- if (s2.referrals && !s2.referralApplied.hyperliquid) {
221
- const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
222
- if (hlRef) {
223
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
224
- const s = loadSettings();
225
- s.referralApplied.hyperliquid = true;
226
- saveSettings(s);
227
- }).catch(() => {
228
- const s = loadSettings();
229
- s.referralApplied.hyperliquid = true;
230
- saveSettings(s);
231
- });
232
+ if (pk) {
233
+ const s2 = loadSettings();
234
+ if (s2.referrals && !s2.referralApplied.hyperliquid) {
235
+ const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
236
+ if (hlRef) {
237
+ _hlAdapter.autoSetReferrer(hlRef).then(() => {
238
+ const s = loadSettings();
239
+ s.referralApplied.hyperliquid = true;
240
+ saveSettings(s);
241
+ }).catch(() => {
242
+ const s = loadSettings();
243
+ s.referralApplied.hyperliquid = true;
244
+ saveSettings(s);
245
+ });
246
+ }
232
247
  }
233
248
  }
234
249
  if (!_adapter)
@@ -238,23 +253,24 @@ async function getAdapterForExchange(exchange) {
238
253
  case "lighter": {
239
254
  if (_lighterAdapter)
240
255
  return _lighterAdapter;
241
- const pk = await loadPrivateKey("lighter", opts.privateKey);
242
256
  const { LighterAdapter } = await import("./exchanges/lighter.js");
243
- _lighterAdapter = new LighterAdapter(pk, isTestnet);
257
+ _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
244
258
  await _lighterAdapter.init();
245
- const s3 = loadSettings();
246
- if (s3.referrals && !s3.referralApplied.lighter) {
247
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
248
- if (ltRef) {
249
- _lighterAdapter.useReferralCode(ltRef).then(() => {
250
- const s = loadSettings();
251
- s.referralApplied.lighter = true;
252
- saveSettings(s);
253
- }).catch(() => {
254
- const s = loadSettings();
255
- s.referralApplied.lighter = true;
256
- saveSettings(s);
257
- });
259
+ if (pk) {
260
+ const s3 = loadSettings();
261
+ if (s3.referrals && !s3.referralApplied.lighter) {
262
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
263
+ if (ltRef) {
264
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
265
+ const s = loadSettings();
266
+ s.referralApplied.lighter = true;
267
+ saveSettings(s);
268
+ }).catch(() => {
269
+ const s = loadSettings();
270
+ s.referralApplied.lighter = true;
271
+ saveSettings(s);
272
+ });
273
+ }
258
274
  }
259
275
  }
260
276
  if (!_adapter)
@@ -271,8 +287,8 @@ async function getHLAdapterForDex(dex) {
271
287
  if (_dexAdapters.has(dex))
272
288
  return _dexAdapters.get(dex);
273
289
  const opts = program.opts();
274
- const pk = await loadPrivateKey("hyperliquid", opts.privateKey);
275
- const adapter = new HyperliquidAdapter(pk, opts.network === "testnet");
290
+ const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey);
291
+ const adapter = new HyperliquidAdapter(pk ?? undefined, opts.network === "testnet");
276
292
  if (dex !== "hl")
277
293
  adapter.setDex(dex);
278
294
  await adapter.init();
@@ -297,10 +313,12 @@ registerPortfolioCommands(program, getAdapterForExchange, isJson);
297
313
  registerRiskCommands(program, getAdapterForExchange, isJson);
298
314
  registerHistoryCommands(program, isJson);
299
315
  registerAnalyticsCommands(program, getAdapterForExchange, isJson);
316
+ registerPnlCommands(program, getAdapterForExchange, isJson);
317
+ registerMultilegCommands(program, getAdapterForExchange, isJson);
300
318
  registerSettingsCommands(program, isJson, getAdapterForExchange);
301
319
  registerDexCommands(program, getAdapter, isJson);
302
320
  registerPlanCommands(program, getAdapter, isJson);
303
- registerFundingCommands(program, isJson);
321
+ registerFundingCommands(program, isJson, getAdapterForExchange);
304
322
  registerBacktestCommands(program, isJson);
305
323
  registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
306
324
  registerInitCommand(program);
@@ -322,36 +340,67 @@ program
322
340
  const adapter = await getAdapter();
323
341
  const json = isJson();
324
342
  try {
325
- const [balance, positions, orders] = await Promise.all([
343
+ // Use allSettled to handle unfunded/404 accounts gracefully
344
+ const [balanceResult, positionsResult, ordersResult] = await Promise.allSettled([
326
345
  adapter.getBalance(),
327
346
  adapter.getPositions(),
328
347
  adapter.getOpenOrders(),
329
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"));
330
363
  if (json) {
331
364
  const { jsonOk, printJson } = await import("./utils.js");
332
- return printJson(jsonOk({ exchange: adapter.name, balance, positions, orders }));
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
+ }));
333
373
  }
334
374
  console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Status\n`));
335
- console.log(` Equity: $${Number(balance.equity).toFixed(2)}`);
336
- console.log(` Available: $${Number(balance.available).toFixed(2)}`);
337
- console.log(` Margin Used: $${Number(balance.marginUsed).toFixed(2)}`);
338
- console.log(` Positions: ${positions.length}`);
339
- console.log(` Open Orders: ${orders.length}`);
340
- if (positions.length > 0) {
341
- console.log(chalk.cyan.bold("\n Positions:"));
342
- positions.forEach((p) => {
343
- const color = p.side === "long" ? chalk.green : chalk.red;
344
- const pnlNum = Number(p.unrealizedPnl);
345
- const pnlColor = pnlNum >= 0 ? chalk.green : chalk.red;
346
- 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)}`);
347
- });
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"));
348
378
  }
349
- if (orders.length > 0) {
350
- console.log(chalk.cyan.bold("\n Open Orders:"));
351
- orders.forEach((o) => {
352
- const color = o.side === "buy" ? chalk.green : chalk.red;
353
- 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}`);
354
- });
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
+ }
355
404
  }
356
405
  console.log();
357
406
  }
@@ -375,7 +424,7 @@ program.hook("preAction", () => {
375
424
  });
376
425
  // Smart landing page: `perp` with no subcommand
377
426
  const rawArgs = process.argv.slice(2);
378
- const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "mainnet", "testnet"].includes(a));
427
+ const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "hl", "lt", "pac", "mainnet", "testnet"].includes(a));
379
428
  if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawArgs.includes("--help") && !rawArgs.includes("-V") && !rawArgs.includes("--version"))) {
380
429
  // No subcommand — show smart landing instead of help dump
381
430
  (async () => {
@@ -1,4 +1,5 @@
1
1
  import type { ExchangeAdapter } from "./exchanges/interface.js";
2
+ import type { SpotAdapter } from "./exchanges/spot-interface.js";
2
3
  export interface LiquidityCheck {
3
4
  /** Max executable size (base) within slippage tolerance */
4
5
  maxSize: number;
@@ -32,3 +33,14 @@ export declare function checkArbLiquidity(longAdapter: ExchangeAdapter, shortAda
32
33
  longSlippage: number;
33
34
  shortSlippage: number;
34
35
  }>;
36
+ /**
37
+ * Check liquidity for a spot+perp arb pair.
38
+ * Spot side uses asks (for buy) or bids (for sell).
39
+ * Perp side uses bids (for short entry) or asks (for long entry).
40
+ */
41
+ export declare function checkSpotPerpLiquidity(spotAdapter: SpotAdapter, perpAdapter: ExchangeAdapter, spotSymbol: string, perpSymbol: string, sizeUsd: number, maxSlippagePct?: number, log?: (msg: string) => void): Promise<{
42
+ viable: boolean;
43
+ adjustedSizeUsd: number;
44
+ spotSlippage: number;
45
+ perpSlippage: number;
46
+ }>;
package/dist/liquidity.js CHANGED
@@ -98,3 +98,46 @@ export async function checkArbLiquidity(longAdapter, shortAdapter, symbol, sizeU
98
98
  return { viable: false, adjustedSizeUsd: 0, longSlippage: 0, shortSlippage: 0 };
99
99
  }
100
100
  }
101
+ /**
102
+ * Check liquidity for a spot+perp arb pair.
103
+ * Spot side uses asks (for buy) or bids (for sell).
104
+ * Perp side uses bids (for short entry) or asks (for long entry).
105
+ */
106
+ export async function checkSpotPerpLiquidity(spotAdapter, perpAdapter, spotSymbol, perpSymbol, sizeUsd, maxSlippagePct = 0.5, log) {
107
+ try {
108
+ const [spotOB, perpOB] = await Promise.all([
109
+ spotAdapter.getSpotOrderbook(spotSymbol),
110
+ perpAdapter.getOrderbook(perpSymbol),
111
+ ]);
112
+ // Spot buy = consume asks; Perp short = consume bids
113
+ const spotCheck = computeExecutableSize(spotOB.asks, sizeUsd, maxSlippagePct);
114
+ const perpCheck = computeExecutableSize(perpOB.bids, sizeUsd, maxSlippagePct);
115
+ // Cross-market price gap check (spot vs perp)
116
+ if (spotOB.asks.length > 0 && perpOB.bids.length > 0) {
117
+ const bestSpotAsk = Number(spotOB.asks[0][0]);
118
+ const bestPerpBid = Number(perpOB.bids[0][0]);
119
+ const gapPct = Math.abs(bestSpotAsk - bestPerpBid) / Math.min(bestSpotAsk, bestPerpBid) * 100;
120
+ if (gapPct > 2) {
121
+ log?.(`[LIQ] spot-perp ${spotSymbol}: price gap ${gapPct.toFixed(2)}% too wide`);
122
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: spotCheck.slippagePct, perpSlippage: perpCheck.slippagePct };
123
+ }
124
+ }
125
+ const executableUsd = Math.min(spotCheck.maxSize * spotCheck.avgFillPrice, perpCheck.maxSize * perpCheck.avgFillPrice);
126
+ const minViable = sizeUsd * 0.2;
127
+ if (executableUsd < minViable) {
128
+ log?.(`[LIQ] spot-perp ${spotSymbol}: only $${executableUsd.toFixed(0)} executable (need min $${minViable.toFixed(0)})`);
129
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: spotCheck.slippagePct, perpSlippage: perpCheck.slippagePct };
130
+ }
131
+ const adjustedSizeUsd = Math.min(sizeUsd, executableUsd);
132
+ return {
133
+ viable: true,
134
+ adjustedSizeUsd,
135
+ spotSlippage: spotCheck.slippagePct,
136
+ perpSlippage: perpCheck.slippagePct,
137
+ };
138
+ }
139
+ catch {
140
+ log?.(`[LIQ] spot-perp ${spotSymbol}: orderbook fetch failed`);
141
+ return { viable: false, adjustedSizeUsd: 0, spotSlippage: 0, perpSlippage: 0 };
142
+ }
143
+ }
@@ -0,0 +1,12 @@
1
+ import type { EvmSigner } from "./interface.js";
2
+ export declare class LocalEvmSigner implements EvmSigner {
3
+ private _wallet;
4
+ private constructor();
5
+ static create(privateKey: string): Promise<LocalEvmSigner>;
6
+ getAddress(): string;
7
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
8
+ name: string;
9
+ type: string;
10
+ }>>, value: Record<string, unknown>): Promise<string>;
11
+ signMessage(message: string | Uint8Array): Promise<string>;
12
+ }