get_funding_analysis
Analyze funding income across exchanges to track funding arbitrage profitability with cumulative totals, annualized rates, and per-position breakdown.
Instructions
Analyze funding income across exchanges — cumulative totals, annualized rates, and per-arb-position breakdown. Great for tracking funding arb profitability.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| period | No | Time period: 7d, 30d, 90d, or all | 30d |
| exchange | No | Filter by exchange | |
| symbol | No | Filter by symbol |
Implementation Reference
- src/mcp-server.ts:1540-1617 (handler)The handler implementation for the `get_funding_analysis` tool. It aggregates funding payments and calculates annualized rates and arb position breakdowns.
"get_funding_analysis", "Analyze funding income across exchanges — cumulative totals, annualized rates, and per-arb-position breakdown. Great for tracking funding arb profitability.", { period: z.string().default("30d").describe("Time period: 7d, 30d, 90d, or all"), exchange: z.string().optional().describe("Filter by exchange"), symbol: z.string().optional().describe("Filter by symbol"), }, async ({ period, exchange: exFilter, symbol: symFilter }) => { try { const exchanges = exFilter ? [exFilter] : ["pacifica", "hyperliquid", "lighter"]; const periodMatch = period.match(/^(\d+)(d|w|m)$/); const sinceMs = periodMatch ? Date.now() - parseInt(periodMatch[1]) * ({ d: 86400000, w: 604800000, m: 2592000000 }[periodMatch[2]] ?? 86400000) : 0; interface FundingEntry { exchange: string; symbol: string; payment: number; time: number } const allFunding: FundingEntry[] = []; const posNotionals = new Map<string, number>(); await Promise.allSettled(exchanges.map(async (ex) => { try { const adapter = await getOrCreateAdapter(ex); const [payments, positions] = await Promise.all([ adapter.getFundingPayments(200), adapter.getPositions(), ]); for (const p of payments) { if (sinceMs && p.time < sinceMs) continue; if (symFilter && !p.symbol.toUpperCase().includes(symFilter.toUpperCase())) continue; allFunding.push({ exchange: ex, symbol: p.symbol, payment: Number(p.payment), time: p.time }); } for (const p of positions) { if (Number(p.size) > 0) posNotionals.set(`${ex}:${p.symbol}`, Math.abs(Number(p.size) * Number(p.markPrice))); } } catch { /* skip */ } })); allFunding.sort((a, b) => a.time - b.time); // Aggregate by exchange×symbol const byExSym = new Map<string, { exchange: string; symbol: string; total: number; count: number }>(); let totalFunding = 0; for (const f of allFunding) { totalFunding += f.payment; const key = `${f.exchange}:${f.symbol}`; const e = byExSym.get(key) ?? { exchange: f.exchange, symbol: f.symbol, total: 0, count: 0 }; e.total += f.payment; e.count++; byExSym.set(key, e); } const periodDays = sinceMs ? (Date.now() - sinceMs) / 86400000 : allFunding.length > 1 ? (allFunding[allFunding.length - 1].time - allFunding[0].time) / 86400000 : 1; // Match to arb positions const arbState = loadArbState(); const arbPositions = (arbState?.positions ?? []).map(pos => { const lf = allFunding.filter(f => f.symbol === pos.symbol && f.exchange === pos.longExchange).reduce((s, f) => s + f.payment, 0); const sf = allFunding.filter(f => f.symbol === pos.symbol && f.exchange === pos.shortExchange).reduce((s, f) => s + f.payment, 0); return { symbol: pos.symbol, mode: pos.mode ?? "perp-perp", longExchange: pos.longExchange, shortExchange: pos.shortExchange, netFunding: lf + sf, daysHeld: Math.max(1, Math.round((Date.now() - new Date(pos.entryTime).getTime()) / 86400000)) }; }).filter(a => a.netFunding !== 0); const data = { period, periodDays: Math.round(periodDays), totalFunding, totalPayments: allFunding.length, byExchangeSymbol: [...byExSym.values()].sort((a, b) => b.total - a.total).map(e => ({ ...e, annualizedRate: (() => { const n = posNotionals.get(`${e.exchange}:${e.symbol}`); return n && n > 0 && periodDays > 0 ? (e.total / Math.max(1, periodDays)) * 365 / n * 100 : null; })(), })), arbPositions, }; return { content: [{ type: "text", text: ok(data) }] }; } catch (e) { return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e)) }], isError: true }; } }, );