This commit is contained in:
hemangjoshi37a 2026-02-08 12:59:10 +11:00
parent bdc27679fa
commit a556099d97
6 changed files with 681 additions and 158 deletions

Binary file not shown.

View File

@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom'; 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 { 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 { 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 Sparkline from '../components/Sparkline';
import AccuracyBadge from '../components/AccuracyBadge'; import AccuracyBadge from '../components/AccuracyBadge';
import AccuracyExplainModal from '../components/AccuracyExplainModal'; import AccuracyExplainModal from '../components/AccuracyExplainModal';
@ -1158,6 +1158,7 @@ export default function History() {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<DecisionBadge decision={stock.decision} size="small" /> <DecisionBadge decision={stock.decision} size="small" />
<HoldDaysBadge holdDays={stock.hold_days} decision={stock.decision} />
{predictionCorrect !== null && ( {predictionCorrect !== null && (
<AccuracyBadge <AccuracyBadge
correct={predictionCorrect} correct={predictionCorrect}

View File

@ -1,25 +1,46 @@
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect, useCallback } from 'react';
import { import {
ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle,
Calendar, Activity, LineChart, Database, MessageSquare, FileText, Layers, Calendar, Activity, LineChart, Database, MessageSquare, Layers,
RefreshCw, Play, Loader2 RefreshCw, Play, Loader2, CheckCircle, XCircle, Target, BarChart3, Square
} from 'lucide-react'; } from 'lucide-react';
import { NIFTY_50_STOCKS } from '../types'; import { NIFTY_50_STOCKS } from '../types';
import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations'; import type { DailyRecommendation, StockAnalysis } from '../types';
import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard'; import { sampleRecommendations, getStockHistory as getStaticStockHistory, getRawAnalysis } from '../data/recommendations';
import { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge } from '../components/StockCard';
import AIAnalysisPanel from '../components/AIAnalysisPanel'; import AIAnalysisPanel from '../components/AIAnalysisPanel';
import StockPriceChart from '../components/StockPriceChart'; import StockPriceChart from '../components/StockPriceChart';
import { import {
PipelineOverview, PipelineFlowchart,
AgentReportCard,
DebateViewer, DebateViewer,
RiskDebateViewer, RiskDebateViewer,
DataSourcesPanel DataSourcesPanel
} from '../components/pipeline'; } from '../components/pipeline';
import { api } from '../services/api'; import { api } from '../services/api';
import { useSettings } from '../contexts/SettingsContext'; import { useSettings } from '../contexts/SettingsContext';
import type { FullPipelineData, AgentType } from '../types/pipeline'; import type { FullPipelineData, PipelineStep, PipelineStepStatus } from '../types/pipeline';
// Type for real backtest data from API
interface BacktestResult {
date: string;
decision: string;
return1d: number | null;
return1w: number | null;
predictionCorrect: boolean | null;
isLoading?: boolean;
}
// Type for prediction stats calculated from real data
interface PredictionStats {
totalPredictions: number;
correctPredictions: number;
accuracy: number;
avgReturn: number;
buyAccuracy: number;
sellAccuracy: number;
holdAccuracy: number;
}
type TabType = 'overview' | 'pipeline' | 'debates' | 'data'; type TabType = 'overview' | 'pipeline' | 'debates' | 'data';
@ -37,22 +58,229 @@ export default function StockDetail() {
const [isAnalysisRunning, setIsAnalysisRunning] = useState(false); const [isAnalysisRunning, setIsAnalysisRunning] = useState(false);
const [analysisStatus, setAnalysisStatus] = useState<string | null>(null); const [analysisStatus, setAnalysisStatus] = useState<string | null>(null);
const [analysisProgress, setAnalysisProgress] = useState<string | null>(null); const [analysisProgress, setAnalysisProgress] = useState<string | null>(null);
const [analysisSteps, setAnalysisSteps] = useState<{ completed: number; total: number } | null>(null);
const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol); 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 // API-first loading for recommendation data
const priceHistory = useMemo(() => { const [latestRecommendation, setLatestRecommendation] = useState<DailyRecommendation | null>(null);
return symbol ? getExtendedPriceHistory(symbol, 60) : []; const [analysis, setAnalysis] = useState<StockAnalysis | undefined>(undefined);
const [history, setHistory] = useState<Array<{ date: string; decision: string; confidence?: string; risk?: string }>>([]);
// 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]); }, [symbol]);
// State for real backtest data from API
const [backtestResults, setBacktestResults] = useState<BacktestResult[]>([]);
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<Array<{ date: string; price: number }>>([]);
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(() => { const predictionPoints = useMemo(() => {
return symbol && priceHistory.length > 0 if (history.length === 0 || realPriceHistory.length === 0) return [];
? getPredictionPointsWithPrices(symbol, priceHistory)
: []; const priceDateMap = new Map(realPriceHistory.map(p => [p.date, p.price]));
}, [symbol, priceHistory]); 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<typeof p> => p !== null);
}, [history, realPriceHistory]);
// Function to fetch pipeline data // Function to fetch pipeline data
const fetchPipelineData = async (forceRefresh = false) => { const fetchPipelineData = async (forceRefresh = false) => {
@ -117,6 +345,31 @@ export default function StockDetail() {
setAnalysisStatus('starting'); setAnalysisStatus('starting');
setAnalysisProgress('Starting analysis...'); 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 { try {
// Trigger analysis with settings from context // Trigger analysis with settings from context
await api.runAnalysis(symbol, latestRecommendation.date, { await api.runAnalysis(symbol, latestRecommendation.date, {
@ -128,28 +381,103 @@ export default function StockDetail() {
}); });
setAnalysisStatus('running'); setAnalysisStatus('running');
// Track poll count for periodic full data refresh
let pollCount = 0;
// Poll for status // Poll for status
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
try { try {
const status = await api.getAnalysisStatus(symbol); const status = await api.getAnalysisStatus(symbol);
setAnalysisProgress(status.progress || 'Processing...'); 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') { if (status.status === 'completed') {
clearInterval(pollInterval); clearInterval(pollInterval);
setIsAnalysisRunning(false); setIsAnalysisRunning(false);
setAnalysisStatus('completed'); setAnalysisStatus('completed');
setAnalysisProgress(`✓ Analysis complete: ${status.decision || 'Done'}`); setAnalysisProgress(`Analysis complete: ${status.decision || 'Done'}`);
// Refresh data to show results // 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); await fetchPipelineData(true);
fetchBacktestData();
setTimeout(() => { setTimeout(() => {
setAnalysisProgress(null); setAnalysisProgress(null);
setAnalysisStatus(null); setAnalysisStatus(null);
setAnalysisSteps(null);
}, 5000); }, 5000);
} else if (status.status === 'error') { } else if (status.status === 'error') {
clearInterval(pollInterval); clearInterval(pollInterval);
setIsAnalysisRunning(false); setIsAnalysisRunning(false);
setAnalysisStatus('error'); 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) { } catch (err) {
console.error('Failed to poll analysis status:', 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) { if (!stock) {
return ( return (
<div className="min-h-[60vh] flex items-center justify-center"> <div className="min-h-[60vh] flex items-center justify-center">
@ -257,23 +604,87 @@ export default function StockDetail() {
{/* Analysis Details - Inline */} {/* Analysis Details - Inline */}
{analysis && ( {analysis && (
<div className="p-3 flex items-center gap-4 bg-gray-50/50 dark:bg-slate-700/50"> <div className="p-2 sm:p-3 flex flex-wrap items-center gap-2 sm:gap-4 bg-gray-50/50 dark:bg-slate-700/50">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5 sm:gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Decision:</span> <span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Decision:</span>
<DecisionBadge decision={analysis.decision} size="small" /> <DecisionBadge decision={analysis.decision} size="small" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5 sm:gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Confidence:</span> <span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Confidence:</span>
<ConfidenceBadge confidence={analysis.confidence} /> <ConfidenceBadge confidence={analysis.confidence} />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5 sm:gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Risk:</span> <span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Risk:</span>
<RiskBadge risk={analysis.risk} /> <RiskBadge risk={analysis.risk} />
</div> </div>
{analysis.hold_days && analysis.decision !== 'SELL' && (
<div className="flex items-center gap-1.5 sm:gap-2">
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Hold:</span>
<HoldDaysBadge holdDays={analysis.hold_days} decision={analysis.decision} />
</div>
)}
</div> </div>
)} )}
</section> </section>
{/* Action Buttons Row - Always visible */}
<div className="flex flex-wrap items-center gap-2">
{/* Run Analysis Button */}
<button
onClick={handleRunAnalysis}
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
${isAnalysisRunning
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
: 'bg-nifty-600 text-white hover:bg-nifty-700 shadow-sm'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Run AI analysis for this stock"
>
{isAnalysisRunning ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Play className="w-4 h-4" />
)}
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
</button>
{/* Cancel Analysis Button - only shown when analysis is running */}
{isAnalysisRunning && (
<button
onClick={handleCancelAnalysis}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50"
title="Cancel running analysis"
>
<Square className="w-3.5 h-3.5 fill-current" />
Cancel
</button>
)}
{/* Refresh Button */}
<button
onClick={handleRefresh}
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-600
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Refresh pipeline data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</button>
{lastRefresh && (
<span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">
Updated: {lastRefresh}
</span>
)}
</div>
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="card p-1 flex gap-1 overflow-x-auto"> <div className="card p-1 flex gap-1 overflow-x-auto">
{TABS.map(tab => { {TABS.map(tab => {
@ -284,7 +695,7 @@ export default function StockDetail() {
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={` 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 ${isActive
? 'bg-nifty-600 text-white shadow-md' ? 'bg-nifty-600 text-white shadow-md'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
@ -292,71 +703,51 @@ export default function StockDetail() {
`} `}
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
{tab.label} <span className="hidden sm:inline">{tab.label}</span>
</button> </button>
); );
})} })}
{/* Action Buttons - Show on non-overview tabs */}
{activeTab !== 'overview' && (
<div className="ml-auto flex items-center gap-2">
{lastRefresh && (
<span className="text-xs text-gray-400 dark:text-gray-500">
Updated: {lastRefresh}
</span>
)}
{/* Run Analysis Button */}
<button
onClick={handleRunAnalysis}
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
${isAnalysisRunning
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
: 'bg-nifty-600 text-white hover:bg-nifty-700'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Run AI analysis for this stock"
>
{isAnalysisRunning ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Play className="w-4 h-4" />
)}
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
</button>
{/* Refresh Button */}
<button
onClick={handleRefresh}
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Refresh pipeline data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</button>
</div>
)}
</div> </div>
{/* Analysis Progress Banner */} {/* Analysis Progress Banner */}
{analysisProgress && ( {analysisProgress && (
<div className={`p-3 rounded-lg text-sm font-medium flex items-center gap-2 ${ <div className={`rounded-lg overflow-hidden ${
analysisStatus === 'completed' analysisStatus === 'completed'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' ? 'bg-green-100 dark:bg-green-900/30'
: analysisStatus === 'error' : analysisStatus === 'error'
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' ? 'bg-red-100 dark:bg-red-900/30'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : analysisStatus === 'cancelled'
? 'bg-amber-100 dark:bg-amber-900/30'
: 'bg-blue-100 dark:bg-blue-900/30'
}`}> }`}>
{isAnalysisRunning && <Loader2 className="w-4 h-4 animate-spin" />} <div className={`p-3 text-sm font-medium flex items-center gap-2 ${
{analysisProgress} analysisStatus === 'completed'
? 'text-green-800 dark:text-green-300'
: analysisStatus === 'error'
? 'text-red-800 dark:text-red-300'
: analysisStatus === 'cancelled'
? 'text-amber-800 dark:text-amber-300'
: 'text-blue-800 dark:text-blue-300'
}`}>
{isAnalysisRunning && <Loader2 className="w-4 h-4 animate-spin flex-shrink-0" />}
<span className="flex-1">{analysisProgress}</span>
{analysisSteps && (
<span className="text-xs opacity-75 flex-shrink-0">
{analysisSteps.completed}/{analysisSteps.total}
</span>
)}
</div>
{/* Step progress bar */}
{analysisSteps && isAnalysisRunning && (
<div className="px-3 pb-2">
<div className="h-1.5 bg-blue-200/50 dark:bg-blue-800/30 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 dark:bg-blue-400 rounded-full transition-all duration-700 ease-out"
style={{ width: `${Math.round((analysisSteps.completed / analysisSteps.total) * 100)}%` }}
/>
</div>
</div>
)}
</div> </div>
)} )}
@ -377,83 +768,240 @@ export default function StockDetail() {
{activeTab === 'overview' && ( {activeTab === 'overview' && (
<> <>
{/* Price Chart with Predictions */} {/* Price Chart with Predictions */}
{priceHistory.length > 0 && ( <section className="card overflow-hidden">
<section className="card overflow-hidden"> <div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-800/50">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-800/50"> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <LineChart className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<LineChart className="w-4 h-4 text-nifty-600 dark:text-nifty-400" /> <h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Price History & AI Predictions</h2>
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Price History & AI Predictions</h2> <span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">
</div> {realPriceHistory.length > 0 ? `${realPriceHistory.length} trading days` : ''}
</span>
</div> </div>
<div className="p-4 bg-white dark:bg-slate-800"> </div>
<div className="p-4 bg-white dark:bg-slate-800">
{isLoadingPrices ? (
<div className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
<Loader2 className="w-5 h-5 animate-spin mr-2" />
Loading price data...
</div>
) : realPriceHistory.length > 0 ? (
<StockPriceChart <StockPriceChart
priceHistory={priceHistory} priceHistory={realPriceHistory}
predictions={predictionPoints} predictions={predictionPoints}
symbol={symbol || ''} symbol={symbol || ''}
/> />
</div> ) : (
</section> <div className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
)} No price data available
</div>
)}
</div>
</section>
{/* AI Analysis Panel */} {/* AI Analysis Panel */}
{analysis && getRawAnalysis(symbol || '') && ( {analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && (
<AIAnalysisPanel <AIAnalysisPanel
analysis={getRawAnalysis(symbol || '') || ''} analysis={analysis.raw_analysis || getRawAnalysis(symbol || '') || ''}
decision={analysis.decision} decision={analysis.decision}
/> />
)} )}
{/* Compact Stats Grid */} {/* Prediction Accuracy Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2"> {predictionStats && (
<div className="card p-2.5 text-center"> <section className="card overflow-hidden">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gradient-to-r from-nifty-50 to-blue-50 dark:from-slate-800 dark:to-slate-700">
<div className="flex items-center gap-2">
<Target className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Prediction Accuracy</h2>
</div>
</div>
<div className="p-4">
{/* Main accuracy meter */}
<div className="flex items-center gap-4 mb-4">
<div className="relative w-20 h-20 flex-shrink-0">
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="36"
stroke="currentColor"
strokeWidth="8"
fill="transparent"
className="text-gray-200 dark:text-slate-700"
/>
<circle
cx="40"
cy="40"
r="36"
stroke="currentColor"
strokeWidth="8"
fill="transparent"
strokeDasharray={`${(predictionStats.accuracy / 100) * 226} 226`}
className={predictionStats.accuracy >= 70 ? 'text-green-500' : predictionStats.accuracy >= 50 ? 'text-amber-500' : 'text-red-500'}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-lg font-bold ${predictionStats.accuracy >= 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}%
</span>
</div>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{predictionStats.correctPredictions} of {predictionStats.totalPredictions} predictions correct
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Avg. 1-day return: <span className={predictionStats.avgReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
{predictionStats.avgReturn >= 0 ? '+' : ''}{predictionStats.avgReturn}%
</span>
</div>
</div>
</div>
{/* Accuracy by decision type */}
<div className="grid grid-cols-3 gap-2">
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-2 text-center">
<div className="text-xs text-green-600 dark:text-green-400 font-medium mb-0.5">BUY</div>
<div className="text-sm font-bold text-green-700 dark:text-green-300">{predictionStats.buyAccuracy}%</div>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-2 text-center">
<div className="text-xs text-amber-600 dark:text-amber-400 font-medium mb-0.5">HOLD</div>
<div className="text-sm font-bold text-amber-700 dark:text-amber-300">{predictionStats.holdAccuracy}%</div>
</div>
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-2 text-center">
<div className="text-xs text-red-600 dark:text-red-400 font-medium mb-0.5">SELL</div>
<div className="text-sm font-bold text-red-700 dark:text-red-300">{predictionStats.sellAccuracy}%</div>
</div>
</div>
</div>
</section>
)}
{/* Quick Stats Grid */}
<div className="grid grid-cols-4 gap-2">
<div className="card p-2.5 text-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-slate-800 dark:to-slate-700">
<div className="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div> <div className="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Analyses</div> <div className="text-[10px] text-gray-500 dark:text-gray-400">Total</div>
</div> </div>
<div className="card p-2.5 text-center"> <div className="card p-2.5 text-center bg-gradient-to-br from-green-50 to-green-100/50 dark:from-green-900/20 dark:to-green-900/10">
<div className="text-lg font-bold text-green-600 dark:text-green-400"> <div className="text-lg font-bold text-green-600 dark:text-green-400">
{history.filter((h: { decision: string }) => h.decision === 'BUY').length} {history.filter((h: { decision: string }) => h.decision === 'BUY').length}
</div> </div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Buy</div> <div className="text-[10px] text-green-600 dark:text-green-400">Buy</div>
</div> </div>
<div className="card p-2.5 text-center"> <div className="card p-2.5 text-center bg-gradient-to-br from-amber-50 to-amber-100/50 dark:from-amber-900/20 dark:to-amber-900/10">
<div className="text-lg font-bold text-amber-600 dark:text-amber-400"> <div className="text-lg font-bold text-amber-600 dark:text-amber-400">
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length} {history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
</div> </div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Hold</div> <div className="text-[10px] text-amber-600 dark:text-amber-400">Hold</div>
</div> </div>
<div className="card p-2.5 text-center"> <div className="card p-2.5 text-center bg-gradient-to-br from-red-50 to-red-100/50 dark:from-red-900/20 dark:to-red-900/10">
<div className="text-lg font-bold text-red-600 dark:text-red-400"> <div className="text-lg font-bold text-red-600 dark:text-red-400">
{history.filter((h: { decision: string }) => h.decision === 'SELL').length} {history.filter((h: { decision: string }) => h.decision === 'SELL').length}
</div> </div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Sell</div> <div className="text-[10px] text-red-600 dark:text-red-400">Sell</div>
</div> </div>
</div> </div>
{/* Analysis History */} {/* Recommendation History with Real Outcomes */}
<section className="card"> <section className="card overflow-hidden">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50"> <div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gradient-to-r from-gray-50 to-slate-50 dark:from-slate-800 dark:to-slate-700">
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2> <div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2>
{isLoadingBacktest && (
<Loader2 className="w-3 h-3 animate-spin text-nifty-500" />
)}
</div>
<span className="text-[10px] text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-slate-600 px-2 py-0.5 rounded-full">
Real 1-Day Returns
</span>
</div>
</div> </div>
{history.length > 0 ? ( {isLoadingBacktest ? (
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[250px] overflow-y-auto"> <div className="p-8 text-center">
{history.map((entry, idx) => ( <Loader2 className="w-8 h-8 text-nifty-500 animate-spin mx-auto mb-3" />
<div key={idx} className="px-3 py-2 flex items-center justify-between"> <p className="text-sm text-gray-500 dark:text-gray-400">Fetching real market data...</p>
<div className="text-xs text-gray-500 dark:text-gray-400"> </div>
{new Date(entry.date).toLocaleDateString('en-IN', { ) : backtestResults.length > 0 ? (
weekday: 'short', <div className="divide-y divide-gray-100 dark:divide-slate-700 max-h-[320px] overflow-y-auto">
month: 'short', {backtestResults.map((entry, idx) => (
day: 'numeric', <div key={idx} className="px-3 py-2.5 flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
})} {/* Date */}
<div className="w-16 flex-shrink-0">
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
{new Date(entry.date).toLocaleDateString('en-IN', {
day: 'numeric',
month: 'short',
})}
</div>
<div className="text-[10px] text-gray-400 dark:text-gray-500">
{new Date(entry.date).toLocaleDateString('en-IN', { weekday: 'short' })}
</div>
</div> </div>
<DecisionBadge decision={entry.decision} size="small" />
{/* Decision Badge + Hold Days */}
<div className="flex items-center gap-1.5 flex-shrink-0">
<DecisionBadge decision={entry.decision as 'BUY' | 'SELL' | 'HOLD'} size="small" />
{entry.holdDays && entry.decision !== 'SELL' && (
<span className="text-[10px] text-blue-600 dark:text-blue-400 font-medium">{entry.holdDays}d</span>
)}
</div>
{/* Outcome - 1 Day Return */}
{entry.return1d !== null ? (
<>
<div className="flex-1 flex items-center gap-2">
<div className={`text-sm font-semibold ${
entry.return1d >= 0
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{entry.return1d >= 0 ? '+' : ''}{entry.return1d.toFixed(1)}%
</div>
<div className="text-[10px] text-gray-400 dark:text-gray-500">next day</div>
</div>
{/* Prediction Result Icon */}
<div className="flex-shrink-0">
{entry.predictionCorrect !== null ? (
entry.predictionCorrect ? (
<div className="flex items-center gap-1 px-2 py-0.5 bg-green-100 dark:bg-green-900/30 rounded-full">
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
<span className="text-[10px] font-medium text-green-700 dark:text-green-400">Correct</span>
</div>
) : (
<div className="flex items-center gap-1 px-2 py-0.5 bg-red-100 dark:bg-red-900/30 rounded-full">
<XCircle className="w-3 h-3 text-red-600 dark:text-red-400" />
<span className="text-[10px] font-medium text-red-700 dark:text-red-400">Wrong</span>
</div>
)
) : (
<span className="text-[10px] text-gray-400">N/A</span>
)}
</div>
</>
) : (
<div className="flex-1 text-xs text-gray-400 dark:text-gray-500 italic">
Awaiting market data...
</div>
)}
</div> </div>
))} ))}
</div> </div>
) : history.length > 0 ? (
<div className="p-8 text-center">
<AlertTriangle className="w-10 h-10 text-amber-400 mx-auto mb-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">Unable to fetch real market data</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">Check if backend service is running</p>
</div>
) : ( ) : (
<div className="p-6 text-center"> <div className="p-8 text-center">
<Calendar className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" /> <Calendar className="w-10 h-10 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
<p className="text-sm text-gray-500 dark:text-gray-400">No history yet</p> <p className="text-sm text-gray-500 dark:text-gray-400">No recommendation history yet</p>
</div> </div>
)} )}
</section> </section>
@ -461,37 +1009,11 @@ export default function StockDetail() {
)} )}
{activeTab === 'pipeline' && ( {activeTab === 'pipeline' && (
<div className="space-y-4"> <PipelineFlowchart
{/* Pipeline Overview */} pipelineData={pipelineData}
<section className="card p-4"> isAnalyzing={isAnalysisRunning}
<div className="flex items-center gap-2 mb-4"> isLoading={isLoadingPipeline}
<Layers className="w-5 h-5 text-nifty-600 dark:text-nifty-400" /> />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Analysis Pipeline</h2>
</div>
<PipelineOverview
steps={pipelineData?.pipeline_steps || []}
onStepClick={(step) => console.log('Step clicked:', step)}
/>
</section>
{/* Agent Reports Grid */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Agent Reports</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{(['market', 'news', 'social_media', 'fundamentals'] as AgentType[]).map(agentType => (
<AgentReportCard
key={agentType}
agentType={agentType}
report={pipelineData?.agent_reports?.[agentType]}
isLoading={isLoadingPipeline}
/>
))}
</div>
</section>
</div>
)} )}
{activeTab === 'debates' && ( {activeTab === 'debates' && (

BIN
hold-days-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
hold-days-history.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
hold-days-stock-detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB