#!/usr/bin/env npx tsx // Get funding rates from Hyperliquid import { getClient } from '../core/client.js'; import { formatPercent, annualizeFundingRate, parseArgs } from '../core/utils.js'; interface FundingDisplay { coin: string; assetId: number; hourlyRate: number; annualizedRate: number; premium: number; openInterest: number; markPx: number; } async function main() { const args = parseArgs(process.argv.slice(2)); const topN = parseInt(args.top as string) || 20; const filterCoin = args.coin as string | undefined; const sortBy = (args.sort as string) || 'annualized'; // annualized, hourly, oi const showAll = args.all as boolean; const jsonOutput = args.json as boolean; const client = getClient(); const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false); try { const meta = await client.getMetaAndAssetCtxs(); const fundingData: FundingDisplay[] = []; // Main dex assets for (let i = 0; i < meta.meta.universe.length; i++) { const asset = meta.meta.universe[i]; const ctx = meta.assetCtxs[i]; if (filterCoin && asset.name !== filterCoin) continue; const hourlyRate = parseFloat(ctx.funding); const annualizedRate = annualizeFundingRate(hourlyRate); const premium = parseFloat(ctx.premium ?? '0'); const openInterest = parseFloat(ctx.openInterest); const markPx = parseFloat(ctx.markPx); // Skip if OI is very low (unless showing all) if (!showAll && openInterest < 10000) continue; fundingData.push({ coin: asset.name, assetId: i, hourlyRate, annualizedRate, premium, openInterest, markPx, }); } // Include HIP-3 dex assets if (client.isTestnet && (includeHip3 || showAll) && !(filterCoin?.includes(':'))) { console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Use --coin dexName:COIN to view a specific HIP-3 asset.\n'); } if (includeHip3 || showAll || (filterCoin && filterCoin.includes(':'))) { // On testnet, load the specific dex on demand if user specified dex:COIN if (client.isTestnet && filterCoin?.includes(':')) { await client.loadSingleHip3Dex(filterCoin.split(':')[0]); } const allPerps = await client.getAllPerpMetas(); for (const dexData of allPerps) { if (!dexData.dexName) continue; // Skip main dex (already loaded) for (let i = 0; i < dexData.meta.universe.length; i++) { const asset = dexData.meta.universe[i]; const ctx = dexData.assetCtxs[i]; if (!ctx) continue; // API returns names already prefixed (e.g., "xyz:CL") const coinName = asset.name; if (filterCoin && coinName !== filterCoin) continue; const hourlyRate = parseFloat(ctx.funding); const annualizedRate = annualizeFundingRate(hourlyRate); const openInterest = parseFloat(ctx.openInterest); const markPx = parseFloat(ctx.markPx); if (!showAll && openInterest < 1000) continue; let assetId = -1; try { assetId = client.getAssetIndex(coinName); } catch { /* not registered */ } fundingData.push({ coin: coinName, assetId, hourlyRate, annualizedRate, premium: 0, openInterest, markPx, }); } } } // Sort if (sortBy === 'hourly' || sortBy === 'annualized') { fundingData.sort((a, b) => Math.abs(b.annualizedRate) - Math.abs(a.annualizedRate)); } else if (sortBy === 'oi') { fundingData.sort((a, b) => b.openInterest - a.openInterest); } // Limit const displayData = filterCoin ? fundingData : fundingData.slice(0, topN); if (jsonOutput) { console.log(JSON.stringify(displayData, null, 2)); return; } if (displayData.length === 0) { console.log(filterCoin ? `No data for ${filterCoin}` : 'No funding data available'); return; } console.log('Open Broker - Funding Rates'); console.log('===========================\n'); // Table header console.log('Coin | Hourly Rate | Annualized | Premium | Open Interest | Mark'); console.log('---------|-------------|------------|-------------|---------------|----------'); for (const data of displayData) { const hourlyStr = formatRate(data.hourlyRate * 100, 6); const annualStr = formatRate(data.annualizedRate * 100, 2); const premiumStr = formatRate(data.premium * 100, 4); const oiStr = formatOI(data.openInterest); const markStr = formatMark(data.markPx); console.log( `${data.coin.padEnd(8)} | ${hourlyStr.padStart(11)} | ${annualStr.padStart(10)} | ` + `${premiumStr.padStart(11)} | ${oiStr.padStart(13)} | ${markStr.padStart(8)}` ); } // Legend console.log('\nLegend:'); console.log(' Hourly Rate: Funding rate per hour'); console.log(' Annualized: Hourly Ɨ 8760 hours'); console.log(' Premium: (Mark - Oracle) / Oracle'); console.log(' OI: Open Interest in contracts'); // Highlight high funding opportunities const highFunding = fundingData.filter(d => Math.abs(d.annualizedRate) > 0.25); if (highFunding.length > 0 && !filterCoin) { console.log('\nšŸ’° High Funding Opportunities (>25% annualized):'); for (const data of highFunding.slice(0, 5)) { const direction = data.annualizedRate > 0 ? 'SHORT pays LONG' : 'LONG pays SHORT'; console.log( ` ${data.coin}: ${formatRate(data.annualizedRate * 100, 1)}% - ${direction}` ); } } } catch (error) { console.error('Error fetching funding rates:', error); process.exit(1); } } function formatRate(rate: number, decimals: number): string { const sign = rate >= 0 ? '+' : ''; return `${sign}${rate.toFixed(decimals)}%`; } function formatOI(oi: number): string { if (oi >= 1_000_000) return `${(oi / 1_000_000).toFixed(2)}M`; if (oi >= 1_000) return `${(oi / 1_000).toFixed(1)}K`; return oi.toFixed(0); } function formatMark(price: number): string { if (price >= 10000) return `$${(price / 1000).toFixed(1)}K`; if (price >= 100) return `$${price.toFixed(0)}`; if (price >= 1) return `$${price.toFixed(2)}`; return `$${price.toFixed(4)}`; } main();