perp-cli 0.7.39 → 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.js +2 -2
- package/dist/bot/presets.js +2 -2
- package/dist/bot/strategies/funding-arb-strategy.js +59 -42
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +1 -1
- package/package.json +1 -1
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 ──
|
|
@@ -190,9 +190,44 @@ export class FundingArbStrategy {
|
|
|
190
190
|
ctx.log(` [ARB] Need 2+ exchanges, have ${adapters.size}. Skipping.`);
|
|
191
191
|
return [];
|
|
192
192
|
}
|
|
193
|
-
//
|
|
194
|
-
const
|
|
193
|
+
// ── Pre-filter exchanges by margin availability ──
|
|
194
|
+
const exchangeBalances = new Map();
|
|
195
|
+
const availableAdapters = new Map();
|
|
195
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) {
|
|
196
231
|
const rates = await this.fetchRates(a, name);
|
|
197
232
|
const map = new Map();
|
|
198
233
|
for (const r of rates)
|
|
@@ -345,6 +380,7 @@ export class FundingArbStrategy {
|
|
|
345
380
|
// ── Find new opportunities ──
|
|
346
381
|
const arbPositions = ctx.state.get("arbOpenPositions").length;
|
|
347
382
|
const exchangeNames = [...ratesByExchange.keys()];
|
|
383
|
+
const scanExchangeNames = exchangeNames.filter(n => availableAdapters.has(n));
|
|
348
384
|
const opportunities = [];
|
|
349
385
|
const allSymbols = new Set();
|
|
350
386
|
for (const [, map] of ratesByExchange) {
|
|
@@ -356,7 +392,7 @@ export class FundingArbStrategy {
|
|
|
356
392
|
let minHourly = Infinity, maxHourly = -Infinity;
|
|
357
393
|
let minExchange = "", maxExchange = "";
|
|
358
394
|
let minRate = 0, maxRate = 0;
|
|
359
|
-
for (const exName of
|
|
395
|
+
for (const exName of scanExchangeNames) {
|
|
360
396
|
const rateInfo = this.findRate(ratesByExchange, exName, sym);
|
|
361
397
|
if (!rateInfo)
|
|
362
398
|
continue;
|
|
@@ -442,7 +478,6 @@ export class FundingArbStrategy {
|
|
|
442
478
|
if (arbPositions < params.max_positions) {
|
|
443
479
|
// Skip symbols already in open positions
|
|
444
480
|
const openSymbols = new Set(ctx.state.get("arbOpenPositions").map(p => p.symbol.toUpperCase()));
|
|
445
|
-
let skipMargin = 0;
|
|
446
481
|
let skipCooldown = 0;
|
|
447
482
|
for (const best of opportunities) {
|
|
448
483
|
if (openSymbols.has(best.symbol.toUpperCase()))
|
|
@@ -480,39 +515,24 @@ export class FundingArbStrategy {
|
|
|
480
515
|
]);
|
|
481
516
|
}
|
|
482
517
|
catch { /* non-critical, some exchanges set leverage on order */ }
|
|
483
|
-
// Balance + margin check
|
|
518
|
+
// Balance + margin check (use pre-fetched balances from tick start)
|
|
484
519
|
let targetSizeUsd = params.size_usd;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
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)`);
|
|
497
531
|
continue;
|
|
498
532
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const shortAvail = parseFloat(shortBal.available);
|
|
502
|
-
const minAvail = Math.min(longAvail, shortAvail);
|
|
503
|
-
const requiredMarginPerLeg = targetSizeUsd / leverage;
|
|
504
|
-
if (minAvail < requiredMarginPerLeg) {
|
|
505
|
-
// Try reducing size to what's affordable
|
|
506
|
-
const maxAffordableNotional = minAvail * leverage * 0.8;
|
|
507
|
-
if (maxAffordableNotional < 10) {
|
|
508
|
-
ctx.log(` [ARB] Skip ${best.symbol}: insufficient margin ($${minAvail.toFixed(2)} < $${requiredMarginPerLeg.toFixed(2)} needed at ${leverage}x)`);
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordableNotional.toFixed(0)} (limited by margin $${minAvail.toFixed(2)} at ${leverage}x)`);
|
|
512
|
-
targetSizeUsd = Math.floor(maxAffordableNotional);
|
|
513
|
-
}
|
|
533
|
+
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordableNotional.toFixed(0)} (limited by margin $${minAvail.toFixed(2)} at ${leverage}x)`);
|
|
534
|
+
targetSizeUsd = Math.floor(maxAffordableNotional);
|
|
514
535
|
}
|
|
515
|
-
catch { /* non-critical, proceed with original size */ }
|
|
516
536
|
// Liquidity check
|
|
517
537
|
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
|
|
518
538
|
if (!liq.viable)
|
|
@@ -574,8 +594,10 @@ export class FundingArbStrategy {
|
|
|
574
594
|
ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
|
|
575
595
|
}
|
|
576
596
|
// Verify both positions actually exist (catches instant liquidation)
|
|
577
|
-
//
|
|
578
|
-
|
|
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
|
|
579
601
|
try {
|
|
580
602
|
const [longPositions, shortPositions] = await Promise.all([
|
|
581
603
|
longAdapter.getPositions(),
|
|
@@ -631,13 +653,8 @@ export class FundingArbStrategy {
|
|
|
631
653
|
return [{ type: "noop" }];
|
|
632
654
|
}
|
|
633
655
|
// Log skip summary only when there were skips (avoids noise when nothing happened)
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
if (skipMargin > 0)
|
|
637
|
-
parts.push(`${skipMargin} margin`);
|
|
638
|
-
if (skipCooldown > 0)
|
|
639
|
-
parts.push(`${skipCooldown} cooldown`);
|
|
640
|
-
ctx.log(` [ARB] Skipped ${skipMargin + skipCooldown} opportunities (${parts.join(", ")})`);
|
|
656
|
+
if (skipCooldown > 0) {
|
|
657
|
+
ctx.log(` [ARB] Skipped ${skipCooldown} opportunities (${skipCooldown} cooldown)`);
|
|
641
658
|
}
|
|
642
659
|
}
|
|
643
660
|
}
|
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() {
|