get_arb_compare
Compare active arbitrage positions side by side to analyze ROI, funding income, price PnL, and annualized returns for each position.
Instructions
Compare active arbitrage positions side by side — ROI, funding income, price PnL, and annualized returns for each position
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| period | No | Funding lookup period: 7d, 30d, 90d, or all | all |
Implementation Reference
- src/mcp-server.ts:1676-1733 (handler)The handler implementation for the get_arb_compare tool, which compares active arbitrage positions.
"get_arb_compare", "Compare active arbitrage positions side by side — ROI, funding income, price PnL, and annualized returns for each position", { period: z.string().default("all").describe("Funding lookup period: 7d, 30d, 90d, or all"), }, async ({ period }) => { try { const arbState = loadArbState(); const positions = arbState?.positions ?? []; if (positions.length === 0) { return { content: [{ type: "text", text: ok({ positions: [], message: "No active arb positions" }) }] }; } const periodMatch = period === "all" ? null : period.match(/^(\d+)(d|w|m)$/); const sinceMs = periodMatch ? Date.now() - parseInt(periodMatch[1]) * ({ d: 86400000, w: 604800000, m: 2592000000 }[periodMatch[2]] ?? 86400000) : 0; const exchangeSet = new Set<string>(); for (const pos of positions) { exchangeSet.add(pos.longExchange); exchangeSet.add(pos.shortExchange); } const fundingByExSym = new Map<string, number>(); const currentPrices = new Map<string, number>(); await Promise.allSettled([...exchangeSet].map(async (ex) => { try { const adapter = await getOrCreateAdapter(ex); const [funding, livePos] = await Promise.all([adapter.getFundingPayments(200), adapter.getPositions()]); for (const f of funding) { if (sinceMs && f.time < sinceMs) continue; const key = `${ex}:${f.symbol}`; fundingByExSym.set(key, (fundingByExSym.get(key) ?? 0) + Number(f.payment)); } for (const p of livePos) currentPrices.set(`${ex}:${p.symbol}`, Number(p.markPrice)); } catch { /* skip */ } })); const rows = positions.map(pos => { const daysHeld = Math.max(1, Math.round((Date.now() - new Date(pos.entryTime).getTime()) / 86400000)); const funding = (fundingByExSym.get(`${pos.longExchange}:${pos.symbol}`) ?? 0) + (fundingByExSym.get(`${pos.shortExchange}:${pos.symbol}`) ?? 0); const longPrice = currentPrices.get(`${pos.longExchange}:${pos.symbol}`) ?? pos.entryLongPrice; const shortPrice = currentPrices.get(`${pos.shortExchange}:${pos.symbol}`) ?? pos.entryShortPrice; const pricePnl = (longPrice - pos.entryLongPrice) * pos.longSize + (pos.entryShortPrice - shortPrice) * pos.shortSize; const totalPnl = pricePnl + funding; const notional = pos.entryLongPrice * pos.longSize + pos.entryShortPrice * pos.shortSize; const roi = notional > 0 ? (totalPnl / notional) * 100 : 0; const fundingRoi = notional > 0 ? (funding / notional) * 100 : 0; return { symbol: pos.symbol, mode: pos.mode ?? "perp-perp", longExchange: pos.longExchange, shortExchange: pos.shortExchange, daysHeld, funding, pricePnl, totalPnl, roi, annualizedRoi: daysHeld > 0 ? (fundingRoi / daysHeld) * 365 : 0 }; }).sort((a, b) => b.totalPnl - a.totalPnl); const totals = { funding: rows.reduce((s, r) => s + r.funding, 0), pricePnl: rows.reduce((s, r) => s + r.pricePnl, 0), totalPnl: rows.reduce((s, r) => s + r.totalPnl, 0) }; return { content: [{ type: "text", text: ok({ period, positionCount: rows.length, positions: rows, totals }) }] }; } catch (e) { return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e)) }], isError: true }; } }, );