perp-cli 0.7.20 → 0.7.23

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.
@@ -2,6 +2,7 @@ import { registerStrategy } from "../strategy-registry.js";
2
2
  import { checkArbLiquidity, checkSpotPerpLiquidity } from "../../liquidity.js";
3
3
  import { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
4
4
  import { computeSignedAnnualSpread, annualizeRate } from "../../funding.js";
5
+ import { logExecution, pruneExecutionLog } from "../../execution-log.js";
5
6
  export class FundingArbStrategy {
6
7
  name = "funding-arb";
7
8
  describe() {
@@ -22,6 +23,11 @@ export class FundingArbStrategy {
22
23
  }
23
24
  _config = {};
24
25
  async init(ctx, _snapshot) {
26
+ // Prune old execution logs (keep 30 days)
27
+ try {
28
+ pruneExecutionLog(30);
29
+ }
30
+ catch { /* non-critical */ }
25
31
  this._config = ctx.config;
26
32
  const params = this.params;
27
33
  ctx.state.set("arbRunning", true);
@@ -181,7 +187,7 @@ export class FundingArbStrategy {
181
187
  const rates = await this.fetchRates(a, name);
182
188
  const map = new Map();
183
189
  for (const r of rates)
184
- map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
190
+ map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals });
185
191
  ratesByExchange.set(name, map);
186
192
  }
187
193
  // ── Fetch funding income periodically (every 5 minutes) ──
@@ -271,6 +277,7 @@ export class FundingArbStrategy {
271
277
  ]);
272
278
  toClose.push(pos);
273
279
  ctx.log(` [ARB] Closed ${pos.symbol} position`);
280
+ logExecution({ type: "arb_close", exchange: `${pos.longExchange}↔${pos.shortExchange}`, symbol: pos.symbol, side: "close", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, signedSpread: signedSpread } });
274
281
  }
275
282
  catch (err) {
276
283
  const msg = err instanceof Error ? err.message : String(err);
@@ -364,13 +371,16 @@ export class FundingArbStrategy {
364
371
  }
365
372
  opportunities.sort((a, b) => b.spread - a.spread);
366
373
  if (opportunities.length > 0) {
367
- const top = opportunities.slice(0, 3);
368
- for (const opp of top) {
369
- if (opp.mode === "spot-perp") {
370
- ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread [spot-perp] (short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
371
- }
372
- else {
373
- ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
374
+ // Only log opportunities when there's room for new positions
375
+ if (arbPositions < params.max_positions) {
376
+ const top = opportunities.slice(0, 3);
377
+ for (const opp of top) {
378
+ if (opp.mode === "spot-perp") {
379
+ ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread [spot-perp] (short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
380
+ }
381
+ else {
382
+ ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
383
+ }
374
384
  }
375
385
  }
376
386
  // Auto-execute: try each opportunity until one succeeds
@@ -414,8 +424,10 @@ export class FundingArbStrategy {
414
424
  const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
415
425
  if (!liq.viable)
416
426
  continue;
417
- // Compute matched size
418
- let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
427
+ // Compute matched size (use actual precision from exchange market data)
428
+ const longDec = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.sizeDecimals;
429
+ const shortDec = ratesByExchange.get(best.shortExchange)?.get(best.symbol)?.sizeDecimals;
430
+ let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
419
431
  if (!matched) {
420
432
  ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
421
433
  continue;
@@ -434,7 +446,7 @@ export class FundingArbStrategy {
434
446
  if (lotMatch) {
435
447
  const lotSize = parseFloat(lotMatch[1]);
436
448
  ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
437
- matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize });
449
+ matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize, longSizeDecimals: longDec, shortSizeDecimals: shortDec });
438
450
  if (!matched) {
439
451
  ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
440
452
  continue;
@@ -473,11 +485,13 @@ export class FundingArbStrategy {
473
485
  ctx.state.set("arbOpenPositions", currentPositions);
474
486
  ctx.state.set("arbPositions", currentPositions.length);
475
487
  ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
488
+ logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "long/short", size: matched.size, notional: matched.notional, status: "success", dryRun: false, meta: { mode: "perp-perp", spread: best.spread } });
476
489
  return [{ type: "noop" }];
477
490
  }
478
491
  catch (err) {
479
492
  const msg = err instanceof Error ? err.message : String(err);
480
493
  ctx.log(` [ARB] Execution failed for ${best.symbol}: ${msg}`);
494
+ logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "long/short", size: matched?.size ?? "0", status: "failed", error: msg, dryRun: false, meta: { mode: "perp-perp" } });
481
495
  continue;
482
496
  }
483
497
  }
@@ -662,11 +676,13 @@ export class FundingArbStrategy {
662
676
  ctx.state.set("arbOpenPositions", currentPositions);
663
677
  ctx.state.set("arbPositions", currentPositions.length);
664
678
  ctx.log(` [ARB] Spot-perp position opened! (${currentPositions.length}/${params.max_positions})`);
679
+ logExecution({ type: "arb_entry", exchange: `${opp.longExchange}↔${opp.shortExchange}`, symbol: opp.symbol, side: "spot-long/perp-short", size: matched.size, notional: matched.notional, status: "success", dryRun: false, meta: { mode: "spot-perp", spread: opp.spread } });
665
680
  return true;
666
681
  }
667
682
  catch (err) {
668
683
  const msg = err instanceof Error ? err.message : String(err);
669
684
  ctx.log(` [ARB] Open spot-perp failed for ${opp.symbol}: ${msg}`);
685
+ logExecution({ type: "arb_entry", exchange: opp.shortExchange, symbol: opp.symbol, side: "spot-long/perp-short", size: "0", status: "failed", error: msg, dryRun: false, meta: { mode: "spot-perp" } });
670
686
  return false;
671
687
  }
672
688
  }
@@ -765,11 +781,12 @@ export class FundingArbStrategy {
765
781
  try {
766
782
  const markets = await adapter.getMarkets();
767
783
  return markets
768
- .filter(m => m.fundingRate != null) // skip symbols with unavailable rates
784
+ .filter(m => m.fundingRate != null)
769
785
  .map(m => ({
770
786
  symbol: m.symbol,
771
787
  rate: parseFloat(m.fundingRate),
772
788
  price: parseFloat(m.markPrice),
789
+ sizeDecimals: m.sizeDecimals,
773
790
  }));
774
791
  }
775
792
  catch {
@@ -75,6 +75,8 @@ export class AsterAdapter {
75
75
  const ticker = tickerMap.get(sym);
76
76
  const premium = premiumMap.get(sym);
77
77
  const maxLev = Number(s.maxLeverage ?? 50);
78
+ const lotFilter = s.filters
79
+ ?.find(f => f.filterType === "LOT_SIZE");
78
80
  return {
79
81
  symbol: this._fromApi(sym),
80
82
  markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
@@ -83,6 +85,8 @@ export class AsterAdapter {
83
85
  volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
84
86
  openInterest: "0",
85
87
  maxLeverage: maxLev,
88
+ sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : undefined,
89
+ stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : undefined,
86
90
  };
87
91
  });
88
92
  this._marketsCache = result;
@@ -10,6 +10,8 @@ export interface ExchangeMarketInfo {
10
10
  volume24h: string;
11
11
  openInterest: string;
12
12
  maxLeverage: number;
13
+ sizeDecimals?: number;
14
+ stepSize?: string;
13
15
  }
14
16
  export interface ExchangePosition {
15
17
  symbol: string;
@@ -18,7 +18,7 @@ const EXCHANGE_FUNDING_HOURS = {
18
18
  hyperliquid: 1,
19
19
  pacifica: 1,
20
20
  lighter: 8,
21
- aster: 8,
21
+ aster: 1,
22
22
  };
23
23
  /**
24
24
  * Get funding period in hours for an exchange.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.20",
3
+ "version": "0.7.23",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",