calculate_xirr
Calculate XIRR from cash flows and portfolio valuation to get annualized return, monthly return, total gain, and convergence status for investments with irregular contributions and withdrawals.
Instructions
Calculate XIRR (Extended Internal Rate of Return) from a series of dated cash flows and a current portfolio valuation. Returns annualized rate, monthly rate, total gain, and convergence status. Useful for measuring actual investment performance accounting for irregular contributions and withdrawals.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| cashFlows | Yes | List of cash flows. Use positive amounts for investments (buy), negative for withdrawals (sell). | |
| currentValue | Yes | Current valuation of the portfolio at valuationDate. | |
| valuationDate | Yes | Date of the current valuation in YYYY-MM-DD format. |
Implementation Reference
- src/xirr.ts:94-128 (handler)Core XIRR calculation function. Takes cash flows (with dates), current portfolio value, and valuation date. Computes the monthly rate via Newton's method (falling back to bisection), then derives annual rate, total investment, total gain, and gain rate. Returns all results plus convergence status.
export function calculateXirr(input: XirrInput): XirrResult { const { cashFlows, currentValue, valuationDate } = input; if (cashFlows.length === 0) { throw new Error('cashFlows must not be empty'); } const baseDate = cashFlows[0].date; const amounts = cashFlows.map((cf) => cf.amount); amounts.push(-currentValue); const months = cashFlows.map((cf) => toElapsedMonths(baseDate, cf.date)); months.push(toElapsedMonths(baseDate, valuationDate)); const solved = solveXirrNewton(amounts, months) ?? solveXirrBisection(amounts, months); const converged = solved !== null; const monthlyRate = solved ?? 0; const annualRate = (Math.pow(1 + monthlyRate, 12) - 1) * 100; const totalInvestment = cashFlows.reduce((sum, cf) => sum + cf.amount, 0); const totalGain = currentValue - totalInvestment; const gainRate = totalInvestment !== 0 ? (totalGain / totalInvestment) * 100 : 0; return { monthlyRate: monthlyRate * 100, annualRate, totalInvestment, totalGain, gainRate, converged, }; } - src/xirr.ts:34-58 (helper)Helper functions computing the XIRR net present value (NPV) and its derivative with respect to the monthly rate, used by Newton's method.
function xirrNpv( rate: number, amounts: number[], elapsedMonths: number[], ): number { let npv = 0; for (let i = 0; i < amounts.length; i++) { npv += amounts[i] / Math.pow(1 + rate, elapsedMonths[i]); } return npv; } function xirrNpvDerivative( rate: number, amounts: number[], elapsedMonths: number[], ): number { let derivative = 0; for (let i = 0; i < amounts.length; i++) { derivative += (-elapsedMonths[i] * amounts[i]) / Math.pow(1 + rate, elapsedMonths[i] + 1); } return derivative; } - src/xirr.ts:60-92 (helper)Two root-finding solvers: Newton's method (up to 100 iterations, starting at rate=0.01) with bisection fallback (up to 200 iterations, bounds -0.99 to 1.0). Returns null if Newton diverges.
function solveXirrNewton( amounts: number[], elapsedMonths: number[], ): number | null { let rate = 0.01; for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) { const npv = xirrNpv(rate, amounts, elapsedMonths); if (Math.abs(npv) < CONVERGENCE_THRESHOLD) return rate; const derivative = xirrNpvDerivative(rate, amounts, elapsedMonths); if (derivative === 0) return null; rate = rate - npv / derivative; } return null; } function solveXirrBisection( amounts: number[], elapsedMonths: number[], ): number | null { let low = BISECTION_LOWER_BOUND; let high = BISECTION_UPPER_BOUND; for (let i = 0; i < BISECTION_MAX_ITERATIONS; i++) { const mid = (low + high) / 2; const npv = xirrNpv(mid, amounts, elapsedMonths); if (Math.abs(npv) < CONVERGENCE_THRESHOLD) return mid; if (npv * xirrNpv(low, amounts, elapsedMonths) < 0) { high = mid; } else { low = mid; } } return (low + high) / 2; } - src/xirr.ts:1-19 (schema)TypeScript interfaces defining the input (CashFlow, XirrInput) and output (XirrResult) schemas for the calculateXirr function.
export interface CashFlow { date: Date; amount: number; } export interface XirrInput { cashFlows: CashFlow[]; currentValue: number; valuationDate: Date; } export interface XirrResult { monthlyRate: number; annualRate: number; totalInvestment: number; totalGain: number; gainRate: number; converged: boolean; } - src/index.ts:132-231 (registration)Tool registration in the MCP server. The tool named 'calculate_xirr' is registered with its JSON schema via ListToolsRequestSchema, and dispatched via CallToolRequestSchema to handleCalculateXirr.
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'calculate_xirr', description: 'Calculate XIRR (Extended Internal Rate of Return) from a series of dated cash flows and a current portfolio valuation. Returns annualized rate, monthly rate, total gain, and convergence status. Useful for measuring actual investment performance accounting for irregular contributions and withdrawals.', inputSchema: { type: 'object', properties: { cashFlows: { type: 'array', description: 'List of cash flows. Use positive amounts for investments (buy), negative for withdrawals (sell).', items: { type: 'object', properties: { date: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: 'Date of the cash flow in YYYY-MM-DD format.', }, amount: { type: 'number', description: 'Cash flow amount. Positive = invested, Negative = withdrawn.', }, }, required: ['date', 'amount'], }, minItems: 1, }, currentValue: { type: 'number', description: 'Current valuation of the portfolio at valuationDate.', }, valuationDate: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: 'Date of the current valuation in YYYY-MM-DD format.', }, }, required: ['cashFlows', 'currentValue', 'valuationDate'], }, }, { name: 'parse_rakuten_csv', description: 'Parse a Rakuten Securities transaction CSV (取引履歴) and convert it into a normalized cash flow list. Handles 買付/売却 transaction types automatically. The output can be fed directly into calculate_xirr.', inputSchema: { type: 'object', properties: { csvContent: { type: 'string', description: 'Raw CSV text exported from Rakuten Securities. Note: Rakuten exports are typically Shift_JIS encoded; decode to UTF-8 before passing.', }, }, required: ['csvContent'], }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let text: string; switch (name) { case 'calculate_xirr': text = handleCalculateXirr(args); break; case 'parse_rakuten_csv': text = handleParseRakutenCsv(args); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: 'text', text, }, ], }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { content: [ { type: 'text', text: JSON.stringify({ ok: false, error: message }, null, 2), }, ], isError: true, }; }