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.
@@ -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 ?? 10);
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: 10,
134
+ monitor_interval_sec: 30,
135
135
  };
136
136
  }
137
137
  export function quickDCAConfig(opts) {
@@ -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: 10,
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: 10,
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
- // Fetch rates from all exchanges
194
- const ratesByExchange = new Map();
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 exchangeNames) {
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
- try {
486
- const [longBal, shortBal] = await Promise.all([
487
- longAdapter.getBalance(),
488
- shortAdapter.getBalance(),
489
- ]);
490
- // Check margin usage (skip if > 70%)
491
- const longEquity = parseFloat(longBal.equity);
492
- const shortEquity = parseFloat(shortBal.equity);
493
- const longMarginPct = longEquity > 0 ? parseFloat(longBal.marginUsed) / longEquity * 100 : 0;
494
- const shortMarginPct = shortEquity > 0 ? parseFloat(shortBal.marginUsed) / shortEquity * 100 : 0;
495
- if (longMarginPct > 70 || shortMarginPct > 70) {
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
- // Required margin per leg = sizeUsd / leverage
500
- const longAvail = parseFloat(longBal.available);
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
- // Wait for cache to expire before checking (exchanges cache positions for ~5s)
578
- await new Promise(resolve => setTimeout(resolve, 6000));
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 (skipMargin > 0 || skipCooldown > 0) {
635
- const parts = [];
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 = 5000;
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 = 5_000; // 5s — balance, positions, orders
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.39",
3
+ "version": "0.7.42",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",