show_portfolio
Displays current stock holdings with P&L, optional risk metrics, and FX return decomposition for non-USD reporting.
Instructions
Current holdings derived from the user's transaction log: ticker, shares, avg cost, current price, market value, P&L (absolute and %), plus an optional risk summary (Sharpe, volatility, drawdown) from snapshot history and FX return decomposition when a non-USD display currency is requested. For the daily check-in, prefer get_brief which already includes this data plus weights, news, earnings, and macro context in one call.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| currency | No | Display currency for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition per holding showing how much of P&L came from price vs exchange rate movement. |
Implementation Reference
- apps/mcp/src/tools/portfolio.ts:35-155 (handler)The handler function that executes show_portfolio logic. It reads all transactions and prices from the DB, aggregates holdings (ticker, shares, avg price, cost basis, market value, P&L), optionally builds a risk summary (Sharpe, Sortino, volatility, drawdown) via buildRiskMetrics, and optionally computes FX return decomposition when a non-USD currency is requested.
async ({ currency }) => { const repo = getRepository(); const db = getDb(); const txns = db.select().from(transactions).all(); const priceMap = new Map( db .select() .from(prices) .all() .map((p) => [p.ticker, p]), ); const holdingsMap = aggregateHoldings(txns); const holdings = [...holdingsMap.entries()].map(([ticker, h]) => { const p = priceMap.get(ticker); const avgPrice = h.costShares > 0 ? h.totalCost / h.costShares : null; const costBasis = avgPrice != null ? avgPrice * h.costShares : 0; const marketValue = p ? p.current_price * h.shares : null; return { ticker, shares: h.shares, avgPrice, costBasis, currentPrice: p?.current_price ?? null, marketValue, pnl: marketValue != null ? marketValue - costBasis : null, pnlPct: marketValue != null && costBasis > 0 ? ((marketValue - costBasis) / costBasis) * 100 : null, name: p?.name ?? null, syncedAt: p?.synced_at ?? null, }; }); let riskSummary: { sharpe: number; sortino: number | null; annualized_return_pct: number; annualized_vol_pct: number; max_drawdown_pct: number; trading_days: number; risk_free_rate_pct: number; note: string; } | null = null; const riskResult = await buildRiskMetrics(repo, null, { fred_key: getFredKey() }); if (riskResult.ok) { const m = riskResult.metrics; riskSummary = { sharpe: m.ratios.sharpe, sortino: m.ratios.sortino, annualized_return_pct: m.returns.annualized_pct, annualized_vol_pct: m.volatility.annualized_pct, max_drawdown_pct: m.drawdown.max_pct, trading_days: m.period.trading_days, risk_free_rate_pct: m.ratios.risk_free_rate_pct, note: sharpeNoteText(m.ratios.sharpe), }; } type FxRow = { ticker: string; price_return_pct: number; fx_return_pct: number; total_return_pct: number }; let fxDecomposition: FxRow[] | null = null; const cur = currency?.toUpperCase(); if (cur && cur !== 'USD') { const fxRows: FxRow[] = []; const cachedFxRates = db.select().from(fxRates).all(); const getLatestFxPerUsd = (c: string): number | null => { const entries = cachedFxRates .filter((r) => r.currency === c) .sort((a, b) => b.date.localeCompare(a.date)); return entries[0]?.rate_to_usd ?? null; }; const curFxPerUsd = getLatestFxPerUsd(cur); if (curFxPerUsd != null) { for (const [ticker, h] of holdingsMap.entries()) { const p = priceMap.get(ticker); if (!p) continue; const avgCostUsd = h.costShares > 0 ? h.totalCost / h.costShares : null; if (!avgCostUsd) continue; const buyTxns = repo.transactions.getAll(ticker).filter((t) => t.type === 'buy'); if (buyTxns.length === 0) continue; let totalNotional = 0; let weightedFxSum = 0; for (const txn of buyTxns) { const fxRecord = repo.fx.getRateOnOrBefore(txn.date, cur); if (!fxRecord) continue; const notional = txn.shares * txn.price; totalNotional += notional; weightedFxSum += notional * fxRecord.rate_to_usd; } if (totalNotional === 0) continue; const avgBuyFxPerUsd = weightedFxSum / totalNotional; const priceRetPct = (p.current_price / avgCostUsd - 1) * 100; const fxRetPct = (curFxPerUsd / avgBuyFxPerUsd - 1) * 100; const totalRetPct = ((1 + priceRetPct / 100) * (1 + fxRetPct / 100) - 1) * 100; fxRows.push({ ticker, price_return_pct: Math.round(priceRetPct * 100) / 100, fx_return_pct: Math.round(fxRetPct * 100) / 100, total_return_pct: Math.round(totalRetPct * 100) / 100, }); } if (fxRows.length > 0) fxDecomposition = fxRows; } } return ok({ holdings, risk_summary: riskSummary, fx_decomposition: fxDecomposition ? { rows: fxDecomposition, note: fxDecompNoteText(cur!) } : null, }); }, - Input schema for show_porttool: optional 'currency' string parameter (Zod schema) for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition showing price vs exchange rate return.
{ currency: z .string() .optional() .describe( 'Display currency for FX return decomposition (e.g. KRW, JPY). When provided and not USD, returns fx_decomposition per holding showing how much of P&L came from price vs exchange rate movement.', ), }, - apps/mcp/src/tools/portfolio.ts:23-25 (registration)The registration function registerPortfolioTools calls server.tool('show_portfolio', ...) to register the tool on the MCP server. Called from apps/mcp/src/index.ts line 19.
export function registerPortfolioTools(server: McpServer): void { server.tool( 'show_portfolio', - apps/mcp/src/index.ts:19-19 (registration)Where registerPortfolioTools is called to register the show_portfolio (and other portfolio) tools on the MCP server instance.
registerPortfolioTools(server); - apps/mcp/src/tools/portfolio.ts:1-20 (helper)Imports used by the show_portfolio handler: aggregateHoldings, getDb, getRepository, getFinnhubKey, getFredKey, buildRiskMetrics, fxDecompNoteText, sharpeNoteText, and the 'ok' response helper.
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { assembleBriefData } from '@firma/brief'; import { buildRiskMetrics, ensureTodaySnapshot } from '@firma/portfolio'; import { fxDecompNoteText, localDate, sharpeNoteText } from '@firma/utils'; import { asc, eq } from 'drizzle-orm'; import { z } from 'zod'; import { aggregateHoldings, balanceEntries, flowEntries, fxRates, getDb, getFinnhubKey, getFredKey, getRepository, portfolioSnapshots, prices, recordBriefCallAndCheckCta, transactions, } from '../db.ts';