perp-cli 0.9.4 → 0.9.7
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.
- package/dist/bot/engine.js +2 -1
- package/dist/bot/strategies/funding-arb-utils.d.ts +19 -0
- package/dist/bot/strategies/funding-arb-utils.js +40 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-v2-strategy.js +12 -30
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +27 -30
- package/dist/bot/strategies/spot-perp-executor.d.ts +0 -2
- package/dist/bot/strategies/spot-perp-executor.js +3 -6
- package/dist/funding/normalize.js +1 -1
- package/package.json +1 -1
package/dist/bot/engine.js
CHANGED
|
@@ -76,10 +76,11 @@ mode = "headless") {
|
|
|
76
76
|
tick: 0,
|
|
77
77
|
log,
|
|
78
78
|
};
|
|
79
|
-
// Pass extra adapters to strategy context
|
|
79
|
+
// Pass extra adapters and risk config to strategy context
|
|
80
80
|
if (extraAdapters) {
|
|
81
81
|
strategyCtx.state.set("extraAdapters", extraAdapters);
|
|
82
82
|
}
|
|
83
|
+
strategyCtx.state.set("riskConfig", config.risk);
|
|
83
84
|
const isDryRun = false; // TODO: wire up from config when dry-run mode is added
|
|
84
85
|
let running = true;
|
|
85
86
|
// ── TUI / JSON mode setup ──
|
|
@@ -38,6 +38,25 @@ export interface RecoveredPosition {
|
|
|
38
38
|
}
|
|
39
39
|
/** Recover arb positions from exchange state (perp-perp + spot-perp, same + cross exchange). */
|
|
40
40
|
export declare function recoverArbPositions(adapters: Map<string, ExchangeAdapter>, log: (msg: string) => void): Promise<RecoveredPosition[]>;
|
|
41
|
+
export interface FundingScore {
|
|
42
|
+
symbol: string;
|
|
43
|
+
exchange: string;
|
|
44
|
+
avgRate: number;
|
|
45
|
+
consistency: number;
|
|
46
|
+
annualized: number;
|
|
47
|
+
payments: number;
|
|
48
|
+
}
|
|
49
|
+
/** Returns true if a reduceOnly rejection indicates the position is already gone. */
|
|
50
|
+
export declare function isPositionGone(msg: string): boolean;
|
|
51
|
+
/** Compute funding period in hours from history timestamps. Falls back to static map. */
|
|
52
|
+
export declare function computeFundingHoursFromHistory(history: {
|
|
53
|
+
time: number;
|
|
54
|
+
}[]): number;
|
|
55
|
+
/** Score a symbol's funding rate using history. Returns null if insufficient data. */
|
|
56
|
+
export declare function scoreFunding(adapter: ExchangeAdapter, symbol: string, exchangeName: string, periods: number, cache?: Map<string, {
|
|
57
|
+
score: FundingScore;
|
|
58
|
+
ts: number;
|
|
59
|
+
}>): Promise<FundingScore | null>;
|
|
41
60
|
/** Fetch funding rates from an exchange adapter. */
|
|
42
61
|
export declare function fetchRates(adapter: ExchangeAdapter, exchangeName: string): Promise<{
|
|
43
62
|
symbol: string;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Shared utilities for funding arbitrage strategies (v1 + v2).
|
|
3
3
|
* Extracted to keep individual strategy files under size limits.
|
|
4
4
|
*/
|
|
5
|
+
import { getFundingHours } from "../../funding.js";
|
|
6
|
+
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
5
7
|
// ── Adapter helpers ──
|
|
6
8
|
/** Build adapter map from primary adapter + extraAdapters in context state. */
|
|
7
9
|
export function buildAdapterMap(ctx) {
|
|
@@ -206,6 +208,44 @@ export async function recoverArbPositions(adapters, log) {
|
|
|
206
208
|
}
|
|
207
209
|
return recovered;
|
|
208
210
|
}
|
|
211
|
+
/** Returns true if a reduceOnly rejection indicates the position is already gone. */
|
|
212
|
+
export function isPositionGone(msg) {
|
|
213
|
+
return msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
|
|
214
|
+
}
|
|
215
|
+
/** Compute funding period in hours from history timestamps. Falls back to static map. */
|
|
216
|
+
export function computeFundingHoursFromHistory(history) {
|
|
217
|
+
if (history.length < 2)
|
|
218
|
+
return getFundingHours("unknown"); // fallback to 1
|
|
219
|
+
const times = history.map(h => h.time).sort((a, b) => a - b);
|
|
220
|
+
let totalInterval = 0;
|
|
221
|
+
for (let i = 1; i < times.length; i++)
|
|
222
|
+
totalInterval += times[i] - times[i - 1];
|
|
223
|
+
const avgIntervalH = totalInterval / (times.length - 1) / 3600000;
|
|
224
|
+
return avgIntervalH < 1.5 ? 1 : avgIntervalH < 5 ? 4 : 8;
|
|
225
|
+
}
|
|
226
|
+
/** Score a symbol's funding rate using history. Returns null if insufficient data. */
|
|
227
|
+
export async function scoreFunding(adapter, symbol, exchangeName, periods, cache) {
|
|
228
|
+
const key = `${exchangeName}:${symbol}`;
|
|
229
|
+
const cached = cache?.get(key);
|
|
230
|
+
if (cached && Date.now() - cached.ts < 10 * 60 * 1000)
|
|
231
|
+
return cached.score;
|
|
232
|
+
try {
|
|
233
|
+
const history = await adapter.getFundingHistory(symbol, periods);
|
|
234
|
+
if (history.length < 2)
|
|
235
|
+
return null;
|
|
236
|
+
const rates = history.map(h => parseFloat(h.rate));
|
|
237
|
+
const fH = computeFundingHoursFromHistory(history);
|
|
238
|
+
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
239
|
+
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
240
|
+
const annualized = annualizeHourlyRate(avgRate / fH);
|
|
241
|
+
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
242
|
+
cache?.set(key, { score, ts: Date.now() });
|
|
243
|
+
return score;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
209
249
|
/** Fetch funding rates from an exchange adapter. */
|
|
210
250
|
export async function fetchRates(adapter, exchangeName) {
|
|
211
251
|
try {
|
|
@@ -30,7 +30,7 @@ export declare class FundingArbV2Strategy implements Strategy {
|
|
|
30
30
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
31
31
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
32
32
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
33
|
-
private
|
|
33
|
+
private scoreFundingLocal;
|
|
34
34
|
private calcSignedSpread;
|
|
35
35
|
private checkCloseByHistory;
|
|
36
36
|
private findBestPerpShort;
|
|
@@ -2,9 +2,10 @@ 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 { getFundingHours } from "../../funding.js";
|
|
5
|
+
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
5
6
|
import { logExecution, pruneExecutionLog } from "../../execution-log.js";
|
|
6
7
|
import { invalidateCache } from "../../cache.js";
|
|
7
|
-
import { buildAdapterMap, getSpotAdapter, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, findRate, matchSymbol, getPriceEstimate, fetchRates, recoverArbPositions, } from "./funding-arb-utils.js";
|
|
8
|
+
import { buildAdapterMap, getSpotAdapter, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, findRate, matchSymbol, getPriceEstimate, fetchRates, recoverArbPositions, scoreFunding, isPositionGone, } from "./funding-arb-utils.js";
|
|
8
9
|
let _posIdCounter = 0;
|
|
9
10
|
function genPosId() { return `v2-${Date.now()}-${++_posIdCounter}`; }
|
|
10
11
|
// ── Strategy ──
|
|
@@ -235,7 +236,7 @@ export class FundingArbV2Strategy {
|
|
|
235
236
|
?? findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
236
237
|
if (!currentInfo)
|
|
237
238
|
continue;
|
|
238
|
-
const curAnn = (currentInfo.rate / (currentInfo.fundingHours ?? getFundingHours(pos.shortExchange)))
|
|
239
|
+
const curAnn = annualizeHourlyRate(currentInfo.rate / (currentInfo.fundingHours ?? getFundingHours(pos.shortExchange)));
|
|
239
240
|
if (best.score.annualized - curAnn < p.rotation_threshold)
|
|
240
241
|
continue;
|
|
241
242
|
ctx.log(` [ARBv2] Rotating ${pos.symbol} short: ${pos.shortExchange} (${curAnn.toFixed(1)}%) -> ${best.exchange} (${best.score.annualized.toFixed(1)}%)`);
|
|
@@ -302,10 +303,10 @@ export class FundingArbV2Strategy {
|
|
|
302
303
|
}
|
|
303
304
|
if (!minEx || !maxEx || minEx === maxEx)
|
|
304
305
|
continue;
|
|
305
|
-
const spread = (maxH - minH)
|
|
306
|
+
const spread = annualizeHourlyRate(maxH - minH);
|
|
306
307
|
if (spread < p.min_spread)
|
|
307
308
|
continue;
|
|
308
|
-
const shortScore = await this.
|
|
309
|
+
const shortScore = await this.scoreFundingLocal(adapters.get(maxEx), sym, maxEx, p.history_periods);
|
|
309
310
|
if (shortScore && shortScore.consistency < p.min_consistency)
|
|
310
311
|
continue;
|
|
311
312
|
scoredOpps.push({ symbol: sym, longExchange: minEx, shortExchange: maxEx, spread, score: shortScore ?? { symbol: sym, exchange: maxEx, avgRate: maxR, consistency: 1, annualized: spread, payments: 0 }, mode: "perp-perp", shortRate: maxR, longRate: minR });
|
|
@@ -373,34 +374,15 @@ export class FundingArbV2Strategy {
|
|
|
373
374
|
// ═══════════════════════════════════════════════════════════════════
|
|
374
375
|
// Core scoring / decision methods
|
|
375
376
|
// ═══════════════════════════════════════════════════════════════════
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const cached = this._fundingScoreCache.get(key);
|
|
379
|
-
if (cached && Date.now() - cached.ts < 10 * 60 * 1000)
|
|
380
|
-
return cached.score;
|
|
381
|
-
try {
|
|
382
|
-
const history = await adapter.getFundingHistory(symbol, periods);
|
|
383
|
-
if (history.length === 0)
|
|
384
|
-
return null;
|
|
385
|
-
const rates = history.map(h => parseFloat(h.rate));
|
|
386
|
-
const fH = getFundingHours(exchange);
|
|
387
|
-
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
388
|
-
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
389
|
-
const annualized = (avgRate / fH) * 8760 * 100;
|
|
390
|
-
const score = { symbol, exchange, avgRate, consistency, annualized, payments: rates.length };
|
|
391
|
-
this._fundingScoreCache.set(key, { score, ts: Date.now() });
|
|
392
|
-
return score;
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
377
|
+
scoreFundingLocal(adapter, symbol, exchange, periods) {
|
|
378
|
+
return scoreFunding(adapter, symbol, exchange, periods, this._fundingScoreCache);
|
|
397
379
|
}
|
|
398
380
|
calcSignedSpread(pos, ratesByExchange) {
|
|
399
381
|
if (pos.mode === "spot-perp") {
|
|
400
382
|
const ri = findRate(ratesByExchange, pos.shortExchange, getPerpSymbol(pos.symbol, pos.shortExchange))
|
|
401
383
|
?? findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
402
384
|
const fH = ri?.fundingHours ?? getFundingHours(pos.shortExchange);
|
|
403
|
-
return ri ? (ri.rate / fH)
|
|
385
|
+
return ri ? annualizeHourlyRate(ri.rate / fH) : 0;
|
|
404
386
|
}
|
|
405
387
|
const li = findRate(ratesByExchange, pos.longExchange, pos.symbol);
|
|
406
388
|
const si = findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
@@ -408,7 +390,7 @@ export class FundingArbV2Strategy {
|
|
|
408
390
|
return 0;
|
|
409
391
|
const lH = li.rate / (li.fundingHours ?? getFundingHours(pos.longExchange));
|
|
410
392
|
const sH = si.rate / (si.fundingHours ?? getFundingHours(pos.shortExchange));
|
|
411
|
-
return (sH - lH)
|
|
393
|
+
return annualizeHourlyRate(sH - lH);
|
|
412
394
|
}
|
|
413
395
|
checkCloseByHistory(pos, ratesByExchange, p) {
|
|
414
396
|
const minHoldMs = p.min_hold_hours * 60 * 60 * 1000;
|
|
@@ -437,8 +419,8 @@ export class FundingArbV2Strategy {
|
|
|
437
419
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
438
420
|
if (!ri || ri.rate <= 0)
|
|
439
421
|
continue;
|
|
440
|
-
const score = await this.
|
|
441
|
-
?? await this.
|
|
422
|
+
const score = await this.scoreFundingLocal(adapter, perpSym, exName, p.history_periods)
|
|
423
|
+
?? await this.scoreFundingLocal(adapter, symbol, exName, p.history_periods);
|
|
442
424
|
if (!score)
|
|
443
425
|
continue;
|
|
444
426
|
if (!best || score.annualized > best.score.annualized)
|
|
@@ -813,7 +795,7 @@ export class FundingArbV2Strategy {
|
|
|
813
795
|
ctx.log(` [ARBv2] Close perp-perp: adapters not found`);
|
|
814
796
|
return false;
|
|
815
797
|
}
|
|
816
|
-
const isGone =
|
|
798
|
+
const isGone = isPositionGone;
|
|
817
799
|
let closeLongOk = false;
|
|
818
800
|
let longGone = false;
|
|
819
801
|
try {
|
|
@@ -34,7 +34,7 @@ export declare class SpotPerpArbStrategy implements Strategy {
|
|
|
34
34
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
35
35
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
36
36
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
37
|
-
private
|
|
37
|
+
private scoreFundingLocal;
|
|
38
38
|
private findBestPerp;
|
|
39
39
|
private calcAnnualized;
|
|
40
40
|
private shouldClose;
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { registerStrategy } from "../strategy-registry.js";
|
|
11
11
|
import { getFundingHours } from "../../funding.js";
|
|
12
|
+
import { annualizeHourlyRate } from "../../funding/normalize.js";
|
|
12
13
|
import { pruneExecutionLog } from "../../execution-log.js";
|
|
13
|
-
import { buildAdapterMap, getSpotAdapter, getPerpSymbol, findRate, matchSymbol, fetchRates, } from "./funding-arb-utils.js";
|
|
14
|
+
import { buildAdapterMap, getSpotAdapter, getPerpSymbol, findRate, matchSymbol, fetchRates, scoreFunding, } from "./funding-arb-utils.js";
|
|
14
15
|
import { SpotPerpExecutor } from "./spot-perp-executor.js";
|
|
15
16
|
let _posIdCounter = 0;
|
|
16
17
|
function genPosId() { return `spa-${Date.now()}-${++_posIdCounter}`; }
|
|
@@ -227,7 +228,7 @@ export class SpotPerpArbStrategy {
|
|
|
227
228
|
?? findRate(ratesByExchange, pos.perpExchange, pos.symbol);
|
|
228
229
|
if (!currentInfo)
|
|
229
230
|
continue;
|
|
230
|
-
const curAnn = (currentInfo.rate / (currentInfo.fundingHours ?? getFundingHours(pos.perpExchange)))
|
|
231
|
+
const curAnn = annualizeHourlyRate(currentInfo.rate / (currentInfo.fundingHours ?? getFundingHours(pos.perpExchange)));
|
|
231
232
|
if (best.score.annualized - curAnn < p.rotation_threshold)
|
|
232
233
|
continue;
|
|
233
234
|
ctx.log(` [SPA] Rotating ${pos.symbol} short: ${pos.perpExchange} (${curAnn.toFixed(1)}%) -> ${best.exchange} (${best.score.annualized.toFixed(1)}%)`);
|
|
@@ -346,28 +347,8 @@ export class SpotPerpArbStrategy {
|
|
|
346
347
|
return [{ type: "cancel_all" }];
|
|
347
348
|
}
|
|
348
349
|
// ── Scoring (decision logic) ──
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const key = `${exchangeName}:${symbol}`;
|
|
352
|
-
const cached = this._fundingScoreCache.get(key);
|
|
353
|
-
if (cached && Date.now() - cached.ts < 10 * 60 * 1000)
|
|
354
|
-
return cached.score;
|
|
355
|
-
try {
|
|
356
|
-
const history = await adapter.getFundingHistory(symbol, p.history_periods);
|
|
357
|
-
if (history.length === 0)
|
|
358
|
-
return null;
|
|
359
|
-
const rates = history.map(h => parseFloat(h.rate));
|
|
360
|
-
const fH = getFundingHours(exchangeName);
|
|
361
|
-
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
362
|
-
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
363
|
-
const annualized = (avgRate / fH) * 8760 * 100;
|
|
364
|
-
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
365
|
-
this._fundingScoreCache.set(key, { score, ts: Date.now() });
|
|
366
|
-
return score;
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
350
|
+
scoreFundingLocal(adapter, symbol, exchangeName) {
|
|
351
|
+
return scoreFunding(adapter, symbol, exchangeName, this.params.history_periods, this._fundingScoreCache);
|
|
371
352
|
}
|
|
372
353
|
async findBestPerp(symbol, adapters, ratesByExchange) {
|
|
373
354
|
let best = null;
|
|
@@ -376,8 +357,8 @@ export class SpotPerpArbStrategy {
|
|
|
376
357
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
377
358
|
if (!ri || ri.rate <= 0)
|
|
378
359
|
continue;
|
|
379
|
-
const score = await this.
|
|
380
|
-
?? await this.
|
|
360
|
+
const score = await this.scoreFundingLocal(adapter, perpSym, exName)
|
|
361
|
+
?? await this.scoreFundingLocal(adapter, symbol, exName);
|
|
381
362
|
if (!score)
|
|
382
363
|
continue;
|
|
383
364
|
if (!best || score.annualized > best.score.annualized)
|
|
@@ -391,7 +372,7 @@ export class SpotPerpArbStrategy {
|
|
|
391
372
|
if (!ri)
|
|
392
373
|
return null; // rate unknown — don't assume 0
|
|
393
374
|
const fH = ri.fundingHours ?? getFundingHours(pos.perpExchange);
|
|
394
|
-
return (ri.rate / fH)
|
|
375
|
+
return annualizeHourlyRate(ri.rate / fH);
|
|
395
376
|
}
|
|
396
377
|
shouldClose(pos, ratesByExchange, p) {
|
|
397
378
|
const minHoldMs = p.min_hold_hours * 60 * 60 * 1000;
|
|
@@ -440,15 +421,31 @@ export class SpotPerpArbStrategy {
|
|
|
440
421
|
this._failCooldown.set(`${symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
441
422
|
return false;
|
|
442
423
|
}
|
|
443
|
-
const actualAnn = (rateInfo.rate / actualFh)
|
|
424
|
+
const actualAnn = annualizeHourlyRate(rateInfo.rate / actualFh);
|
|
444
425
|
if (actualAnn < p.min_rate) {
|
|
445
426
|
ctx.log(` [SPA] Skip ${symbol}: actual rate ${actualAnn.toFixed(1)}% (${actualFh}h funding) below min_rate ${p.min_rate}%`);
|
|
446
427
|
this._failCooldown.set(`${symbol}:entry`, Date.now() + 10 * 60 * 1000);
|
|
447
428
|
return false;
|
|
448
429
|
}
|
|
449
430
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
431
|
+
// Risk policy: enforce max_position_usd from risk config
|
|
432
|
+
const riskConfig = ctx.state.get("riskConfig");
|
|
433
|
+
const maxTotalPositionUsd = riskConfig?.max_position_usd ?? Infinity;
|
|
434
|
+
const positions = ctx.state.get("spaPositions");
|
|
435
|
+
let currentTotalUsd = 0;
|
|
436
|
+
for (const pos of positions) {
|
|
437
|
+
const ri = findRate(ratesByExchange, pos.perpExchange, getPerpSymbol(pos.symbol, pos.perpExchange))
|
|
438
|
+
?? findRate(ratesByExchange, pos.perpExchange, pos.symbol);
|
|
439
|
+
const price = ri?.price ?? 0;
|
|
440
|
+
currentTotalUsd += price * parseFloat(pos.size);
|
|
441
|
+
}
|
|
442
|
+
const remainingBudget = maxTotalPositionUsd - currentTotalUsd;
|
|
443
|
+
if (remainingBudget < 10) {
|
|
444
|
+
ctx.log(` [SPA] Skip ${symbol}: risk limit — total positions $${currentTotalUsd.toFixed(0)}/$${maxTotalPositionUsd} (no budget)`);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
// Size decision (capped by risk budget)
|
|
448
|
+
let targetSizeUsd = Math.min(p.size_usd, remainingBudget);
|
|
452
449
|
const sameExchange = spotExchange === perpExchange;
|
|
453
450
|
if (sameExchange) {
|
|
454
451
|
const perpBal = await perpAdapter.getBalance();
|
|
@@ -20,8 +20,6 @@ export declare class SpotPerpExecutor {
|
|
|
20
20
|
transferToSpot(spotAdapter: SpotAdapter, exchange: string, amount: number): Promise<void>;
|
|
21
21
|
/** Transfer USDC: spot -> perp account. */
|
|
22
22
|
transferToPerp(spotAdapter: SpotAdapter, exchange: string, amount: number): Promise<void>;
|
|
23
|
-
/** Returns true if a reduceOnly rejection indicates the position is already gone. */
|
|
24
|
-
private isPositionGone;
|
|
25
23
|
/**
|
|
26
24
|
* Full spot-perp entry flow:
|
|
27
25
|
* 1. Check liquidity
|
|
@@ -8,7 +8,7 @@ import { logExecution } from "../../execution-log.js";
|
|
|
8
8
|
import { invalidateCache } from "../../cache.js";
|
|
9
9
|
import { checkSpotPerpLiquidity } from "../../liquidity.js";
|
|
10
10
|
import { computeSpotPerpMatchedSize } from "../../arb-sizing.js";
|
|
11
|
-
import { getSpotAdapter as getSpotAdapterUtil, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, matchSymbol, getPriceEstimate, } from "./funding-arb-utils.js";
|
|
11
|
+
import { getSpotAdapter as getSpotAdapterUtil, transferUsdcToSpot, transferUsdcToPerp, getPerpSymbol, matchSymbol, getPriceEstimate, isPositionGone, } from "./funding-arb-utils.js";
|
|
12
12
|
// ── Executor ──
|
|
13
13
|
export class SpotPerpExecutor {
|
|
14
14
|
_spotAdapterCache = new Map();
|
|
@@ -30,10 +30,7 @@ export class SpotPerpExecutor {
|
|
|
30
30
|
async transferToPerp(spotAdapter, exchange, amount) {
|
|
31
31
|
await transferUsdcToPerp(spotAdapter, exchange, amount);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
isPositionGone(msg) {
|
|
35
|
-
return msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
|
|
36
|
-
}
|
|
33
|
+
// isPositionGone() is now in funding-arb-utils.ts
|
|
37
34
|
/**
|
|
38
35
|
* Full spot-perp entry flow:
|
|
39
36
|
* 1. Check liquidity
|
|
@@ -197,7 +194,7 @@ export class SpotPerpExecutor {
|
|
|
197
194
|
catch (err) {
|
|
198
195
|
const msg = err instanceof Error ? err.message : String(err);
|
|
199
196
|
// ReduceOnly rejected = position already gone, proceed to sell spot
|
|
200
|
-
if (
|
|
197
|
+
if (isPositionGone(msg)) {
|
|
201
198
|
log(` [SPA] Perp already closed (ReduceOnly rejected)`);
|
|
202
199
|
}
|
|
203
200
|
else {
|