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-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