@@ -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