perp-cli 0.9.7 → 0.9.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/dist/bot/strategies/funding-arb-utils.js +9 -1
- package/dist/bot/strategies/funding-arb-v2-strategy.js +20 -11
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +2 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +26 -18
- package/dist/bot/strategies/spot-perp-executor.js +4 -4
- package/dist/exchanges/aster.js +2 -1
- package/dist/exchanges/hyperliquid.d.ts +1 -0
- package/dist/exchanges/hyperliquid.js +1 -0
- package/dist/exchanges/interface.d.ts +2 -0
- package/package.json +1 -1
|
@@ -262,8 +262,16 @@ export async function fetchRates(adapter, exchangeName) {
|
|
|
262
262
|
m.fundingHours = await aster.getFundingHours(m.symbol);
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
+
// For non-aster exchanges, set fundingHours from static map if not already set
|
|
266
|
+
if (exchangeName !== "aster") {
|
|
267
|
+
const defaultFH = getFundingHours(exchangeName);
|
|
268
|
+
for (const m of withRates) {
|
|
269
|
+
if (m.fundingHours == null)
|
|
270
|
+
m.fundingHours = defaultFH;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
265
273
|
return withRates
|
|
266
|
-
.filter(m => m.fundingHours != null) // skip symbols with unknown funding period
|
|
274
|
+
.filter(m => m.fundingHours != null) // skip aster symbols with unknown funding period
|
|
267
275
|
.map(m => ({
|
|
268
276
|
symbol: m.symbol,
|
|
269
277
|
rate: parseFloat(m.fundingRate),
|
|
@@ -529,7 +529,18 @@ export class FundingArbV2Strategy {
|
|
|
529
529
|
const pBal = exchangeBalances.get(opp.shortExchange);
|
|
530
530
|
if (!sBal || !pBal)
|
|
531
531
|
return false;
|
|
532
|
-
|
|
532
|
+
let spotAvail = sBal.available;
|
|
533
|
+
const spotExAdapter = adapters.get(spotExName);
|
|
534
|
+
if (spotExAdapter && !spotExAdapter.isUnifiedAccount) {
|
|
535
|
+
try {
|
|
536
|
+
const sb = await spotAdapter.getSpotBalances();
|
|
537
|
+
const su = sb.find(b => b.token.toUpperCase().startsWith("USDC"));
|
|
538
|
+
if (su)
|
|
539
|
+
spotAvail += parseFloat(su.available);
|
|
540
|
+
}
|
|
541
|
+
catch { /* non-critical */ }
|
|
542
|
+
}
|
|
543
|
+
const maxNtl = Math.min(spotAvail, pBal.available * leverage) * 0.8;
|
|
533
544
|
if (maxNtl < targetSizeUsd) {
|
|
534
545
|
if (maxNtl < 10)
|
|
535
546
|
return false;
|
|
@@ -629,17 +640,15 @@ export class FundingArbV2Strategy {
|
|
|
629
640
|
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
630
641
|
return false;
|
|
631
642
|
}
|
|
632
|
-
// Transfer USDC back (same-exchange)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
await transferUsdcToPerp(spotAdapter, spotExName, Math.floor(proceeds));
|
|
640
|
-
}
|
|
641
|
-
catch { /* non-fatal */ }
|
|
643
|
+
// Transfer USDC back to perp (always — not just same-exchange)
|
|
644
|
+
try {
|
|
645
|
+
const spotBals = await spotAdapter.getSpotBalances();
|
|
646
|
+
const usdcBal = spotBals.find(b => b.token.toUpperCase().startsWith("USDC"));
|
|
647
|
+
const usdcAmt = usdcBal ? parseFloat(usdcBal.total) : 0;
|
|
648
|
+
if (usdcAmt > 1)
|
|
649
|
+
await transferUsdcToPerp(spotAdapter, spotExName, Math.floor(usdcAmt));
|
|
642
650
|
}
|
|
651
|
+
catch { /* non-fatal */ }
|
|
643
652
|
// Close perp short
|
|
644
653
|
const perpSym = getPerpSymbol(pos.symbol, pos.shortExchange);
|
|
645
654
|
try {
|
|
@@ -35,7 +35,8 @@ export declare class SpotPerpArbStrategy implements Strategy {
|
|
|
35
35
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
36
36
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
37
37
|
private scoreFundingLocal;
|
|
38
|
-
|
|
38
|
+
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
|
|
39
|
+
private findPerpCandidates;
|
|
39
40
|
private calcAnnualized;
|
|
40
41
|
private shouldClose;
|
|
41
42
|
private tryOpenPosition;
|
|
@@ -125,7 +125,8 @@ export class SpotPerpArbStrategy {
|
|
|
125
125
|
availableAdapters.set(name, a);
|
|
126
126
|
}
|
|
127
127
|
catch (err) {
|
|
128
|
-
|
|
128
|
+
const msg = (err instanceof Error ? err.message : String(err)).slice(0, 120);
|
|
129
|
+
ctx.log(` [SPA] getBalance failed for ${name}: ${msg}`);
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
const ratesByExchange = new Map();
|
|
@@ -221,7 +222,8 @@ export class SpotPerpArbStrategy {
|
|
|
221
222
|
const rcd = this._failCooldown.get(`${pos.id}:rotate`) ?? 0;
|
|
222
223
|
if (Date.now() < rcd)
|
|
223
224
|
continue;
|
|
224
|
-
const
|
|
225
|
+
const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange);
|
|
226
|
+
const best = candidates[0];
|
|
225
227
|
if (!best || best.exchange === pos.perpExchange)
|
|
226
228
|
continue;
|
|
227
229
|
const currentInfo = findRate(ratesByExchange, pos.perpExchange, getPerpSymbol(pos.symbol, pos.perpExchange))
|
|
@@ -277,10 +279,12 @@ export class SpotPerpArbStrategy {
|
|
|
277
279
|
continue;
|
|
278
280
|
if (Date.now() < (this._failCooldown.get(`${base}:entry`) ?? 0))
|
|
279
281
|
continue;
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange);
|
|
283
|
+
for (const cand of candidates) {
|
|
284
|
+
if (cand.score.annualized < p.min_rate || cand.score.consistency < p.min_consistency)
|
|
285
|
+
continue;
|
|
286
|
+
scoredOpps.push({ symbol: base, spotExchange: spotExName, perpExchange: cand.exchange, annualized: cand.score.annualized, score: cand.score });
|
|
287
|
+
}
|
|
284
288
|
}
|
|
285
289
|
}
|
|
286
290
|
catch { /* no spot */ }
|
|
@@ -350,8 +354,9 @@ export class SpotPerpArbStrategy {
|
|
|
350
354
|
scoreFundingLocal(adapter, symbol, exchangeName) {
|
|
351
355
|
return scoreFunding(adapter, symbol, exchangeName, this.params.history_periods, this._fundingScoreCache);
|
|
352
356
|
}
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
|
|
358
|
+
async findPerpCandidates(symbol, adapters, ratesByExchange) {
|
|
359
|
+
const candidates = [];
|
|
355
360
|
for (const [exName, adapter] of adapters) {
|
|
356
361
|
const perpSym = getPerpSymbol(symbol, exName);
|
|
357
362
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
@@ -361,10 +366,10 @@ export class SpotPerpArbStrategy {
|
|
|
361
366
|
?? await this.scoreFundingLocal(adapter, symbol, exName);
|
|
362
367
|
if (!score)
|
|
363
368
|
continue;
|
|
364
|
-
|
|
365
|
-
best = { exchange: exName, rate: ri.rate, score };
|
|
369
|
+
candidates.push({ exchange: exName, rate: ri.rate, score });
|
|
366
370
|
}
|
|
367
|
-
|
|
371
|
+
candidates.sort((a, b) => b.score.annualized - a.score.annualized);
|
|
372
|
+
return candidates;
|
|
368
373
|
}
|
|
369
374
|
calcAnnualized(pos, ratesByExchange) {
|
|
370
375
|
const ri = findRate(ratesByExchange, pos.perpExchange, getPerpSymbol(pos.symbol, pos.perpExchange))
|
|
@@ -466,15 +471,18 @@ export class SpotPerpArbStrategy {
|
|
|
466
471
|
const pBal = exchangeBalances.get(perpExchange);
|
|
467
472
|
if (!sBal || !pBal)
|
|
468
473
|
return false;
|
|
469
|
-
// Check spot USDC balance
|
|
474
|
+
// Check spot USDC balance (only add if separate from perp — skip for unified accounts like HL)
|
|
470
475
|
let spotAvailable = sBal.available;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
+
const isUnified = spotExAdapter?.isUnifiedAccount ?? false;
|
|
477
|
+
if (!isUnified) {
|
|
478
|
+
try {
|
|
479
|
+
const spotBals = await spotAdapter.getSpotBalances();
|
|
480
|
+
const spotUsdc = spotBals.find(b => b.token.toUpperCase().startsWith("USDC"));
|
|
481
|
+
if (spotUsdc)
|
|
482
|
+
spotAvailable += parseFloat(spotUsdc.available);
|
|
483
|
+
}
|
|
484
|
+
catch { /* non-critical */ }
|
|
476
485
|
}
|
|
477
|
-
catch { /* non-critical */ }
|
|
478
486
|
const maxNtl = Math.min(spotAvailable, pBal.available * leverage) * 0.8;
|
|
479
487
|
if (maxNtl < targetSizeUsd) {
|
|
480
488
|
if (maxNtl < 10) {
|
|
@@ -121,8 +121,8 @@ export class SpotPerpExecutor {
|
|
|
121
121
|
const postBals = await spotAdapter.getSpotBalances();
|
|
122
122
|
const bought = postBals.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === symbol.toUpperCase());
|
|
123
123
|
const boughtAmt = bought ? parseFloat(bought.total) : 0;
|
|
124
|
-
if (boughtAmt < parseFloat(matched.size) * 0.
|
|
125
|
-
log(` [SPA] Spot buy returned OK but ${symbol} balance too low (${boughtAmt.toFixed(4)}) — aborting`);
|
|
124
|
+
if (boughtAmt < parseFloat(matched.size) * 0.8) {
|
|
125
|
+
log(` [SPA] Spot buy returned OK but ${symbol} balance too low (${boughtAmt.toFixed(4)} < ${matched.size} × 80%) — aborting`);
|
|
126
126
|
if (actualTransferred > 0) {
|
|
127
127
|
try {
|
|
128
128
|
await transferUsdcToPerp(spotAdapter, spotExchange, actualTransferred);
|
|
@@ -223,8 +223,8 @@ export class SpotPerpExecutor {
|
|
|
223
223
|
const postBals = await spotAdapter.getSpotBalances();
|
|
224
224
|
const remaining = postBals.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === symbol.toUpperCase());
|
|
225
225
|
const remainingAmt = remaining ? parseFloat(remaining.total) : 0;
|
|
226
|
-
if (remainingAmt >= parseFloat(size) * 0.
|
|
227
|
-
log(` [SPA] CRITICAL: Spot sell returned OK but ${symbol} balance unchanged (${remainingAmt.toFixed(4)}) — re-hedging perp`);
|
|
226
|
+
if (remainingAmt >= parseFloat(size) * 0.8) {
|
|
227
|
+
log(` [SPA] CRITICAL: Spot sell returned OK but ${symbol} balance unchanged (${remainingAmt.toFixed(4)} >= ${size} × 80%) — re-hedging perp`);
|
|
228
228
|
try {
|
|
229
229
|
await perpAdapter.marketOrder(perpSym, "sell", size);
|
|
230
230
|
}
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -457,7 +457,8 @@ export class AsterAdapter {
|
|
|
457
457
|
}
|
|
458
458
|
if (!res.ok) {
|
|
459
459
|
const text = await res.text();
|
|
460
|
-
|
|
460
|
+
const clean = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
461
|
+
throw new Error(`${method} ${path} failed (${res.status}): ${clean}`);
|
|
461
462
|
}
|
|
462
463
|
return res.json();
|
|
463
464
|
}
|
|
@@ -66,6 +66,8 @@ export interface ExchangeAdapter {
|
|
|
66
66
|
readonly name: string;
|
|
67
67
|
readonly chain?: string;
|
|
68
68
|
readonly aliases?: readonly string[];
|
|
69
|
+
/** True if perp and spot share the same balance (e.g., Hyperliquid). False if separate accounts (e.g., Lighter). */
|
|
70
|
+
readonly isUnifiedAccount?: boolean;
|
|
69
71
|
init?(): Promise<void>;
|
|
70
72
|
getMarkets(): Promise<ExchangeMarketInfo[]>;
|
|
71
73
|
getOrderbook(symbol: string): Promise<{
|