perp-cli 0.7.37 → 0.7.42
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/config.d.ts +1 -0
- package/dist/bot/config.js +2 -2
- package/dist/bot/presets.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +72 -47
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +1 -1
- package/package.json +1 -1
package/dist/bot/config.d.ts
CHANGED
package/dist/bot/config.js
CHANGED
|
@@ -30,7 +30,7 @@ function parseBotConfig(raw) {
|
|
|
30
30
|
const name = String(raw.name ?? `bot-${Date.now()}`);
|
|
31
31
|
const exchange = String(raw.exchange ?? "hyperliquid");
|
|
32
32
|
const symbol = String(raw.symbol ?? "ETH");
|
|
33
|
-
const monitorInterval = Number(raw.monitor_interval_sec ?? raw.interval ??
|
|
33
|
+
const monitorInterval = Number(raw.monitor_interval_sec ?? raw.interval ?? 30);
|
|
34
34
|
// Strategy
|
|
35
35
|
const stratRaw = (raw.strategy ?? raw.params ?? {});
|
|
36
36
|
const stratType = String(stratRaw.type ?? raw.strategy_type ?? "grid");
|
|
@@ -131,7 +131,7 @@ export function quickGridConfig(opts) {
|
|
|
131
131
|
...(opts.maxRuntime ? [{ type: "time_after", value: opts.maxRuntime }] : []),
|
|
132
132
|
],
|
|
133
133
|
risk: { ...DEFAULT_RISK, max_drawdown: opts.maxDrawdown },
|
|
134
|
-
monitor_interval_sec:
|
|
134
|
+
monitor_interval_sec: 30,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
export function quickDCAConfig(opts) {
|
package/dist/bot/presets.js
CHANGED
|
@@ -58,7 +58,7 @@ const gridStandard = {
|
|
|
58
58
|
{ type: "volatility_above", value: 12 },
|
|
59
59
|
],
|
|
60
60
|
risk: { ...DEFAULT_RISK, max_drawdown: 100, max_daily_loss: 50 },
|
|
61
|
-
monitor_interval_sec:
|
|
61
|
+
monitor_interval_sec: 30,
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
64
64
|
const gridAggressive = {
|
|
@@ -111,7 +111,7 @@ const gridLongBias = {
|
|
|
111
111
|
entry_conditions: [{ type: "always", value: 0 }],
|
|
112
112
|
exit_conditions: [],
|
|
113
113
|
risk: { ...DEFAULT_RISK, max_drawdown: 150, max_daily_loss: 75 },
|
|
114
|
-
monitor_interval_sec:
|
|
114
|
+
monitor_interval_sec: 30,
|
|
115
115
|
}),
|
|
116
116
|
};
|
|
117
117
|
// ── DCA Presets ──
|
|
@@ -16,6 +16,7 @@ export class FundingArbStrategy {
|
|
|
16
16
|
{ name: "max_positions", type: "number", required: false, default: 3, description: "Max concurrent positions" },
|
|
17
17
|
{ name: "exchanges", type: "string", required: true, description: "Comma-separated exchange names" },
|
|
18
18
|
{ name: "leverage", type: "number", required: false, default: 3, description: "Max leverage to use" },
|
|
19
|
+
{ name: "min_hold_hours", type: "number", required: false, default: 2, description: "Min hold time before close (hours)" },
|
|
19
20
|
],
|
|
20
21
|
};
|
|
21
22
|
}
|
|
@@ -88,7 +89,7 @@ export class FundingArbStrategy {
|
|
|
88
89
|
const longEx = pA.side === "long" ? exA : exB;
|
|
89
90
|
const shortEx = pA.side === "short" ? exA : exB;
|
|
90
91
|
const size = pA.side === "long" ? pA.size : pB.size;
|
|
91
|
-
recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size, mode: "perp-perp" });
|
|
92
|
+
recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size, mode: "perp-perp", entryTime: 0 });
|
|
92
93
|
used.add(keyA);
|
|
93
94
|
used.add(keyB);
|
|
94
95
|
break;
|
|
@@ -135,7 +136,7 @@ export class FundingArbStrategy {
|
|
|
135
136
|
const spotBal = nonUsdc.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === base);
|
|
136
137
|
ctx.log(` [ARB] ${name} perp ${perp.symbol}(${perp.side}) → base=${base}, spotMatch=${spotBal ? spotBal.token : "none"}`);
|
|
137
138
|
if (spotBal && perp.side === "short") {
|
|
138
|
-
recovered.push({ symbol: base, longExchange: `${name}-spot`, shortExchange: name, entrySpread: 0, size: perp.size, mode: "spot-perp" });
|
|
139
|
+
recovered.push({ symbol: base, longExchange: `${name}-spot`, shortExchange: name, entrySpread: 0, size: perp.size, mode: "spot-perp", entryTime: 0 });
|
|
139
140
|
used.add(perpKey);
|
|
140
141
|
ctx.log(` [ARB] Detected spot-perp hedge: ${base} ${name}-spot↔${name}`);
|
|
141
142
|
}
|
|
@@ -189,9 +190,44 @@ export class FundingArbStrategy {
|
|
|
189
190
|
ctx.log(` [ARB] Need 2+ exchanges, have ${adapters.size}. Skipping.`);
|
|
190
191
|
return [];
|
|
191
192
|
}
|
|
192
|
-
//
|
|
193
|
-
const
|
|
193
|
+
// ── Pre-filter exchanges by margin availability ──
|
|
194
|
+
const exchangeBalances = new Map();
|
|
195
|
+
const availableAdapters = new Map();
|
|
194
196
|
for (const [name, a] of adapters) {
|
|
197
|
+
try {
|
|
198
|
+
const bal = await a.getBalance();
|
|
199
|
+
const equity = parseFloat(bal.equity);
|
|
200
|
+
const marginUsed = parseFloat(bal.marginUsed);
|
|
201
|
+
const marginPct = equity > 0 ? (marginUsed / equity) * 100 : 100;
|
|
202
|
+
exchangeBalances.set(name, { equity, available: parseFloat(bal.available), marginPct });
|
|
203
|
+
if (marginPct < 70) {
|
|
204
|
+
availableAdapters.set(name, a);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping for this tick`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
availableAdapters.set(name, a); // on error, include it
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Determine which exchanges need rate fetching.
|
|
215
|
+
// When at max_positions we only need rates for exchanges that hold open positions
|
|
216
|
+
// (to evaluate close conditions). Exchanges with no positions can be skipped.
|
|
217
|
+
const atMaxPositions = openPositions.length >= (params.max_positions ?? 3);
|
|
218
|
+
const positionExchanges = new Set();
|
|
219
|
+
if (atMaxPositions) {
|
|
220
|
+
for (const p of openPositions) {
|
|
221
|
+
positionExchanges.add(p.longExchange.replace(/-spot$/i, ""));
|
|
222
|
+
positionExchanges.add(p.shortExchange);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const exchangesToScan = atMaxPositions
|
|
226
|
+
? [...adapters].filter(([name]) => positionExchanges.has(name))
|
|
227
|
+
: [...adapters];
|
|
228
|
+
// Fetch rates — all exchanges if room for new positions, only position exchanges if full
|
|
229
|
+
const ratesByExchange = new Map();
|
|
230
|
+
for (const [name, a] of exchangesToScan) {
|
|
195
231
|
const rates = await this.fetchRates(a, name);
|
|
196
232
|
const map = new Map();
|
|
197
233
|
for (const r of rates)
|
|
@@ -273,7 +309,10 @@ export class FundingArbStrategy {
|
|
|
273
309
|
signedSpread = (shortHourly - longHourly) * 8760 * 100;
|
|
274
310
|
}
|
|
275
311
|
// Close if funding flipped (losing money) or spread below close threshold
|
|
276
|
-
const
|
|
312
|
+
const minHoldMs = (params.min_hold_hours ?? 2) * 60 * 60 * 1000;
|
|
313
|
+
const holdTime = pos.entryTime > 0 ? Date.now() - pos.entryTime : Infinity;
|
|
314
|
+
const isFundingFlipped = signedSpread < 0;
|
|
315
|
+
const shouldClose = isFundingFlipped || (holdTime >= minHoldMs && signedSpread < params.close_spread);
|
|
277
316
|
if (shouldClose) {
|
|
278
317
|
// Cooldown: skip if close recently failed for this symbol
|
|
279
318
|
const closeKey = `${pos.symbol}:close`;
|
|
@@ -341,6 +380,7 @@ export class FundingArbStrategy {
|
|
|
341
380
|
// ── Find new opportunities ──
|
|
342
381
|
const arbPositions = ctx.state.get("arbOpenPositions").length;
|
|
343
382
|
const exchangeNames = [...ratesByExchange.keys()];
|
|
383
|
+
const scanExchangeNames = exchangeNames.filter(n => availableAdapters.has(n));
|
|
344
384
|
const opportunities = [];
|
|
345
385
|
const allSymbols = new Set();
|
|
346
386
|
for (const [, map] of ratesByExchange) {
|
|
@@ -352,7 +392,7 @@ export class FundingArbStrategy {
|
|
|
352
392
|
let minHourly = Infinity, maxHourly = -Infinity;
|
|
353
393
|
let minExchange = "", maxExchange = "";
|
|
354
394
|
let minRate = 0, maxRate = 0;
|
|
355
|
-
for (const exName of
|
|
395
|
+
for (const exName of scanExchangeNames) {
|
|
356
396
|
const rateInfo = this.findRate(ratesByExchange, exName, sym);
|
|
357
397
|
if (!rateInfo)
|
|
358
398
|
continue;
|
|
@@ -438,7 +478,6 @@ export class FundingArbStrategy {
|
|
|
438
478
|
if (arbPositions < params.max_positions) {
|
|
439
479
|
// Skip symbols already in open positions
|
|
440
480
|
const openSymbols = new Set(ctx.state.get("arbOpenPositions").map(p => p.symbol.toUpperCase()));
|
|
441
|
-
let skipMargin = 0;
|
|
442
481
|
let skipCooldown = 0;
|
|
443
482
|
for (const best of opportunities) {
|
|
444
483
|
if (openSymbols.has(best.symbol.toUpperCase()))
|
|
@@ -476,39 +515,24 @@ export class FundingArbStrategy {
|
|
|
476
515
|
]);
|
|
477
516
|
}
|
|
478
517
|
catch { /* non-critical, some exchanges set leverage on order */ }
|
|
479
|
-
// Balance + margin check
|
|
518
|
+
// Balance + margin check (use pre-fetched balances from tick start)
|
|
480
519
|
let targetSizeUsd = params.size_usd;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
skipMargin++;
|
|
520
|
+
const longBal = exchangeBalances.get(best.longExchange);
|
|
521
|
+
const shortBal = exchangeBalances.get(best.shortExchange);
|
|
522
|
+
if (!longBal || !shortBal)
|
|
523
|
+
continue;
|
|
524
|
+
const minAvail = Math.min(longBal.available, shortBal.available);
|
|
525
|
+
const requiredMarginPerLeg = targetSizeUsd / leverage;
|
|
526
|
+
if (minAvail < requiredMarginPerLeg) {
|
|
527
|
+
// Try reducing size to what's affordable
|
|
528
|
+
const maxAffordableNotional = minAvail * leverage * 0.8;
|
|
529
|
+
if (maxAffordableNotional < 10) {
|
|
530
|
+
ctx.log(` [ARB] Skip ${best.symbol}: insufficient margin ($${minAvail.toFixed(2)} < $${requiredMarginPerLeg.toFixed(2)} at ${leverage}x)`);
|
|
493
531
|
continue;
|
|
494
532
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const shortAvail = parseFloat(shortBal.available);
|
|
498
|
-
const minAvail = Math.min(longAvail, shortAvail);
|
|
499
|
-
const requiredMarginPerLeg = targetSizeUsd / leverage;
|
|
500
|
-
if (minAvail < requiredMarginPerLeg) {
|
|
501
|
-
// Try reducing size to what's affordable
|
|
502
|
-
const maxAffordableNotional = minAvail * leverage * 0.8;
|
|
503
|
-
if (maxAffordableNotional < 10) {
|
|
504
|
-
ctx.log(` [ARB] Skip ${best.symbol}: insufficient margin ($${minAvail.toFixed(2)} < $${requiredMarginPerLeg.toFixed(2)} needed at ${leverage}x)`);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordableNotional.toFixed(0)} (limited by margin $${minAvail.toFixed(2)} at ${leverage}x)`);
|
|
508
|
-
targetSizeUsd = Math.floor(maxAffordableNotional);
|
|
509
|
-
}
|
|
533
|
+
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordableNotional.toFixed(0)} (limited by margin $${minAvail.toFixed(2)} at ${leverage}x)`);
|
|
534
|
+
targetSizeUsd = Math.floor(maxAffordableNotional);
|
|
510
535
|
}
|
|
511
|
-
catch { /* non-critical, proceed with original size */ }
|
|
512
536
|
// Liquidity check
|
|
513
537
|
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
|
|
514
538
|
if (!liq.viable)
|
|
@@ -570,15 +594,19 @@ export class FundingArbStrategy {
|
|
|
570
594
|
ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
|
|
571
595
|
}
|
|
572
596
|
// Verify both positions actually exist (catches instant liquidation)
|
|
573
|
-
//
|
|
574
|
-
|
|
597
|
+
// Invalidate cache so getPositions returns fresh data
|
|
598
|
+
const { invalidateCache } = await import("../../cache.js");
|
|
599
|
+
invalidateCache("acct");
|
|
600
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // brief wait for exchange settlement
|
|
575
601
|
try {
|
|
576
602
|
const [longPositions, shortPositions] = await Promise.all([
|
|
577
603
|
longAdapter.getPositions(),
|
|
578
604
|
shortAdapter.getPositions(),
|
|
579
605
|
]);
|
|
580
|
-
const
|
|
581
|
-
const
|
|
606
|
+
const symUpper = best.symbol.toUpperCase();
|
|
607
|
+
const matchSymbol = (s) => { const u = s.toUpperCase(); return u === symUpper || u === symUpper + "-PERP" || u.replace(/-PERP$/, "") === symUpper; };
|
|
608
|
+
const longExists = longPositions.some(p => matchSymbol(p.symbol) && parseFloat(p.size) > 0);
|
|
609
|
+
const shortExists = shortPositions.some(p => matchSymbol(p.symbol) && parseFloat(p.size) > 0);
|
|
582
610
|
if (!longExists && !shortExists) {
|
|
583
611
|
// Both missing likely means cache issue — log warning but don't rollback
|
|
584
612
|
ctx.log(` [ARB] Position verification: both sides not visible for ${best.symbol} (may be cache delay)`);
|
|
@@ -617,6 +645,7 @@ export class FundingArbStrategy {
|
|
|
617
645
|
entrySpread: best.spread,
|
|
618
646
|
size: matched.size,
|
|
619
647
|
mode: "perp-perp",
|
|
648
|
+
entryTime: Date.now(),
|
|
620
649
|
});
|
|
621
650
|
ctx.state.set("arbOpenPositions", currentPositions);
|
|
622
651
|
ctx.state.set("arbPositions", currentPositions.length);
|
|
@@ -624,13 +653,8 @@ export class FundingArbStrategy {
|
|
|
624
653
|
return [{ type: "noop" }];
|
|
625
654
|
}
|
|
626
655
|
// Log skip summary only when there were skips (avoids noise when nothing happened)
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
if (skipMargin > 0)
|
|
630
|
-
parts.push(`${skipMargin} margin`);
|
|
631
|
-
if (skipCooldown > 0)
|
|
632
|
-
parts.push(`${skipCooldown} cooldown`);
|
|
633
|
-
ctx.log(` [ARB] Skipped ${skipMargin + skipCooldown} opportunities (${parts.join(", ")})`);
|
|
656
|
+
if (skipCooldown > 0) {
|
|
657
|
+
ctx.log(` [ARB] Skipped ${skipCooldown} opportunities (${skipCooldown} cooldown)`);
|
|
634
658
|
}
|
|
635
659
|
}
|
|
636
660
|
}
|
|
@@ -875,6 +899,7 @@ export class FundingArbStrategy {
|
|
|
875
899
|
entrySpread: opp.spread,
|
|
876
900
|
size: matched.size,
|
|
877
901
|
mode: "spot-perp",
|
|
902
|
+
entryTime: Date.now(),
|
|
878
903
|
});
|
|
879
904
|
ctx.state.set("arbOpenPositions", currentPositions);
|
|
880
905
|
ctx.state.set("arbPositions", currentPositions.length);
|
package/dist/cache.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Dashboard and CLI share cached data via /tmp/perp-cli-cache/.
|
|
5
5
|
* Writes are atomic (write to .tmp then rename) to prevent partial reads.
|
|
6
6
|
*/
|
|
7
|
-
export declare const TTL_ACCOUNT =
|
|
7
|
+
export declare const TTL_ACCOUNT = 15000;
|
|
8
8
|
export declare const TTL_MARKET = 30000;
|
|
9
9
|
export declare function getCached<T>(key: string): T | null;
|
|
10
10
|
export declare function setCached<T>(key: string, data: T, ttlMs: number): void;
|
package/dist/cache.js
CHANGED
|
@@ -9,7 +9,7 @@ import { join } from "path";
|
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
10
|
const CACHE_DIR = join(tmpdir(), "perp-cli-cache");
|
|
11
11
|
// TTL presets
|
|
12
|
-
export const TTL_ACCOUNT =
|
|
12
|
+
export const TTL_ACCOUNT = 15_000; // 15s — balance, positions, orders (ticks are 30s; 15s ensures same-tick dedup)
|
|
13
13
|
export const TTL_MARKET = 30_000; // 30s — funding rates, prices, markets
|
|
14
14
|
let dirReady = false;
|
|
15
15
|
function ensureDir() {
|