add
This commit is contained in:
parent
bdc27679fa
commit
a556099d97
Binary file not shown.
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,55 +604,31 @@ 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>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* Action Buttons Row - Always visible */}
|
||||||
<div className="card p-1 flex gap-1 overflow-x-auto">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{TABS.map(tab => {
|
|
||||||
const Icon = tab.icon;
|
|
||||||
const isActive = activeTab === tab.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
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
|
|
||||||
${isActive
|
|
||||||
? 'bg-nifty-600 text-white shadow-md'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
{tab.label}
|
|
||||||
</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 */}
|
{/* Run Analysis Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleRunAnalysis}
|
onClick={handleRunAnalysis}
|
||||||
|
|
@ -314,7 +637,7 @@ export default function StockDetail() {
|
||||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||||
${isAnalysisRunning
|
${isAnalysisRunning
|
||||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
: 'bg-nifty-600 text-white hover:bg-nifty-700'
|
: 'bg-nifty-600 text-white hover:bg-nifty-700 shadow-sm'
|
||||||
}
|
}
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
`}
|
`}
|
||||||
|
|
@ -328,13 +651,25 @@ export default function StockDetail() {
|
||||||
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
|
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
|
||||||
</button>
|
</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 */}
|
{/* Refresh Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
|
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
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
|
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
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
`}
|
`}
|
||||||
title="Refresh pipeline data"
|
title="Refresh pipeline data"
|
||||||
|
|
@ -342,21 +677,77 @@ export default function StockDetail() {
|
||||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
{lastRefresh && (
|
||||||
|
<span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">
|
||||||
|
Updated: {lastRefresh}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="card p-1 flex gap-1 overflow-x-auto">
|
||||||
|
{TABS.map(tab => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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">
|
||||||
|
{realPriceHistory.length > 0 ? `${realPriceHistory.length} trading days` : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-white dark:bg-slate-800">
|
<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 className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||||
|
No price data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div>
|
<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="text-[10px] text-gray-500 dark:text-gray-400">Analyses</div>
|
<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="card p-2.5 text-center">
|
</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-[10px] text-gray-500 dark:text-gray-400">Total</div>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
<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>
|
<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>
|
||||||
|
) : backtestResults.length > 0 ? (
|
||||||
|
<div className="divide-y divide-gray-100 dark:divide-slate-700 max-h-[320px] overflow-y-auto">
|
||||||
|
{backtestResults.map((entry, idx) => (
|
||||||
|
<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', {
|
{new Date(entry.date).toLocaleDateString('en-IN', {
|
||||||
weekday: 'short',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<DecisionBadge decision={entry.decision} size="small" />
|
<div className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
|
{new Date(entry.date).toLocaleDateString('en-IN', { weekday: 'short' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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">
|
|
||||||
<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}
|
isLoading={isLoadingPipeline}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'debates' && (
|
{activeTab === 'debates' && (
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Loading…
Reference in New Issue