perp-cli 0.4.0 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +12 -9
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +885 -67
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +118 -43
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +181 -92
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +132 -37
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +148 -99
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +145 -0
- package/dist/trade-validator.js +2 -1
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +102 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
package/dist/funding/rates.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/funding/rates.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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);
|
|
300
|
-
|
|
316
|
+
registerPnlCommands(program, getAdapterForExchange, isJson);
|
|
317
|
+
registerMultilegCommands(program, getAdapterForExchange, isJson);
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
350
|
-
console.log(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 () => {
|
package/dist/liquidity.d.ts
CHANGED
|
@@ -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
|
+
}
|