import { xirr } from 'xirr';
export interface CashFlow {
date: Date;
amount: number;
}
/**
* Calculate XIRR for a set of cash flows
* @param cashFlows Array of cash flows with date and amount
* @returns XIRR as a percentage (e.g., 15.5 for 15.5%) or null if calculation fails
*/
export function calculateXIRR(cashFlows: CashFlow[]): number | null {
if (cashFlows.length < 2) {
console.log('[XIRR] Need at least 2 cash flows');
return null;
}
try {
// Convert to format expected by xirr library
const xirrInput = cashFlows.map(cf => ({
amount: cf.amount,
when: cf.date,
}));
const result = xirr(xirrInput);
// Check if result is valid
if (result === null || result === undefined || isNaN(result)) {
console.log('[XIRR] Library returned invalid result');
return null;
}
// Convert to percentage
const percentage = result * 100;
// Sanity check: XIRR should typically be between -100% and 1000%
if (percentage < -100 || percentage > 1000) {
console.warn(`[XIRR] Unusual XIRR value: ${percentage.toFixed(2)}%`);
}
return percentage;
} catch (error: any) {
console.error('[XIRR] Calculation error:', error.message || error);
return null;
}
}
/**
* Calculate XIRR for portfolio based on ledger entries
* @param ledgerEntries Array of ledger entries
* @param currentValue Current portfolio value
* @returns XIRR as a percentage
*/
export function calculatePortfolioXIRR(
ledgerEntries: Array<{ posting_date: Date; debit: number; credit: number }>,
currentValue: number
): number | null {
const cashFlows: CashFlow[] = [];
// Process ledger entries
for (const entry of ledgerEntries) {
const netFlow = entry.credit - entry.debit;
if (netFlow !== 0) {
cashFlows.push({
date: entry.posting_date,
amount: netFlow,
});
}
}
// Add current value as final cash flow
if (currentValue > 0) {
cashFlows.push({
date: new Date(),
amount: currentValue,
});
}
return calculateXIRR(cashFlows);
}
/**
* Calculate XIRR for a specific stock
* @param trades Array of trades for the stock
* @param currentPrice Current price of the stock
* @param currentQuantity Current quantity held (0 for sold positions)
* @returns XIRR as a percentage or null if calculation not possible
*/
export function calculateStockXIRR(
trades: Array<{
trade_date: Date;
trade_type: 'buy' | 'sell';
quantity: number;
price: number;
}>,
currentPrice: number,
currentQuantity: number
): number | null {
if (!trades || trades.length === 0) {
console.log('[XIRR] No trades provided');
return null;
}
const cashFlows: CashFlow[] = [];
// Process trades - buys are negative (money out), sells are positive (money in)
for (const trade of trades) {
const amount = trade.quantity * trade.price;
cashFlows.push({
date: trade.trade_date,
amount: trade.trade_type === 'buy' ? -amount : amount,
});
}
// Add final cash flow based on position status
if (currentQuantity > 0) {
// Active position: Add current market value
if (currentPrice > 0) {
cashFlows.push({
date: new Date(),
amount: currentQuantity * currentPrice,
});
} else {
console.log('[XIRR] Current price is 0, cannot calculate XIRR for active position');
return null;
}
} else if (currentQuantity === 0) {
// Sold position: The sell trades already represent the final cash flows
// No additional cash flow needed - the position is fully closed
// XIRR will calculate based on buy (negative) and sell (positive) cash flows
}
// Validate cash flows
if (cashFlows.length < 2) {
console.log('[XIRR] Insufficient cash flows (need at least 2)');
return null;
}
// Check if we have both positive and negative cash flows
const hasNegative = cashFlows.some(cf => cf.amount < 0);
const hasPositive = cashFlows.some(cf => cf.amount > 0);
if (!hasNegative || !hasPositive) {
console.log('[XIRR] Need both positive and negative cash flows');
return null;
}
// Calculate net cash flow to validate
const netCashFlow = cashFlows.reduce((sum, cf) => sum + cf.amount, 0);
// For debugging
console.log(`[XIRR] Calculating with ${cashFlows.length} cash flows, net: ${netCashFlow.toFixed(2)}`);
return calculateXIRR(cashFlows);
}