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; allBacktestData?: Record>; } 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, allBacktestData?: Record> ): { portfolioData: Array<{ date: string; rawDate: string; value: number; niftyValue: number; return: number; cumulative: number }>; stats: TradeStats; openPositions: Record; } { 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 = {}; 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( (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('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(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) => { const value = parseInt(e.target.value.replace(/,/g, ''), 10); if (!isNaN(value) && value >= 0) { setStartingAmount(value); } }; const openPositionsCount = Object.keys(openPositions).length; return (

Portfolio Simulator

{/* Settings Panel */} {showSettings && (
)} {/* Input Section */}
{[10000, 50000, 100000, 500000].map(amount => ( ))}
{/* Results Section */}
Final Portfolio Value setActiveModal('portfolioValue')} />
{formatINR(finalValue, 0)}
Net Profit/Loss setActiveModal('profitLoss')} />
{isPositive ? '+' : ''}{formatINR(profitLoss, 0)} ({isPositive ? '+' : ''}{totalReturn.toFixed(1)}%)
{/* Trade Stats with Info Buttons */}
setActiveModal('totalTrades')} >
{stats.totalTrades}
Total Trades
setActiveModal('buyTrades')} >
{stats.buyTrades}
Buy Trades
setActiveModal('sellTrades')} >
{stats.sellTrades}
Sell Trades
setShowBrokerageDetails(!showBrokerageDetails)} title="Click for detailed breakdown" >
{formatINR(totalCharges, 0)}
Total Charges
{/* Open Positions Badge */} {openPositionsCount > 0 && (
Open Positions (not yet sold) {openPositionsCount} stocks
)} {/* Brokerage Breakdown */} {showBrokerageDetails && includeBrokerage && (
Zerodha Equity Delivery Charges
Brokerage: {formatINR(stats.brokerageBreakdown.brokerage)}
STT: {formatINR(stats.brokerageBreakdown.stt)}
Exchange Charges: {formatINR(stats.brokerageBreakdown.exchangeCharges)}
SEBI Charges: {formatINR(stats.brokerageBreakdown.sebiCharges)}
GST (18%): {formatINR(stats.brokerageBreakdown.gst)}
Stamp Duty: {formatINR(stats.brokerageBreakdown.stampDuty)}
Total Turnover: {formatINR(stats.brokerageBreakdown.turnover, 0)}
)} {/* Comparison with Nifty */}
setActiveModal('comparison')} >
vs Nifty 50 Index
{totalReturn >= 0 ? '+' : ''}{totalReturn.toFixed(1)}%
AI Strategy
{niftyReturn >= 0 ? '+' : ''}{niftyReturn.toFixed(1)}%
Nifty 50
= 0 ? 'text-nifty-600 dark:text-nifty-400' : 'text-red-600 dark:text-red-400'}`}> {outperformance >= 0 ? '+' : ''}{outperformance.toFixed(1)}%
Outperformance
{/* Chart with Nifty Comparison - Fixed Y-axis */} {portfolioData.length > 0 && (
formatINR(v, 0).replace('₹', '')} className="text-gray-500 dark:text-gray-400" width={60} domain={yAxisDomain} /> [ formatINR(Number(value) || 0, 0), name === 'value' ? 'AI Strategy' : 'Nifty 50' ]} /> value === 'value' ? 'AI Strategy' : 'Nifty 50'} />
)} {/* Trade Waterfall Toggle */} {/* Trade Waterfall Chart */} {showTradeWaterfall && stats.trades.length > 0 && (
Each bar represents a trade from buy to sell. Green = Profit, Red = Loss.
({ ...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 }} > formatINR(v, 0)} domain={['dataMin', 'dataMax']} /> [formatINR(Number(value) || 0, 2), 'P/L']} labelFormatter={(_, payload) => { if (payload && payload[0]) { const d = payload[0].payload; return `${d.symbol}: ${d.duration}`; } return ''; }} /> {stats.trades.map((trade, index) => ( = 0 ? '#22c55e' : '#ef4444'} /> ))} formatINR(Number(v) || 0, 0)} style={{ fontSize: 9, fill: '#6b7280' }} />
)} {/* Daily Breakdown (Collapsible) */} {showBreakdown && (
{portfolioData.map((day, idx) => ( ))}
Date Return AI Value Nifty
{day.date} {day.return >= 0 ? '+' : ''}{day.return.toFixed(1)}% {formatINR(day.value, 0)} {formatINR(day.niftyValue, 0)}
)} {/* Demo Data Notice */} {isUsingMockData && (
Simulation uses demo data. Results are illustrative only.
)}

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)}` : ''}

{/* Info Modals */} setActiveModal(null)} title="Total Trades" icon={} >

Total Trades represents the sum of all buy and sell transactions executed during the simulation period.

Calculation:
Total Trades = Buy Trades + Sell Trades
= {stats.buyTrades} + {stats.sellTrades} = {stats.totalTrades}

Note: A complete round-trip trade (buy then sell) counts as 2 trades.

setActiveModal(null)} title="Buy Trades" icon={} >

Buy Trades counts when a new position is opened based on AI's BUY recommendation.

When is a Buy Trade counted?
  • When AI recommends BUY and no position exists
  • When AI recommends BUY after a previous SELL

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.

setActiveModal(null)} title="Sell Trades" icon={} >

Sell Trades counts when a position is closed based on AI's SELL recommendation.

When is a Sell Trade counted?
  • When AI recommends SELL while holding a position
  • Position must have been opened via BUY or carried via HOLD

Note: Brokerage is calculated when a sell trade completes a round-trip transaction.

setActiveModal(null)} title="Final Portfolio Value" icon={} >

Final Portfolio Value is the total worth of your investments at the end of the simulation period.

Calculation:
Final Value = Portfolio Value - Total Charges
= {formatINR(currentValue, 0)} - {formatINR(totalCharges, 0)} = {formatINR(finalValue, 0)}

This includes all realized gains/losses from completed trades and deducts Zerodha brokerage charges.

setActiveModal(null)} title="Net Profit/Loss" icon={} >

Net Profit/Loss shows your actual earnings or losses after all charges.

Calculation:
Net P/L = Final Value - Starting Investment
= {formatINR(finalValue, 0)} - {formatINR(startingAmount, 0)} = = 0 ? 'text-green-600' : 'text-red-600'}>{formatINR(profitLoss, 0)}
Return = ({formatINR(profitLoss, 0)} / {formatINR(startingAmount, 0)}) × 100 = {totalReturn.toFixed(2)}%
setActiveModal(null)} title="vs Nifty 50 Index" icon={} >

This compares the AI strategy's performance against simply investing in the Nifty 50 index.

AI Strategy Return: = 0 ? 'text-green-600' : 'text-red-600'}>{totalReturn.toFixed(2)}%
Nifty 50 Return: = 0 ? 'text-green-600' : 'text-red-600'}>{niftyReturn.toFixed(2)}%
Outperformance (Alpha): = 0 ? 'text-nifty-600' : 'text-red-600'}>{outperformance.toFixed(2)}%

{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.` }

); } // Export the type for use in other components export { type InvestmentMode as PortfolioInvestmentMode };