TradingAgents/frontend/src/components/PortfolioSimulator.tsx

856 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Legend, BarChart, Bar, Cell, LabelList } from 'recharts';
import { Calculator, ChevronDown, ChevronUp, IndianRupee, Settings2, BarChart3, Info, TrendingUp, TrendingDown, ArrowRightLeft, Wallet, PiggyBank, Receipt, HelpCircle, AlertCircle } from 'lucide-react';
import { sampleRecommendations, getNifty50IndexHistory, getBacktestResult } from '../data/recommendations';
import { calculateBrokerage, formatINR, type BrokerageBreakdown } from '../utils/brokerageCalculator';
import InfoModal, { InfoButton } from './InfoModal';
import type { Decision, DailyRecommendation } from '../types';
interface PortfolioSimulatorProps {
className?: string;
recommendations?: DailyRecommendation[];
isUsingMockData?: boolean;
nifty50Prices?: Record<string, number>;
allBacktestData?: Record<string, Record<string, number>>;
}
export type InvestmentMode = 'all50' | 'topPicks';
interface TradeRecord {
symbol: string;
entryDate: string;
entryPrice: number;
exitDate: string;
exitPrice: number;
quantity: number;
brokerage: BrokerageBreakdown;
profitLoss: number;
}
interface TradeStats {
totalTrades: number;
buyTrades: number;
sellTrades: number;
brokerageBreakdown: BrokerageBreakdown;
trades: TradeRecord[];
}
// Smart trade counting logic using Zerodha brokerage for Equity Delivery
function calculateSmartTrades(
recommendations: typeof sampleRecommendations,
mode: InvestmentMode,
startingAmount: number,
nifty50Prices?: Record<string, number>,
allBacktestData?: Record<string, Record<string, number>>
): {
portfolioData: Array<{ date: string; rawDate: string; value: number; niftyValue: number; return: number; cumulative: number }>;
stats: TradeStats;
openPositions: Record<string, { entryDate: string; entryPrice: number; decision: Decision }>;
} {
const hasRealNifty = nifty50Prices && Object.keys(nifty50Prices).length > 0;
const niftyHistory = hasRealNifty ? null : getNifty50IndexHistory();
const sortedRecs = [...recommendations].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
// Precompute real Nifty start price for comparison
const sortedNiftyDates = hasRealNifty ? Object.keys(nifty50Prices).sort() : [];
const niftyStartPrice = hasRealNifty && sortedNiftyDates.length > 0
? nifty50Prices[sortedNiftyDates[0]]
: null;
// Track open positions per stock
const openPositions: Record<string, { entryDate: string; entryPrice: number; decision: Decision }> = {};
const completedTrades: TradeRecord[] = [];
let buyTrades = 0;
let sellTrades = 0;
const getStocksToTrack = (rec: typeof recommendations[0]) => {
if (mode === 'topPicks') {
return rec.top_picks.map(p => p.symbol);
}
return Object.keys(rec.analysis);
};
const stockCount = mode === 'topPicks' ? 3 : 50;
const investmentPerStock = startingAmount / stockCount;
let portfolioValue = startingAmount;
let niftyValue = startingAmount;
const niftyStartValue = niftyHistory?.[0]?.value || 21500;
const portfolioData = sortedRecs.map((rec) => {
const stocks = getStocksToTrack(rec);
let dayReturn = 0;
let stocksTracked = 0;
stocks.forEach(symbol => {
const analysis = rec.analysis[symbol];
if (!analysis || !analysis.decision) return;
const decision = analysis.decision;
const prevPosition = openPositions[symbol];
const backtest = getBacktestResult(symbol);
const currentPrice = backtest?.current_price || 1000;
const quantity = Math.floor(investmentPerStock / currentPrice);
if (decision === 'BUY') {
if (!prevPosition) {
openPositions[symbol] = { entryDate: rec.date, entryPrice: currentPrice, decision };
buyTrades++;
} else if (prevPosition.decision === 'SELL') {
buyTrades++;
openPositions[symbol] = { entryDate: rec.date, entryPrice: currentPrice, decision };
} else {
openPositions[symbol].decision = decision;
}
// Use real backtest return if available, otherwise 0 (neutral)
const realBuyReturn = allBacktestData?.[rec.date]?.[symbol];
dayReturn += realBuyReturn !== undefined ? realBuyReturn : 0;
stocksTracked++;
} else if (decision === 'HOLD') {
if (prevPosition) {
openPositions[symbol].decision = decision;
}
// Use real backtest return if available, otherwise 0 (neutral)
const realHoldReturn = allBacktestData?.[rec.date]?.[symbol];
dayReturn += realHoldReturn !== undefined ? realHoldReturn : 0;
stocksTracked++;
} else if (decision === 'SELL') {
if (prevPosition && (prevPosition.decision === 'BUY' || prevPosition.decision === 'HOLD')) {
sellTrades++;
// Use real backtest return for exit price if available, otherwise break-even
const realSellReturn = allBacktestData?.[rec.date]?.[symbol];
const exitPrice = realSellReturn !== undefined
? currentPrice * (1 + realSellReturn / 100)
: currentPrice;
const brokerage = calculateBrokerage({
buyPrice: prevPosition.entryPrice,
sellPrice: exitPrice,
quantity,
tradeType: 'delivery',
});
const grossProfit = (exitPrice - prevPosition.entryPrice) * quantity;
const profitLoss = grossProfit - brokerage.totalCharges;
completedTrades.push({
symbol,
entryDate: prevPosition.entryDate,
entryPrice: prevPosition.entryPrice,
exitDate: rec.date,
exitPrice,
quantity,
brokerage,
profitLoss,
});
delete openPositions[symbol];
}
stocksTracked++;
}
});
const avgDayReturn = stocksTracked > 0 ? dayReturn / stocksTracked : 0;
portfolioValue = portfolioValue * (1 + avgDayReturn / 100);
// Use real Nifty50 prices if available, otherwise use mock history
if (hasRealNifty && niftyStartPrice) {
const closestDate = sortedNiftyDates.find(d => d >= rec.date) || sortedNiftyDates[sortedNiftyDates.length - 1];
if (closestDate && nifty50Prices[closestDate]) {
niftyValue = startingAmount * (nifty50Prices[closestDate] / niftyStartPrice);
}
} else if (niftyHistory) {
const niftyPoint = niftyHistory.find(n => n.date === rec.date);
if (niftyPoint) {
niftyValue = startingAmount * (niftyPoint.value / niftyStartValue);
}
}
return {
date: new Date(rec.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
rawDate: rec.date,
value: Math.round(portfolioValue),
niftyValue: Math.round(niftyValue),
return: avgDayReturn,
cumulative: ((portfolioValue - startingAmount) / startingAmount) * 100,
};
});
const totalBrokerage = completedTrades.reduce<BrokerageBreakdown>(
(acc, trade) => ({
brokerage: acc.brokerage + trade.brokerage.brokerage,
stt: acc.stt + trade.brokerage.stt,
exchangeCharges: acc.exchangeCharges + trade.brokerage.exchangeCharges,
sebiCharges: acc.sebiCharges + trade.brokerage.sebiCharges,
gst: acc.gst + trade.brokerage.gst,
stampDuty: acc.stampDuty + trade.brokerage.stampDuty,
totalCharges: acc.totalCharges + trade.brokerage.totalCharges,
netProfit: acc.netProfit + trade.brokerage.netProfit,
turnover: acc.turnover + trade.brokerage.turnover,
}),
{ brokerage: 0, stt: 0, exchangeCharges: 0, sebiCharges: 0, gst: 0, stampDuty: 0, totalCharges: 0, netProfit: 0, turnover: 0 }
);
return {
portfolioData,
stats: {
totalTrades: buyTrades + sellTrades,
buyTrades,
sellTrades,
brokerageBreakdown: totalBrokerage,
trades: completedTrades,
},
openPositions,
};
}
// Helper for consistent positive/negative color classes
function getValueColorClass(value: number): string {
return value >= 0
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400';
}
export default function PortfolioSimulator({
className = '',
recommendations = sampleRecommendations,
isUsingMockData = true, // Default to true since this uses simulated returns
nifty50Prices,
allBacktestData,
}: PortfolioSimulatorProps) {
const [startingAmount, setStartingAmount] = useState(100000);
const [showBreakdown, setShowBreakdown] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showBrokerageDetails, setShowBrokerageDetails] = useState(false);
const [showTradeWaterfall, setShowTradeWaterfall] = useState(false);
const [investmentMode, setInvestmentMode] = useState<InvestmentMode>('all50');
const [includeBrokerage, setIncludeBrokerage] = useState(true);
// Modal state - single state for all modals instead of 7 separate booleans
type ModalType = 'totalTrades' | 'buyTrades' | 'sellTrades' | 'portfolioValue' | 'profitLoss' | 'comparison' | null;
const [activeModal, setActiveModal] = useState<ModalType>(null);
const { portfolioData, stats, openPositions } = useMemo(() => {
return calculateSmartTrades(
recommendations,
investmentMode,
startingAmount,
nifty50Prices,
allBacktestData
);
}, [recommendations, investmentMode, startingAmount, nifty50Prices, allBacktestData]);
const lastDataPoint = portfolioData[portfolioData.length - 1];
const currentValue = lastDataPoint?.value ?? startingAmount;
const niftyValue = lastDataPoint?.niftyValue ?? startingAmount;
const totalCharges = includeBrokerage ? stats.brokerageBreakdown.totalCharges : 0;
const finalValue = currentValue - totalCharges;
const totalReturn = ((finalValue - startingAmount) / startingAmount) * 100;
const profitLoss = finalValue - startingAmount;
const isPositive = profitLoss >= 0;
const niftyReturn = ((niftyValue - startingAmount) / startingAmount) * 100;
const outperformance = totalReturn - niftyReturn;
// Calculate Y-axis domain with padding
const yAxisDomain = useMemo(() => {
if (portfolioData.length === 0) return [0, startingAmount * 1.2];
const allValues = portfolioData.flatMap(d => [d.value, d.niftyValue]);
const minValue = Math.min(...allValues);
const maxValue = Math.max(...allValues);
const padding = (maxValue - minValue) * 0.1;
return [Math.floor((minValue - padding) / 1000) * 1000, Math.ceil((maxValue + padding) / 1000) * 1000];
}, [portfolioData, startingAmount]);
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value.replace(/,/g, ''), 10);
if (!isNaN(value) && value >= 0) {
setStartingAmount(value);
}
};
const openPositionsCount = Object.keys(openPositions).length;
return (
<div className={`card p-4 ${className}`}>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Calculator className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Portfolio Simulator</h2>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-1.5 rounded-lg transition-colors ${
showSettings
? 'bg-nifty-100 text-nifty-600 dark:bg-nifty-900/30 dark:text-nifty-400'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
}`}
title="Settings"
>
<Settings2 className="w-4 h-4" />
</button>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="mb-4 p-3 bg-gray-50 dark:bg-slate-700/50 rounded-lg space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
Investment Strategy
</label>
<div className="flex gap-2">
<button
onClick={() => setInvestmentMode('all50')}
className={`flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-all ${
investmentMode === 'all50'
? 'bg-nifty-600 text-white'
: 'bg-white dark:bg-slate-600 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-slate-500 hover:bg-gray-50 dark:hover:bg-slate-500'
}`}
>
All 50 Stocks
</button>
<button
onClick={() => setInvestmentMode('topPicks')}
className={`flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-all ${
investmentMode === 'topPicks'
? 'bg-nifty-600 text-white'
: 'bg-white dark:bg-slate-600 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-slate-500 hover:bg-gray-50 dark:hover:bg-slate-500'
}`}
>
Top Picks Only
</button>
</div>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={includeBrokerage}
onChange={(e) => setIncludeBrokerage(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-nifty-600 focus:ring-nifty-500"
/>
<span className="text-xs text-gray-600 dark:text-gray-400">Include Zerodha Equity Delivery Charges</span>
</label>
</div>
</div>
)}
{/* Input Section */}
<div className="mb-4">
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">
Starting Investment
</label>
<div className="relative">
<IndianRupee className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={startingAmount.toLocaleString('en-IN')}
onChange={handleAmountChange}
className="w-full pl-9 pr-4 py-2 rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
/>
</div>
<div className="flex gap-2 mt-2">
{[10000, 50000, 100000, 500000].map(amount => (
<button
key={amount}
onClick={() => setStartingAmount(amount)}
className={`px-2 py-1 text-xs rounded ${
startingAmount === amount
? 'bg-nifty-600 text-white'
: 'bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-600'
}`}
>
{formatINR(amount, 0)}
</button>
))}
</div>
</div>
{/* Results Section */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 relative">
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 mb-1">
<span>Final Portfolio Value</span>
<InfoButton onClick={() => setActiveModal('portfolioValue')} />
</div>
<div className={`text-xl font-bold ${getValueColorClass(profitLoss)}`}>
{formatINR(finalValue, 0)}
</div>
</div>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 mb-1">
<span>Net Profit/Loss</span>
<InfoButton onClick={() => setActiveModal('profitLoss')} />
</div>
<div className={`text-xl font-bold ${getValueColorClass(profitLoss)}`}>
{isPositive ? '+' : ''}{formatINR(profitLoss, 0)}
<span className="text-sm ml-1">({isPositive ? '+' : ''}{totalReturn.toFixed(1)}%)</span>
</div>
</div>
</div>
{/* Trade Stats with Info Buttons */}
<div className="grid grid-cols-4 gap-2 mb-4">
<div
className="p-2 rounded-lg bg-blue-50 dark:bg-blue-900/20 text-center cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
onClick={() => setActiveModal('totalTrades')}
>
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">{stats.totalTrades}</div>
<div className="text-[10px] text-blue-600/70 dark:text-blue-400/70 flex items-center justify-center gap-0.5">
Total Trades <HelpCircle className="w-2.5 h-2.5" />
</div>
</div>
<div
className="p-2 rounded-lg bg-green-50 dark:bg-green-900/20 text-center cursor-pointer hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors"
onClick={() => setActiveModal('buyTrades')}
>
<div className="text-lg font-bold text-green-600 dark:text-green-400">{stats.buyTrades}</div>
<div className="text-[10px] text-green-600/70 dark:text-green-400/70 flex items-center justify-center gap-0.5">
Buy Trades <HelpCircle className="w-2.5 h-2.5" />
</div>
</div>
<div
className="p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-center cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
onClick={() => setActiveModal('sellTrades')}
>
<div className="text-lg font-bold text-red-600 dark:text-red-400">{stats.sellTrades}</div>
<div className="text-[10px] text-red-600/70 dark:text-red-400/70 flex items-center justify-center gap-0.5">
Sell Trades <HelpCircle className="w-2.5 h-2.5" />
</div>
</div>
<div
className="p-2 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-center cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-900/30 transition-colors"
onClick={() => setShowBrokerageDetails(!showBrokerageDetails)}
title="Click for detailed breakdown"
>
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">{formatINR(totalCharges, 0)}</div>
<div className="text-[10px] text-amber-600/70 dark:text-amber-400/70 flex items-center justify-center gap-0.5">
Total Charges <Info className="w-2.5 h-2.5" />
</div>
</div>
</div>
{/* Open Positions Badge */}
{openPositionsCount > 0 && (
<div className="mb-4 p-2 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800/30">
<div className="flex items-center justify-between text-xs">
<span className="text-purple-700 dark:text-purple-300 flex items-center gap-1">
<Wallet className="w-3.5 h-3.5" />
Open Positions (not yet sold)
</span>
<span className="font-bold text-purple-600 dark:text-purple-400">{openPositionsCount} stocks</span>
</div>
</div>
)}
{/* Brokerage Breakdown */}
{showBrokerageDetails && includeBrokerage && (
<div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800/30">
<div className="flex items-center gap-2 mb-2">
<Receipt className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span className="text-xs font-semibold text-amber-800 dark:text-amber-300">Zerodha Equity Delivery Charges</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Brokerage:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.brokerage)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">STT:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.stt)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Exchange Charges:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.exchangeCharges)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">SEBI Charges:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.sebiCharges)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">GST (18%):</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.gst)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600 dark:text-gray-400">Stamp Duty:</span>
<span className="font-medium text-gray-800 dark:text-gray-200">{formatINR(stats.brokerageBreakdown.stampDuty)}</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-amber-200 dark:border-amber-700 flex justify-between">
<span className="text-xs font-semibold text-amber-800 dark:text-amber-300">Total Turnover:</span>
<span className="text-xs font-bold text-amber-800 dark:text-amber-300">{formatINR(stats.brokerageBreakdown.turnover, 0)}</span>
</div>
</div>
)}
{/* Comparison with Nifty */}
<div
className="mb-4 p-3 rounded-lg bg-gradient-to-r from-nifty-50 to-blue-50 dark:from-nifty-900/20 dark:to-blue-900/20 border border-nifty-100 dark:border-nifty-800/30 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => setActiveModal('comparison')}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">vs Nifty 50 Index</span>
</div>
<HelpCircle className="w-3.5 h-3.5 text-gray-400" />
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className={`text-sm font-bold ${getValueColorClass(totalReturn)}`}>
{totalReturn >= 0 ? '+' : ''}{totalReturn.toFixed(1)}%
</div>
<div className="text-[10px] text-gray-500">AI Strategy</div>
</div>
<div>
<div className={`text-sm font-bold ${getValueColorClass(niftyReturn)}`}>
{niftyReturn >= 0 ? '+' : ''}{niftyReturn.toFixed(1)}%
</div>
<div className="text-[10px] text-gray-500">Nifty 50</div>
</div>
<div>
<div className={`text-sm font-bold ${outperformance >= 0 ? 'text-nifty-600 dark:text-nifty-400' : 'text-red-600 dark:text-red-400'}`}>
{outperformance >= 0 ? '+' : ''}{outperformance.toFixed(1)}%
</div>
<div className="text-[10px] text-gray-500">Outperformance</div>
</div>
</div>
</div>
{/* Chart with Nifty Comparison - Fixed Y-axis */}
{portfolioData.length > 0 && (
<div className="h-48 mb-4">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<LineChart data={portfolioData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="date"
tick={{ fontSize: 10 }}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
tick={{ fontSize: 10 }}
tickFormatter={(v) => formatINR(v, 0).replace('₹', '')}
className="text-gray-500 dark:text-gray-400"
width={60}
domain={yAxisDomain}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value, name) => [
formatINR(Number(value) || 0, 0),
name === 'value' ? 'AI Strategy' : 'Nifty 50'
]}
/>
<Legend
wrapperStyle={{ fontSize: '10px' }}
formatter={(value) => value === 'value' ? 'AI Strategy' : 'Nifty 50'}
/>
<ReferenceLine
y={startingAmount}
stroke="#94a3b8"
strokeDasharray="5 5"
label={{ value: 'Start', fontSize: 10, fill: '#94a3b8' }}
/>
<Line
type="monotone"
dataKey="value"
name="value"
stroke={isPositive ? '#22c55e' : '#ef4444'}
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="niftyValue"
name="niftyValue"
stroke="#6366f1"
strokeWidth={2}
strokeDasharray="4 4"
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Trade Waterfall Toggle */}
<button
onClick={() => setShowTradeWaterfall(!showTradeWaterfall)}
className="flex items-center justify-between w-full px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-slate-700/50 rounded-lg transition-colors mb-2"
>
<span className="flex items-center gap-2">
<ArrowRightLeft className="w-4 h-4" />
Trade Timeline ({stats.trades.length} completed trades)
</span>
{showTradeWaterfall ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{/* Trade Waterfall Chart */}
{showTradeWaterfall && stats.trades.length > 0 && (
<div className="mb-4 p-3 bg-gray-50 dark:bg-slate-700/30 rounded-lg">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-2">
Each bar represents a trade from buy to sell. Green = Profit, Red = Loss.
</div>
<div className="h-64 overflow-y-auto">
<div style={{ height: Math.max(200, stats.trades.length * 28) }}>
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<BarChart
data={stats.trades.map((t, i) => ({
...t,
idx: i,
displayName: `${t.symbol}`,
duration: `${new Date(t.entryDate).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })} → ${new Date(t.exitDate).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}`,
}))}
layout="vertical"
margin={{ top: 5, right: 60, bottom: 5, left: 70 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" horizontal={false} />
<XAxis
type="number"
tick={{ fontSize: 9 }}
tickFormatter={(v) => formatINR(v, 0)}
domain={['dataMin', 'dataMax']}
/>
<YAxis
type="category"
dataKey="displayName"
tick={{ fontSize: 10 }}
width={65}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '11px',
}}
formatter={(value) => [formatINR(Number(value) || 0, 2), 'P/L']}
labelFormatter={(_, payload) => {
if (payload && payload[0]) {
const d = payload[0].payload;
return `${d.symbol}: ${d.duration}`;
}
return '';
}}
/>
<Bar dataKey="profitLoss" radius={[0, 4, 4, 0]}>
{stats.trades.map((trade, index) => (
<Cell
key={`cell-${index}`}
fill={trade.profitLoss >= 0 ? '#22c55e' : '#ef4444'}
/>
))}
<LabelList
dataKey="profitLoss"
position="right"
formatter={(v) => formatINR(Number(v) || 0, 0)}
style={{ fontSize: 9, fill: '#6b7280' }}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
)}
{/* Daily Breakdown (Collapsible) */}
<button
onClick={() => setShowBreakdown(!showBreakdown)}
className="flex items-center justify-between w-full px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-slate-700/50 rounded-lg transition-colors"
>
<span>Daily Breakdown</span>
{showBreakdown ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showBreakdown && (
<div className="mt-2 border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-slate-700">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">Date</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Return</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">AI Value</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Nifty</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-slate-700">
{portfolioData.map((day, idx) => (
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-slate-700/50">
<td className="px-3 py-2 text-gray-700 dark:text-gray-300">{day.date}</td>
<td className={`px-3 py-2 text-right font-medium ${getValueColorClass(day.return)}`}>
{day.return >= 0 ? '+' : ''}{day.return.toFixed(1)}%
</td>
<td className="px-3 py-2 text-right text-gray-700 dark:text-gray-300">
{formatINR(day.value, 0)}
</td>
<td className="px-3 py-2 text-right text-indigo-600 dark:text-indigo-400">
{formatINR(day.niftyValue, 0)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Demo Data Notice */}
{isUsingMockData && (
<div className="flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mt-3">
<AlertCircle className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<span className="text-[10px] text-amber-700 dark:text-amber-300">
Simulation uses demo data. Results are illustrative only.
</span>
</div>
)}
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-3 text-center">
Simulated using Zerodha Equity Delivery rates (0% brokerage, STT 0.1%, Exchange 0.00345%, SEBI 0.0001%, Stamp 0.015%).
{investmentMode === 'topPicks' ? ' Investing in Top Picks only.' : ' Investing in all 50 stocks.'}
{includeBrokerage ? ` Total Charges: ${formatINR(totalCharges, 0)}` : ''}
</p>
{/* Info Modals */}
<InfoModal
isOpen={activeModal === 'totalTrades'}
onClose={() => setActiveModal(null)}
title="Total Trades"
icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p><strong>Total Trades</strong> represents the sum of all buy and sell transactions executed during the simulation period.</p>
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<div className="font-semibold text-blue-800 dark:text-blue-200 mb-1">Calculation:</div>
<code className="text-xs">Total Trades = Buy Trades + Sell Trades</code>
<div className="mt-2 text-xs">= {stats.buyTrades} + {stats.sellTrades} = <strong>{stats.totalTrades}</strong></div>
</div>
<p className="text-xs text-gray-500">Note: A complete round-trip trade (buy then sell) counts as 2 trades.</p>
</div>
</InfoModal>
<InfoModal
isOpen={activeModal === 'buyTrades'}
onClose={() => setActiveModal(null)}
title="Buy Trades"
icon={<TrendingUp className="w-5 h-5 text-green-600 dark:text-green-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p><strong>Buy Trades</strong> counts when a new position is opened based on AI's BUY recommendation.</p>
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="font-semibold text-green-800 dark:text-green-200 mb-2">When is a Buy Trade counted?</div>
<ul className="text-xs space-y-1 list-disc list-inside">
<li>When AI recommends BUY and no position exists</li>
<li>When AI recommends BUY after a previous SELL</li>
</ul>
</div>
<p className="text-xs text-gray-500">Note: If AI recommends BUY while already holding (from previous BUY or HOLD), no new buy trade is counted - the position is simply carried forward.</p>
</div>
</InfoModal>
<InfoModal
isOpen={activeModal === 'sellTrades'}
onClose={() => setActiveModal(null)}
title="Sell Trades"
icon={<TrendingDown className="w-5 h-5 text-red-600 dark:text-red-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p><strong>Sell Trades</strong> counts when a position is closed based on AI's SELL recommendation.</p>
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div className="font-semibold text-red-800 dark:text-red-200 mb-2">When is a Sell Trade counted?</div>
<ul className="text-xs space-y-1 list-disc list-inside">
<li>When AI recommends SELL while holding a position</li>
<li>Position must have been opened via BUY or carried via HOLD</li>
</ul>
</div>
<p className="text-xs text-gray-500">Note: Brokerage is calculated when a sell trade completes a round-trip transaction.</p>
</div>
</InfoModal>
<InfoModal
isOpen={activeModal === 'portfolioValue'}
onClose={() => setActiveModal(null)}
title="Final Portfolio Value"
icon={<PiggyBank className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p><strong>Final Portfolio Value</strong> is the total worth of your investments at the end of the simulation period.</p>
<div className="p-3 bg-nifty-50 dark:bg-nifty-900/20 rounded-lg">
<div className="font-semibold text-nifty-800 dark:text-nifty-200 mb-1">Calculation:</div>
<code className="text-xs">Final Value = Portfolio Value - Total Charges</code>
<div className="mt-2 text-xs">
= {formatINR(currentValue, 0)} - {formatINR(totalCharges, 0)} = <strong>{formatINR(finalValue, 0)}</strong>
</div>
</div>
<p className="text-xs text-gray-500">This includes all realized gains/losses from completed trades and deducts Zerodha brokerage charges.</p>
</div>
</InfoModal>
<InfoModal
isOpen={activeModal === 'profitLoss'}
onClose={() => setActiveModal(null)}
title="Net Profit/Loss"
icon={<Calculator className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p><strong>Net Profit/Loss</strong> shows your actual earnings or losses after all charges.</p>
<div className="p-3 bg-gray-100 dark:bg-slate-700 rounded-lg">
<div className="font-semibold mb-1">Calculation:</div>
<code className="text-xs">Net P/L = Final Value - Starting Investment</code>
<div className="mt-2 text-xs">
= {formatINR(finalValue, 0)} - {formatINR(startingAmount, 0)} = <strong className={profitLoss >= 0 ? 'text-green-600' : 'text-red-600'}>{formatINR(profitLoss, 0)}</strong>
</div>
<div className="mt-2 text-xs">
Return = ({formatINR(profitLoss, 0)} / {formatINR(startingAmount, 0)}) × 100 = <strong>{totalReturn.toFixed(2)}%</strong>
</div>
</div>
</div>
</InfoModal>
<InfoModal
isOpen={activeModal === 'comparison'}
onClose={() => setActiveModal(null)}
title="vs Nifty 50 Index"
icon={<BarChart3 className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />}
>
<div className="space-y-3 text-sm text-gray-600 dark:text-gray-300">
<p>This compares the AI strategy's performance against simply investing in the Nifty 50 index.</p>
<div className="space-y-2">
<div className="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg flex justify-between items-center">
<span>AI Strategy Return:</span>
<strong className={totalReturn >= 0 ? 'text-green-600' : 'text-red-600'}>{totalReturn.toFixed(2)}%</strong>
</div>
<div className="p-2 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg flex justify-between items-center">
<span>Nifty 50 Return:</span>
<strong className={niftyReturn >= 0 ? 'text-green-600' : 'text-red-600'}>{niftyReturn.toFixed(2)}%</strong>
</div>
<div className="p-2 bg-nifty-50 dark:bg-nifty-900/20 rounded-lg flex justify-between items-center">
<span>Outperformance (Alpha):</span>
<strong className={outperformance >= 0 ? 'text-nifty-600' : 'text-red-600'}>{outperformance.toFixed(2)}%</strong>
</div>
</div>
<p className="text-xs text-gray-500">
{outperformance >= 0
? `The AI strategy beat the Nifty 50 index by ${outperformance.toFixed(2)} percentage points.`
: `The AI strategy underperformed the Nifty 50 index by ${Math.abs(outperformance).toFixed(2)} percentage points.`
}
</p>
</div>
</InfoModal>
</div>
);
}
// Export the type for use in other components
export { type InvestmentMode as PortfolioInvestmentMode };