perp-cli 0.7.20 → 0.7.21

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);
@@ -414,8 +421,10 @@ export class FundingArbStrategy {
414
421
  const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
415
422
  if (!liq.viable)
416
423
  continue;
417
- // Compute matched size
418
- let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
424
+ // Compute matched size (use actual precision from exchange market data)
425
+ const longDec = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.sizeDecimals;
426
+ const shortDec = ratesByExchange.get(best.shortExchange)?.get(best.symbol)?.sizeDecimals;
427
+ let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
419
428
  if (!matched) {
420
429
  ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
421
430
  continue;
@@ -434,7 +443,7 @@ export class FundingArbStrategy {
434
443
  if (lotMatch) {
435
444
  const lotSize = parseFloat(lotMatch[1]);
436
445
  ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
437
- matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize });
446
+ matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize, longSizeDecimals: longDec, shortSizeDecimals: shortDec });
438
447
  if (!matched) {
439
448
  ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
440
449
  continue;
@@ -473,11 +482,13 @@ export class FundingArbStrategy {
473
482
  ctx.state.set("arbOpenPositions", currentPositions);
474
483
  ctx.state.set("arbPositions", currentPositions.length);
475
484
  ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
485
+ 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
486
  return [{ type: "noop" }];
477
487
  }
478
488
  catch (err) {
479
489
  const msg = err instanceof Error ? err.message : String(err);
480
490
  ctx.log(` [ARB] Execution failed for ${best.symbol}: ${msg}`);
491
+ 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
492
  continue;
482
493
  }
483
494
  }
@@ -662,11 +673,13 @@ export class FundingArbStrategy {
662
673
  ctx.state.set("arbOpenPositions", currentPositions);
663
674
  ctx.state.set("arbPositions", currentPositions.length);
664
675
  ctx.log(` [ARB] Spot-perp position opened! (${currentPositions.length}/${params.max_positions})`);
676
+ 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
677
  return true;
666
678
  }
667
679
  catch (err) {
668
680
  const msg = err instanceof Error ? err.message : String(err);
669
681
  ctx.log(` [ARB] Open spot-perp failed for ${opp.symbol}: ${msg}`);
682
+ 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
683
  return false;
671
684
  }
672
685
  }
@@ -765,11 +778,12 @@ export class FundingArbStrategy {
765
778
  try {
766
779
  const markets = await adapter.getMarkets();
767
780
  return markets
768
- .filter(m => m.fundingRate != null) // skip symbols with unavailable rates
781
+ .filter(m => m.fundingRate != null)
769
782
  .map(m => ({
770
783
  symbol: m.symbol,
771
784
  rate: parseFloat(m.fundingRate),
772
785
  price: parseFloat(m.markPrice),
786
+ sizeDecimals: m.sizeDecimals,
773
787
  }));
774
788
  }
775
789
  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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.20",
3
+ "version": "0.7.21",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",