Skip to content

Commit 7634d70

Browse files
Hiksangclaude
andcommitted
fix: enable all exchanges as perp candidates, not just aster
- fetchRates: set default fundingHours from static map for non-aster exchanges (was filtering out ALL pacifica/HL/lighter markets) - findPerpCandidates: return all qualifying exchanges sorted by rate (was returning only the single best, causing skip if that exchange had insufficient balance) - Now discovers opportunities like HYPE lighter-spot<>pacifica, AZTEC lighter-spot<>hyperliquid Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c9b09de commit 7634d70

2 files changed

Lines changed: 20 additions & 9 deletions

File tree

src/bot/strategies/funding-arb-utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,14 @@ export async function fetchRates(
306306
}
307307
}
308308

309+
// For non-aster exchanges, set fundingHours from static map if not already set
310+
if (exchangeName !== "aster") {
311+
const defaultFH = getFundingHours(exchangeName);
312+
for (const m of withRates) { if (m.fundingHours == null) m.fundingHours = defaultFH; }
313+
}
314+
309315
return withRates
310-
.filter(m => m.fundingHours != null) // skip symbols with unknown funding period (aster not yet bootstrapped)
316+
.filter(m => m.fundingHours != null) // skip aster symbols with unknown funding period
311317
.map(m => ({
312318
symbol: m.symbol,
313319
rate: parseFloat(m.fundingRate!),

src/bot/strategies/spot-perp-arb-strategy.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ export class SpotPerpArbStrategy implements Strategy {
229229
for (const pos of currentPositions) {
230230
const rcd = this._failCooldown.get(`${pos.id}:rotate`) ?? 0;
231231
if (Date.now() < rcd) continue;
232-
const best = await this.findBestPerp(pos.symbol, adapters, ratesByExchange);
232+
const candidates = await this.findPerpCandidates(pos.symbol, adapters, ratesByExchange);
233+
const best = candidates[0];
233234
if (!best || best.exchange === pos.perpExchange) continue;
234235
const currentInfo = findRate(ratesByExchange, pos.perpExchange, getPerpSymbol(pos.symbol, pos.perpExchange))
235236
?? findRate(ratesByExchange, pos.perpExchange, pos.symbol);
@@ -280,9 +281,11 @@ export class SpotPerpArbStrategy implements Strategy {
280281
if (spotExName === "hyperliquid" && !HL_SPOT_WHITELIST.has(base)) continue;
281282
if (openSymbols.has(base)) continue;
282283
if (Date.now() < (this._failCooldown.get(`${base}:entry`) ?? 0)) continue;
283-
const best = await this.findBestPerp(base, adapters, ratesByExchange);
284-
if (!best || best.score.annualized < p.min_rate || best.score.consistency < p.min_consistency) continue;
285-
scoredOpps.push({ symbol: base, spotExchange: spotExName, perpExchange: best.exchange, annualized: best.score.annualized, score: best.score });
284+
const candidates = await this.findPerpCandidates(base, adapters, ratesByExchange);
285+
for (const cand of candidates) {
286+
if (cand.score.annualized < p.min_rate || cand.score.consistency < p.min_consistency) continue;
287+
scoredOpps.push({ symbol: base, spotExchange: spotExName, perpExchange: cand.exchange, annualized: cand.score.annualized, score: cand.score });
288+
}
286289
}
287290
} catch { /* no spot */ }
288291
}
@@ -339,18 +342,20 @@ export class SpotPerpArbStrategy implements Strategy {
339342
return scoreFunding(adapter, symbol, exchangeName, this.params.history_periods, this._fundingScoreCache);
340343
}
341344

342-
private async findBestPerp(symbol: string, adapters: Map<string, ExchangeAdapter>, ratesByExchange: RateMap): Promise<{ exchange: string; rate: number; score: FundingScore } | null> {
343-
let best: { exchange: string; rate: number; score: FundingScore } | null = null;
345+
/** Find all perp exchanges for a symbol, sorted by annualized rate (highest first). */
346+
private async findPerpCandidates(symbol: string, adapters: Map<string, ExchangeAdapter>, ratesByExchange: RateMap): Promise<{ exchange: string; rate: number; score: FundingScore }[]> {
347+
const candidates: { exchange: string; rate: number; score: FundingScore }[] = [];
344348
for (const [exName, adapter] of adapters) {
345349
const perpSym = getPerpSymbol(symbol, exName);
346350
const ri = findRate(ratesByExchange, exName, perpSym) ?? findRate(ratesByExchange, exName, symbol);
347351
if (!ri || ri.rate <= 0) continue;
348352
const score = await this.scoreFundingLocal(adapter, perpSym, exName)
349353
?? await this.scoreFundingLocal(adapter, symbol, exName);
350354
if (!score) continue;
351-
if (!best || score.annualized > best.score.annualized) best = { exchange: exName, rate: ri.rate, score };
355+
candidates.push({ exchange: exName, rate: ri.rate, score });
352356
}
353-
return best;
357+
candidates.sort((a, b) => b.score.annualized - a.score.annualized);
358+
return candidates;
354359
}
355360

356361
private calcAnnualized(pos: SpotPerpPosition, ratesByExchange: RateMap): number | null {

0 commit comments

Comments
 (0)