portfolio
View cross-exchange portfolio summaries including balances, positions, and risk metrics across all connected exchanges for comprehensive trading oversight.
Instructions
Cross-exchange portfolio summary: balances, positions, and risk metrics across all exchanges
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/mcp-server.ts:249-341 (handler)The MCP tool "portfolio" is implemented directly in `src/mcp-server.ts`. It fetches balance, positions, and open orders from all supported exchanges ("pacifica", "hyperliquid", "lighter"), calculates aggregated portfolio risk metrics, and returns a JSON summary.
"portfolio", "Cross-exchange portfolio summary: balances, positions, and risk metrics across all exchanges", {}, async () => { const EXCHANGES = ["pacifica", "hyperliquid", "lighter"] as const; interface ExchangeSnapshot { exchange: string; connected: boolean; balance: { equity: string; available: string; marginUsed: string; unrealizedPnl: string } | null; positions: Awaited<ReturnType<ExchangeAdapter["getPositions"]>>; openOrders: number; error?: string; } const snapshots: ExchangeSnapshot[] = await Promise.all( EXCHANGES.map(async (name) => { try { const adapter = await getOrCreateAdapter(name); const [balance, positions, orders] = await Promise.all([ adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(), ]); return { exchange: name, connected: true, balance, positions, openOrders: orders.length }; } catch (e) { return { exchange: name, connected: false, balance: null, positions: [], openOrders: 0, error: e instanceof Error ? e.message : String(e), }; } }), ); let totalEquity = 0; let totalAvailable = 0; let totalMarginUsed = 0; let totalUnrealizedPnl = 0; let totalPositions = 0; let totalOpenOrders = 0; const allPositions: (Awaited<ReturnType<ExchangeAdapter["getPositions"]>>[number] & { exchange: string })[] = []; for (const snap of snapshots) { if (snap.balance) { totalEquity += Number(snap.balance.equity); totalAvailable += Number(snap.balance.available); totalMarginUsed += Number(snap.balance.marginUsed); totalUnrealizedPnl += Number(snap.balance.unrealizedPnl); } totalPositions += snap.positions.length; totalOpenOrders += snap.openOrders; for (const pos of snap.positions) { allPositions.push({ ...pos, exchange: snap.exchange }); } } const marginUtilization = totalEquity > 0 ? (totalMarginUsed / totalEquity) * 100 : 0; let largestPosition: { symbol: string; exchange: string; notional: number } | null = null; for (const pos of allPositions) { const notional = Math.abs(Number(pos.size) * Number(pos.markPrice)); if (!largestPosition || notional > largestPosition.notional) { largestPosition = { symbol: pos.symbol, exchange: pos.exchange, notional }; } } const exchangeConcentration = snapshots .filter(s => s.balance && Number(s.balance.equity) > 0) .map(s => ({ exchange: s.exchange, pct: totalEquity > 0 ? (Number(s.balance!.equity) / totalEquity) * 100 : 0, })) .sort((a, b) => b.pct - a.pct); const summary = { totalEquity, totalAvailable, totalMarginUsed, totalUnrealizedPnl, totalPositions, totalOpenOrders, exchanges: snapshots, positions: allPositions, riskMetrics: { marginUtilization, largestPosition, exchangeConcentration }, }; return { content: [{ type: "text", text: ok(summary) }] }; }, );