perp-cli 0.9.6 → 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/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 -36
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-arb-strategy.js +9 -34
- package/dist/bot/strategies/spot-perp-executor.d.ts +0 -2
- package/dist/bot/strategies/spot-perp-executor.js +3 -6
- package/package.json +1 -1
|
@@ -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,40 +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 < 2)
|
|
384
|
-
return null;
|
|
385
|
-
const rates = history.map(h => parseFloat(h.rate));
|
|
386
|
-
// Compute actual funding period from timestamps (no static map dependency)
|
|
387
|
-
const times = history.map(h => h.time).sort((a, b) => a - b);
|
|
388
|
-
let totalInterval = 0;
|
|
389
|
-
for (let i = 1; i < times.length; i++)
|
|
390
|
-
totalInterval += times[i] - times[i - 1];
|
|
391
|
-
const avgIntervalH = totalInterval / (times.length - 1) / 3600000;
|
|
392
|
-
const fH = avgIntervalH < 1.5 ? 1 : avgIntervalH < 5 ? 4 : 8;
|
|
393
|
-
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
394
|
-
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
395
|
-
const annualized = (avgRate / fH) * 8760 * 100;
|
|
396
|
-
const score = { symbol, exchange, avgRate, consistency, annualized, payments: rates.length };
|
|
397
|
-
this._fundingScoreCache.set(key, { score, ts: Date.now() });
|
|
398
|
-
return score;
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
377
|
+
scoreFundingLocal(adapter, symbol, exchange, periods) {
|
|
378
|
+
return scoreFunding(adapter, symbol, exchange, periods, this._fundingScoreCache);
|
|
403
379
|
}
|
|
404
380
|
calcSignedSpread(pos, ratesByExchange) {
|
|
405
381
|
if (pos.mode === "spot-perp") {
|
|
406
382
|
const ri = findRate(ratesByExchange, pos.shortExchange, getPerpSymbol(pos.symbol, pos.shortExchange))
|
|
407
383
|
?? findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
408
384
|
const fH = ri?.fundingHours ?? getFundingHours(pos.shortExchange);
|
|
409
|
-
return ri ? (ri.rate / fH)
|
|
385
|
+
return ri ? annualizeHourlyRate(ri.rate / fH) : 0;
|
|
410
386
|
}
|
|
411
387
|
const li = findRate(ratesByExchange, pos.longExchange, pos.symbol);
|
|
412
388
|
const si = findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
@@ -414,7 +390,7 @@ export class FundingArbV2Strategy {
|
|
|
414
390
|
return 0;
|
|
415
391
|
const lH = li.rate / (li.fundingHours ?? getFundingHours(pos.longExchange));
|
|
416
392
|
const sH = si.rate / (si.fundingHours ?? getFundingHours(pos.shortExchange));
|
|
417
|
-
return (sH - lH)
|
|
393
|
+
return annualizeHourlyRate(sH - lH);
|
|
418
394
|
}
|
|
419
395
|
checkCloseByHistory(pos, ratesByExchange, p) {
|
|
420
396
|
const minHoldMs = p.min_hold_hours * 60 * 60 * 1000;
|
|
@@ -443,8 +419,8 @@ export class FundingArbV2Strategy {
|
|
|
443
419
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
444
420
|
if (!ri || ri.rate <= 0)
|
|
445
421
|
continue;
|
|
446
|
-
const score = await this.
|
|
447
|
-
?? await this.
|
|
422
|
+
const score = await this.scoreFundingLocal(adapter, perpSym, exName, p.history_periods)
|
|
423
|
+
?? await this.scoreFundingLocal(adapter, symbol, exName, p.history_periods);
|
|
448
424
|
if (!score)
|
|
449
425
|
continue;
|
|
450
426
|
if (!best || score.annualized > best.score.annualized)
|
|
@@ -819,7 +795,7 @@ export class FundingArbV2Strategy {
|
|
|
819
795
|
ctx.log(` [ARBv2] Close perp-perp: adapters not found`);
|
|
820
796
|
return false;
|
|
821
797
|
}
|
|
822
|
-
const isGone =
|
|
798
|
+
const isGone = isPositionGone;
|
|
823
799
|
let closeLongOk = false;
|
|
824
800
|
let longGone = false;
|
|
825
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,34 +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 < 2)
|
|
358
|
-
return null;
|
|
359
|
-
const rates = history.map(h => parseFloat(h.rate));
|
|
360
|
-
// Compute actual funding period from timestamps (no static map dependency)
|
|
361
|
-
const times = history.map(h => h.time).sort((a, b) => a - b);
|
|
362
|
-
let totalInterval = 0;
|
|
363
|
-
for (let i = 1; i < times.length; i++)
|
|
364
|
-
totalInterval += times[i] - times[i - 1];
|
|
365
|
-
const avgIntervalH = totalInterval / (times.length - 1) / 3600000;
|
|
366
|
-
const fH = avgIntervalH < 1.5 ? 1 : avgIntervalH < 5 ? 4 : 8;
|
|
367
|
-
const avgRate = rates.reduce((s, r) => s + r, 0) / rates.length;
|
|
368
|
-
const consistency = rates.filter(r => r > 0).length / rates.length;
|
|
369
|
-
const annualized = (avgRate / fH) * 8760 * 100;
|
|
370
|
-
const score = { symbol, exchange: exchangeName, avgRate, consistency, annualized, payments: rates.length };
|
|
371
|
-
this._fundingScoreCache.set(key, { score, ts: Date.now() });
|
|
372
|
-
return score;
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
350
|
+
scoreFundingLocal(adapter, symbol, exchangeName) {
|
|
351
|
+
return scoreFunding(adapter, symbol, exchangeName, this.params.history_periods, this._fundingScoreCache);
|
|
377
352
|
}
|
|
378
353
|
async findBestPerp(symbol, adapters, ratesByExchange) {
|
|
379
354
|
let best = null;
|
|
@@ -382,8 +357,8 @@ export class SpotPerpArbStrategy {
|
|
|
382
357
|
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
|
|
383
358
|
if (!ri || ri.rate <= 0)
|
|
384
359
|
continue;
|
|
385
|
-
const score = await this.
|
|
386
|
-
?? await this.
|
|
360
|
+
const score = await this.scoreFundingLocal(adapter, perpSym, exName)
|
|
361
|
+
?? await this.scoreFundingLocal(adapter, symbol, exName);
|
|
387
362
|
if (!score)
|
|
388
363
|
continue;
|
|
389
364
|
if (!best || score.annualized > best.score.annualized)
|
|
@@ -397,7 +372,7 @@ export class SpotPerpArbStrategy {
|
|
|
397
372
|
if (!ri)
|
|
398
373
|
return null; // rate unknown — don't assume 0
|
|
399
374
|
const fH = ri.fundingHours ?? getFundingHours(pos.perpExchange);
|
|
400
|
-
return (ri.rate / fH)
|
|
375
|
+
return annualizeHourlyRate(ri.rate / fH);
|
|
401
376
|
}
|
|
402
377
|
shouldClose(pos, ratesByExchange, p) {
|
|
403
378
|
const minHoldMs = p.min_hold_hours * 60 * 60 * 1000;
|
|
@@ -446,7 +421,7 @@ export class SpotPerpArbStrategy {
|
|
|
446
421
|
this._failCooldown.set(`${symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
447
422
|
return false;
|
|
448
423
|
}
|
|
449
|
-
const actualAnn = (rateInfo.rate / actualFh)
|
|
424
|
+
const actualAnn = annualizeHourlyRate(rateInfo.rate / actualFh);
|
|
450
425
|
if (actualAnn < p.min_rate) {
|
|
451
426
|
ctx.log(` [SPA] Skip ${symbol}: actual rate ${actualAnn.toFixed(1)}% (${actualFh}h funding) below min_rate ${p.min_rate}%`);
|
|
452
427
|
this._failCooldown.set(`${symbol}:entry`, Date.now() + 10 * 60 * 1000);
|
|
@@ -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 {
|