diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db index 9d729e07..e6ccf03f 100644 Binary files a/frontend/backend/recommendations.db and b/frontend/backend/recommendations.db differ diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx index 283e9efb..70a2e346 100644 --- a/frontend/src/pages/History.tsx +++ b/frontend/src/pages/History.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Loader2, AlertCircle } from 'lucide-react'; import { sampleRecommendations, getBacktestResult as getStaticBacktestResult, calculateAccuracyMetrics as calculateStaticAccuracyMetrics, getDateStats as getStaticDateStats, getOverallStats as getStaticOverallStats, getReturnBreakdown as getStaticReturnBreakdown } from '../data/recommendations'; -import { DecisionBadge } from '../components/StockCard'; +import { DecisionBadge, HoldDaysBadge } from '../components/StockCard'; import Sparkline from '../components/Sparkline'; import AccuracyBadge from '../components/AccuracyBadge'; import AccuracyExplainModal from '../components/AccuracyExplainModal'; @@ -1158,6 +1158,7 @@ export default function History() {
+ {predictionCorrect !== null && ( (null); const [analysisProgress, setAnalysisProgress] = useState(null); + const [analysisSteps, setAnalysisSteps] = useState<{ completed: number; total: number } | null>(null); const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol); - const latestRecommendation = sampleRecommendations[0]; - const analysis = latestRecommendation?.analysis[symbol || '']; - const history = symbol ? getStockHistory(symbol) : []; - // Get price history and prediction points for the chart - const priceHistory = useMemo(() => { - return symbol ? getExtendedPriceHistory(symbol, 60) : []; + // API-first loading for recommendation data + const [latestRecommendation, setLatestRecommendation] = useState(null); + const [analysis, setAnalysis] = useState(undefined); + const [history, setHistory] = useState>([]); + // Fetch recommendation and stock history from API + useEffect(() => { + const fetchData = async () => { + try { + // Fetch latest recommendation from API + const rec = await api.getLatestRecommendation(); + if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) { + setLatestRecommendation(rec); + setAnalysis(rec.analysis[symbol || '']); + } else { + // Fallback to static data + const mockRec = sampleRecommendations[0]; + setLatestRecommendation(mockRec); + setAnalysis(mockRec?.analysis[symbol || '']); + } + } catch { + // Fallback to static data + const mockRec = sampleRecommendations[0]; + setLatestRecommendation(mockRec); + setAnalysis(mockRec?.analysis[symbol || '']); + } + + try { + // Fetch stock history from API + const historyData = await api.getStockHistory(symbol || ''); + if (historyData && historyData.history && historyData.history.length > 0) { + setHistory(historyData.history); + } else { + // Fallback to static data + setHistory(symbol ? getStaticStockHistory(symbol) : []); + } + } catch { + // Fallback to static data + setHistory(symbol ? getStaticStockHistory(symbol) : []); + } + + }; + + fetchData(); }, [symbol]); + // State for real backtest data from API + const [backtestResults, setBacktestResults] = useState([]); + const [isLoadingBacktest, setIsLoadingBacktest] = useState(false); + + // Fetch real backtest data for all history entries + const fetchBacktestData = useCallback(async () => { + if (!symbol || history.length === 0) return; + + setIsLoadingBacktest(true); + + const results: BacktestResult[] = []; + + for (const entry of history) { + try { + const backtest = await api.getBacktestResult(entry.date, symbol); + + if (backtest.available) { + // Calculate prediction correctness based on 1-day return + // BUY is correct if return > 0, HOLD is correct if return > 0, SELL is correct if return < 0 + let predictionCorrect: boolean | null = null; + if (backtest.actual_return_1d !== undefined && backtest.actual_return_1d !== null) { + if (entry.decision === 'BUY' || entry.decision === 'HOLD') { + // BUY and HOLD are correct if stock price went up + predictionCorrect = backtest.actual_return_1d > 0; + } else if (entry.decision === 'SELL') { + // SELL is correct if stock price went down + predictionCorrect = backtest.actual_return_1d < 0; + } + } + + results.push({ + date: entry.date, + decision: entry.decision, + return1d: backtest.actual_return_1d ?? null, + return1w: backtest.actual_return_1w ?? null, + predictionCorrect, + }); + } else { + // No backtest data available for this date + results.push({ + date: entry.date, + decision: entry.decision, + return1d: null, + return1w: null, + predictionCorrect: null, + }); + } + } catch (err) { + console.error(`Failed to fetch backtest for ${entry.date}:`, err); + results.push({ + date: entry.date, + decision: entry.decision, + return1d: null, + return1w: null, + predictionCorrect: null, + }); + } + } + + setBacktestResults(results); + setIsLoadingBacktest(false); + }, [symbol, history]); + + // Fetch backtest data when symbol changes + useEffect(() => { + fetchBacktestData(); + }, [fetchBacktestData]); + + // Calculate prediction stats from real backtest data + const predictionStats = useMemo((): PredictionStats | null => { + if (backtestResults.length === 0) return null; + + const resultsWithData = backtestResults.filter(r => r.return1d !== null); + if (resultsWithData.length === 0) return null; + + let correct = 0; + let totalReturn = 0; + let buyTotal = 0, buyCorrect = 0; + let sellTotal = 0, sellCorrect = 0; + let holdTotal = 0, holdCorrect = 0; + + for (const result of resultsWithData) { + if (result.return1d !== null) { + totalReturn += result.return1d; + } + if (result.predictionCorrect !== null) { + if (result.predictionCorrect) correct++; + + if (result.decision === 'BUY') { + buyTotal++; + if (result.predictionCorrect) buyCorrect++; + } else if (result.decision === 'SELL') { + sellTotal++; + if (result.predictionCorrect) sellCorrect++; + } else { + holdTotal++; + if (result.predictionCorrect) holdCorrect++; + } + } + } + + const totalWithResult = resultsWithData.filter(r => r.predictionCorrect !== null).length; + + return { + totalPredictions: resultsWithData.length, + correctPredictions: correct, + accuracy: totalWithResult > 0 ? Math.round((correct / totalWithResult) * 100) : 0, + avgReturn: resultsWithData.length > 0 ? Math.round((totalReturn / resultsWithData.length) * 10) / 10 : 0, + buyAccuracy: buyTotal > 0 ? Math.round((buyCorrect / buyTotal) * 100) : 0, + sellAccuracy: sellTotal > 0 ? Math.round((sellCorrect / sellTotal) * 100) : 0, + holdAccuracy: holdTotal > 0 ? Math.round((holdCorrect / holdTotal) * 100) : 0, + }; + }, [backtestResults]); + + // Real price history from API + const [realPriceHistory, setRealPriceHistory] = useState>([]); + const [isLoadingPrices, setIsLoadingPrices] = useState(false); + + // Fetch real price history from yfinance via backend + useEffect(() => { + if (!symbol) return; + + const fetchPrices = async () => { + setIsLoadingPrices(true); + try { + const data = await api.getStockPriceHistory(symbol, 90); + if (data.prices && data.prices.length > 0) { + setRealPriceHistory(data.prices); + } + } catch (err) { + console.error('Failed to fetch price history:', err); + } finally { + setIsLoadingPrices(false); + } + }; + + fetchPrices(); + }, [symbol]); + + // Build prediction points from real history data (API-sourced dates + decisions) const predictionPoints = useMemo(() => { - return symbol && priceHistory.length > 0 - ? getPredictionPointsWithPrices(symbol, priceHistory) - : []; - }, [symbol, priceHistory]); + if (history.length === 0 || realPriceHistory.length === 0) return []; + + const priceDateMap = new Map(realPriceHistory.map(p => [p.date, p.price])); + const MAX_DATE_TOLERANCE_MS = 4 * 24 * 60 * 60 * 1000; // 4 days max (handles weekends/holidays) + + return history + .map(entry => { + // Find exact date match first + const price = priceDateMap.get(entry.date); + if (price !== undefined) { + return { date: entry.date, decision: entry.decision as 'BUY' | 'SELL' | 'HOLD', price }; + } + + // Find closest date within tolerance (skip if prediction date is outside price range) + const entryTime = new Date(entry.date).getTime(); + let closestPoint: { date: string; price: number } | null = null; + let closestDiff = Infinity; + for (const p of realPriceHistory) { + const diff = Math.abs(new Date(p.date).getTime() - entryTime); + if (diff < closestDiff) { + closestDiff = diff; + closestPoint = p; + } + } + + if (closestPoint && closestDiff <= MAX_DATE_TOLERANCE_MS) { + return { date: closestPoint.date, decision: entry.decision as 'BUY' | 'SELL' | 'HOLD', price: closestPoint.price }; + } + + return null; // Prediction date too far from any price data — skip + }) + .filter((p): p is NonNullable => p !== null); + }, [history, realPriceHistory]); // Function to fetch pipeline data const fetchPipelineData = async (forceRefresh = false) => { @@ -117,6 +345,31 @@ export default function StockDetail() { setAnalysisStatus('starting'); setAnalysisProgress('Starting analysis...'); + // Auto-switch to pipeline tab so user sees live progress + setActiveTab('pipeline'); + + // Step ordering for pipeline visualization + const STEP_ORDER = [ + 'market_analyst', 'social_media_analyst', 'news_analyst', 'fundamentals_analyst', + 'bull_researcher', 'bear_researcher', 'research_manager', 'trader', + 'aggressive_analyst', 'conservative_analyst', 'neutral_analyst', 'risk_manager', + ]; + + // Initialize pipeline data with all-pending steps + setPipelineData({ + date: latestRecommendation.date, + symbol: symbol, + agent_reports: {}, + debates: {}, + pipeline_steps: STEP_ORDER.map((name, idx) => ({ + step_number: idx + 1, + step_name: name, + status: 'pending' as PipelineStepStatus, + })), + data_sources: [], + status: 'in_progress', + }); + try { // Trigger analysis with settings from context await api.runAnalysis(symbol, latestRecommendation.date, { @@ -128,28 +381,103 @@ export default function StockDetail() { }); setAnalysisStatus('running'); + // Track poll count for periodic full data refresh + let pollCount = 0; + // Poll for status const pollInterval = setInterval(async () => { try { const status = await api.getAnalysisStatus(symbol); setAnalysisProgress(status.progress || 'Processing...'); + // Update step counts for progress indicator + if (status.steps_completed !== undefined && status.steps_total !== undefined) { + setAnalysisSteps({ completed: status.steps_completed, total: status.steps_total }); + } + + // Build live pipeline data from status response + if (status.pipeline_steps) { + const livePipelineSteps: PipelineStep[] = STEP_ORDER.map((stepName, idx) => { + const stepData = status.pipeline_steps?.[stepName]; + return { + step_number: idx + 1, + step_name: stepName, + status: (stepData?.status as PipelineStepStatus) || 'pending', + duration_ms: stepData?.duration_ms, + }; + }); + + // Update pipeline data with live step statuses + setPipelineData(prev => ({ + date: latestRecommendation?.date || prev?.date || '', + symbol: symbol || prev?.symbol || '', + agent_reports: prev?.agent_reports || {}, + debates: prev?.debates || {}, + pipeline_steps: livePipelineSteps, + data_sources: prev?.data_sources || [], + status: 'in_progress', + })); + } + + // Every 5th poll (~10s), fetch full pipeline data for agent reports/debates + pollCount++; + if (pollCount % 5 === 0) { + try { + const fullData = await api.getPipelineData(latestRecommendation.date, symbol, true); + if (fullData && (fullData.agent_reports || fullData.debates)) { + setPipelineData(prev => ({ + ...prev!, + agent_reports: fullData.agent_reports || prev?.agent_reports || {}, + debates: fullData.debates || prev?.debates || {}, + data_sources: fullData.data_sources || prev?.data_sources || [], + // Keep live step statuses if available, otherwise use fetched + pipeline_steps: prev?.pipeline_steps?.some(s => s.status === 'running') + ? prev!.pipeline_steps + : fullData.pipeline_steps || prev?.pipeline_steps || [], + })); + } + } catch { /* ignore full data refresh errors during analysis */ } + } + if (status.status === 'completed') { clearInterval(pollInterval); setIsAnalysisRunning(false); setAnalysisStatus('completed'); - setAnalysisProgress(`✓ Analysis complete: ${status.decision || 'Done'}`); - // Refresh data to show results + setAnalysisProgress(`Analysis complete: ${status.decision || 'Done'}`); + // Refresh recommendation and pipeline data to show final results + try { + const rec = await api.getLatestRecommendation(); + if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) { + setLatestRecommendation(rec); + setAnalysis(rec.analysis[symbol || '']); + } + const historyData = await api.getStockHistory(symbol || ''); + if (historyData?.history?.length > 0) { + setHistory(historyData.history); + } + } catch { /* ignore refresh errors */ } await fetchPipelineData(true); + fetchBacktestData(); setTimeout(() => { setAnalysisProgress(null); setAnalysisStatus(null); + setAnalysisSteps(null); }, 5000); } else if (status.status === 'error') { clearInterval(pollInterval); setIsAnalysisRunning(false); setAnalysisStatus('error'); - setAnalysisProgress(`✗ Error: ${status.error}`); + setAnalysisProgress(`Error: ${status.error}`); + } else if (status.status === 'cancelled') { + clearInterval(pollInterval); + setIsAnalysisRunning(false); + setAnalysisStatus('cancelled'); + setAnalysisProgress('Analysis cancelled'); + setTimeout(() => { + setAnalysisProgress(null); + setAnalysisStatus(null); + setAnalysisSteps(null); + }, 3000); } } catch (err) { console.error('Failed to poll analysis status:', err); @@ -173,6 +501,25 @@ export default function StockDetail() { } }; + // Cancel Analysis handler + const handleCancelAnalysis = async () => { + if (!symbol) return; + + try { + await api.cancelAnalysis(symbol); + setIsAnalysisRunning(false); + setAnalysisStatus('cancelled'); + setAnalysisProgress('Analysis cancelled'); + setTimeout(() => { + setAnalysisProgress(null); + setAnalysisStatus(null); + setAnalysisSteps(null); + }, 3000); + } catch (error) { + console.error('Failed to cancel analysis:', error); + } + }; + if (!stock) { return (
@@ -257,23 +604,87 @@ export default function StockDetail() { {/* Analysis Details - Inline */} {analysis && ( -
-
- Decision: +
+
+ Decision:
-
- Confidence: +
+ Confidence:
-
- Risk: +
+ Risk:
+ {analysis.hold_days && analysis.decision !== 'SELL' && ( +
+ Hold: + +
+ )}
)} + {/* Action Buttons Row - Always visible */} +
+ {/* Run Analysis Button */} + + + {/* Cancel Analysis Button - only shown when analysis is running */} + {isAnalysisRunning && ( + + )} + + {/* Refresh Button */} + + + {lastRefresh && ( + + Updated: {lastRefresh} + + )} +
+ {/* Tab Navigation */}
{TABS.map(tab => { @@ -284,7 +695,7 @@ export default function StockDetail() { key={tab.id} onClick={() => setActiveTab(tab.id)} className={` - flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap + flex items-center gap-2 px-3 sm:px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${isActive ? 'bg-nifty-600 text-white shadow-md' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700' @@ -292,71 +703,51 @@ export default function StockDetail() { `} > - {tab.label} + {tab.label} ); })} - - {/* Action Buttons - Show on non-overview tabs */} - {activeTab !== 'overview' && ( -
- {lastRefresh && ( - - Updated: {lastRefresh} - - )} - - {/* Run Analysis Button */} - - - {/* Refresh Button */} - -
- )}
{/* Analysis Progress Banner */} {analysisProgress && ( -
- {isAnalysisRunning && } - {analysisProgress} +
+ {isAnalysisRunning && } + {analysisProgress} + {analysisSteps && ( + + {analysisSteps.completed}/{analysisSteps.total} + + )} +
+ {/* Step progress bar */} + {analysisSteps && isAnalysisRunning && ( +
+
+
+
+
+ )}
)} @@ -377,83 +768,240 @@ export default function StockDetail() { {activeTab === 'overview' && ( <> {/* Price Chart with Predictions */} - {priceHistory.length > 0 && ( -
-
-
- -

Price History & AI Predictions

-
+
+
+
+ +

Price History & AI Predictions

+ + {realPriceHistory.length > 0 ? `${realPriceHistory.length} trading days` : ''} +
-
+
+
+ {isLoadingPrices ? ( +
+ + Loading price data... +
+ ) : realPriceHistory.length > 0 ? ( -
-
- )} + ) : ( +
+ No price data available +
+ )} +
+
{/* AI Analysis Panel */} - {analysis && getRawAnalysis(symbol || '') && ( + {analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && ( )} - {/* Compact Stats Grid */} -
-
+ {/* Prediction Accuracy Stats */} + {predictionStats && ( +
+
+
+ +

Prediction Accuracy

+
+
+
+ {/* Main accuracy meter */} +
+
+ + + = 70 ? 'text-green-500' : predictionStats.accuracy >= 50 ? 'text-amber-500' : 'text-red-500'} + strokeLinecap="round" + /> + +
+ = 70 ? 'text-green-600 dark:text-green-400' : predictionStats.accuracy >= 50 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400'}`}> + {predictionStats.accuracy}% + +
+
+
+
+ {predictionStats.correctPredictions} of {predictionStats.totalPredictions} predictions correct +
+
+ Avg. 1-day return: = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}> + {predictionStats.avgReturn >= 0 ? '+' : ''}{predictionStats.avgReturn}% + +
+
+
+ + {/* Accuracy by decision type */} +
+
+
BUY
+
{predictionStats.buyAccuracy}%
+
+
+
HOLD
+
{predictionStats.holdAccuracy}%
+
+
+
SELL
+
{predictionStats.sellAccuracy}%
+
+
+
+
+ )} + + {/* Quick Stats Grid */} +
+
{history.length}
-
Analyses
+
Total
-
+
{history.filter((h: { decision: string }) => h.decision === 'BUY').length}
-
Buy
+
Buy
-
+
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
-
Hold
+
Hold
-
+
{history.filter((h: { decision: string }) => h.decision === 'SELL').length}
-
Sell
+
Sell
- {/* Analysis History */} -
-
-

Recommendation History

+ {/* Recommendation History with Real Outcomes */} +
+
+
+
+ +

Recommendation History

+ {isLoadingBacktest && ( + + )} +
+ + Real 1-Day Returns + +
- {history.length > 0 ? ( -
- {history.map((entry, idx) => ( -
-
- {new Date(entry.date).toLocaleDateString('en-IN', { - weekday: 'short', - month: 'short', - day: 'numeric', - })} + {isLoadingBacktest ? ( +
+ +

Fetching real market data...

+
+ ) : backtestResults.length > 0 ? ( +
+ {backtestResults.map((entry, idx) => ( +
+ {/* Date */} +
+
+ {new Date(entry.date).toLocaleDateString('en-IN', { + day: 'numeric', + month: 'short', + })} +
+
+ {new Date(entry.date).toLocaleDateString('en-IN', { weekday: 'short' })} +
- + + {/* Decision Badge + Hold Days */} +
+ + {entry.holdDays && entry.decision !== 'SELL' && ( + {entry.holdDays}d + )} +
+ + {/* Outcome - 1 Day Return */} + {entry.return1d !== null ? ( + <> +
+
= 0 + ? 'text-green-600 dark:text-green-400' + : 'text-red-600 dark:text-red-400' + }`}> + {entry.return1d >= 0 ? '+' : ''}{entry.return1d.toFixed(1)}% +
+
next day
+
+ + {/* Prediction Result Icon */} +
+ {entry.predictionCorrect !== null ? ( + entry.predictionCorrect ? ( +
+ + Correct +
+ ) : ( +
+ + Wrong +
+ ) + ) : ( + N/A + )} +
+ + ) : ( +
+ Awaiting market data... +
+ )}
))}
+ ) : history.length > 0 ? ( +
+ +

Unable to fetch real market data

+

Check if backend service is running

+
) : ( -
- -

No history yet

+
+ +

No recommendation history yet

)}
@@ -461,37 +1009,11 @@ export default function StockDetail() { )} {activeTab === 'pipeline' && ( -
- {/* Pipeline Overview */} -
-
- -

Analysis Pipeline

-
- console.log('Step clicked:', step)} - /> -
- - {/* Agent Reports Grid */} -
-
- -

Agent Reports

-
-
- {(['market', 'news', 'social_media', 'fundamentals'] as AgentType[]).map(agentType => ( - - ))} -
-
-
+ )} {activeTab === 'debates' && ( diff --git a/hold-days-dashboard.png b/hold-days-dashboard.png new file mode 100644 index 00000000..1a999b58 Binary files /dev/null and b/hold-days-dashboard.png differ diff --git a/hold-days-history.png b/hold-days-history.png new file mode 100644 index 00000000..b65e705b Binary files /dev/null and b/hold-days-history.png differ diff --git a/hold-days-stock-detail.png b/hold-days-stock-detail.png new file mode 100644 index 00000000..14099294 Binary files /dev/null and b/hold-days-stock-detail.png differ