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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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)
|
|
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 {
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -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;
|