import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, ChevronDown, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Clock, Zap, Award, ArrowUpRight, ArrowDownRight, Play, Loader2, FileText, MessageSquare, Search, XCircle, AlertTriangle } from 'lucide-react'; import type { ReturnBreakdown } from '../types'; import { NIFTY_50_STOCKS } from '../types'; import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import Sparkline from '../components/Sparkline'; import AccuracyExplainModal from '../components/AccuracyExplainModal'; import ReturnExplainModal from '../components/ReturnExplainModal'; import OverallReturnModal, { type OverallReturnBreakdown } from '../components/OverallReturnModal'; import AccuracyTrendChart, { type AccuracyTrendPoint } from '../components/AccuracyTrendChart'; import ReturnDistributionChart from '../components/ReturnDistributionChart'; import RiskMetricsCard from '../components/RiskMetricsCard'; import PortfolioSimulator, { type InvestmentMode } from '../components/PortfolioSimulator'; import IndexComparisonChart from '../components/IndexComparisonChart'; import InfoModal from '../components/InfoModal'; import { api } from '../services/api'; import { useSettings } from '../contexts/SettingsContext'; import type { StockAnalysis, DailyRecommendation, RiskMetrics, ReturnBucket, CumulativeReturnPoint } from '../types'; // Type for batch backtest data (per date, per symbol) type BacktestByDate = Record>; // Helper for consistent positive/negative color classes function getValueColorClass(value: number): string { return value >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'; } // Format percentage without negative zero (e.g. "-0.0" becomes "0.0") function fmtPct(val: number, decimals = 1): string { const s = val.toFixed(decimals); if (s === '-0.0' || s === '-0.00') return s.replace('-', ''); return s; } // Investment Mode Toggle Component function InvestmentModeToggle({ mode, onChange, size = 'sm' }: { mode: InvestmentMode; onChange: (mode: InvestmentMode) => void; size?: 'sm' | 'md'; }) { const sizeClasses = size === 'sm' ? 'px-2.5 py-1 text-[10px]' : 'px-3 py-1.5 text-xs'; return (
); } // Pulsing skeleton bar for loading states function SkeletonBar({ className = '' }: { className?: string }) { return
; } // Section header with icon and optional right content function SectionHeader({ icon, title, right, subtitle }: { icon: React.ReactNode; title: string; right?: React.ReactNode; subtitle?: string }) { return (
{icon}

{title}

{subtitle &&

{subtitle}

}
{right}
); } export default function History() { const [selectedDate, setSelectedDate] = useState(null); const [showAccuracyModal, setShowAccuracyModal] = useState(false); const [showReturnModal, setShowReturnModal] = useState(false); const [returnModalDate, setReturnModalDate] = useState(null); const [showOverallModal, setShowOverallModal] = useState(false); // Investment modes for various sections const [dateFilterMode, setDateFilterMode] = useState('all50'); const [summaryMode, setSummaryMode] = useState('all50'); const [indexChartMode, setIndexChartMode] = useState('all50'); const [distributionMode, setDistributionMode] = useState('all50'); // Performance Summary modal state type SummaryModalType = 'daysTracked' | 'avgReturn' | 'buySignals' | 'sellSignals' | null; const [activeSummaryModal, setActiveSummaryModal] = useState(null); // Backtest feature state const [backtestDateInput, setBacktestDateInput] = useState(''); const [isRunningBacktest, setIsRunningBacktest] = useState(false); const [detailedBacktest, setDetailedBacktest] = useState<{ date: string; total_stocks: number; stocks: Array<{ symbol: string; company_name: string; rank?: number; decision: string; confidence: string; risk: string; hold_days: number; hold_days_elapsed: number; hold_period_active: boolean; price_at_prediction: number | null; price_current: number | null; price_at_hold_end: number | null; return_current: number | null; return_at_hold: number | null; prediction_correct: boolean | null; formula: string; raw_analysis: string; agent_summary: Record; debate_summary: Record; }>; } | null>(null); const [expandedStock, setExpandedStock] = useState(null); const [activeAgentTab, setActiveAgentTab] = useState('market'); const [isLoadingDetailed, setIsLoadingDetailed] = useState(false); const [backtestMessage, setBacktestMessage] = useState<{ type: 'error' | 'info' | 'progress'; text: string } | null>(null); const backtestPollRef = useRef | null>(null); const { settings } = useSettings(); // ========================================================== // SINGLE-FETCH: All data loaded in one API call // ========================================================== const [recommendations, setRecommendations] = useState([]); const [batchBacktestByDate, setBatchBacktestByDate] = useState({}); const [isLoading, setIsLoading] = useState(true); const [nifty50Prices, setNifty50Prices] = useState>({}); const [apiAccuracyMetrics, setApiAccuracyMetrics] = useState<{ overall_accuracy: number; total_predictions: number; correct_predictions: number; by_decision: Record; } | null>(null); const [loadTimeMs, setLoadTimeMs] = useState(null); // Single useEffect: fetch the bundle useEffect(() => { const fetchBundle = async () => { setIsLoading(true); const t0 = performance.now(); try { const bundle = await api.getHistoryBundle(); if (bundle.recommendations && bundle.recommendations.length > 0) { setRecommendations(bundle.recommendations); } if (bundle.backtest_by_date) { setBatchBacktestByDate(bundle.backtest_by_date); } if (bundle.accuracy && bundle.accuracy.total_predictions > 0) { setApiAccuracyMetrics(bundle.accuracy); } if (bundle.nifty50_prices && Object.keys(bundle.nifty50_prices).length > 0) { setNifty50Prices(bundle.nifty50_prices); } setLoadTimeMs(Math.round(performance.now() - t0)); } catch (error) { console.error('Failed to fetch history bundle:', error); setLoadTimeMs(Math.round(performance.now() - t0)); } finally { setIsLoading(false); } }; fetchBundle(); }, []); // If Nifty50 wasn't in the bundle (cache cold), retry once after 3s const niftyRetried = useRef(false); useEffect(() => { if (!isLoading && Object.keys(nifty50Prices).length === 0 && !niftyRetried.current) { niftyRetried.current = true; const timer = setTimeout(async () => { try { const data = await api.getNifty50History(); if (data.prices && Object.keys(data.prices).length > 0) { setNifty50Prices(data.prices); } } catch { /* ignore */ } }, 3000); return () => clearTimeout(timer); } }, [isLoading, nifty50Prices]); const dates = recommendations.map(r => r.date); const hasBacktestData = Object.keys(batchBacktestByDate).length > 0; // ========================================================== // COMPUTED: All chart data derived synchronously from bundle // ========================================================== // Accuracy metrics const accuracyMetrics = useMemo(() => { if (apiAccuracyMetrics && apiAccuracyMetrics.total_predictions > 0) { return { total_predictions: apiAccuracyMetrics.total_predictions, correct_predictions: apiAccuracyMetrics.correct_predictions, success_rate: apiAccuracyMetrics.overall_accuracy / 100, buy_accuracy: (apiAccuracyMetrics.by_decision?.BUY?.accuracy || 0) / 100, sell_accuracy: (apiAccuracyMetrics.by_decision?.SELL?.accuracy || 0) / 100, hold_accuracy: (apiAccuracyMetrics.by_decision?.HOLD?.accuracy || 0) / 100, }; } return { total_predictions: 0, correct_predictions: 0, success_rate: 0, buy_accuracy: 0, sell_accuracy: 0, hold_accuracy: 0 }; }, [apiAccuracyMetrics]); // Accuracy trend data const accuracyTrendData = useMemo(() => { if (!hasBacktestData) return []; const sortedDates = [...recommendations] .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) .map(r => r.date); const trendData: AccuracyTrendPoint[] = []; for (const date of sortedDates) { const rec = recommendations.find(r => r.date === date); const dateBacktest = batchBacktestByDate[date]; if (!rec || !dateBacktest) continue; let totalBuy = 0, correctBuy = 0, totalSell = 0, correctSell = 0, totalHold = 0, correctHold = 0; for (const symbol of Object.keys(rec.analysis)) { const stockAnalysis = rec.analysis[symbol]; const bt = dateBacktest[symbol]; const primaryRet = bt?.return_at_hold ?? bt?.return_1d; if (!stockAnalysis?.decision || primaryRet === undefined || primaryRet === null) continue; const predictionCorrect = (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? primaryRet > 0 : primaryRet < 0; if (stockAnalysis.decision === 'BUY') { totalBuy++; if (predictionCorrect) correctBuy++; } else if (stockAnalysis.decision === 'SELL') { totalSell++; if (predictionCorrect) correctSell++; } else { totalHold++; if (predictionCorrect) correctHold++; } } const totalPredictions = totalBuy + totalSell + totalHold; const totalCorrect = correctBuy + correctSell + correctHold; if (totalPredictions < 3) continue; trendData.push({ date, overall: totalPredictions > 0 ? Math.round((totalCorrect / totalPredictions) * 100) : 0, buy: totalBuy > 0 ? Math.round((correctBuy / totalBuy) * 100) : 0, sell: totalSell > 0 ? Math.round((correctSell / totalSell) * 100) : 0, hold: totalHold > 0 ? Math.round((correctHold / totalHold) * 100) : 0, }); } return trendData; }, [batchBacktestByDate, hasBacktestData, recommendations]); // All chart data computed from batch backtest const chartData = useMemo(() => { if (recommendations.length === 0 || !hasBacktestData) { return { riskMetrics: undefined as RiskMetrics | undefined, returnDistribution: undefined as ReturnBucket[] | undefined, cumulativeReturns: undefined as CumulativeReturnPoint[] | undefined, overallBreakdown: undefined as OverallReturnBreakdown | undefined, topPicksCumulativeReturns: undefined as CumulativeReturnPoint[] | undefined, topPicksReturnDistribution: undefined as ReturnBucket[] | undefined, dateReturns: {} as Record, allBacktestData: {} as Record>, dailyReturnsArray: [] as number[], topPicksDailyReturns: [] as number[], }; } const sortedDates = [...recommendations] .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) .map(r => r.date); // Risk metrics accumulators const dailyReturns: number[] = []; let wins = 0, losses = 0, totalWinReturn = 0, totalLossReturn = 0; let totalCorrect = 0, totalPredictions = 0; // Return distribution buckets const returnBuckets: ReturnBucket[] = [ { range: '< -3%', min: -Infinity, max: -3, count: 0, stocks: [] }, { range: '-3% to -2%', min: -3, max: -2, count: 0, stocks: [] }, { range: '-2% to -1%', min: -2, max: -1, count: 0, stocks: [] }, { range: '-1% to 0%', min: -1, max: 0, count: 0, stocks: [] }, { range: '0% to 1%', min: 0, max: 1, count: 0, stocks: [] }, { range: '1% to 2%', min: 1, max: 2, count: 0, stocks: [] }, { range: '2% to 3%', min: 2, max: 3, count: 0, stocks: [] }, { range: '> 3%', min: 3, max: Infinity, count: 0, stocks: [] }, ]; // Cumulative returns const cumulativeData: CumulativeReturnPoint[] = []; let aiMultiplier = 1; // Nifty50 price ratio approach: direct comparison to start price // This avoids losing Nifty returns on days without backtest data const sortedNiftyDates = Object.keys(nifty50Prices).sort(); const hasNiftyData = sortedNiftyDates.length > 0; const niftyStartPrice = hasNiftyData ? nifty50Prices[sortedNiftyDates[0]] : null; const getNiftyReturnForDate = (date: string): number => { if (!hasNiftyData || !niftyStartPrice) return 0; const closestDate = sortedNiftyDates.find(d => d >= date) || sortedNiftyDates[sortedNiftyDates.length - 1]; if (!closestDate || !nifty50Prices[closestDate]) return 0; return ((nifty50Prices[closestDate] / niftyStartPrice) - 1) * 100; }; const dateReturnsMap: Record = {}; const allBacktest: Record> = {}; let latestDateWithData: string | null = null; for (const date of sortedDates) { const rec = recommendations.find(r => r.date === date); const dateBacktest = batchBacktestByDate[date]; if (!rec || !dateBacktest) continue; let dateCorrectCount = 0, dateTotalCount = 0, dateCorrectReturn = 0, dateIncorrectReturn = 0; for (const symbol of Object.keys(rec.analysis)) { const stockAnalysis = rec.analysis[symbol]; const bt = dateBacktest[symbol]; const primaryRet = bt?.return_at_hold ?? bt?.return_1d; if (!stockAnalysis?.decision || primaryRet === undefined || primaryRet === null) continue; if (!allBacktest[date]) allBacktest[date] = {}; allBacktest[date][symbol] = primaryRet; const predictionCorrect = (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? primaryRet > 0 : primaryRet < 0; totalPredictions++; if (predictionCorrect) { totalCorrect++; dateCorrectCount++; dateCorrectReturn += (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? primaryRet : Math.abs(primaryRet); } else { dateIncorrectReturn += (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? primaryRet : -Math.abs(primaryRet); } dateTotalCount++; } if (dateTotalCount > 0) latestDateWithData = date; if (dateTotalCount > 0) { const correctAvg = dateCorrectCount > 0 ? dateCorrectReturn / dateCorrectCount : 0; const incorrectAvg = (dateTotalCount - dateCorrectCount) > 0 ? dateIncorrectReturn / (dateTotalCount - dateCorrectCount) : 0; const weightedReturn = (correctAvg * (dateCorrectCount / dateTotalCount)) + (incorrectAvg * ((dateTotalCount - dateCorrectCount) / dateTotalCount)); dailyReturns.push(weightedReturn); dateReturnsMap[date] = Math.round(weightedReturn * 10) / 10; if (weightedReturn > 0) { wins++; totalWinReturn += weightedReturn; } else if (weightedReturn < 0) { losses++; totalLossReturn += Math.abs(weightedReturn); } aiMultiplier *= (1 + weightedReturn / 100); const niftyCumulativeReturn = getNiftyReturnForDate(date); cumulativeData.push({ date, value: Math.round(aiMultiplier * 10000) / 100, aiReturn: Math.round((aiMultiplier - 1) * 1000) / 10, indexReturn: Math.round(niftyCumulativeReturn * 10) / 10, }); } } // Return distribution from latest date if (latestDateWithData) { const rec = recommendations.find(r => r.date === latestDateWithData); const dateBacktest = batchBacktestByDate[latestDateWithData]; if (rec && dateBacktest) { for (const symbol of Object.keys(rec.analysis)) { const bt = dateBacktest[symbol]; const retVal = bt?.return_at_hold ?? bt?.return_1d; if (retVal === undefined || retVal === null) continue; for (const bucket of returnBuckets) { if (retVal >= bucket.min && retVal < bucket.max) { bucket.count++; bucket.stocks.push(symbol); break; } } } } } // Risk metrics let riskMetrics: RiskMetrics | undefined; if (dailyReturns.length > 0) { const mean = dailyReturns.reduce((a, b) => a + b, 0) / dailyReturns.length; const variance = dailyReturns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / dailyReturns.length; const volatility = Math.sqrt(variance); const riskFreeRate = 0.02; const sharpeRatio = volatility > 0 ? (mean - riskFreeRate) / volatility : 0; let peak = 100, maxDrawdown = 0, maxDrawdownTrough = 100, maxDrawdownPeak = 100, currentValue = 100; for (const ret of dailyReturns) { currentValue = currentValue * (1 + ret / 100); if (currentValue > peak) peak = currentValue; const drawdown = ((peak - currentValue) / peak) * 100; if (drawdown > maxDrawdown) { maxDrawdown = drawdown; maxDrawdownPeak = peak; maxDrawdownTrough = currentValue; } } const avgWin = wins > 0 ? totalWinReturn / wins : 0; const avgLoss = losses > 0 ? totalLossReturn / losses : 0; riskMetrics = { sharpeRatio: Math.round(sharpeRatio * 100) / 100, maxDrawdown: Math.round(maxDrawdown * 10) / 10, winLossRatio: Math.round((avgLoss > 0 ? avgWin / avgLoss : avgWin) * 100) / 100, winRate: Math.round(totalPredictions > 0 ? (totalCorrect / totalPredictions) * 100 : 0), volatility: Math.round(volatility * 100) / 100, totalTrades: totalPredictions, meanReturn: Math.round(mean * 100) / 100, riskFreeRate, winningTrades: wins, losingTrades: losses, avgWinReturn: Math.round(avgWin * 100) / 100, avgLossReturn: Math.round(avgLoss * 100) / 100, peakValue: Math.round(maxDrawdownPeak * 100) / 100, troughValue: Math.round(maxDrawdownTrough * 100) / 100, }; } // Overall breakdown let overallBreakdown: OverallReturnBreakdown | undefined; if (cumulativeData.length > 0) { const breakdownDailyReturns: { date: string; return: number; multiplier: number; cumulative: number }[] = []; let cumulativeMultiplier = 1; for (let i = 0; i < cumulativeData.length; i++) { const point = cumulativeData[i]; const dailyReturn = i === 0 ? point.aiReturn : Math.round((((1 + point.aiReturn / 100) / (1 + cumulativeData[i - 1].aiReturn / 100)) - 1) * 1000) / 10; const dailyMultiplier = 1 + dailyReturn / 100; cumulativeMultiplier *= dailyMultiplier; breakdownDailyReturns.push({ date: point.date, return: dailyReturn, multiplier: Math.round(dailyMultiplier * 10000) / 10000, cumulative: Math.round((cumulativeMultiplier - 1) * 1000) / 10, }); } const finalMultiplier = 1 + cumulativeData[cumulativeData.length - 1].aiReturn / 100; overallBreakdown = { dailyReturns: breakdownDailyReturns, finalMultiplier: Math.round(finalMultiplier * 10000) / 10000, finalReturn: Math.round((finalMultiplier - 1) * 1000) / 10, formula: '', }; } // Top Picks data const topPicksCumulative: CumulativeReturnPoint[] = []; const topPicksDistribution: ReturnBucket[] = [ { range: '< -3%', min: -Infinity, max: -3, count: 0, stocks: [] }, { range: '-3% to -2%', min: -3, max: -2, count: 0, stocks: [] }, { range: '-2% to -1%', min: -2, max: -1, count: 0, stocks: [] }, { range: '-1% to 0%', min: -1, max: 0, count: 0, stocks: [] }, { range: '0% to 1%', min: 0, max: 1, count: 0, stocks: [] }, { range: '1% to 2%', min: 1, max: 2, count: 0, stocks: [] }, { range: '2% to 3%', min: 2, max: 3, count: 0, stocks: [] }, { range: '> 3%', min: 3, max: Infinity, count: 0, stocks: [] }, ]; let topPicksMultiplier = 1; let latestTopPicksDateWithData: string | null = null; const topPicksDailyReturnsArr: number[] = []; for (const date of sortedDates) { const rec = recommendations.find(r => r.date === date); const dateBacktest = batchBacktestByDate[date]; if (!rec || !rec.top_picks || !dateBacktest) continue; let dateReturn = 0, dateCount = 0; for (const pick of rec.top_picks) { const bt = dateBacktest[pick.symbol]; const retVal = bt?.return_at_hold ?? bt?.return_1d; if (retVal !== undefined && retVal !== null) { dateReturn += retVal; dateCount++; } } if (dateCount > 0) latestTopPicksDateWithData = date; if (dateCount > 0) { const avgReturn = dateReturn / dateCount; topPicksDailyReturnsArr.push(avgReturn); topPicksMultiplier *= (1 + avgReturn / 100); const topPicksNiftyReturn = getNiftyReturnForDate(date); topPicksCumulative.push({ date, value: Math.round(topPicksMultiplier * 10000) / 100, aiReturn: Math.round((topPicksMultiplier - 1) * 1000) / 10, indexReturn: Math.round(topPicksNiftyReturn * 10) / 10, }); } } if (latestTopPicksDateWithData) { const rec = recommendations.find(r => r.date === latestTopPicksDateWithData); const dateBacktest = batchBacktestByDate[latestTopPicksDateWithData]; if (rec && dateBacktest) { for (const pick of rec.top_picks) { const bt = dateBacktest[pick.symbol]; const retVal = bt?.return_at_hold ?? bt?.return_1d; if (retVal !== undefined && retVal !== null) { for (const bucket of topPicksDistribution) { if (retVal >= bucket.min && retVal < bucket.max) { bucket.count++; bucket.stocks.push(pick.symbol); break; } } } } } } return { riskMetrics, returnDistribution: returnBuckets, cumulativeReturns: cumulativeData, overallBreakdown, topPicksCumulativeReturns: topPicksCumulative, topPicksReturnDistribution: topPicksDistribution, dateReturns: dateReturnsMap, allBacktestData: allBacktest, dailyReturnsArray: dailyReturns, topPicksDailyReturns: topPicksDailyReturnsArr, }; }, [batchBacktestByDate, hasBacktestData, recommendations, nifty50Prices]); // Overall stats const overallStats = useMemo(() => { if (recommendations.length > 0 && chartData.dailyReturnsArray && chartData.dailyReturnsArray.length > 0) { const mean = chartData.dailyReturnsArray.reduce((a, b) => a + b, 0) / chartData.dailyReturnsArray.length; return { totalDays: recommendations.length, totalPredictions: accuracyMetrics.total_predictions, avgDailyReturn: Math.round(mean * 10) / 10, avgMonthlyReturn: 0, overallAccuracy: Math.round(accuracyMetrics.success_rate * 100), bestDay: null, worstDay: null, }; } return { totalDays: recommendations.length, totalPredictions: 0, avgDailyReturn: 0, avgMonthlyReturn: 0, overallAccuracy: 0, bestDay: null, worstDay: null }; }, [recommendations, chartData.dailyReturnsArray, accuracyMetrics]); // Filtered stats for Performance Summary const filteredStats = useMemo(() => { if (summaryMode === 'all50') { const signalTotals = recommendations.reduce( (acc, r) => ({ buy: acc.buy + r.summary.buy, sell: acc.sell + r.summary.sell, hold: acc.hold + r.summary.hold }), { buy: 0, sell: 0, hold: 0 } ); return { totalDays: dates.length, avgDailyReturn: overallStats.avgDailyReturn, buySignals: signalTotals.buy, sellSignals: signalTotals.sell, holdSignals: signalTotals.hold }; } const topPicksMean = chartData.topPicksDailyReturns.length > 0 ? chartData.topPicksDailyReturns.reduce((a, b) => a + b, 0) / chartData.topPicksDailyReturns.length : 0; return { totalDays: dates.length, avgDailyReturn: Math.round(topPicksMean * 10) / 10, buySignals: recommendations.reduce((acc, r) => acc + r.top_picks.length, 0), sellSignals: 0, holdSignals: 0, }; }, [summaryMode, dates.length, overallStats.avgDailyReturn, recommendations, chartData.topPicksDailyReturns]); // Date stats const dateStatsMap = useMemo(() => { return Object.fromEntries(dates.map(date => { const rec = recommendations.find(r => r.date === date); if (rec) { const stocks = Object.values(rec.analysis); return [date, { date, avgReturn1d: chartData.dateReturns[date] ?? 0, avgReturn1m: 0, totalStocks: stocks.length, correctPredictions: 0, accuracy: 0, buyCount: rec.summary.buy, sellCount: rec.summary.sell, holdCount: rec.summary.hold, }]; } return [date, { date, avgReturn1d: 0, avgReturn1m: 0, totalStocks: 0, correctPredictions: 0, accuracy: 0, buyCount: 0, sellCount: 0, holdCount: 0 }]; })); }, [dates, recommendations, chartData.dateReturns]); const getRecommendation = (date: string) => recommendations.find(r => r.date === date); const getFilteredStocks = (date: string) => { const rec = getRecommendation(date); if (!rec) return []; let stocks: StockAnalysis[]; if (dateFilterMode === 'topPicks') { stocks = rec.top_picks.map(pick => rec.analysis[pick.symbol]).filter(Boolean); } else { stocks = Object.values(rec.analysis); } return [...stocks].sort((a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity)); }; // Build ReturnBreakdown for the modal (from already-loaded batch data) const buildReturnBreakdown = useCallback((date: string): ReturnBreakdown | null => { const rec = recommendations.find(r => r.date === date); const dateBacktest = batchBacktestByDate[date]; if (!rec || !dateBacktest) return null; const correctStocks: { symbol: string; decision: string; return1d: number }[] = []; const incorrectStocks: { symbol: string; decision: string; return1d: number }[] = []; let correctTotal = 0, incorrectTotal = 0; for (const symbol of Object.keys(rec.analysis)) { const stockAnalysis = rec.analysis[symbol]; const bt = dateBacktest[symbol]; const retVal = bt?.return_at_hold ?? bt?.return_1d; if (!stockAnalysis?.decision || retVal === undefined || retVal === null) continue; const isCorrect = (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? retVal > 0 : retVal < 0; const entry = { symbol, decision: stockAnalysis.decision, return1d: retVal }; if (isCorrect) { correctStocks.push(entry); correctTotal += (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? retVal : Math.abs(retVal); } else { incorrectStocks.push(entry); incorrectTotal += (stockAnalysis.decision === 'BUY' || stockAnalysis.decision === 'HOLD') ? retVal : -Math.abs(retVal); } } const totalCount = correctStocks.length + incorrectStocks.length; if (totalCount === 0) return null; const correctAvg = correctStocks.length > 0 ? correctTotal / correctStocks.length : 0; const incorrectAvg = incorrectStocks.length > 0 ? incorrectTotal / incorrectStocks.length : 0; const correctWeight = correctStocks.length / totalCount; const incorrectWeight = incorrectStocks.length / totalCount; const weightedReturn = (correctAvg * correctWeight) + (incorrectAvg * incorrectWeight); correctStocks.sort((a, b) => Math.abs(b.return1d) - Math.abs(a.return1d)); incorrectStocks.sort((a, b) => Math.abs(b.return1d) - Math.abs(a.return1d)); return { correctPredictions: { count: correctStocks.length, totalReturn: correctTotal, avgReturn: correctAvg, stocks: correctStocks.slice(0, 5) }, incorrectPredictions: { count: incorrectStocks.length, totalReturn: incorrectTotal, avgReturn: incorrectAvg, stocks: incorrectStocks.slice(0, 5) }, weightedReturn: Math.round(weightedReturn * 10) / 10, formula: `(${correctAvg.toFixed(2)}% x ${correctStocks.length}/${totalCount}) + (${incorrectAvg.toFixed(2)}% x ${incorrectStocks.length}/${totalCount}) = ${weightedReturn.toFixed(2)}%`, }; }, [recommendations, batchBacktestByDate]); // Load detailed backtest data for a specific date const loadDetailedBacktest = useCallback(async (date: string) => { setIsLoadingDetailed(true); setDetailedBacktest(null); setExpandedStock(null); try { const data = await api.getDetailedBacktest(date); setDetailedBacktest(data); } catch (error) { console.error('Failed to load detailed backtest:', error); } finally { setIsLoadingDetailed(false); } }, []); // Cleanup poll on unmount useEffect(() => { return () => { if (backtestPollRef.current) clearInterval(backtestPollRef.current); }; }, []); const handleRunBacktest = useCallback(async () => { if (!backtestDateInput) return; setBacktestMessage(null); if (backtestPollRef.current) { clearInterval(backtestPollRef.current); backtestPollRef.current = null; } // If date already has data in the loaded bundle, select it and load detailed view if (dates.includes(backtestDateInput)) { setSelectedDate(backtestDateInput); loadDetailedBacktest(backtestDateInput); return; } // Try fetching existing data first setIsRunningBacktest(true); setBacktestMessage({ type: 'progress', text: 'Checking for existing data...' }); try { const data = await api.getDetailedBacktest(backtestDateInput); if (data && data.stocks && data.stocks.length > 0) { setSelectedDate(backtestDateInput); setDetailedBacktest(data); setIsRunningBacktest(false); setBacktestMessage(null); return; } } catch { // No existing data, proceed to run analysis } // No data — auto-trigger bulk analysis for this date setBacktestMessage({ type: 'progress', text: `Starting analysis for ${backtestDateInput}... This runs all 50 stocks through the AI pipeline.` }); try { await api.runBulkAnalysis(backtestDateInput, { deep_think_model: settings.deepThinkModel, quick_think_model: settings.quickThinkModel, provider: settings.provider, api_key: settings.anthropicApiKey || undefined, max_debate_rounds: settings.maxDebateRounds, parallel_workers: settings.parallelWorkers, }); // Poll for progress backtestPollRef.current = setInterval(async () => { try { const status = await api.getBulkAnalysisStatus(); const pct = status.total > 0 ? Math.round((status.completed / status.total) * 100) : 0; const currentStocks = status.current_symbols?.join(', ') || status.current_symbol || ''; if (status.status === 'completed' || status.status === 'idle') { if (backtestPollRef.current) { clearInterval(backtestPollRef.current); backtestPollRef.current = null; } setBacktestMessage({ type: 'progress', text: 'Analysis complete! Loading detailed backtest...' }); // Load the results try { const data = await api.getDetailedBacktest(backtestDateInput); setSelectedDate(backtestDateInput); setDetailedBacktest(data); setBacktestMessage(null); } catch { setBacktestMessage({ type: 'error', text: 'Analysis completed but failed to load results. Try clicking a date card.' }); } setIsRunningBacktest(false); } else if (status.status === 'failed' || status.cancelled) { if (backtestPollRef.current) { clearInterval(backtestPollRef.current); backtestPollRef.current = null; } setIsRunningBacktest(false); setBacktestMessage({ type: 'error', text: `Analysis ${status.cancelled ? 'cancelled' : 'failed'}. ${status.completed}/${status.total} stocks completed.` }); } else { setBacktestMessage({ type: 'progress', text: `Analyzing stocks... ${status.completed}/${status.total} done (${pct}%)${currentStocks ? ` — Currently: ${currentStocks}` : ''}` }); } } catch { // Poll error, keep trying } }, 3000); } catch (error) { console.error('Failed to start analysis:', error); setIsRunningBacktest(false); setBacktestMessage({ type: 'error', text: 'Failed to start analysis. Check that the backend is running.' }); } }, [backtestDateInput, dates, loadDetailedBacktest, settings]); const handleCancelBacktest = useCallback(async () => { try { await api.cancelBulkAnalysis(); } catch { // ignore } if (backtestPollRef.current) { clearInterval(backtestPollRef.current); backtestPollRef.current = null; } setIsRunningBacktest(false); setBacktestMessage({ type: 'error', text: 'Analysis cancelled.' }); }, []); // ========================================================== // RENDER // ========================================================== if (isLoading) { return (

Loading historical data...

Fetching recommendations & backtest results

); } return (
{/* Page Header */}

Historical Performance

AI recommendations with real backtest results and market validation

{dates.length} days
{loadTimeMs !== null && (
{loadTimeMs}ms
)}
{/* Prediction Accuracy - Hero Card */}

Prediction Accuracy

{accuracyMetrics.total_predictions} predictions tracked

{/* Circular gauge for Overall accuracy */}
{(accuracyMetrics.success_rate * 100).toFixed(0)}% Overall

{accuracyMetrics.correct_predictions}/{accuracyMetrics.total_predictions} correct

{/* Progress bars for Buy / Sell / Hold */}
{/* Buy */}
Buy Accuracy
{(accuracyMetrics.buy_accuracy * 100).toFixed(0)}%
{/* Sell */}
Sell Accuracy
{(accuracyMetrics.sell_accuracy * 100).toFixed(0)}%
{/* Hold */}
Hold Accuracy
{(accuracyMetrics.hold_accuracy * 100).toFixed(0)}%
{/* Accuracy Trend Chart */}
} title="Accuracy Trend" subtitle={accuracyTrendData.length > 0 ? `${accuracyTrendData.length} trading days tracked` : undefined} />
{/* Risk Metrics */}
} title="Risk Metrics" subtitle={chartData.riskMetrics ? `${chartData.riskMetrics.totalTrades} trades analyzed` : undefined} />
{/* Portfolio Simulator */} {/* Date Selector */}
} title="Select Date" right={
} /> {/* Backtest Date Input */}
setBacktestDateInput(e.target.value)} className="flex-1 px-3 py-1.5 text-sm bg-white dark:bg-slate-600 border border-gray-200 dark:border-slate-500 rounded-lg focus:ring-2 focus:ring-nifty-500 focus:border-transparent outline-none text-gray-900 dark:text-gray-100" max={new Date().toISOString().split('T')[0]} />
{/* Backtest feedback message */} {backtestMessage && (
{backtestMessage.type === 'progress' && } {backtestMessage.text} {backtestMessage.type === 'progress' ? ( ) : ( )}
{backtestMessage.type === 'progress' && (
Note: Price data & technical indicators use historical data for the selected date. News & analyst ratings reflect current data (yfinance limitation).
)}
)}
{dates.map((date) => { const rec = getRecommendation(date); const stats = dateStatsMap[date]; const avgReturn = stats?.avgReturn1d ?? 0; const hasData = chartData.dateReturns[date] !== undefined; const isPositive = avgReturn >= 0; const filteredSummary = dateFilterMode === 'topPicks' ? { buy: rec?.top_picks.length || 0, sell: 0, hold: 0 } : rec?.summary || { buy: 0, sell: 0, hold: 0 }; return (
); })} {/* Overall Summary Card */}
{/* Selected Date Details */} {selectedDate && (

{new Date(selectedDate).toLocaleDateString('en-IN', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', })}

{dateFilterMode === 'all50' ? ( <> {getRecommendation(selectedDate)?.summary.buy} Buy {getRecommendation(selectedDate)?.summary.sell} Sell {getRecommendation(selectedDate)?.summary.hold} Hold ) : ( {getRecommendation(selectedDate)?.top_picks.length} Top Picks (BUY) )}
{/* Detailed Backtest Action Bar */}
{detailedBacktest?.date === selectedDate ? `Detailed backtest: ${detailedBacktest.total_stocks} stocks with live P&L` : 'View live P&L with full explainability'}
{detailedBacktest && detailedBacktest.date === selectedDate ? ( /* Detailed expandable view */
{detailedBacktest.stocks.map((stock) => { const isExpanded = expandedStock === stock.symbol; const returnVal = stock.return_at_hold ?? stock.return_current; const isPositiveReturn = returnVal !== null && returnVal >= 0; return (
{/* Stock Summary Row */} {/* Expanded Detail */} {isExpanded && (
{/* P&L Formula */}
P&L Formula
                            {stock.formula || 'No formula available'}
                          
{/* Hold Period Progress */}
Hold Period
{stock.hold_period_active ? 'Active' : 'Completed'}
{stock.hold_days_elapsed} / {stock.hold_days} days elapsed {stock.prediction_correct !== null && ( Prediction {stock.prediction_correct ? 'Correct' : 'Incorrect'} )}
{/* Decision Reasoning */}
Decision Reasoning
{stock.confidence} confidence, {stock.risk} risk

{stock.raw_analysis || 'No analysis text available'}

{/* Agent Reports */} {Object.keys(stock.agent_summary).length > 0 && (
Agent Reports
{Object.keys(stock.agent_summary).map((key) => ( ))}

{stock.agent_summary[activeAgentTab] || 'No report available for this agent'}

)} {/* Debate Summary */} {Object.keys(stock.debate_summary).length > 0 && (
Debate Summary
{Object.entries(stock.debate_summary).map(([type, summary]) => (
{type}:{' '} {summary}
))}
)} {/* Link to full stock detail */} View full stock detail
)}
); })}
) : ( /* Simple stock list */
{getFilteredStocks(selectedDate).map((stock: StockAnalysis) => { const bt = batchBacktestByDate[selectedDate]?.[stock.symbol]; let nextDayReturn: number | null = null; let predictionCorrect: boolean | null = null; if (bt) { nextDayReturn = bt.return_at_hold ?? bt.return_1d ?? null; if (nextDayReturn !== null) { predictionCorrect = (stock.decision === 'BUY' || stock.decision === 'HOLD') ? nextDayReturn > 0 : nextDayReturn < 0; } } return (
{stock.symbol} {(stock.company_name && stock.company_name !== stock.symbol) ? stock.company_name : (NIFTY_50_STOCKS.find(n => n.symbol === stock.symbol)?.company_name || stock.company_name)}
{nextDayReturn !== null && ( {nextDayReturn >= 0 ? '+' : ''}{fmtPct(nextDayReturn)}% {bt?.hold_days && /{bt.hold_days}d} )}
); })}
)}
)} {/* Performance Summary */}
} title="Performance Summary" right={} />
{[ { label: 'Days Tracked', value: filteredStats.totalDays.toString(), icon: , color: 'nifty', modal: 'daysTracked' as SummaryModalType }, { label: 'Avg Return', value: `${filteredStats.avgDailyReturn >= 0 ? '+' : ''}${fmtPct(filteredStats.avgDailyReturn)}%`, icon: , color: filteredStats.avgDailyReturn >= 0 ? 'emerald' : 'red', modal: 'avgReturn' as SummaryModalType }, { label: summaryMode === 'topPicks' ? 'Top Picks' : 'Buy Signals', value: filteredStats.buySignals.toString(), icon: , color: 'emerald', modal: 'buySignals' as SummaryModalType }, { label: 'Sell Signals', value: filteredStats.sellSignals.toString(), icon: , color: 'red', modal: 'sellSignals' as SummaryModalType }, ].map(({ label, value, icon, color, modal }) => (
setActiveSummaryModal(modal)} >
{value}
{icon} {label}
))}

{summaryMode === 'topPicks' ? 'Performance based on Top Picks recommendations only (3 stocks per day)' : 'Returns measured over hold period (or 1-day when no hold period specified)'}

{/* AI vs Nifty50 Index Comparison */}
} title="AI Strategy vs Nifty50 Index" right={} />

{(indexChartMode === 'topPicks' ? chartData.topPicksCumulativeReturns : chartData.cumulativeReturns)?.length ? ( <>Cumulative returns for {indexChartMode === 'topPicks' ? 'Top Picks' : 'All 50 stocks'} over {(indexChartMode === 'topPicks' ? chartData.topPicksCumulativeReturns : chartData.cumulativeReturns)?.length} trading days ) : ( <>AI strategy vs Nifty50 index cumulative returns )}

{/* Return Distribution */}
} title="Return Distribution" right={} />

{(distributionMode === 'topPicks' ? chartData.topPicksReturnDistribution : chartData.returnDistribution)?.some(b => b.count > 0) ? ( <>Distribution of {distributionMode === 'topPicks' ? 'Top Picks' : 'all 50 stocks'} hold-period returns. Click bars to see stocks. ) : ( <>Distribution of hold-period returns across all predictions )}

{/* Modals */} setShowAccuracyModal(false)} metrics={accuracyMetrics} /> setShowReturnModal(false)} breakdown={returnModalDate ? buildReturnBreakdown(returnModalDate) : null} date={returnModalDate || ''} /> setShowOverallModal(false)} breakdown={chartData.overallBreakdown} cumulativeData={chartData.cumulativeReturns} /> {/* Performance Summary Modals */} setActiveSummaryModal(null)} title="Days Tracked" icon={}>

Days Tracked shows the total number of trading days where AI recommendations have been recorded and analyzed.

Current Count:
{filteredStats.totalDays} days

Each day includes analysis for {summaryMode === 'topPicks' ? '3 top picks' : 'all 50 Nifty stocks'}.

setActiveSummaryModal(null)} title="Average Return" icon={}>

Average Return measures the mean percentage price change over each stock's recommended hold period.

How it's calculated:
  1. Record stock price at recommendation time
  2. Record price after the recommended hold period
  3. Calculate: (Exit - Entry) / Entry x 100
  4. Average all returns across stocks
= 0 ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20'} rounded-lg`}>
Current Average:
= 0 ? 'text-green-600' : 'text-red-600'}`}> {filteredStats.avgDailyReturn >= 0 ? '+' : ''}{filteredStats.avgDailyReturn.toFixed(2)}%
setActiveSummaryModal(null)} title={summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} icon={}>

{summaryMode === 'topPicks' ? 'Top Pick Signals' : 'Buy Signals'} counts all {summaryMode === 'topPicks' ? 'Top Picks' : 'BUY recommendations'} across all tracked days.

Total: {filteredStats.buySignals}
setActiveSummaryModal(null)} title="Sell Signals" icon={}>

Sell Signals counts every SELL recommendation issued across all tracked days.

{summaryMode === 'topPicks' ? (
Note: Top Picks mode only shows BUY recommendations, so sell signals are 0.
) : (
Total: {filteredStats.sellSignals}
)}
); }