perp-cli 0.7.47 → 0.7.50
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.
|
@@ -201,12 +201,21 @@ export class FundingArbStrategy {
|
|
|
201
201
|
const equity = parseFloat(bal.equity);
|
|
202
202
|
const marginUsed = parseFloat(bal.marginUsed);
|
|
203
203
|
const marginPct = equity > 0 ? (marginUsed / equity) * 100 : 100;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
const available = parseFloat(bal.available);
|
|
205
|
+
exchangeBalances.set(name, { equity, available, marginPct });
|
|
206
|
+
// Skip if: margin > 90%, available < $5, or exchange on liquidation cooldown
|
|
207
|
+
const exchCooldown = this._failCooldown.get(`${name}:exchange`) ?? 0;
|
|
208
|
+
if (Date.now() < exchCooldown) {
|
|
209
|
+
ctx.log(` [ARB] ${name} on liquidation cooldown — skipping`);
|
|
210
|
+
}
|
|
211
|
+
else if (available < 5) {
|
|
212
|
+
ctx.log(` [ARB] ${name} available $${available.toFixed(2)} too low — skipping`);
|
|
213
|
+
}
|
|
214
|
+
else if (marginPct >= 90) {
|
|
215
|
+
ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping`);
|
|
207
216
|
}
|
|
208
217
|
else {
|
|
209
|
-
|
|
218
|
+
availableAdapters.set(name, a);
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
catch (err) {
|
|
@@ -334,7 +343,9 @@ export class FundingArbStrategy {
|
|
|
334
343
|
const longAdapter = adapters.get(pos.longExchange);
|
|
335
344
|
const shortAdapter = adapters.get(pos.shortExchange);
|
|
336
345
|
if (longAdapter && shortAdapter) {
|
|
346
|
+
const isPositionGone = (msg) => msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
|
|
337
347
|
let closeLongOk = false;
|
|
348
|
+
let longPositionGone = false;
|
|
338
349
|
try {
|
|
339
350
|
const closeLongResult = await longAdapter.marketOrder(pos.symbol, "sell", pos.size, { reduceOnly: true });
|
|
340
351
|
closeLongOk = true;
|
|
@@ -342,11 +353,20 @@ export class FundingArbStrategy {
|
|
|
342
353
|
}
|
|
343
354
|
catch (err) {
|
|
344
355
|
const msg = err instanceof Error ? err.message : String(err);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
if (isPositionGone(msg)) {
|
|
357
|
+
// Position already liquidated/closed on this exchange
|
|
358
|
+
longPositionGone = true;
|
|
359
|
+
ctx.log(` [ARB] Long position gone for ${pos.symbol} on ${pos.longExchange} (liquidated?) — force closing short`);
|
|
360
|
+
// Cooldown the exchange for 30 minutes to prevent liquidation-entry cycle
|
|
361
|
+
this._failCooldown.set(`${pos.longExchange}:exchange`, Date.now() + 30 * 60 * 1000);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
ctx.log(` [ARB] Close long leg failed for ${pos.symbol}: ${msg} (cooldown 5m)`);
|
|
365
|
+
logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "long" } });
|
|
366
|
+
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
367
|
+
}
|
|
348
368
|
}
|
|
349
|
-
if (!closeLongOk)
|
|
369
|
+
if (!closeLongOk && !longPositionGone)
|
|
350
370
|
continue;
|
|
351
371
|
try {
|
|
352
372
|
const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size, { reduceOnly: true });
|
|
@@ -354,25 +374,45 @@ export class FundingArbStrategy {
|
|
|
354
374
|
}
|
|
355
375
|
catch (err) {
|
|
356
376
|
const msg = err instanceof Error ? err.message : String(err);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
362
|
-
ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
|
|
363
|
-
// Note: re-open is NOT reduceOnly (it's a new position)
|
|
364
|
-
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
|
|
377
|
+
if (isPositionGone(msg)) {
|
|
378
|
+
// Short position also gone — both liquidated, just clean up tracking
|
|
379
|
+
ctx.log(` [ARB] Short position also gone for ${pos.symbol} on ${pos.shortExchange} — removing from tracking`);
|
|
380
|
+
this._failCooldown.set(`${pos.shortExchange}:exchange`, Date.now() + 30 * 60 * 1000);
|
|
365
381
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
ctx.log(` [ARB]
|
|
369
|
-
logExecution({ type: "
|
|
382
|
+
else if (closeLongOk) {
|
|
383
|
+
// Long was closed but short failed (not liquidated) — rollback long
|
|
384
|
+
ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
|
|
385
|
+
logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "short" } });
|
|
386
|
+
try {
|
|
387
|
+
await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
388
|
+
ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
|
|
389
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
|
|
390
|
+
}
|
|
391
|
+
catch (rbErr) {
|
|
392
|
+
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
393
|
+
ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
|
|
394
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
|
|
395
|
+
}
|
|
396
|
+
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Long was gone, short failed (not liquidated) — orphaned short, force close without reduceOnly
|
|
401
|
+
ctx.log(` [ARB] Force closing orphaned short for ${pos.symbol}: ${msg}`);
|
|
402
|
+
try {
|
|
403
|
+
await shortAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
404
|
+
ctx.log(` [ARB] Force closed short ${pos.size} ${pos.symbol} on ${pos.shortExchange}`);
|
|
405
|
+
}
|
|
406
|
+
catch (forceErr) {
|
|
407
|
+
const forceMsg = forceErr instanceof Error ? forceErr.message : String(forceErr);
|
|
408
|
+
ctx.log(` [ARB] CRITICAL: Force close short failed for ${pos.symbol}: ${forceMsg}`);
|
|
409
|
+
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 10 * 60 * 1000);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
370
412
|
}
|
|
371
|
-
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
372
|
-
continue;
|
|
373
413
|
}
|
|
374
414
|
toClose.push(pos);
|
|
375
|
-
ctx.log(` [ARB] Closed ${pos.symbol} position`);
|
|
415
|
+
ctx.log(` [ARB] Closed ${pos.symbol} position${longPositionGone ? ' (long was liquidated)' : ''}`);
|
|
376
416
|
}
|
|
377
417
|
}
|
|
378
418
|
}
|
|
@@ -595,20 +635,7 @@ export class FundingArbStrategy {
|
|
|
595
635
|
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
596
636
|
ctx.log(` [ARB] CRITICAL: Rollback failed for ${best.symbol}: ${rbMsg}`);
|
|
597
637
|
logExecution({ type: "multi_leg_rollback", exchange: best.longExchange, symbol: best.symbol, side: "sell", size: matched.size, status: "failed", error: rbMsg, dryRun: false });
|
|
598
|
-
|
|
599
|
-
const currentPositions = ctx.state.get("arbOpenPositions");
|
|
600
|
-
currentPositions.push({
|
|
601
|
-
symbol: best.symbol,
|
|
602
|
-
longExchange: best.longExchange,
|
|
603
|
-
shortExchange: best.shortExchange, // note: short leg doesn't exist
|
|
604
|
-
entrySpread: 0,
|
|
605
|
-
size: matched.size,
|
|
606
|
-
mode: "perp-perp",
|
|
607
|
-
entryTime: 0, // flag as orphaned
|
|
608
|
-
});
|
|
609
|
-
ctx.state.set("arbOpenPositions", currentPositions);
|
|
610
|
-
ctx.state.set("arbPositions", currentPositions.length);
|
|
611
|
-
ctx.log(` [ARB] Tracking orphaned long position for manual cleanup`);
|
|
638
|
+
ctx.log(` [ARB] CRITICAL: Orphaned long position on ${best.longExchange} for ${best.symbol} — operator must close manually`);
|
|
612
639
|
}
|
|
613
640
|
this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
614
641
|
continue;
|
|
@@ -951,6 +978,11 @@ export class FundingArbStrategy {
|
|
|
951
978
|
const msg = spotErr instanceof Error ? spotErr.message : String(spotErr);
|
|
952
979
|
ctx.log(` [ARB] Open spot-perp failed (spot buy) for ${opp.symbol}: ${msg}`);
|
|
953
980
|
logExecution({ type: "arb_entry", exchange: opp.longExchange, symbol: opp.symbol, side: "buy", size: matched.size, status: "failed", error: msg, dryRun: false, meta: { mode: "spot-perp", leg: "spot" } });
|
|
981
|
+
try {
|
|
982
|
+
await this.transferUsdcToPerp(spotAdapter, exchangeName, transferAmt);
|
|
983
|
+
ctx.log(` [ARB] Transferred $${transferAmt} USDC back to perp after spot buy failure`);
|
|
984
|
+
}
|
|
985
|
+
catch { /* best effort */ }
|
|
954
986
|
return false;
|
|
955
987
|
}
|
|
956
988
|
// Step 3: Short perp
|
|
@@ -1115,9 +1147,12 @@ export class FundingArbStrategy {
|
|
|
1115
1147
|
// Bootstrap aster funding hours lazily (1 API call per unknown symbol, cached permanently)
|
|
1116
1148
|
if (exchangeName === "aster" && "getFundingHours" in adapter) {
|
|
1117
1149
|
const aster = adapter;
|
|
1118
|
-
// Only bootstrap symbols not yet cached (up to
|
|
1119
|
-
const uncached = withRates.filter(m =>
|
|
1120
|
-
|
|
1150
|
+
// Only bootstrap symbols not yet cached (up to 20 per tick to complete faster)
|
|
1151
|
+
const uncached = withRates.filter(m => {
|
|
1152
|
+
const cached = adapter?._fundingHoursCache?.get?.(m.symbol);
|
|
1153
|
+
return cached === undefined; // only truly uncached, not symbols confirmed as 1h
|
|
1154
|
+
});
|
|
1155
|
+
const toBootstrap = uncached.slice(0, 20);
|
|
1121
1156
|
for (const m of toBootstrap) {
|
|
1122
1157
|
const fh = await aster.getFundingHours(m.symbol);
|
|
1123
1158
|
m.fundingHours = fh;
|