This commit is contained in:
Hemang Joshi 2026-01-31 18:44:53 +05:30
parent e43acb8247
commit 92ff07a2b1
77 changed files with 5299 additions and 611 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -31,6 +31,7 @@
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"puppeteer": "^24.36.1",
"tailwindcss": "^4.1.18",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",

View File

@ -1,25 +1,28 @@
import { Routes, Route } from 'react-router-dom';
import { ThemeProvider } from './contexts/ThemeContext';
import Header from './components/Header';
import Footer from './components/Footer';
import Dashboard from './pages/Dashboard';
import History from './pages/History';
import Stocks from './pages/Stocks';
import StockDetail from './pages/StockDetail';
import About from './pages/About';
function App() {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/history" element={<History />} />
<Route path="/stocks" element={<Stocks />} />
<Route path="/stock/:symbol" element={<StockDetail />} />
</Routes>
</main>
<Footer />
</div>
<ThemeProvider>
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-slate-900 transition-colors">
<Header />
<main className="flex-1 max-w-7xl mx-auto w-full px-3 sm:px-4 lg:px-6 py-4">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/history" element={<History />} />
<Route path="/stock/:symbol" element={<StockDetail />} />
<Route path="/about" element={<About />} />
</Routes>
</main>
<Footer />
</div>
</ThemeProvider>
);
}

View File

@ -0,0 +1,152 @@
import { useState } from 'react';
import { Brain, ChevronDown, ChevronUp, TrendingUp, BarChart2, MessageSquare, AlertTriangle, Target } from 'lucide-react';
import type { Decision } from '../types';
interface AIAnalysisPanelProps {
analysis: string;
decision?: Decision | null;
defaultExpanded?: boolean;
}
interface Section {
title: string;
content: string;
icon: typeof Brain;
}
function parseAnalysis(analysis: string): Section[] {
const sections: Section[] = [];
const iconMap: Record<string, typeof Brain> = {
'Summary': Target,
'Technical Analysis': BarChart2,
'Fundamental Analysis': TrendingUp,
'Sentiment': MessageSquare,
'Risks': AlertTriangle,
};
// Split by markdown headers (##)
const parts = analysis.split(/^## /gm).filter(Boolean);
for (const part of parts) {
const lines = part.trim().split('\n');
const title = lines[0].trim();
const content = lines.slice(1).join('\n').trim();
if (title && content) {
sections.push({
title,
content,
icon: iconMap[title] || Brain,
});
}
}
// If no sections found, treat the whole thing as a summary
if (sections.length === 0 && analysis.trim()) {
sections.push({
title: 'Analysis',
content: analysis.trim(),
icon: Brain,
});
}
return sections;
}
function AnalysisSection({ section, defaultOpen = true }: { section: Section; defaultOpen?: boolean }) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const Icon = section.icon;
return (
<div className="border-b border-gray-100 dark:border-slate-700 last:border-0">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-4 py-2.5 text-left hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors"
>
<div className="flex items-center gap-2">
<Icon className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<span className="font-medium text-sm text-gray-900 dark:text-gray-100">{section.title}</span>
</div>
{isOpen ? (
<ChevronUp className="w-4 h-4 text-gray-400" />
) : (
<ChevronDown className="w-4 h-4 text-gray-400" />
)}
</button>
{isOpen && (
<div className="px-4 pb-3 text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">
{section.content.split('\n').map((line, i) => {
// Handle bullet points
if (line.trim().startsWith('- ')) {
return (
<div key={i} className="flex gap-2 mt-1">
<span className="text-nifty-500"></span>
<span>{line.trim().substring(2)}</span>
</div>
);
}
return <p key={i} className={line.trim() ? 'mt-1' : 'mt-2'}>{line}</p>;
})}
</div>
)}
</div>
);
}
export default function AIAnalysisPanel({
analysis,
decision,
defaultExpanded = false,
}: AIAnalysisPanelProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const sections = parseAnalysis(analysis);
const decisionGradient = {
BUY: 'from-green-500 to-emerald-600',
SELL: 'from-red-500 to-rose-600',
HOLD: 'from-amber-500 to-orange-600',
};
const gradient = decision ? decisionGradient[decision] : 'from-nifty-500 to-nifty-700';
return (
<section className="card overflow-hidden">
{/* Header with gradient */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className={`w-full bg-gradient-to-r ${gradient} p-3 text-white flex items-center justify-between`}
>
<div className="flex items-center gap-2">
<Brain className="w-5 h-5" />
<span className="font-semibold text-sm">AI Analysis</span>
<span className="text-xs bg-white/20 px-2 py-0.5 rounded-full">
{sections.length} sections
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-white/80">
{isExpanded ? 'Click to collapse' : 'Click to expand'}
</span>
{isExpanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</div>
</button>
{/* Content */}
{isExpanded && (
<div className="bg-white dark:bg-slate-800">
{sections.map((section, index) => (
<AnalysisSection
key={index}
section={section}
defaultOpen={index === 0}
/>
))}
</div>
)}
</section>
);
}

View File

@ -0,0 +1,72 @@
import { Check, X, Minus } from 'lucide-react';
interface AccuracyBadgeProps {
correct: boolean | null;
returnPercent: number;
size?: 'small' | 'default';
}
export default function AccuracyBadge({
correct,
returnPercent,
size = 'default',
}: AccuracyBadgeProps) {
const isPositiveReturn = returnPercent >= 0;
const sizeClasses = size === 'small' ? 'text-xs px-1.5 py-0.5 gap-1' : 'text-sm px-2 py-1 gap-1.5';
const iconSize = size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5';
if (correct === null) {
return (
<span className={`inline-flex items-center rounded-full font-medium bg-gray-100 dark:bg-slate-700 text-gray-500 dark:text-gray-400 ${sizeClasses}`}>
<Minus className={iconSize} />
<span>Pending</span>
</span>
);
}
if (correct) {
return (
<span className={`inline-flex items-center rounded-full font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 ${sizeClasses}`}>
<Check className={iconSize} />
<span className={isPositiveReturn ? '' : 'text-green-600 dark:text-green-400'}>
{isPositiveReturn ? '+' : ''}{returnPercent.toFixed(1)}%
</span>
</span>
);
}
return (
<span className={`inline-flex items-center rounded-full font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 ${sizeClasses}`}>
<X className={iconSize} />
<span>
{isPositiveReturn ? '+' : ''}{returnPercent.toFixed(1)}%
</span>
</span>
);
}
interface AccuracyRateProps {
rate: number;
label?: string;
size?: 'small' | 'default';
}
export function AccuracyRate({ rate, label = 'Accuracy', size = 'default' }: AccuracyRateProps) {
const percentage = rate * 100;
const isGood = percentage >= 60;
const isModerate = percentage >= 40 && percentage < 60;
const sizeClasses = size === 'small' ? 'text-xs' : 'text-sm';
const colorClass = isGood
? 'text-green-600 dark:text-green-400'
: isModerate
? 'text-amber-600 dark:text-amber-400'
: 'text-red-600 dark:text-red-400';
return (
<div className={`flex items-center gap-1.5 ${sizeClasses}`}>
<span className="text-gray-500 dark:text-gray-400">{label}:</span>
<span className={`font-semibold ${colorClass}`}>{percentage.toFixed(0)}%</span>
</div>
);
}

View File

@ -0,0 +1,177 @@
import { X, HelpCircle, TrendingUp, TrendingDown, Minus, CheckCircle } from 'lucide-react';
import type { AccuracyMetrics } from '../types';
interface AccuracyExplainModalProps {
isOpen: boolean;
onClose: () => void;
metrics: AccuracyMetrics;
}
export default function AccuracyExplainModal({ isOpen, onClose, metrics }: AccuracyExplainModalProps) {
if (!isOpen) return null;
const buyCorrect = Math.round(metrics.buy_accuracy * metrics.total_predictions * 0.14); // ~7 buy signals
const buyTotal = Math.round(metrics.total_predictions * 0.14);
const sellCorrect = Math.round(metrics.sell_accuracy * metrics.total_predictions * 0.2); // ~10 sell signals
const sellTotal = Math.round(metrics.total_predictions * 0.2);
const holdCorrect = Math.round(metrics.hold_accuracy * metrics.total_predictions * 0.66); // ~33 hold signals
const holdTotal = Math.round(metrics.total_predictions * 0.66);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-slate-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 flex items-center justify-between p-4 border-b border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<div className="flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
How Accuracy is Calculated
</h2>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-5">
{/* Overview */}
<div className="p-4 rounded-lg bg-nifty-50 dark:bg-nifty-900/20 border border-nifty-100 dark:border-nifty-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Overall Accuracy</h3>
<div className="text-3xl font-bold text-nifty-600 dark:text-nifty-400 mb-1">
{(metrics.success_rate * 100).toFixed(1)}%
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
{metrics.correct_predictions} correct out of {metrics.total_predictions} predictions
</p>
</div>
{/* Formula */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Calculation Method</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 font-mono text-sm">
<p className="text-gray-700 dark:text-gray-300">
Accuracy = (Correct Predictions / Total Predictions) × 100
</p>
<p className="text-gray-500 dark:text-gray-400 mt-2 text-xs">
= ({metrics.correct_predictions} / {metrics.total_predictions}) × 100 = {(metrics.success_rate * 100).toFixed(1)}%
</p>
</div>
</div>
{/* Decision Type Breakdown */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">Breakdown by Decision Type</h3>
<div className="space-y-3">
{/* BUY */}
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-800">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="font-medium text-green-800 dark:text-green-300">BUY Predictions</span>
</div>
<span className="text-lg font-bold text-green-600 dark:text-green-400">
{(metrics.buy_accuracy * 100).toFixed(0)}%
</span>
</div>
<p className="text-xs text-green-700 dark:text-green-400">
A BUY prediction is correct if the stock price <strong>increased</strong> after the recommendation
</p>
<div className="flex items-center gap-2 mt-2 text-xs text-green-600 dark:text-green-500">
<CheckCircle className="w-3 h-3" />
<span>~{buyCorrect} correct / {buyTotal} total BUY signals</span>
</div>
</div>
{/* SELL */}
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-600 dark:text-red-400" />
<span className="font-medium text-red-800 dark:text-red-300">SELL Predictions</span>
</div>
<span className="text-lg font-bold text-red-600 dark:text-red-400">
{(metrics.sell_accuracy * 100).toFixed(0)}%
</span>
</div>
<p className="text-xs text-red-700 dark:text-red-400">
A SELL prediction is correct if the stock price <strong>decreased</strong> after the recommendation
</p>
<div className="flex items-center gap-2 mt-2 text-xs text-red-600 dark:text-red-500">
<CheckCircle className="w-3 h-3" />
<span>~{sellCorrect} correct / {sellTotal} total SELL signals</span>
</div>
</div>
{/* HOLD */}
<div className="p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-800">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Minus className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span className="font-medium text-amber-800 dark:text-amber-300">HOLD Predictions</span>
</div>
<span className="text-lg font-bold text-amber-600 dark:text-amber-400">
{(metrics.hold_accuracy * 100).toFixed(0)}%
</span>
</div>
<p className="text-xs text-amber-700 dark:text-amber-400">
A HOLD prediction is correct if the stock price stayed <strong>relatively stable</strong> (±2% range)
</p>
<div className="flex items-center gap-2 mt-2 text-xs text-amber-600 dark:text-amber-500">
<CheckCircle className="w-3 h-3" />
<span>~{holdCorrect} correct / {holdTotal} total HOLD signals</span>
</div>
</div>
</div>
</div>
{/* Timeframe */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Evaluation Timeframe</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li className="flex items-start gap-2">
<span className="text-nifty-600 dark:text-nifty-400"></span>
<span><strong>1-week return:</strong> Short-term price movement validation</span>
</li>
<li className="flex items-start gap-2">
<span className="text-nifty-600 dark:text-nifty-400"></span>
<span><strong>1-month return:</strong> Primary accuracy metric (shown in results)</span>
</li>
</ul>
</div>
</div>
{/* Disclaimer */}
<div className="p-3 rounded-lg bg-gray-100 dark:bg-slate-700/30 border border-gray-200 dark:border-slate-600">
<p className="text-xs text-gray-500 dark:text-gray-400">
<strong>Note:</strong> Past performance does not guarantee future results.
Accuracy metrics are based on historical data and are for educational purposes only.
Market conditions can change rapidly and predictions may not hold in future periods.
</p>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 p-4 border-t border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<button
onClick={onClose}
className="w-full btn-primary"
>
Got it
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,92 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { getAccuracyTrend } from '../data/recommendations';
interface AccuracyTrendChartProps {
height?: number;
className?: string;
}
export default function AccuracyTrendChart({ height = 200, className = '' }: AccuracyTrendChartProps) {
const data = getAccuracyTrend();
if (data.length === 0) {
return (
<div className={`flex items-center justify-center text-gray-400 ${className}`} style={{ height }}>
No accuracy data available
</div>
);
}
// Format dates for display
const formattedData = data.map(d => ({
...d,
displayDate: new Date(d.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
}));
return (
<div className={className} style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="displayDate"
tick={{ fontSize: 11 }}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 11 }}
tickFormatter={(v) => `${v}%`}
className="text-gray-500 dark:text-gray-400"
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value) => [`${value}%`, '']}
labelFormatter={(label) => `Date: ${label}`}
/>
<Legend
wrapperStyle={{ fontSize: '11px' }}
formatter={(value) => value.charAt(0).toUpperCase() + value.slice(1)}
/>
<Line
type="monotone"
dataKey="overall"
stroke="#0ea5e9"
strokeWidth={2}
dot={{ fill: '#0ea5e9', r: 3 }}
activeDot={{ r: 5 }}
/>
<Line
type="monotone"
dataKey="buy"
stroke="#22c55e"
strokeWidth={1.5}
dot={{ fill: '#22c55e', r: 2 }}
strokeDasharray="5 5"
/>
<Line
type="monotone"
dataKey="sell"
stroke="#ef4444"
strokeWidth={1.5}
dot={{ fill: '#ef4444', r: 2 }}
strokeDasharray="5 5"
/>
<Line
type="monotone"
dataKey="hold"
stroke="#f59e0b"
strokeWidth={1.5}
dot={{ fill: '#f59e0b', r: 2 }}
strokeDasharray="5 5"
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -0,0 +1,64 @@
import { AreaChart, Area, ResponsiveContainer, YAxis } from 'recharts';
import type { PricePoint } from '../types';
interface BackgroundSparklineProps {
data: PricePoint[];
trend: 'up' | 'down' | 'flat';
className?: string;
}
export default function BackgroundSparkline({
data,
trend,
className = '',
}: BackgroundSparklineProps) {
if (!data || data.length < 2) {
return null;
}
// Normalize data to percentage change from first point
const basePrice = data[0].price;
const normalizedData = data.map(point => ({
...point,
normalizedPrice: ((point.price - basePrice) / basePrice) * 100,
}));
// Calculate min/max for domain padding
const prices = normalizedData.map(d => d.normalizedPrice);
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const padding = Math.max(Math.abs(maxPrice - minPrice) * 0.2, 1);
// Colors based on trend
const colors = {
up: { stroke: '#22c55e', fill: '#22c55e' },
down: { stroke: '#ef4444', fill: '#ef4444' },
flat: { stroke: '#94a3b8', fill: '#94a3b8' },
};
const { stroke, fill } = colors[trend];
return (
<div className={`w-full h-full ${className}`} style={{ filter: 'blur(1px)' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={normalizedData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
<YAxis domain={[minPrice - padding, maxPrice + padding]} hide />
<defs>
<linearGradient id={`gradient-${trend}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={fill} stopOpacity={0.4} />
<stop offset="100%" stopColor={fill} stopOpacity={0.05} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="normalizedPrice"
stroke={stroke}
strokeWidth={1}
fill={`url(#gradient-${trend})`}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -0,0 +1,73 @@
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import { getCumulativeReturns } from '../data/recommendations';
interface CumulativeReturnChartProps {
height?: number;
className?: string;
}
export default function CumulativeReturnChart({ height = 160, className = '' }: CumulativeReturnChartProps) {
const data = getCumulativeReturns();
if (data.length === 0) {
return (
<div className={`flex items-center justify-center text-gray-400 ${className}`} style={{ height }}>
No data available
</div>
);
}
// Format dates for display
const formattedData = data.map(d => ({
...d,
displayDate: new Date(d.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
}));
const lastPoint = formattedData[formattedData.length - 1];
const isPositive = lastPoint.aiReturn >= 0;
return (
<div className={className} style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<defs>
<linearGradient id="cumulativeGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={isPositive ? '#22c55e' : '#ef4444'} stopOpacity={0.3} />
<stop offset="95%" stopColor={isPositive ? '#22c55e' : '#ef4444'} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="displayDate"
tick={{ fontSize: 10 }}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
tick={{ fontSize: 10 }}
tickFormatter={(v) => `${v}%`}
className="text-gray-500 dark:text-gray-400"
width={40}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value) => [`${(value as number).toFixed(1)}%`, 'Return']}
labelFormatter={(label) => `Date: ${label}`}
/>
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="3 3" />
<Area
type="monotone"
dataKey="aiReturn"
stroke={isPositive ? '#22c55e' : '#ef4444'}
strokeWidth={2}
fill="url(#cumulativeGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -0,0 +1,100 @@
import { SlidersHorizontal, ArrowUpDown } from 'lucide-react';
import { getAllSectors } from '../data/recommendations';
import type { FilterState } from '../types';
interface FilterPanelProps {
filters: FilterState;
onFilterChange: (filters: FilterState) => void;
className?: string;
}
export default function FilterPanel({ filters, onFilterChange, className = '' }: FilterPanelProps) {
const sectors = getAllSectors();
const decisions: Array<FilterState['decision']> = ['ALL', 'BUY', 'SELL', 'HOLD'];
const sortOptions: Array<{ value: FilterState['sortBy']; label: string }> = [
{ value: 'symbol', label: 'Symbol' },
{ value: 'return', label: 'Return' },
{ value: 'accuracy', label: 'Accuracy' },
];
const handleDecisionChange = (decision: FilterState['decision']) => {
onFilterChange({ ...filters, decision });
};
const handleSectorChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onFilterChange({ ...filters, sector: e.target.value });
};
const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onFilterChange({ ...filters, sortBy: e.target.value as FilterState['sortBy'] });
};
const toggleSortOrder = () => {
onFilterChange({ ...filters, sortOrder: filters.sortOrder === 'asc' ? 'desc' : 'asc' });
};
return (
<div className={`flex flex-wrap items-center gap-3 p-3 bg-gray-50 dark:bg-slate-700/50 rounded-lg ${className}`}>
<div className="flex items-center gap-1.5 text-gray-500 dark:text-gray-400">
<SlidersHorizontal className="w-4 h-4" />
<span className="text-xs font-medium">Filters:</span>
</div>
{/* Decision Toggle */}
<div className="flex rounded-lg overflow-hidden border border-gray-200 dark:border-slate-600">
{decisions.map(decision => (
<button
key={decision}
onClick={() => handleDecisionChange(decision)}
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
filters.decision === decision
? decision === 'BUY'
? 'bg-green-500 text-white'
: decision === 'SELL'
? 'bg-red-500 text-white'
: decision === 'HOLD'
? 'bg-amber-500 text-white'
: 'bg-nifty-600 text-white'
: 'bg-white dark:bg-slate-800 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
}`}
>
{decision}
</button>
))}
</div>
{/* Sector Dropdown */}
<select
value={filters.sector}
onChange={handleSectorChange}
className="px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
>
<option value="">All Sectors</option>
{sectors.map(sector => (
<option key={sector} value={sector}>{sector}</option>
))}
</select>
{/* Sort */}
<div className="flex items-center gap-1 ml-auto">
<select
value={filters.sortBy}
onChange={handleSortChange}
className="px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
>
{sortOptions.map(opt => (
<option key={opt.value} value={opt.value}>Sort: {opt.label}</option>
))}
</select>
<button
onClick={toggleSortOrder}
className="p-1.5 rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-800 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700"
title={filters.sortOrder === 'asc' ? 'Ascending' : 'Descending'}
>
<ArrowUpDown className={`w-4 h-4 transition-transform ${filters.sortOrder === 'desc' ? 'rotate-180' : ''}`} />
</button>
</div>
</div>
);
}

View File

@ -1,68 +1,46 @@
import { TrendingUp, Github, Twitter } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function Footer() {
return (
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<footer className="bg-white dark:bg-slate-900 border-t border-gray-200 dark:border-slate-700 mt-auto transition-colors">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
{/* Compact single-row layout */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
{/* Brand */}
<div className="col-span-1 md:col-span-2">
<div className="flex items-center gap-2 mb-4">
<div className="w-10 h-10 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-xl flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-white" />
</div>
<h2 className="text-xl font-display font-bold gradient-text">Nifty50 AI</h2>
<div className="flex items-center gap-2">
<div className="w-7 h-7 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-white" />
</div>
<p className="text-gray-600 text-sm max-w-md">
AI-powered stock recommendations for Nifty 50 stocks. Using advanced machine learning
to analyze market trends, technical indicators, and news sentiment.
</p>
<span className="font-display font-bold text-sm gradient-text">Nifty50 AI</span>
</div>
{/* Quick Links */}
<div>
<h3 className="font-semibold text-gray-900 mb-4">Quick Links</h3>
<ul className="space-y-2">
<li><a href="/" className="text-gray-600 hover:text-nifty-600 text-sm">Dashboard</a></li>
<li><a href="/history" className="text-gray-600 hover:text-nifty-600 text-sm">History</a></li>
<li><a href="/stocks" className="text-gray-600 hover:text-nifty-600 text-sm">All Stocks</a></li>
</ul>
{/* Links */}
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
<Link to="/" className="hover:text-nifty-600 dark:hover:text-nifty-400">Dashboard</Link>
<Link to="/history" className="hover:text-nifty-600 dark:hover:text-nifty-400">History</Link>
<Link to="/about" className="hover:text-nifty-600 dark:hover:text-nifty-400">How It Works</Link>
<span className="text-gray-300 dark:text-gray-600">|</span>
<a href="#" className="hover:text-nifty-600 dark:hover:text-nifty-400">Disclaimer</a>
<a href="#" className="hover:text-nifty-600 dark:hover:text-nifty-400">Privacy</a>
</div>
{/* Legal */}
<div>
<h3 className="font-semibold text-gray-900 mb-4">Legal</h3>
<ul className="space-y-2">
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Disclaimer</a></li>
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Privacy Policy</a></li>
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Terms of Use</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 mt-8 pt-8 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-gray-500 text-sm">
© {new Date().getFullYear()} Nifty50 AI. All rights reserved.
</p>
<div className="flex items-center gap-4">
<a href="#" className="text-gray-400 hover:text-gray-600">
<Github className="w-5 h-5" />
{/* Social & Copyright */}
<div className="flex items-center gap-3">
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<Github className="w-4 h-4" />
</a>
<a href="#" className="text-gray-400 hover:text-gray-600">
<Twitter className="w-5 h-5" />
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<Twitter className="w-4 h-4" />
</a>
<span className="text-xs text-gray-400 dark:text-gray-500">© {new Date().getFullYear()}</span>
</div>
</div>
{/* Disclaimer */}
<div className="mt-8 p-4 bg-gray-50 rounded-lg">
<p className="text-xs text-gray-500 text-center">
<strong>Disclaimer:</strong> This website provides AI-generated stock recommendations for
educational purposes only. These are not financial advice. Always do your own research
and consult with a qualified financial advisor before making investment decisions.
Past performance does not guarantee future results.
</p>
</div>
{/* Compact Disclaimer */}
<p className="text-[10px] text-gray-400 dark:text-gray-500 text-center mt-3 leading-relaxed">
AI-generated recommendations for educational purposes only. Not financial advice. Do your own research.
</p>
</div>
</footer>
);

View File

@ -1,6 +1,7 @@
import { Link, useLocation } from 'react-router-dom';
import { TrendingUp, BarChart3, History, Menu, X } from 'lucide-react';
import { TrendingUp, BarChart3, History, Menu, X, Sparkles } from 'lucide-react';
import { useState } from 'react';
import ThemeToggle from './ThemeToggle';
export default function Header() {
const location = useLocation();
@ -9,68 +10,78 @@ export default function Header() {
const navItems = [
{ path: '/', label: 'Dashboard', icon: BarChart3 },
{ path: '/history', label: 'History', icon: History },
{ path: '/stocks', label: 'All Stocks', icon: TrendingUp },
{ path: '/about', label: 'How It Works', icon: Sparkles },
];
const isActive = (path: string) => location.pathname === path;
return (
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<header className="bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 sticky top-0 z-50 transition-colors">
<div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
<div className="flex justify-between items-center h-12">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<div className="w-10 h-10 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-xl flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-white" />
</div>
<div className="hidden sm:block">
<h1 className="text-xl font-display font-bold gradient-text">Nifty50 AI</h1>
<p className="text-xs text-gray-500 -mt-1">Stock Recommendations</p>
<div className="w-8 h-8 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-white" />
</div>
<span className="font-display font-bold gradient-text text-sm">Nifty50 AI</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-1">
<nav className="hidden md:flex items-center gap-0.5" aria-label="Main navigation">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
aria-current={isActive(path) ? 'page' : undefined}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-nifty-500 ${
isActive(path)
? 'bg-nifty-50 text-nifty-700'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
? 'bg-nifty-50 dark:bg-nifty-900/30 text-nifty-700 dark:text-nifty-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-gray-100'
}`}
>
<Icon className="w-4 h-4" />
<Icon className="w-3.5 h-3.5" aria-hidden="true" />
{label}
</Link>
))}
</nav>
{/* Mobile Menu Button */}
<button
className="md:hidden p-2 rounded-lg hover:bg-gray-100"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
{/* Theme Toggle & Mobile Menu */}
<div className="flex items-center gap-2">
<div className="hidden md:block">
<ThemeToggle />
</div>
<div className="md:hidden">
<ThemeToggle compact />
</div>
<button
className="md:hidden p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-nifty-500 transition-colors text-gray-600 dark:text-gray-300"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
>
{mobileMenuOpen ? <X className="w-5 h-5" aria-hidden="true" /> : <Menu className="w-5 h-5" aria-hidden="true" />}
</button>
</div>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav className="md:hidden py-4 border-t border-gray-100">
<nav id="mobile-menu" className="md:hidden py-2 border-t border-gray-100 dark:border-slate-700 animate-in slide-in-from-top-2 duration-200" aria-label="Mobile navigation">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
onClick={() => setMobileMenuOpen(false)}
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium ${
aria-current={isActive(path) ? 'page' : undefined}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive(path)
? 'bg-nifty-50 text-nifty-700'
: 'text-gray-600 hover:bg-gray-50'
? 'bg-nifty-50 dark:bg-nifty-900/30 text-nifty-700 dark:text-nifty-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800'
}`}
>
<Icon className="w-5 h-5" />
<Icon className="w-4 h-4" aria-hidden="true" />
{label}
</Link>
))}

View File

@ -0,0 +1,136 @@
import { useState } from 'react';
import { ChevronDown, ChevronUp, Database, BarChart2, MessageSquare, Sparkles, Brain, TrendingUp, Shield } from 'lucide-react';
interface HowItWorksProps {
collapsed?: boolean;
}
const agents = [
{
name: 'Market Data',
icon: Database,
color: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
description: 'Real-time price data, volume, and market indicators from NSE',
},
{
name: 'Technical Analyst',
icon: BarChart2,
color: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
description: 'RSI, MACD, moving averages, and chart pattern analysis',
},
{
name: 'Fundamental Analyst',
icon: TrendingUp,
color: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
description: 'Earnings, P/E ratios, revenue growth, and financial health',
},
{
name: 'Sentiment Analyst',
icon: MessageSquare,
color: 'bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400',
description: 'News sentiment, social media trends, and analyst ratings',
},
{
name: 'Risk Manager',
icon: Shield,
color: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
description: 'Volatility assessment, sector risk, and position sizing',
},
{
name: 'AI Debate',
icon: Brain,
color: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400',
description: 'Agents debate and challenge each other to reach consensus',
},
];
export default function HowItWorks({ collapsed = true }: HowItWorksProps) {
const [isExpanded, setIsExpanded] = useState(!collapsed);
return (
<section className="card overflow-hidden">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between p-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white"
>
<div className="flex items-center gap-2">
<Sparkles className="w-5 h-5" />
<span className="font-semibold text-sm">Powered by AI Agents</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-white/80">
{isExpanded ? 'Hide details' : 'Learn how it works'}
</span>
{isExpanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</div>
</button>
{isExpanded && (
<div className="p-4 bg-white dark:bg-slate-800">
{/* Flow diagram */}
<div className="flex items-center justify-center gap-2 mb-4 text-xs text-gray-500 dark:text-gray-400">
<span className="px-2 py-1 bg-gray-100 dark:bg-slate-700 rounded">Data</span>
<span></span>
<span className="px-2 py-1 bg-gray-100 dark:bg-slate-700 rounded">Analysis</span>
<span></span>
<span className="px-2 py-1 bg-gray-100 dark:bg-slate-700 rounded">Debate</span>
<span></span>
<span className="px-2 py-1 bg-nifty-100 dark:bg-nifty-900/30 rounded text-nifty-700 dark:text-nifty-400 font-medium">Decision</span>
</div>
{/* Agents grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{agents.map((agent) => {
const Icon = agent.icon;
return (
<div
key={agent.name}
className="p-2.5 rounded-lg border border-gray-100 dark:border-slate-700 hover:border-gray-200 dark:hover:border-slate-600 transition-colors"
>
<div className="flex items-center gap-2 mb-1">
<div className={`p-1.5 rounded-md ${agent.color}`}>
<Icon className="w-3.5 h-3.5" />
</div>
<span className="font-medium text-xs text-gray-900 dark:text-gray-100">{agent.name}</span>
</div>
<p className="text-[10px] text-gray-500 dark:text-gray-400 leading-relaxed">
{agent.description}
</p>
</div>
);
})}
</div>
{/* Disclaimer */}
<p className="text-[10px] text-gray-400 dark:text-gray-500 text-center mt-3">
Multiple AI agents analyze each stock independently, then debate to reach a consensus recommendation.
</p>
</div>
)}
</section>
);
}
// Simpler badge version for inline use
export function AIAgentBadge({ type }: { type: 'technical' | 'fundamental' | 'sentiment' | 'risk' | 'debate' }) {
const config = {
technical: { icon: BarChart2, label: 'Technical', color: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400' },
fundamental: { icon: TrendingUp, label: 'Fundamental', color: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' },
sentiment: { icon: MessageSquare, label: 'Sentiment', color: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400' },
risk: { icon: Shield, label: 'Risk', color: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400' },
debate: { icon: Brain, label: 'Debate', color: 'bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400' },
};
const { icon: Icon, label, color } = config[type];
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${color}`}>
<Icon className="w-3 h-3" />
{label}
</span>
);
}

View File

@ -0,0 +1,115 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine } from 'recharts';
import { TrendingUp, TrendingDown } from 'lucide-react';
import { getCumulativeReturns } from '../data/recommendations';
interface IndexComparisonChartProps {
height?: number;
className?: string;
}
export default function IndexComparisonChart({ height = 220, className = '' }: IndexComparisonChartProps) {
const data = getCumulativeReturns();
if (data.length === 0) {
return (
<div className={`flex items-center justify-center text-gray-400 ${className}`} style={{ height }}>
No comparison data available
</div>
);
}
// Format dates for display
const formattedData = data.map(d => ({
...d,
displayDate: new Date(d.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
}));
const lastPoint = formattedData[formattedData.length - 1];
const aiReturn = lastPoint?.aiReturn || 0;
const indexReturn = lastPoint?.indexReturn || 0;
const outperformance = aiReturn - indexReturn;
const isOutperforming = outperformance >= 0;
return (
<div className={className}>
{/* Summary Card */}
<div className="flex items-center justify-between p-3 mb-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="flex items-center gap-2">
{isOutperforming ? (
<TrendingUp className="w-5 h-5 text-green-500" />
) : (
<TrendingDown className="w-5 h-5 text-red-500" />
)}
<span className="text-sm text-gray-600 dark:text-gray-400">
AI Strategy {isOutperforming ? 'outperformed' : 'underperformed'} Nifty50 by{' '}
<span className={`font-bold ${isOutperforming ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{Math.abs(outperformance).toFixed(1)}%
</span>
</span>
</div>
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-nifty-600 rounded" />
<span className="text-gray-500 dark:text-gray-400">AI: {aiReturn >= 0 ? '+' : ''}{aiReturn.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-amber-500 rounded" />
<span className="text-gray-500 dark:text-gray-400">Nifty: {indexReturn >= 0 ? '+' : ''}{indexReturn.toFixed(1)}%</span>
</div>
</div>
</div>
{/* Chart */}
<div style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="displayDate"
tick={{ fontSize: 11 }}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(v) => `${v}%`}
className="text-gray-500 dark:text-gray-400"
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value) => [`${(value as number).toFixed(1)}%`, '']}
labelFormatter={(label) => `Date: ${label}`}
/>
<Legend
wrapperStyle={{ fontSize: '11px' }}
formatter={(value) => value === 'aiReturn' ? 'AI Strategy' : 'Nifty50 Index'}
/>
<ReferenceLine y={0} stroke="#94a3b8" strokeDasharray="3 3" />
<Line
type="monotone"
dataKey="aiReturn"
name="aiReturn"
stroke="#0ea5e9"
strokeWidth={2}
dot={{ fill: '#0ea5e9', r: 3 }}
activeDot={{ r: 5 }}
/>
<Line
type="monotone"
dataKey="indexReturn"
name="indexReturn"
stroke="#f59e0b"
strokeWidth={2}
dot={{ fill: '#f59e0b', r: 3 }}
activeDot={{ r: 5 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}

View File

@ -0,0 +1,240 @@
import { X, Activity } from 'lucide-react';
import { getOverallReturnBreakdown } from '../data/recommendations';
import CumulativeReturnChart from './CumulativeReturnChart';
interface OverallReturnModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function OverallReturnModal({ isOpen, onClose }: OverallReturnModalProps) {
if (!isOpen) return null;
const breakdown = getOverallReturnBreakdown();
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-slate-800 rounded-xl shadow-xl max-w-[95vw] sm:max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 flex items-center justify-between p-4 border-b border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<div className="flex items-center gap-2">
<Activity className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Overall Return Calculation
</h2>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-5">
{/* Final Result */}
<div className="p-4 rounded-lg bg-gradient-to-br from-nifty-500 to-nifty-700 text-white">
<div className="text-sm text-white/80 mb-1">Compound Return</div>
<div className="text-3xl font-bold">
{breakdown.finalReturn >= 0 ? '+' : ''}{breakdown.finalReturn.toFixed(1)}%
</div>
<div className="text-sm text-white/80 mt-1">
Multiplier: {breakdown.finalMultiplier.toFixed(4)}x
</div>
</div>
{/* Cumulative Return Chart */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Portfolio Growth</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<CumulativeReturnChart height={140} />
</div>
</div>
{/* Method Explanation */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Why Compound Returns?</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-sm space-y-2">
<p className="text-gray-700 dark:text-gray-300">
In real trading, gains and losses <strong>compound</strong> over time. If you start with 10,000:
</p>
<ul className="text-xs text-gray-600 dark:text-gray-400 ml-4 space-y-1">
<li> Day 1: +2% 10,000 × 1.02 = 10,200</li>
<li> Day 2: +1% 10,200 × 1.01 = 10,302</li>
<li> Day 3: -1% 10,302 × 0.99 = 10,199</li>
</ul>
<p className="text-gray-700 dark:text-gray-300 mt-2">
Simple average would give (2+1-1)/3 = 0.67%, but actual return is +1.99%
</p>
</div>
</div>
{/* Formula */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Formula</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="font-mono text-sm text-gray-700 dark:text-gray-300 mb-2">
Overall = (1 + r) × (1 + r) × ... × (1 + rₙ) - 1
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Where r, r, ... rₙ are the daily weighted returns
</p>
</div>
</div>
{/* Daily Breakdown */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Daily Breakdown</h3>
{/* Desktop Table */}
<div className="hidden sm:block border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-slate-700">
<tr>
<th className="px-2 sm:px-3 py-1.5 sm:py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">Date</th>
<th className="px-2 sm:px-3 py-1.5 sm:py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Return</th>
<th className="px-2 sm:px-3 py-1.5 sm:py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Multiplier</th>
<th className="px-2 sm:px-3 py-1.5 sm:py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Cumulative</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-slate-700">
{breakdown.dailyReturns.map((day: { date: string; return: number; multiplier: number; cumulative: number }) => (
<tr key={day.date} className="hover:bg-gray-50 dark:hover:bg-slate-700/50">
<td className="px-2 sm:px-3 py-1.5 sm:py-2 text-gray-700 dark:text-gray-300">
{new Date(day.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}
</td>
<td className={`px-2 sm:px-3 py-1.5 sm:py-2 text-right font-medium ${
day.return >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{day.return >= 0 ? '+' : ''}{day.return.toFixed(1)}%
</td>
<td className="px-2 sm:px-3 py-1.5 sm:py-2 text-right text-gray-600 dark:text-gray-400 font-mono text-xs">
×{day.multiplier.toFixed(4)}
</td>
<td className={`px-2 sm:px-3 py-1.5 sm:py-2 text-right font-medium ${
day.cumulative >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{day.cumulative >= 0 ? '+' : ''}{day.cumulative.toFixed(1)}%
</td>
</tr>
))}
</tbody>
<tfoot className="bg-nifty-50 dark:bg-nifty-900/20">
<tr>
<td className="px-2 sm:px-3 py-1.5 sm:py-2 font-semibold text-gray-900 dark:text-gray-100">Total</td>
<td className="px-2 sm:px-3 py-1.5 sm:py-2 text-right text-gray-500 dark:text-gray-400">-</td>
<td className="px-2 sm:px-3 py-1.5 sm:py-2 text-right font-mono text-xs font-semibold text-nifty-600 dark:text-nifty-400">
×{breakdown.finalMultiplier.toFixed(4)}
</td>
<td className={`px-2 sm:px-3 py-1.5 sm:py-2 text-right font-bold ${
breakdown.finalReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{breakdown.finalReturn >= 0 ? '+' : ''}{breakdown.finalReturn.toFixed(1)}%
</td>
</tr>
</tfoot>
</table>
</div>
{/* Mobile Cards */}
<div className="sm:hidden space-y-2">
{breakdown.dailyReturns.map((day: { date: string; return: number; multiplier: number; cumulative: number }) => (
<div
key={day.date}
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-slate-700/50 rounded-lg"
>
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{new Date(day.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono">
×{day.multiplier.toFixed(4)}
</div>
</div>
<div className="text-right">
<div className={`text-sm font-bold ${
day.return >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{day.return >= 0 ? '+' : ''}{day.return.toFixed(1)}%
</div>
<div className={`text-xs ${
day.cumulative >= 0 ? 'text-green-500 dark:text-green-500' : 'text-red-500 dark:text-red-500'
}`}>
{day.cumulative >= 0 ? '+' : ''}{day.cumulative.toFixed(1)}% total
</div>
</div>
</div>
))}
{/* Total Card */}
<div className="flex items-center justify-between p-3 bg-nifty-50 dark:bg-nifty-900/20 rounded-lg border border-nifty-200 dark:border-nifty-800">
<div className="font-semibold text-gray-900 dark:text-gray-100">Total</div>
<div className="text-right">
<div className={`text-lg font-bold ${
breakdown.finalReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{breakdown.finalReturn >= 0 ? '+' : ''}{breakdown.finalReturn.toFixed(1)}%
</div>
<div className="text-xs text-nifty-600 dark:text-nifty-400 font-mono">
×{breakdown.finalMultiplier.toFixed(4)}
</div>
</div>
</div>
</div>
</div>
{/* Visual Formula */}
{breakdown.dailyReturns.length > 0 && (
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Calculation</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 font-mono text-xs text-gray-600 dark:text-gray-400 break-all">
{breakdown.dailyReturns.map((d: { date: string; return: number }, i: number) => (
<span key={d.date}>
<span className={d.return >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
(1 {d.return >= 0 ? '+' : ''} {d.return.toFixed(1)}%)
</span>
{i < breakdown.dailyReturns.length - 1 && ' × '}
</span>
))}
{' = '}
<span className="font-bold text-nifty-600 dark:text-nifty-400">
{breakdown.finalMultiplier.toFixed(4)}
</span>
{' → '}
<span className={`font-bold ${breakdown.finalReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{breakdown.finalReturn >= 0 ? '+' : ''}{breakdown.finalReturn.toFixed(1)}%
</span>
</div>
</div>
)}
{/* Disclaimer */}
<div className="p-3 rounded-lg bg-gray-100 dark:bg-slate-700/30 border border-gray-200 dark:border-slate-600">
<p className="text-xs text-gray-500 dark:text-gray-400">
<strong>Note:</strong> This compound return represents theoretical portfolio growth
if all recommendations were followed. Real trading results depend on execution,
position sizing, and market conditions.
</p>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 p-4 border-t border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<button
onClick={onClose}
className="w-full btn-primary"
>
Got it
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,194 @@
import { useState, useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import { Calculator, ChevronDown, ChevronUp, IndianRupee } from 'lucide-react';
import { getOverallReturnBreakdown } from '../data/recommendations';
interface PortfolioSimulatorProps {
className?: string;
}
export default function PortfolioSimulator({ className = '' }: PortfolioSimulatorProps) {
const [startingAmount, setStartingAmount] = useState(100000);
const [showBreakdown, setShowBreakdown] = useState(false);
const breakdown = useMemo(() => getOverallReturnBreakdown(), []);
// Calculate portfolio values over time
const portfolioData = useMemo(() => {
let value = startingAmount;
return breakdown.dailyReturns.map(day => {
value = value * day.multiplier;
return {
date: new Date(day.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
value: Math.round(value),
return: day.return,
cumulative: day.cumulative,
};
});
}, [breakdown.dailyReturns, startingAmount]);
const currentValue = portfolioData.length > 0
? portfolioData[portfolioData.length - 1].value
: startingAmount;
const totalReturn = ((currentValue - startingAmount) / startingAmount) * 100;
const profitLoss = currentValue - startingAmount;
const isPositive = profitLoss >= 0;
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value.replace(/,/g, ''), 10);
if (!isNaN(value) && value >= 0) {
setStartingAmount(value);
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
maximumFractionDigits: 0,
}).format(value);
};
return (
<div className={`card p-4 ${className}`}>
<div className="flex items-center gap-2 mb-4">
<Calculator className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Portfolio Simulator</h2>
</div>
{/* Input Section */}
<div className="mb-4">
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">
Starting Investment
</label>
<div className="relative">
<IndianRupee className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={startingAmount.toLocaleString('en-IN')}
onChange={handleAmountChange}
className="w-full pl-9 pr-4 py-2 rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
/>
</div>
<div className="flex gap-2 mt-2">
{[10000, 50000, 100000, 500000].map(amount => (
<button
key={amount}
onClick={() => setStartingAmount(amount)}
className={`px-2 py-1 text-xs rounded ${
startingAmount === amount
? 'bg-nifty-600 text-white'
: 'bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-600'
}`}
>
{formatCurrency(amount)}
</button>
))}
</div>
</div>
{/* Results Section */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Current Value</div>
<div className={`text-xl font-bold ${isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{formatCurrency(currentValue)}
</div>
</div>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Profit/Loss</div>
<div className={`text-xl font-bold ${isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{isPositive ? '+' : ''}{formatCurrency(profitLoss)}
<span className="text-sm ml-1">({isPositive ? '+' : ''}{totalReturn.toFixed(1)}%)</span>
</div>
</div>
</div>
{/* Chart */}
{portfolioData.length > 0 && (
<div className="h-40 mb-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={portfolioData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="date"
tick={{ fontSize: 10 }}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
tick={{ fontSize: 10 }}
tickFormatter={(v) => formatCurrency(v).replace('₹', '')}
className="text-gray-500 dark:text-gray-400"
width={60}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value) => [formatCurrency(value as number), 'Value']}
/>
<ReferenceLine
y={startingAmount}
stroke="#94a3b8"
strokeDasharray="5 5"
label={{ value: 'Start', fontSize: 10, fill: '#94a3b8' }}
/>
<Line
type="monotone"
dataKey="value"
stroke={isPositive ? '#22c55e' : '#ef4444'}
strokeWidth={2}
dot={{ fill: isPositive ? '#22c55e' : '#ef4444', r: 3 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Daily Breakdown (Collapsible) */}
<button
onClick={() => setShowBreakdown(!showBreakdown)}
className="flex items-center justify-between w-full px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-slate-700/50 rounded-lg transition-colors"
>
<span>Daily Breakdown</span>
{showBreakdown ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showBreakdown && (
<div className="mt-2 border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-slate-700">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">Date</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Return</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400">Value</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-slate-700">
{portfolioData.map((day, idx) => (
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-slate-700/50">
<td className="px-3 py-2 text-gray-700 dark:text-gray-300">{day.date}</td>
<td className={`px-3 py-2 text-right font-medium ${
day.return >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{day.return >= 0 ? '+' : ''}{day.return.toFixed(1)}%
</td>
<td className="px-3 py-2 text-right text-gray-700 dark:text-gray-300">
{formatCurrency(day.value)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-3 text-center">
Simulated returns based on AI recommendation performance. Past performance does not guarantee future results.
</p>
</div>
);
}

View File

@ -0,0 +1,118 @@
import { useState } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { X } from 'lucide-react';
import { getReturnDistribution } from '../data/recommendations';
interface ReturnDistributionChartProps {
height?: number;
className?: string;
}
export default function ReturnDistributionChart({ height = 200, className = '' }: ReturnDistributionChartProps) {
const [selectedBucket, setSelectedBucket] = useState<{ range: string; stocks: string[] } | null>(null);
const data = getReturnDistribution();
if (data.every(d => d.count === 0)) {
return (
<div className={`flex items-center justify-center text-gray-400 ${className}`} style={{ height }}>
No distribution data available
</div>
);
}
// Color gradient from red (negative) to green (positive)
const getBarColor = (range: string) => {
if (range.includes('< -3') || range.includes('-3 to -2')) return '#ef4444';
if (range.includes('-2 to -1')) return '#f87171';
if (range.includes('-1 to 0')) return '#fca5a5';
if (range.includes('0 to 1')) return '#86efac';
if (range.includes('1 to 2')) return '#4ade80';
if (range.includes('2 to 3') || range.includes('> 3')) return '#22c55e';
return '#94a3b8';
};
const handleBarClick = (data: { range: string; stocks: string[] }) => {
if (data.stocks.length > 0) {
setSelectedBucket(data);
}
};
return (
<div className={className}>
<div style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
dataKey="range"
tick={{ fontSize: 10 }}
angle={-45}
textAnchor="end"
height={60}
className="text-gray-500 dark:text-gray-400"
/>
<YAxis
tick={{ fontSize: 11 }}
className="text-gray-500 dark:text-gray-400"
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tooltip-bg, #fff)',
border: '1px solid var(--tooltip-border, #e5e7eb)',
borderRadius: '8px',
fontSize: '12px',
}}
formatter={(value) => [`${value} stocks`, 'Count']}
labelFormatter={(label) => `Return: ${label}`}
/>
<Bar
dataKey="count"
cursor="pointer"
onClick={(_data, index) => {
if (typeof index === 'number' && data[index]) {
handleBarClick(data[index]);
}
}}
fill="#0ea5e9"
shape={(props: { x: number; y: number; width: number; height: number; index?: number }) => {
const { x, y, width, height, index: idx } = props;
const fill = typeof idx === 'number' ? getBarColor(data[idx]?.range || '') : '#0ea5e9';
return <rect x={x} y={y} width={width} height={height} fill={fill} rx={2} />;
}}
/>
</BarChart>
</ResponsiveContainer>
</div>
{/* Selected bucket modal */}
{selectedBucket && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setSelectedBucket(null)} />
<div className="relative bg-white dark:bg-slate-800 rounded-xl shadow-xl max-w-sm w-full p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
Stocks with {selectedBucket.range} return
</h3>
<button
onClick={() => setSelectedBucket(null)}
className="p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700"
>
<X className="w-4 h-4 text-gray-500" />
</button>
</div>
<div className="flex flex-wrap gap-2">
{selectedBucket.stocks.map(symbol => (
<span
key={symbol}
className="px-2 py-1 text-xs font-medium bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-gray-300 rounded"
>
{symbol}
</span>
))}
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,219 @@
import { X, CheckCircle, XCircle, Calculator } from 'lucide-react';
import type { ReturnBreakdown } from '../data/recommendations';
interface ReturnExplainModalProps {
isOpen: boolean;
onClose: () => void;
breakdown: ReturnBreakdown | null;
date: string;
}
export default function ReturnExplainModal({ isOpen, onClose, breakdown, date }: ReturnExplainModalProps) {
if (!isOpen || !breakdown) return null;
const formattedDate = new Date(date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
});
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-slate-800 rounded-xl shadow-xl max-w-[95vw] sm:max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 flex items-center justify-between p-4 border-b border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<div className="flex items-center gap-2">
<Calculator className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Return Calculation
</h2>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-5">
{/* Date & Result */}
<div className="p-4 rounded-lg bg-nifty-50 dark:bg-nifty-900/20 border border-nifty-100 dark:border-nifty-800">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">{formattedDate}</div>
<div className={`text-3xl font-bold ${breakdown.weightedReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{breakdown.weightedReturn >= 0 ? '+' : ''}{breakdown.weightedReturn.toFixed(1)}%
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Weighted Average Return
</p>
</div>
{/* Method Explanation */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Calculation Method</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-sm space-y-2">
<p className="text-gray-700 dark:text-gray-300">
<strong>1. Correct Predictions</strong> Contribute <span className="text-green-600 dark:text-green-400">positively</span>
</p>
<ul className="text-xs text-gray-600 dark:text-gray-400 ml-4 space-y-1">
<li> BUY that went up add the gain</li>
<li> SELL that went down add the avoided loss</li>
<li> HOLD that stayed flat small positive</li>
</ul>
<p className="text-gray-700 dark:text-gray-300 mt-2">
<strong>2. Incorrect Predictions</strong> Contribute <span className="text-red-600 dark:text-red-400">negatively</span>
</p>
<ul className="text-xs text-gray-600 dark:text-gray-400 ml-4 space-y-1">
<li> BUY that went down subtract the loss</li>
<li> SELL that went up subtract missed gain</li>
<li> HOLD that moved subtract missed opportunity</li>
</ul>
<p className="text-gray-700 dark:text-gray-300 mt-2">
<strong>3. Weighted Average</strong>
</p>
<div className="p-2 bg-white dark:bg-slate-800 rounded border border-gray-200 dark:border-slate-600 font-mono text-xs">
(Correct Avg × Correct Weight) + (Incorrect Avg × Incorrect Weight)
</div>
</div>
</div>
{/* Correct Predictions Breakdown */}
<div>
<div className="flex items-center gap-2 mb-2">
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
<h3 className="font-semibold text-green-800 dark:text-green-300">Correct Predictions</h3>
<span className="text-xs text-gray-500 dark:text-gray-400">
({breakdown.correctPredictions.count} stocks)
</span>
</div>
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-800">
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<div className="text-xs text-gray-500 dark:text-gray-400">Average Return</div>
<div className="text-lg font-bold text-green-600 dark:text-green-400">
+{breakdown.correctPredictions.avgReturn.toFixed(1)}%
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400">Weight</div>
<div className="text-lg font-bold text-green-600 dark:text-green-400">
{breakdown.correctPredictions.count}/{breakdown.correctPredictions.count + breakdown.incorrectPredictions.count}
</div>
</div>
</div>
{breakdown.correctPredictions.stocks.length > 0 && (
<div className="border-t border-green-200 dark:border-green-700 pt-2">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Top performers:</div>
<div className="space-y-1">
{breakdown.correctPredictions.stocks.map((stock: { symbol: string; decision: string; return1d: number }) => (
<div key={stock.symbol} className="flex items-center justify-between text-xs">
<span className="font-medium text-gray-700 dark:text-gray-300">
{stock.symbol}
<span className={`ml-1 ${
stock.decision === 'BUY' ? 'text-green-600 dark:text-green-400' :
stock.decision === 'SELL' ? 'text-red-600 dark:text-red-400' :
'text-amber-600 dark:text-amber-400'
}`}>
({stock.decision})
</span>
</span>
<span className="text-green-600 dark:text-green-400">+{stock.return1d.toFixed(1)}%</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Incorrect Predictions Breakdown */}
<div>
<div className="flex items-center gap-2 mb-2">
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
<h3 className="font-semibold text-red-800 dark:text-red-300">Incorrect Predictions</h3>
<span className="text-xs text-gray-500 dark:text-gray-400">
({breakdown.incorrectPredictions.count} stocks)
</span>
</div>
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800">
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<div className="text-xs text-gray-500 dark:text-gray-400">Average Return</div>
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{breakdown.incorrectPredictions.avgReturn.toFixed(1)}%
</div>
</div>
<div>
<div className="text-xs text-gray-500 dark:text-gray-400">Weight</div>
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{breakdown.incorrectPredictions.count}/{breakdown.correctPredictions.count + breakdown.incorrectPredictions.count}
</div>
</div>
</div>
{breakdown.incorrectPredictions.stocks.length > 0 && (
<div className="border-t border-red-200 dark:border-red-700 pt-2">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Worst performers:</div>
<div className="space-y-1">
{breakdown.incorrectPredictions.stocks.map((stock: { symbol: string; decision: string; return1d: number }) => (
<div key={stock.symbol} className="flex items-center justify-between text-xs">
<span className="font-medium text-gray-700 dark:text-gray-300">
{stock.symbol}
<span className={`ml-1 ${
stock.decision === 'BUY' ? 'text-green-600 dark:text-green-400' :
stock.decision === 'SELL' ? 'text-red-600 dark:text-red-400' :
'text-amber-600 dark:text-amber-400'
}`}>
({stock.decision})
</span>
</span>
<span className="text-red-600 dark:text-red-400">{stock.return1d.toFixed(1)}%</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Final Calculation */}
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">Final Calculation</h3>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50">
<div className="font-mono text-xs text-gray-600 dark:text-gray-400 break-all">
{breakdown.formula}
</div>
</div>
</div>
{/* Disclaimer */}
<div className="p-3 rounded-lg bg-gray-100 dark:bg-slate-700/30 border border-gray-200 dark:border-slate-600">
<p className="text-xs text-gray-500 dark:text-gray-400">
<strong>Note:</strong> This weighted return represents the theoretical gain/loss
if you followed all predictions for the day. Actual results may vary based on
execution timing, transaction costs, and market conditions.
</p>
</div>
</div>
{/* Footer */}
<div className="sticky bottom-0 p-4 border-t border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800">
<button
onClick={onClose}
className="w-full btn-primary"
>
Got it
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,101 @@
import { HelpCircle, TrendingUp, TrendingDown, Activity, Target } from 'lucide-react';
import { calculateRiskMetrics } from '../data/recommendations';
import { useState } from 'react';
interface RiskMetricsCardProps {
className?: string;
}
export default function RiskMetricsCard({ className = '' }: RiskMetricsCardProps) {
const [showTooltip, setShowTooltip] = useState<string | null>(null);
const metrics = calculateRiskMetrics();
const tooltips: Record<string, string> = {
sharpe: 'Sharpe Ratio measures risk-adjusted returns. Higher is better (>1 is good, >2 is excellent).',
drawdown: 'Maximum Drawdown shows the largest peak-to-trough decline. Lower is better.',
winloss: 'Win/Loss Ratio compares average winning trade to average losing trade. Higher means bigger wins than losses.',
winrate: 'Win Rate is the percentage of predictions that were correct.',
};
const getColor = (metric: string, value: number) => {
switch (metric) {
case 'sharpe':
return value >= 1 ? 'text-green-600 dark:text-green-400' : value >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400';
case 'drawdown':
return value <= 5 ? 'text-green-600 dark:text-green-400' : value <= 15 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400';
case 'winloss':
return value >= 1.5 ? 'text-green-600 dark:text-green-400' : value >= 1 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400';
case 'winrate':
return value >= 70 ? 'text-green-600 dark:text-green-400' : value >= 50 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400';
default:
return 'text-gray-700 dark:text-gray-300';
}
};
const cards = [
{
id: 'sharpe',
label: 'Sharpe Ratio',
value: metrics.sharpeRatio.toFixed(2),
icon: Activity,
color: getColor('sharpe', metrics.sharpeRatio),
},
{
id: 'drawdown',
label: 'Max Drawdown',
value: `${metrics.maxDrawdown.toFixed(1)}%`,
icon: TrendingDown,
color: getColor('drawdown', metrics.maxDrawdown),
},
{
id: 'winloss',
label: 'Win/Loss Ratio',
value: metrics.winLossRatio.toFixed(2),
icon: TrendingUp,
color: getColor('winloss', metrics.winLossRatio),
},
{
id: 'winrate',
label: 'Win Rate',
value: `${metrics.winRate}%`,
icon: Target,
color: getColor('winrate', metrics.winRate),
},
];
return (
<div className={`grid grid-cols-2 sm:grid-cols-4 gap-3 ${className}`}>
{cards.map((card) => {
const Icon = card.icon;
return (
<div
key={card.id}
className="relative p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center group"
>
<div className="flex items-center justify-center gap-1 mb-1">
<Icon className={`w-4 h-4 ${card.color}`} />
<span className={`text-xl font-bold ${card.color}`}>{card.value}</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="text-xs text-gray-500 dark:text-gray-400">{card.label}</span>
<button
onClick={() => setShowTooltip(showTooltip === card.id ? null : card.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
<HelpCircle className="w-3 h-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" />
</button>
</div>
{/* Tooltip */}
{showTooltip === card.id && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-xs rounded-lg shadow-lg z-10 w-48">
{tooltips[card.id]}
<div className="absolute top-full left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900 dark:border-t-gray-100" />
</div>
)}
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,65 @@
import { LineChart, Line, ResponsiveContainer, YAxis } from 'recharts';
import type { PricePoint } from '../types';
interface SparklineProps {
data: PricePoint[];
width?: number;
height?: number;
positive?: boolean;
className?: string;
}
export default function Sparkline({
data,
width = 80,
height = 24,
positive = true,
className = '',
}: SparklineProps) {
if (!data || data.length < 2) {
return (
<div
className={`flex items-center justify-center text-gray-300 dark:text-gray-600 ${className}`}
style={{ width, height }}
>
<span className="text-[10px]">No data</span>
</div>
);
}
// Normalize data to percentage change from first point for better visual variation
const basePrice = data[0].price;
const normalizedData = data.map(point => ({
...point,
normalizedPrice: ((point.price - basePrice) / basePrice) * 100,
}));
// Calculate min/max for domain padding
const prices = normalizedData.map(d => d.normalizedPrice);
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const padding = Math.max(Math.abs(maxPrice - minPrice) * 0.15, 0.5);
const color = positive ? '#22c55e' : '#ef4444';
return (
<div className={className} style={{ width, height, minWidth: width, minHeight: height }}>
<ResponsiveContainer width="100%" height="100%" minWidth={width} minHeight={height}>
<LineChart data={normalizedData} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
<YAxis
domain={[minPrice - padding, maxPrice + padding]}
hide
/>
<Line
type="monotone"
dataKey="normalizedPrice"
stroke={color}
strokeWidth={1.5}
dot={false}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -5,34 +5,39 @@ import type { StockAnalysis, Decision } from '../types';
interface StockCardProps {
stock: StockAnalysis;
showDetails?: boolean;
compact?: boolean;
}
export function DecisionBadge({ decision }: { decision: Decision | null }) {
export function DecisionBadge({ decision, size = 'default' }: { decision: Decision | null; size?: 'small' | 'default' }) {
if (!decision) return null;
const config = {
BUY: {
bg: 'bg-green-100',
text: 'text-green-800',
bg: 'bg-green-100 dark:bg-green-900/30',
text: 'text-green-800 dark:text-green-400',
icon: TrendingUp,
},
SELL: {
bg: 'bg-red-100',
text: 'text-red-800',
bg: 'bg-red-100 dark:bg-red-900/30',
text: 'text-red-800 dark:text-red-400',
icon: TrendingDown,
},
HOLD: {
bg: 'bg-amber-100',
text: 'text-amber-800',
bg: 'bg-amber-100 dark:bg-amber-900/30',
text: 'text-amber-800 dark:text-amber-400',
icon: Minus,
},
};
const { bg, text, icon: Icon } = config[decision];
const sizeClasses = size === 'small'
? 'px-2 py-0.5 text-xs gap-1'
: 'px-2.5 py-0.5 text-xs gap-1';
const iconSize = size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5';
return (
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold ${bg} ${text}`}>
<Icon className="w-4 h-4" />
<span className={`inline-flex items-center rounded-full font-semibold ${bg} ${text} ${sizeClasses}`}>
<Icon className={iconSize} />
{decision}
</span>
);
@ -42,9 +47,9 @@ export function ConfidenceBadge({ confidence }: { confidence?: string }) {
if (!confidence) return null;
const colors = {
HIGH: 'bg-green-50 text-green-700 border-green-200',
MEDIUM: 'bg-amber-50 text-amber-700 border-amber-200',
LOW: 'bg-gray-50 text-gray-700 border-gray-200',
HIGH: 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800',
MEDIUM: 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-800',
LOW: 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600',
};
return (
@ -58,9 +63,9 @@ export function RiskBadge({ risk }: { risk?: string }) {
if (!risk) return null;
const colors = {
HIGH: 'text-red-600',
MEDIUM: 'text-amber-600',
LOW: 'text-green-600',
HIGH: 'text-red-600 dark:text-red-400',
MEDIUM: 'text-amber-600 dark:text-amber-400',
LOW: 'text-green-600 dark:text-green-400',
};
return (
@ -70,26 +75,51 @@ export function RiskBadge({ risk }: { risk?: string }) {
);
}
export default function StockCard({ stock, showDetails = true }: StockCardProps) {
export default function StockCard({ stock, showDetails = true, compact = false }: StockCardProps) {
if (compact) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group focus:outline-none focus:bg-nifty-50 dark:focus:bg-nifty-900/30"
role="listitem"
aria-label={`${stock.symbol} - ${stock.company_name} - ${stock.decision} recommendation`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} aria-hidden="true" />
<span className="font-medium text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 text-xs hidden sm:inline" aria-hidden="true">·</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate hidden sm:inline">{stock.company_name}</span>
</div>
<div className="flex items-center gap-2">
<DecisionBadge decision={stock.decision} />
<ChevronRight className="w-4 h-4 text-gray-300 dark:text-gray-600 group-hover:text-nifty-600 dark:group-hover:text-nifty-400 transition-colors" aria-hidden="true" />
</div>
</Link>
);
}
return (
<Link
to={`/stock/${stock.symbol}`}
className="card-hover p-4 flex items-center justify-between group"
className="card-hover p-3 flex items-center justify-between group"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1">
<h3 className="font-semibold text-gray-900 truncate">{stock.symbol}</h3>
<div className="flex items-center gap-2 mb-0.5">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm">{stock.symbol}</h3>
<DecisionBadge decision={stock.decision} />
</div>
<p className="text-sm text-gray-500 truncate">{stock.company_name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{stock.company_name}</p>
{showDetails && (
<div className="flex items-center gap-3 mt-2">
<div className="flex items-center gap-2 mt-1.5">
<ConfidenceBadge confidence={stock.confidence} />
<RiskBadge risk={stock.risk} />
</div>
)}
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors flex-shrink-0" />
<ChevronRight className="w-4 h-4 text-gray-400 dark:text-gray-500 group-hover:text-nifty-600 dark:group-hover:text-nifty-400 transition-colors flex-shrink-0" />
</Link>
);
}
@ -98,7 +128,7 @@ export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors"
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${
@ -106,9 +136,9 @@ export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} />
<div>
<span className="font-medium text-gray-900">{stock.symbol}</span>
<span className="text-gray-400 mx-2">·</span>
<span className="text-sm text-gray-500">{stock.company_name}</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 mx-2">·</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{stock.company_name}</span>
</div>
</div>
<DecisionBadge decision={stock.decision} />

View File

@ -0,0 +1,265 @@
import { useMemo } from 'react';
import {
ComposedChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
} from 'recharts';
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import type { PricePoint, Decision } from '../types';
interface PredictionPoint {
date: string;
decision: Decision;
price?: number;
}
interface StockPriceChartProps {
priceHistory: PricePoint[];
predictions?: PredictionPoint[];
symbol: string;
showArea?: boolean;
}
// Custom tooltip component
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-600 rounded-lg shadow-lg p-3">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
{new Date(label).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</p>
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{data.price.toLocaleString('en-IN', { minimumFractionDigits: 2 })}
</p>
{data.prediction && (
<div className={`mt-1 text-xs font-medium flex items-center gap-1 ${
data.prediction === 'BUY' ? 'text-green-600 dark:text-green-400' :
data.prediction === 'SELL' ? 'text-red-600 dark:text-red-400' :
'text-amber-600 dark:text-amber-400'
}`}>
{data.prediction === 'BUY' && <TrendingUp className="w-3 h-3" />}
{data.prediction === 'SELL' && <TrendingDown className="w-3 h-3" />}
{data.prediction === 'HOLD' && <Minus className="w-3 h-3" />}
AI: {data.prediction}
</div>
)}
</div>
);
}
return null;
};
// Custom prediction marker component with arrow symbols
const PredictionMarker = (props: any) => {
const { cx, cy, payload } = props;
if (!payload?.prediction || cx === undefined || cy === undefined) return null;
const colors = {
BUY: { fill: '#22c55e', stroke: '#16a34a' },
SELL: { fill: '#ef4444', stroke: '#dc2626' },
HOLD: { fill: '#f59e0b', stroke: '#d97706' },
};
const color = colors[payload.prediction as Decision] || colors.HOLD;
// Render different shapes based on prediction type
if (payload.prediction === 'BUY') {
// Up arrow
return (
<g>
<circle cx={cx} cy={cy} r={10} fill={color.fill} fillOpacity={0.2} />
<path
d={`M ${cx} ${cy - 6} L ${cx + 5} ${cy + 2} L ${cx + 2} ${cy + 2} L ${cx + 2} ${cy + 6} L ${cx - 2} ${cy + 6} L ${cx - 2} ${cy + 2} L ${cx - 5} ${cy + 2} Z`}
fill={color.fill}
stroke={color.stroke}
strokeWidth={1}
/>
</g>
);
} else if (payload.prediction === 'SELL') {
// Down arrow
return (
<g>
<circle cx={cx} cy={cy} r={10} fill={color.fill} fillOpacity={0.2} />
<path
d={`M ${cx} ${cy + 6} L ${cx + 5} ${cy - 2} L ${cx + 2} ${cy - 2} L ${cx + 2} ${cy - 6} L ${cx - 2} ${cy - 6} L ${cx - 2} ${cy - 2} L ${cx - 5} ${cy - 2} Z`}
fill={color.fill}
stroke={color.stroke}
strokeWidth={1}
/>
</g>
);
} else {
// Equal/minus sign for HOLD
return (
<g>
<circle cx={cx} cy={cy} r={10} fill={color.fill} fillOpacity={0.2} />
<rect x={cx - 5} y={cy - 4} width={10} height={2.5} fill={color.fill} rx={1} />
<rect x={cx - 5} y={cy + 1.5} width={10} height={2.5} fill={color.fill} rx={1} />
</g>
);
}
};
export default function StockPriceChart({
priceHistory,
predictions = [],
symbol,
showArea = true,
}: StockPriceChartProps) {
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === 'dark';
// Theme-aware colors
const gridColor = isDark ? '#475569' : '#e5e7eb';
const tickColor = isDark ? '#94a3b8' : '#6b7280';
// Merge price history with predictions
const chartData = useMemo(() => {
const predictionMap = new Map(
predictions.map(p => [p.date, p.decision])
);
return priceHistory.map(point => ({
...point,
prediction: predictionMap.get(point.date) || null,
}));
}, [priceHistory, predictions]);
// Calculate price range for Y-axis
const { minPrice, maxPrice } = useMemo(() => {
const prices = priceHistory.map(p => p.price);
const min = Math.min(...prices);
const max = Math.max(...prices);
const padding = (max - min) * 0.1;
return {
minPrice: Math.floor(min - padding),
maxPrice: Math.ceil(max + padding),
};
}, [priceHistory]);
// Calculate overall trend
const trend = useMemo(() => {
if (priceHistory.length < 2) return 'flat';
const first = priceHistory[0].price;
const last = priceHistory[priceHistory.length - 1].price;
const change = ((last - first) / first) * 100;
return change > 0 ? 'up' : change < 0 ? 'down' : 'flat';
}, [priceHistory]);
const trendColor = trend === 'up' ? '#22c55e' : trend === 'down' ? '#ef4444' : '#6b7280';
const gradientId = `gradient-${symbol}`;
if (priceHistory.length === 0) {
return (
<div className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
No price data available
</div>
);
}
// Background color based on theme
const chartBgColor = isDark ? '#1e293b' : '#ffffff';
return (
<div className="w-full" style={{ backgroundColor: chartBgColor }}>
<ResponsiveContainer width="100%" height={280} minWidth={200} minHeight={200}>
<ComposedChart
data={chartData}
margin={{ top: 20, right: 20, left: 10, bottom: 20 }}
style={{ backgroundColor: 'transparent' }}
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={trendColor} stopOpacity={0.3} />
<stop offset="95%" stopColor={trendColor} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke={gridColor}
strokeOpacity={0.5}
vertical={false}
/>
<XAxis
dataKey="date"
tick={{ fontSize: 10, fill: tickColor }}
tickLine={false}
axisLine={false}
tickFormatter={(date) => new Date(date).toLocaleDateString('en-IN', {
month: 'short',
day: 'numeric',
})}
interval="preserveStartEnd"
minTickGap={50}
/>
<YAxis
domain={[minPrice, maxPrice]}
tick={{ fontSize: 10, fill: tickColor }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
width={60}
/>
<Tooltip content={<CustomTooltip />} />
{showArea && (
<Area
type="monotone"
dataKey="price"
stroke="transparent"
fill={`url(#${gradientId})`}
/>
)}
<Line
type="monotone"
dataKey="price"
stroke={trendColor}
strokeWidth={2}
dot={(props: any) => {
const { payload, cx, cy } = props;
if (payload?.prediction && cx !== undefined && cy !== undefined) {
return <PredictionMarker cx={cx} cy={cy} payload={payload} />;
}
return <g />; // Return empty group for non-prediction points
}}
activeDot={{ r: 4, fill: trendColor }}
isAnimationActive={false}
/>
</ComposedChart>
</ResponsiveContainer>
{/* Legend */}
<div className="flex items-center justify-center gap-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-1.5">
<TrendingUp className="w-4 h-4 text-green-500" />
<span>BUY Signal</span>
</div>
<div className="flex items-center gap-1.5">
<Minus className="w-4 h-4 text-amber-500" />
<span>HOLD Signal</span>
</div>
<div className="flex items-center gap-1.5">
<TrendingDown className="w-4 h-4 text-red-500" />
<span>SELL Signal</span>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,61 @@
import { Sun, Moon, Monitor } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
interface ThemeToggleProps {
compact?: boolean;
}
export default function ThemeToggle({ compact = false }: ThemeToggleProps) {
const { theme, setTheme } = useTheme();
const themes = [
{ value: 'light' as const, icon: Sun, label: 'Light' },
{ value: 'dark' as const, icon: Moon, label: 'Dark' },
{ value: 'system' as const, icon: Monitor, label: 'System' },
];
if (compact) {
// Simple cycling button for mobile
const currentIndex = themes.findIndex(t => t.value === theme);
const nextTheme = themes[(currentIndex + 1) % themes.length];
const CurrentIcon = themes[currentIndex].icon;
return (
<button
onClick={() => setTheme(nextTheme.value)}
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
aria-label={`Current theme: ${theme}. Click to switch to ${nextTheme.label}`}
>
<CurrentIcon className="w-4 h-4" />
</button>
);
}
return (
<div
className="flex items-center gap-0.5 p-0.5 bg-gray-100 dark:bg-slate-700 rounded-lg"
role="radiogroup"
aria-label="Theme selection"
>
{themes.map(({ value, icon: Icon, label }) => {
const isActive = theme === value;
return (
<button
key={value}
onClick={() => setTheme(value)}
role="radio"
aria-checked={isActive}
aria-label={label}
className={`p-1.5 rounded-md transition-all ${
isActive
? 'bg-white dark:bg-slate-600 text-nifty-600 dark:text-nifty-400 shadow-sm'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
}`}
>
<Icon className="w-3.5 h-3.5" />
</button>
);
})}
</div>
);
}

View File

@ -1,6 +1,8 @@
import { Link } from 'react-router-dom';
import { Trophy, TrendingUp, AlertTriangle, ChevronRight } from 'lucide-react';
import { Trophy, AlertTriangle, TrendingUp, TrendingDown } from 'lucide-react';
import type { TopPick, StockToAvoid } from '../types';
import BackgroundSparkline from './BackgroundSparkline';
import { getBacktestResult } from '../data/recommendations';
interface TopPicksProps {
picks: TopPick[];
@ -8,54 +10,61 @@ interface TopPicksProps {
export default function TopPicks({ picks }: TopPicksProps) {
const medals = ['🥇', '🥈', '🥉'];
const bgColors = [
'bg-gradient-to-br from-amber-50 to-yellow-50 border-amber-200',
'bg-gradient-to-br from-gray-50 to-slate-50 border-gray-200',
'bg-gradient-to-br from-orange-50 to-amber-50 border-orange-200',
];
return (
<div className="card p-6">
<div className="flex items-center gap-2 mb-6">
<Trophy className="w-6 h-6 text-amber-500" />
<h2 className="section-title">Top Picks</h2>
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Trophy className="w-5 h-5 text-amber-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Top Picks</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">({picks.length})</span>
</div>
<div className="grid gap-4">
{picks.map((pick, index) => (
<Link
key={pick.symbol}
to={`/stock/${pick.symbol}`}
className={`block p-4 rounded-xl border-2 ${bgColors[index]} hover:shadow-md transition-all group`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<span className="text-2xl">{medals[index]}</span>
<div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{picks.map((pick, index) => {
const backtest = getBacktestResult(pick.symbol);
return (
<Link
key={pick.symbol}
to={`/stock/${pick.symbol}`}
className="card-hover p-3 group bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800 relative overflow-hidden"
>
{/* Background Chart */}
{backtest && (
<div className="absolute inset-0 opacity-[0.08]">
<BackgroundSparkline
data={backtest.price_history}
trend="up"
/>
</div>
)}
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg text-gray-900">{pick.symbol}</h3>
<span className="badge-buy">
<TrendingUp className="w-3 h-3 mr-1" />
{pick.decision}
</span>
<span className="text-lg">{medals[index]}</span>
<span className="font-bold text-gray-900 dark:text-gray-100">{pick.symbol}</span>
</div>
<p className="text-sm text-gray-600">{pick.company_name}</p>
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500 text-white text-xs font-medium">
<TrendingUp className="w-3 h-3" />
BUY
</div>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">{pick.reason}</p>
<div className="flex items-center justify-between">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
pick.risk_level === 'LOW' ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400' :
pick.risk_level === 'HIGH' ? 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400' :
'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400'
}`}>
{pick.risk_level} Risk
</span>
<span className="text-xs text-green-600 dark:text-green-400 font-medium group-hover:underline">View </span>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors" />
</div>
<p className="text-sm text-gray-700 mt-3 leading-relaxed">{pick.reason}</p>
<div className="mt-3 flex items-center gap-2">
<span className={`text-xs px-2 py-1 rounded-full ${
pick.risk_level === 'LOW' ? 'bg-green-100 text-green-700' :
pick.risk_level === 'HIGH' ? 'bg-red-100 text-red-700' :
'bg-amber-100 text-amber-700'
}`}>
{pick.risk_level} Risk
</span>
</div>
</Link>
))}
</Link>
);
})}
</div>
</div>
);
@ -67,29 +76,47 @@ interface StocksToAvoidProps {
export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
return (
<div className="card p-6">
<div className="flex items-center gap-2 mb-6">
<AlertTriangle className="w-6 h-6 text-red-500" />
<h2 className="section-title">Stocks to Avoid</h2>
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Stocks to Avoid</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">({stocks.length})</span>
</div>
<div className="space-y-3">
{stocks.map((stock) => (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="block p-4 rounded-lg bg-red-50 border border-red-100 hover:bg-red-100 transition-colors group"
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="font-bold text-red-800">{stock.symbol}</span>
<span className="badge-sell text-xs">SELL</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{stocks.map((stock) => {
const backtest = getBacktestResult(stock.symbol);
return (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="card-hover p-3 group bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-900/20 dark:to-rose-900/20 border-red-200 dark:border-red-800 relative overflow-hidden"
>
{/* Background Chart */}
{backtest && (
<div className="absolute inset-0 opacity-[0.08]">
<BackgroundSparkline
data={backtest.price_history}
trend="down"
/>
</div>
)}
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-500 text-white text-xs font-medium">
<TrendingDown className="w-3 h-3" />
SELL
</div>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">{stock.reason}</p>
<span className="text-xs text-red-600 dark:text-red-400 font-medium group-hover:underline">View </span>
</div>
<ChevronRight className="w-4 h-4 text-red-400 group-hover:text-red-600" />
</div>
<p className="text-sm text-red-700">{stock.reason}</p>
</Link>
))}
</Link>
);
})}
</div>
</div>
);

View File

@ -0,0 +1,82 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const STORAGE_KEY = 'nifty50-theme';
function getSystemTheme(): 'light' | 'dark' {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function getStoredTheme(): Theme {
if (typeof window === 'undefined') return 'system';
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
return 'system';
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(
theme === 'system' ? getSystemTheme() : theme
);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
};
// Update resolved theme when theme or system preference changes
useEffect(() => {
const updateResolvedTheme = () => {
const resolved = theme === 'system' ? getSystemTheme() : theme;
setResolvedTheme(resolved);
// Update document class
const root = document.documentElement;
if (resolved === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
updateResolvedTheme();
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (theme === 'system') {
updateResolvedTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

File diff suppressed because it is too large Load Diff

View File

@ -35,10 +35,18 @@
scroll-behavior: smooth;
}
html.dark {
color-scheme: dark;
}
body {
@apply font-sans antialiased bg-gray-50 text-gray-900;
margin: 0;
}
html.dark body {
@apply bg-slate-900 text-gray-100;
}
}
@layer components {
@ -46,8 +54,20 @@
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden;
}
html.dark .card {
@apply bg-slate-800 border-slate-700;
}
.card-hover {
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-200 hover:shadow-md hover:border-gray-200;
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-200 hover:shadow-md hover:border-gray-200 focus-within:ring-2 focus-within:ring-nifty-500 focus-within:ring-offset-1;
}
html.dark .card-hover {
@apply bg-slate-800 border-slate-700 hover:border-slate-600;
}
html.dark .card-hover:focus-within {
@apply ring-offset-slate-900;
}
.btn {
@ -58,10 +78,18 @@
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200 bg-nifty-600 text-white hover:bg-nifty-700 focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2;
}
html.dark .btn-primary {
@apply focus:ring-offset-slate-900;
}
.btn-secondary {
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200 bg-gray-100 text-gray-700 hover:bg-gray-200;
}
html.dark .btn-secondary {
@apply bg-slate-700 text-gray-200 hover:bg-slate-600;
}
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
@ -70,25 +98,108 @@
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-bull-light text-bull-dark;
}
html.dark .badge-buy {
@apply bg-green-900/30 text-green-400;
}
.badge-sell {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-bear-light text-bear-dark;
}
html.dark .badge-sell {
@apply bg-red-900/30 text-red-400;
}
.badge-hold {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-hold-light text-hold-dark;
}
html.dark .badge-hold {
@apply bg-amber-900/30 text-amber-400;
}
.gradient-text {
@apply bg-gradient-to-r from-nifty-600 to-nifty-800 bg-clip-text text-transparent;
}
html.dark .gradient-text {
@apply from-nifty-400 to-nifty-600;
}
.section-title {
@apply text-2xl font-display font-semibold text-gray-900;
}
html.dark .section-title {
@apply text-gray-100;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
/* Mobile touch target - minimum 44px for accessibility */
.touch-target {
min-height: 44px;
min-width: 44px;
}
/* Animation utilities */
.animate-in {
animation: animate-in 0.2s ease-out;
}
.slide-in-from-top-2 {
--tw-enter-translate-y: -0.5rem;
}
@keyframes animate-in {
from {
opacity: 0;
transform: translateY(var(--tw-enter-translate-y, 0));
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Smooth scrollbar styling */
.scroll-smooth {
scroll-behavior: smooth;
}
/* Recharts dark mode fix */
.recharts-wrapper,
.recharts-surface {
background-color: transparent !important;
}
/* Custom scrollbar for stock lists */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: transparent;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
html.dark .overflow-y-auto::-webkit-scrollbar-thumb {
background: #475569;
}
html.dark .overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
}

View File

@ -0,0 +1,267 @@
import { Link } from 'react-router-dom';
import { ArrowLeft, Brain, BarChart2, TrendingUp, MessageSquare, Shield, Database, Sparkles, Target, Users, Zap, Clock } from 'lucide-react';
const agents = [
{
name: 'Technical Analyst',
icon: BarChart2,
color: 'from-purple-500 to-purple-600',
bgColor: 'bg-purple-50 dark:bg-purple-900/20',
description: 'Analyzes price charts, volume patterns, and technical indicators like RSI, MACD, Bollinger Bands, and moving averages to identify trends and momentum.',
capabilities: ['Chart pattern recognition', 'Support/resistance levels', 'Momentum indicators', 'Volume analysis'],
},
{
name: 'Fundamental Analyst',
icon: TrendingUp,
color: 'from-green-500 to-green-600',
bgColor: 'bg-green-50 dark:bg-green-900/20',
description: 'Evaluates company financials, earnings reports, P/E ratios, debt levels, and industry position to assess intrinsic value.',
capabilities: ['Earnings analysis', 'Valuation metrics', 'Financial health', 'Growth trajectory'],
},
{
name: 'Sentiment Analyst',
icon: MessageSquare,
color: 'from-amber-500 to-amber-600',
bgColor: 'bg-amber-50 dark:bg-amber-900/20',
description: 'Monitors news sentiment, social media trends, analyst ratings, and market psychology to gauge investor sentiment.',
capabilities: ['News sentiment', 'Social media trends', 'Analyst ratings', 'Market psychology'],
},
{
name: 'Risk Manager',
icon: Shield,
color: 'from-red-500 to-red-600',
bgColor: 'bg-red-50 dark:bg-red-900/20',
description: 'Assesses volatility, sector risks, market conditions, and potential downsides to determine appropriate risk levels.',
capabilities: ['Volatility assessment', 'Sector correlation', 'Downside risk', 'Position sizing'],
},
];
const features = [
{
icon: Users,
title: 'Multi-Agent Collaboration',
description: 'Multiple specialized AI agents work together, each bringing unique expertise to the analysis.',
},
{
icon: Brain,
title: 'AI Debate System',
description: 'Agents debate their findings, challenge assumptions, and reach consensus through reasoned discussion.',
},
{
icon: Database,
title: 'Real-Time Data',
description: 'Analysis is based on current market data, company financials, and news from reliable sources.',
},
{
icon: Target,
title: 'Clear Recommendations',
description: 'Final decisions are clear BUY, SELL, or HOLD with confidence levels and risk assessments.',
},
];
const dataFlow = [
{ step: 1, title: 'Data Collection', description: 'Market data, financials, and news are gathered', icon: Database },
{ step: 2, title: 'Independent Analysis', description: 'Each agent analyzes data from their perspective', icon: BarChart2 },
{ step: 3, title: 'AI Debate', description: 'Agents discuss and challenge findings', icon: MessageSquare },
{ step: 4, title: 'Consensus Decision', description: 'Final recommendation with confidence rating', icon: Target },
];
export default function About() {
return (
<div className="space-y-6">
{/* Back Button */}
<Link
to="/"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-nifty-600 dark:hover:text-nifty-400 transition-colors"
>
<ArrowLeft className="w-3.5 h-3.5" />
Back to Dashboard
</Link>
{/* Hero Section */}
<section className="card overflow-hidden">
<div className="bg-gradient-to-r from-indigo-500 via-purple-500 to-nifty-600 p-6 text-white">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-white/20 rounded-xl">
<Sparkles className="w-8 h-8" />
</div>
<div>
<h1 className="text-2xl font-display font-bold">How TradingAgents Works</h1>
<p className="text-white/80 text-sm">AI-powered stock analysis for Nifty 50</p>
</div>
</div>
<p className="text-white/90 text-sm leading-relaxed max-w-2xl">
TradingAgents uses a team of specialized AI agents that analyze stocks from multiple perspectives,
debate their findings, and reach consensus recommendations. This multi-agent approach provides
more balanced and thoroughly reasoned analysis than any single model.
</p>
</div>
{/* Key Features */}
<div className="p-4 bg-white dark:bg-slate-800 grid grid-cols-2 md:grid-cols-4 gap-3">
{features.map((feature) => {
const Icon = feature.icon;
return (
<div key={feature.title} className="text-center p-3">
<div className="w-10 h-10 mx-auto mb-2 bg-nifty-100 dark:bg-nifty-900/30 rounded-xl flex items-center justify-center">
<Icon className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
</div>
<h3 className="font-semibold text-xs text-gray-900 dark:text-gray-100 mb-1">{feature.title}</h3>
<p className="text-[10px] text-gray-500 dark:text-gray-400 leading-relaxed">{feature.description}</p>
</div>
);
})}
</div>
</section>
{/* Analysis Flow */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-4">
<Zap className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Analysis Process</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{dataFlow.map((item, index) => {
const Icon = item.icon;
return (
<div key={item.step} className="relative">
<div className="p-3 rounded-lg border border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<div className="flex items-center gap-2 mb-2">
<span className="w-5 h-5 rounded-full bg-nifty-600 text-white text-xs font-bold flex items-center justify-center">
{item.step}
</span>
<Icon className="w-4 h-4 text-gray-400 dark:text-gray-500" />
</div>
<h3 className="font-semibold text-xs text-gray-900 dark:text-gray-100 mb-1">{item.title}</h3>
<p className="text-[10px] text-gray-500 dark:text-gray-400">{item.description}</p>
</div>
{index < dataFlow.length - 1 && (
<div className="hidden md:block absolute top-1/2 -right-2 w-4 text-gray-300 dark:text-gray-600">
</div>
)}
</div>
);
})}
</div>
</section>
{/* Agent Cards */}
<section>
<div className="flex items-center gap-2 mb-3">
<Brain className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Meet the AI Agents</h2>
</div>
<div className="grid md:grid-cols-2 gap-3">
{agents.map((agent) => {
const Icon = agent.icon;
return (
<div key={agent.name} className="card overflow-hidden">
<div className={`bg-gradient-to-r ${agent.color} p-3 text-white`}>
<div className="flex items-center gap-2">
<Icon className="w-5 h-5" />
<h3 className="font-semibold text-sm">{agent.name}</h3>
</div>
</div>
<div className={`p-3 ${agent.bgColor}`}>
<p className="text-xs text-gray-600 dark:text-gray-300 mb-2">{agent.description}</p>
<div className="flex flex-wrap gap-1.5">
{agent.capabilities.map((cap) => (
<span
key={cap}
className="text-[10px] px-2 py-0.5 bg-white dark:bg-slate-700 rounded-full text-gray-600 dark:text-gray-300"
>
{cap}
</span>
))}
</div>
</div>
</div>
);
})}
</div>
</section>
{/* Debate Section */}
<section className="card p-4 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-indigo-900/20 dark:to-purple-900/20 border-indigo-100 dark:border-indigo-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
<Brain className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h2 className="font-semibold text-gray-900 dark:text-gray-100">The AI Debate Process</h2>
<p className="text-xs text-gray-500 dark:text-gray-400">How agents reach consensus</p>
</div>
</div>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<p>
After each agent completes their analysis, they engage in a structured debate. The Technical
Analyst might argue for a BUY based on strong momentum, while the Risk Manager highlights
elevated volatility concerns.
</p>
<p>
Through multiple rounds of discussion, agents refine their positions, consider counterarguments,
and ultimately reach a consensus. This process mimics how investment committees at professional
firms make decisions.
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 italic">
The final recommendation reflects the collective intelligence of all agents, weighted by the
strength of their arguments and supporting evidence.
</p>
</div>
</section>
{/* Data Sources */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Database className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Data Sources</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div className="p-2 rounded-lg bg-gray-50 dark:bg-slate-700">
<span className="text-gray-500 dark:text-gray-400">Price Data:</span>
<span className="ml-1 text-gray-900 dark:text-gray-100">NSE/BSE</span>
</div>
<div className="p-2 rounded-lg bg-gray-50 dark:bg-slate-700">
<span className="text-gray-500 dark:text-gray-400">Financials:</span>
<span className="ml-1 text-gray-900 dark:text-gray-100">Quarterly Reports</span>
</div>
<div className="p-2 rounded-lg bg-gray-50 dark:bg-slate-700">
<span className="text-gray-500 dark:text-gray-400">News:</span>
<span className="ml-1 text-gray-900 dark:text-gray-100">Financial Media</span>
</div>
<div className="p-2 rounded-lg bg-gray-50 dark:bg-slate-700">
<span className="text-gray-500 dark:text-gray-400">Updates:</span>
<span className="ml-1 text-gray-900 dark:text-gray-100">Daily</span>
</div>
</div>
</section>
{/* Disclaimer */}
<section className="card p-4 bg-amber-50 dark:bg-amber-900/20 border-amber-100 dark:border-amber-800">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<div>
<h2 className="font-semibold text-amber-800 dark:text-amber-300 text-sm mb-1">Important Disclaimer</h2>
<p className="text-xs text-amber-700 dark:text-amber-400 leading-relaxed">
TradingAgents provides AI-generated stock analysis for educational and informational purposes only.
These recommendations do not constitute financial advice. Always conduct your own research and consult
with a qualified financial advisor before making investment decisions. Past performance does not
guarantee future results. Investing in stocks involves risk, including potential loss of principal.
</p>
</div>
</div>
</section>
{/* CTA */}
<Link
to="/"
className="card flex items-center justify-center p-4 bg-gradient-to-r from-nifty-600 to-nifty-700 text-white hover:from-nifty-700 hover:to-nifty-800 transition-all"
>
<span className="font-semibold">View Today's Recommendations </span>
</Link>
</div>
);
}

View File

@ -1,185 +1,239 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, RefreshCw, Filter, ChevronRight, PieChart } from 'lucide-react';
import SummaryStats from '../components/SummaryStats';
import { Calendar, RefreshCw, Filter, ChevronRight, TrendingUp, TrendingDown, Minus, History, Search, X } from 'lucide-react';
import TopPicks, { StocksToAvoid } from '../components/TopPicks';
import StockCard from '../components/StockCard';
import { SummaryPieChart } from '../components/Charts';
import { getLatestRecommendation } from '../data/recommendations';
import type { Decision } from '../types';
import { DecisionBadge } from '../components/StockCard';
import HowItWorks from '../components/HowItWorks';
import BackgroundSparkline from '../components/BackgroundSparkline';
import { getLatestRecommendation, getBacktestResult } from '../data/recommendations';
import type { Decision, StockAnalysis } from '../types';
type FilterType = 'ALL' | Decision;
export default function Dashboard() {
const recommendation = getLatestRecommendation();
const [filter, setFilter] = useState<FilterType>('ALL');
const [searchQuery, setSearchQuery] = useState('');
if (!recommendation) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="min-h-[40vh] flex items-center justify-center">
<div className="text-center">
<RefreshCw className="w-12 h-12 text-gray-300 mx-auto mb-4 animate-spin" />
<h2 className="text-xl font-semibold text-gray-700">Loading recommendations...</h2>
<RefreshCw className="w-10 h-10 text-gray-300 mx-auto mb-3 animate-spin" />
<h2 className="text-lg font-semibold text-gray-700">Loading recommendations...</h2>
</div>
</div>
);
}
const stocks = Object.values(recommendation.analysis);
const filteredStocks = filter === 'ALL'
? stocks
: stocks.filter(s => s.decision === filter);
const filteredStocks = useMemo(() => {
let result = filter === 'ALL' ? stocks : stocks.filter(s => s.decision === filter);
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
result = result.filter(s =>
s.symbol.toLowerCase().includes(query) ||
s.company_name.toLowerCase().includes(query)
);
}
return result;
}, [stocks, filter, searchQuery]);
const filterButtons: { label: string; value: FilterType; count: number }[] = [
{ label: 'All', value: 'ALL', count: stocks.length },
{ label: 'Buy', value: 'BUY', count: recommendation.summary.buy },
{ label: 'Sell', value: 'SELL', count: recommendation.summary.sell },
{ label: 'Hold', value: 'HOLD', count: recommendation.summary.hold },
];
const { buy, sell, hold, total } = recommendation.summary;
const buyPct = ((buy / total) * 100).toFixed(0);
const holdPct = ((hold / total) * 100).toFixed(0);
const sellPct = ((sell / total) * 100).toFixed(0);
return (
<div className="space-y-8">
{/* Hero Section */}
<section className="text-center py-8">
<h1 className="text-4xl md:text-5xl font-display font-bold text-gray-900 mb-4">
Nifty 50 <span className="gradient-text">AI Recommendations</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
AI-powered daily stock analysis for all Nifty 50 stocks. Get actionable buy, sell, and hold recommendations.
</p>
<div className="flex items-center justify-center gap-2 mt-4 text-sm text-gray-500">
<Calendar className="w-4 h-4" />
<span>Last updated: {new Date(recommendation.date).toLocaleDateString('en-IN', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}</span>
<div className="space-y-4">
{/* Compact Header with Stats */}
<section className="card p-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-display font-bold text-gray-900 dark:text-gray-100">
Nifty 50 <span className="gradient-text">AI Recommendations</span>
</h1>
<div className="flex items-center gap-2 mt-1 text-sm text-gray-500 dark:text-gray-400">
<Calendar className="w-3.5 h-3.5" />
<span>{new Date(recommendation.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
})}</span>
</div>
</div>
{/* Inline Stats */}
<div className="flex items-center gap-3" role="group" aria-label="Summary statistics">
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-green-50 dark:bg-green-900/30 rounded-lg cursor-pointer hover:bg-green-100 dark:hover:bg-green-900/50 transition-colors" onClick={() => setFilter('BUY')} title="Click to filter Buy stocks">
<TrendingUp className="w-4 h-4 text-green-600 dark:text-green-400" aria-hidden="true" />
<span className="font-bold text-green-700 dark:text-green-400">{buy}</span>
<span className="text-xs text-green-600 dark:text-green-400">Buy ({buyPct}%)</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-amber-50 dark:bg-amber-900/30 rounded-lg cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors" onClick={() => setFilter('HOLD')} title="Click to filter Hold stocks">
<Minus className="w-4 h-4 text-amber-600 dark:text-amber-400" aria-hidden="true" />
<span className="font-bold text-amber-700 dark:text-amber-400">{hold}</span>
<span className="text-xs text-amber-600 dark:text-amber-400">Hold ({holdPct}%)</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-red-50 dark:bg-red-900/30 rounded-lg cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/50 transition-colors" onClick={() => setFilter('SELL')} title="Click to filter Sell stocks">
<TrendingDown className="w-4 h-4 text-red-600 dark:text-red-400" aria-hidden="true" />
<span className="font-bold text-red-700 dark:text-red-400">{sell}</span>
<span className="text-xs text-red-600 dark:text-red-400">Sell ({sellPct}%)</span>
</div>
</div>
</div>
{/* Progress bar */}
<div className="mt-3">
<div className="flex h-2 rounded-full overflow-hidden bg-gray-100 dark:bg-slate-700">
<div className="bg-green-500 transition-all" style={{ width: `${buyPct}%` }} />
<div className="bg-amber-500 transition-all" style={{ width: `${holdPct}%` }} />
<div className="bg-red-500 transition-all" style={{ width: `${sellPct}%` }} />
</div>
</div>
</section>
{/* Summary Stats */}
<SummaryStats
total={recommendation.summary.total}
buy={recommendation.summary.buy}
sell={recommendation.summary.sell}
hold={recommendation.summary.hold}
date={recommendation.date}
/>
{/* How It Works Section */}
<HowItWorks collapsed={true} />
{/* Chart and Stats Section */}
<div className="grid lg:grid-cols-2 gap-6">
<div className="card p-6">
<div className="flex items-center gap-2 mb-4">
<PieChart className="w-5 h-5 text-nifty-600" />
<h2 className="section-title text-lg">Decision Distribution</h2>
</div>
<SummaryPieChart
buy={recommendation.summary.buy}
sell={recommendation.summary.sell}
hold={recommendation.summary.hold}
/>
</div>
<div className="card p-6">
<h2 className="section-title text-lg mb-4">Quick Analysis</h2>
<div className="space-y-4">
<div className="p-4 bg-green-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-green-800">Bullish Signals</span>
<span className="text-2xl font-bold text-green-600">{recommendation.summary.buy}</span>
</div>
<p className="text-sm text-green-700">
{((recommendation.summary.buy / recommendation.summary.total) * 100).toFixed(0)}% of stocks show buying opportunities
</p>
</div>
<div className="p-4 bg-amber-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-amber-800">Neutral Position</span>
<span className="text-2xl font-bold text-amber-600">{recommendation.summary.hold}</span>
</div>
<p className="text-sm text-amber-700">
{((recommendation.summary.hold / recommendation.summary.total) * 100).toFixed(0)}% of stocks recommend holding
</p>
</div>
<div className="p-4 bg-red-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-red-800">Bearish Signals</span>
<span className="text-2xl font-bold text-red-600">{recommendation.summary.sell}</span>
</div>
<p className="text-sm text-red-700">
{((recommendation.summary.sell / recommendation.summary.total) * 100).toFixed(0)}% of stocks suggest selling
</p>
</div>
</div>
</div>
</div>
{/* Top Picks and Avoid Section */}
<div className="grid lg:grid-cols-2 gap-6">
{/* Top Picks and Avoid Section - Side by Side Compact */}
<div className="grid lg:grid-cols-2 gap-4">
<TopPicks picks={recommendation.top_picks} />
<StocksToAvoid stocks={recommendation.stocks_to_avoid} />
</div>
{/* All Stocks Section */}
{/* All Stocks Section with Integrated Filter */}
<section className="card">
<div className="p-6 border-b border-gray-100">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<h2 className="section-title">All Stocks</h2>
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-400 dark:text-gray-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">All {total} Stocks</h2>
</div>
<div className="flex gap-1.5" role="group" aria-label="Filter stocks by recommendation">
<button
onClick={() => setFilter('ALL')}
aria-pressed={filter === 'ALL'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'ALL'
? 'bg-nifty-600 text-white shadow-sm'
: 'bg-gray-100 dark:bg-slate-600 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-500'
}`}
>
All ({total})
</button>
<button
onClick={() => setFilter('BUY')}
aria-pressed={filter === 'BUY'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'BUY'
? 'bg-green-600 text-white shadow-sm'
: 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/50'
}`}
>
Buy ({buy})
</button>
<button
onClick={() => setFilter('HOLD')}
aria-pressed={filter === 'HOLD'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'HOLD'
? 'bg-amber-600 text-white shadow-sm'
: 'bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/50'
}`}
>
Hold ({hold})
</button>
<button
onClick={() => setFilter('SELL')}
aria-pressed={filter === 'SELL'}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
filter === 'SELL'
? 'bg-red-600 text-white shadow-sm'
: 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50'
}`}
>
Sell ({sell})
</button>
</div>
</div>
<div className="flex gap-2">
{filterButtons.map(({ label, value, count }) => (
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500" />
<input
type="text"
placeholder="Search by symbol or company name..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-9 py-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:border-transparent"
/>
{searchQuery && (
<button
key={value}
onClick={() => setFilter(value)}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
filter === value
? 'bg-nifty-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
>
{label} ({count})
<X className="w-4 h-4" />
</button>
))}
)}
</div>
</div>
</div>
<div className="divide-y divide-gray-100">
{filteredStocks.map((stock) => (
<StockCard key={stock.symbol} stock={stock} />
))}
<div className="p-2 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 max-h-[400px] overflow-y-auto" role="list" aria-label="Stock recommendations list">
{filteredStocks.map((stock: StockAnalysis) => {
const backtest = getBacktestResult(stock.symbol);
const trend = stock.decision === 'BUY' ? 'up' : stock.decision === 'SELL' ? 'down' : 'flat';
return (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="card-hover p-2 group relative overflow-hidden"
role="listitem"
>
{/* Background Chart */}
{backtest && (
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline
data={backtest.price_history.slice(-15)}
trend={trend}
/>
</div>
)}
{/* Content */}
<div className="relative z-10">
<div className="flex items-center gap-1.5 mb-0.5">
<span className="font-semibold text-sm text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<DecisionBadge decision={stock.decision} size="small" />
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{stock.company_name}</p>
</div>
</Link>
);
})}
</div>
{filteredStocks.length === 0 && (
<div className="p-12 text-center">
<p className="text-gray-500">No stocks match the selected filter.</p>
<div className="p-8 text-center">
<p className="text-gray-500 dark:text-gray-400 text-sm">No stocks match the selected filter.</p>
</div>
)}
</section>
{/* CTA Section */}
<section className="card bg-gradient-to-r from-nifty-600 to-nifty-800 text-white p-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h2 className="text-2xl font-display font-bold mb-2">
Track Historical Recommendations
</h2>
<p className="text-nifty-100">
View past recommendations and track how our AI predictions performed over time.
</p>
</div>
<Link
to="/history"
className="inline-flex items-center gap-2 bg-white text-nifty-700 px-6 py-3 rounded-lg font-semibold hover:bg-nifty-50 transition-colors"
>
View History
<ChevronRight className="w-5 h-5" />
</Link>
{/* Compact CTA */}
<Link
to="/history"
className="card flex items-center justify-between p-4 bg-gradient-to-r from-nifty-600 to-nifty-700 text-white hover:from-nifty-700 hover:to-nifty-800 transition-all group focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2"
aria-label="View historical stock recommendations"
>
<div className="flex items-center gap-3">
<History className="w-5 h-5" aria-hidden="true" />
<span className="font-semibold">View Historical Recommendations</span>
</div>
</section>
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
</Link>
</div>
);
}

View File

@ -1,132 +1,375 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3 } from 'lucide-react';
import { sampleRecommendations } from '../data/recommendations';
import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield } from 'lucide-react';
import { sampleRecommendations, getBacktestResult, calculateAccuracyMetrics, getDateStats, getOverallStats, getReturnBreakdown } from '../data/recommendations';
import { DecisionBadge } from '../components/StockCard';
import Sparkline from '../components/Sparkline';
import AccuracyBadge from '../components/AccuracyBadge';
import AccuracyExplainModal from '../components/AccuracyExplainModal';
import ReturnExplainModal from '../components/ReturnExplainModal';
import OverallReturnModal from '../components/OverallReturnModal';
import AccuracyTrendChart from '../components/AccuracyTrendChart';
import ReturnDistributionChart from '../components/ReturnDistributionChart';
import RiskMetricsCard from '../components/RiskMetricsCard';
import PortfolioSimulator from '../components/PortfolioSimulator';
import IndexComparisonChart from '../components/IndexComparisonChart';
import type { StockAnalysis } from '../types';
export default function History() {
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const [showAccuracyModal, setShowAccuracyModal] = useState(false);
const [showReturnModal, setShowReturnModal] = useState(false);
const [returnModalDate, setReturnModalDate] = useState<string | null>(null);
const [showOverallModal, setShowOverallModal] = useState(false);
const dates = sampleRecommendations.map(r => r.date);
const accuracyMetrics = calculateAccuracyMetrics();
const overallStats = useMemo(() => getOverallStats(), []);
// Pre-calculate date stats for all dates
const dateStatsMap = useMemo(() => {
const map: Record<string, ReturnType<typeof getDateStats>> = {};
dates.forEach(date => {
map[date] = getDateStats(date);
});
return map;
}, [dates]);
const getRecommendation = (date: string) => {
return sampleRecommendations.find(r => r.date === date);
};
return (
<div className="space-y-8">
{/* Header */}
<section className="text-center py-8">
<h1 className="text-4xl font-display font-bold text-gray-900 mb-4">
Historical <span className="gradient-text">Recommendations</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Browse past AI recommendations and track performance over time.
<div className="space-y-4">
{/* Compact Header */}
<section className="card p-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-gray-100">
Historical <span className="gradient-text">Recommendations</span>
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse past AI recommendations with backtest results</p>
</div>
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1.5 text-nifty-600 dark:text-nifty-400">
<BarChart3 className="w-4 h-4" />
<span className="font-semibold">{dates.length}</span>
<span className="text-gray-500 dark:text-gray-400">days</span>
</div>
</div>
</div>
</section>
{/* Accuracy Metrics */}
<section className="card p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Target className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Prediction Accuracy</h2>
</div>
<button
onClick={() => setShowAccuracyModal(true)}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-nifty-600 dark:hover:text-nifty-400 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
title="How is accuracy calculated?"
>
<HelpCircle className="w-4 h-4" />
<span className="hidden sm:inline">How it's calculated</span>
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 rounded-lg bg-nifty-50 dark:bg-nifty-900/20 text-center">
<div className="text-2xl font-bold text-nifty-600 dark:text-nifty-400">
{(accuracyMetrics.success_rate * 100).toFixed(0)}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Overall Accuracy</div>
</div>
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 text-center">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{(accuracyMetrics.buy_accuracy * 100).toFixed(0)}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Buy Accuracy</div>
</div>
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-center">
<div className="text-2xl font-bold text-red-600 dark:text-red-400">
{(accuracyMetrics.sell_accuracy * 100).toFixed(0)}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Sell Accuracy</div>
</div>
<div className="p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 text-center">
<div className="text-2xl font-bold text-amber-600 dark:text-amber-400">
{(accuracyMetrics.hold_accuracy * 100).toFixed(0)}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Hold Accuracy</div>
</div>
</div>
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-2 text-center">
Based on {accuracyMetrics.total_predictions} predictions tracked over time
</p>
</section>
{/* Accuracy Trend Chart */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-3">
<LineChart className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Accuracy Trend</h2>
</div>
<AccuracyTrendChart height={200} />
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-2 text-center">
Prediction accuracy over the past {dates.length} trading days
</p>
</section>
{/* Risk Metrics */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Shield className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Risk Metrics</h2>
</div>
<RiskMetricsCard />
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-2 text-center">
Risk-adjusted performance metrics for the AI trading strategy
</p>
</section>
{/* Portfolio Simulator */}
<PortfolioSimulator />
{/* Date Selector */}
<div className="card p-6">
<div className="flex items-center gap-2 mb-4">
<Calendar className="w-5 h-5 text-nifty-600" />
<h2 className="font-semibold text-gray-900">Select Date</h2>
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Calendar 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">Select Date</h2>
</div>
<div className="flex flex-wrap gap-2">
{dates.map((date) => {
const rec = getRecommendation(date);
const stats = dateStatsMap[date];
const avgReturn = stats?.avgReturn1d ?? 0;
const isPositive = avgReturn >= 0;
return (
<button
key={date}
onClick={() => setSelectedDate(selectedDate === date ? null : date)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
selectedDate === date
? 'bg-nifty-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<div>{new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}</div>
<div className="text-xs opacity-75 mt-0.5">
{rec?.summary.buy}B / {rec?.summary.sell}S / {rec?.summary.hold}H
</div>
</button>
<div key={date} className="relative group">
<button
onClick={() => setSelectedDate(selectedDate === date ? null : date)}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all min-w-[90px] ${
selectedDate === date
? 'bg-nifty-600 text-white ring-2 ring-nifty-400'
: 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-600'
}`}
>
<div className="font-semibold">{new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}</div>
<div className={`text-sm font-bold mt-0.5 ${
selectedDate === date
? 'text-white'
: isPositive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'
}`}>
{isPositive ? '+' : ''}{avgReturn.toFixed(1)}%
</div>
<div className={`text-[10px] mt-0.5 ${selectedDate === date ? 'text-white/80' : 'opacity-60'}`}>
{rec?.summary.buy}B/{rec?.summary.sell}S/{rec?.summary.hold}H
</div>
</button>
{/* Help button for return explanation */}
<button
onClick={(e) => {
e.stopPropagation();
setReturnModalDate(date);
setShowReturnModal(true);
}}
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-white dark:bg-slate-600 shadow-sm border border-gray-200 dark:border-slate-500 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
title="How is this calculated?"
>
<Calculator className="w-2.5 h-2.5 text-gray-500 dark:text-gray-300" />
</button>
</div>
);
})}
{/* Overall Summary Card */}
<div className="relative group">
<button
onClick={() => setShowOverallModal(true)}
className="px-3 py-2 rounded-lg text-xs font-medium min-w-[100px] bg-gradient-to-br from-nifty-500 to-nifty-700 text-white hover:from-nifty-600 hover:to-nifty-800 transition-all text-left"
>
<div className="font-semibold flex items-center gap-1">
<Activity className="w-3 h-3" />
Overall
</div>
<div className="text-sm font-bold mt-0.5">
{overallStats.avgDailyReturn >= 0 ? '+' : ''}{overallStats.avgDailyReturn.toFixed(1)}%
</div>
<div className="text-[10px] mt-0.5 text-white/80">
{overallStats.overallAccuracy}% accurate
</div>
</button>
<button
onClick={(e) => {
e.stopPropagation();
setShowOverallModal(true);
}}
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-white dark:bg-slate-600 shadow-sm border border-gray-200 dark:border-slate-500 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-100 dark:hover:bg-slate-500"
title="How is overall return calculated?"
>
<HelpCircle className="w-2.5 h-2.5 text-gray-500 dark:text-gray-300" />
</button>
</div>
</div>
</div>
{/* Selected Date Details */}
{selectedDate && (
<div className="card">
<div className="p-6 border-b border-gray-100">
<div className="flex items-center justify-between">
<h2 className="section-title">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<h2 className="font-semibold text-gray-900 dark:text-gray-100">
{new Date(selectedDate).toLocaleDateString('en-IN', {
weekday: 'long',
year: 'numeric',
month: 'long',
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</h2>
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1 text-green-600">
<TrendingUp className="w-4 h-4" />
<div className="flex items-center gap-3 text-xs">
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
<TrendingUp className="w-3 h-3" />
{getRecommendation(selectedDate)?.summary.buy} Buy
</span>
<span className="flex items-center gap-1 text-red-600">
<TrendingDown className="w-4 h-4" />
<span className="flex items-center gap-1 text-red-600 dark:text-red-400">
<TrendingDown className="w-3 h-3" />
{getRecommendation(selectedDate)?.summary.sell} Sell
</span>
<span className="flex items-center gap-1 text-amber-600">
<Minus className="w-4 h-4" />
<span className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
<Minus className="w-3 h-3" />
{getRecommendation(selectedDate)?.summary.hold} Hold
</span>
</div>
</div>
</div>
<div className="divide-y divide-gray-100">
{Object.values(getRecommendation(selectedDate)?.analysis || {}).map((stock) => (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-4 hover:bg-gray-50 transition-colors group"
>
<div>
<div className="flex items-center gap-3">
<span className="font-semibold text-gray-900">{stock.symbol}</span>
<DecisionBadge decision={stock.decision} />
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[60vh] sm:max-h-[400px] overflow-y-auto">
{Object.values(getRecommendation(selectedDate)?.analysis || {}).map((stock: StockAnalysis) => {
const backtest = getBacktestResult(stock.symbol);
// Use next-day return for the display
const nextDayReturn = backtest?.actual_return_1d ?? 0;
const isPositive = nextDayReturn >= 0;
return (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<span className="font-medium text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 hidden sm:inline truncate">{stock.company_name}</span>
</div>
<p className="text-sm text-gray-500 mt-1">{stock.company_name}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600" />
</Link>
))}
<div className="flex items-center gap-3">
{/* Sparkline */}
{backtest && (
<Sparkline
data={backtest.price_history}
width={60}
height={24}
positive={isPositive}
/>
)}
{/* Next-Day Return Badge */}
{backtest && (
<AccuracyBadge
correct={backtest.prediction_correct}
returnPercent={nextDayReturn}
size="small"
/>
)}
<DecisionBadge decision={stock.decision} size="small" />
<ChevronRight className="w-4 h-4 text-gray-300 dark:text-gray-600 group-hover:text-nifty-600 dark:group-hover:text-nifty-400" />
</div>
</Link>
);
})}
</div>
</div>
)}
{/* Summary Cards */}
<div className="grid md:grid-cols-3 gap-6">
<div className="card p-6 text-center">
<BarChart3 className="w-12 h-12 text-nifty-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">{dates.length}</h3>
<p className="text-gray-600">Days of Analysis</p>
{/* Performance Summary Cards */}
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Activity 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">Performance Summary</h2>
</div>
<div className="card p-6 text-center">
<TrendingUp className="w-12 h-12 text-green-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.buy, 0)}
</h3>
<p className="text-gray-600">Total Buy Signals</p>
</div>
<div className="card p-6 text-center">
<TrendingDown className="w-12 h-12 text-red-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.sell, 0)}
</h3>
<p className="text-gray-600">Total Sell Signals</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center">
<div className="text-xl font-bold text-nifty-600 dark:text-nifty-400">{overallStats.totalDays}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Days Tracked</div>
</div>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center">
<div className={`text-xl font-bold ${overallStats.avgDailyReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{overallStats.avgDailyReturn >= 0 ? '+' : ''}{overallStats.avgDailyReturn.toFixed(1)}%
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Avg Next-Day Return</div>
</div>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center">
<div className="text-xl font-bold text-green-600 dark:text-green-400">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.buy, 0)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Buy Signals</div>
</div>
<div className="p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center">
<div className="text-xl font-bold text-red-600 dark:text-red-400">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.sell, 0)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Sell Signals</div>
</div>
</div>
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-3 text-center">
Next-day return = Price change on the trading day after recommendation
</p>
</div>
{/* AI vs Nifty50 Index Comparison */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-3">
<BarChart3 className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">AI Strategy vs Nifty50 Index</h2>
</div>
<IndexComparisonChart height={220} />
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-2 text-center">
Comparison of cumulative returns between AI strategy and Nifty50 index
</p>
</section>
{/* Return Distribution */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-3">
<PieChart className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Return Distribution</h2>
</div>
<ReturnDistributionChart height={200} />
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-2 text-center">
Distribution of next-day returns across all predictions. Click bars to see stocks.
</p>
</section>
{/* Accuracy Explanation Modal */}
<AccuracyExplainModal
isOpen={showAccuracyModal}
onClose={() => setShowAccuracyModal(false)}
metrics={accuracyMetrics}
/>
{/* Return Calculation Modal */}
<ReturnExplainModal
isOpen={showReturnModal}
onClose={() => setShowReturnModal(false)}
breakdown={returnModalDate ? getReturnBreakdown(returnModalDate) : null}
date={returnModalDate || ''}
/>
{/* Overall Return Modal */}
<OverallReturnModal
isOpen={showOverallModal}
onClose={() => setShowOverallModal(false)}
/>
</div>
);
}

View File

@ -1,8 +1,11 @@
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Info, Calendar, Activity } from 'lucide-react';
import { useMemo } from 'react';
import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Calendar, Activity, LineChart } from 'lucide-react';
import { NIFTY_50_STOCKS } from '../types';
import { sampleRecommendations, getStockHistory } from '../data/recommendations';
import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations';
import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard';
import AIAnalysisPanel from '../components/AIAnalysisPanel';
import StockPriceChart from '../components/StockPriceChart';
export default function StockDetail() {
const { symbol } = useParams<{ symbol: string }>();
@ -12,15 +15,26 @@ export default function StockDetail() {
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) : [];
}, [symbol]);
const predictionPoints = useMemo(() => {
return symbol && priceHistory.length > 0
? getPredictionPointsWithPrices(symbol, priceHistory)
: [];
}, [symbol, priceHistory]);
if (!stock) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-700 mb-2">Stock Not Found</h2>
<p className="text-gray-500 mb-4">The stock "{symbol}" was not found in Nifty 50.</p>
<Link to="/stocks" className="btn-primary">
View All Stocks
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">Stock Not Found</h2>
<p className="text-gray-500 dark:text-gray-400 mb-4">The stock "{symbol}" was not found in Nifty 50.</p>
<Link to="/" className="btn-primary">
Back to Dashboard
</Link>
</div>
</div>
@ -43,171 +57,177 @@ export default function StockDetail() {
const bgGradient = analysis?.decision ? decisionColor[analysis.decision] : 'from-gray-500 to-gray-600';
return (
<div className="space-y-8">
<div className="space-y-4">
{/* Back Button */}
<div>
<Link
to="/stocks"
className="inline-flex items-center gap-2 text-gray-600 hover:text-nifty-600 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to All Stocks
</Link>
</div>
<Link
to="/"
className="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-nifty-600 dark:hover:text-nifty-400 transition-colors"
>
<ArrowLeft className="w-3.5 h-3.5" />
Back to Dashboard
</Link>
{/* Stock Header */}
{/* Compact Stock Header */}
<section className="card overflow-hidden">
<div className={`bg-gradient-to-r ${bgGradient} p-6 text-white`}>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-display font-bold">{stock.symbol}</h1>
{analysis?.decision && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-white/20 backdrop-blur-sm">
<DecisionIcon className="w-4 h-4" />
{analysis.decision}
</span>
)}
</div>
<p className="text-white/90 text-lg">{stock.company_name}</p>
<div className="flex items-center gap-2 mt-3 text-white/80">
<Building2 className="w-4 h-4" />
<span>{stock.sector || 'N/A'}</span>
<div className={`bg-gradient-to-r ${bgGradient} p-4 text-white`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-display font-bold">{stock.symbol}</h1>
{analysis?.decision && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-white/20">
<DecisionIcon className="w-3 h-3" />
{analysis.decision}
</span>
)}
</div>
<p className="text-white/90 text-sm">{stock.company_name}</p>
</div>
</div>
<div className="text-right">
<div className="text-sm text-white/80 mb-1">Latest Analysis</div>
<div className="flex items-center gap-2 text-white/90">
<Calendar className="w-4 h-4" />
<div className="text-right text-xs">
<div className="flex items-center gap-1.5 text-white/80">
<Building2 className="w-3 h-3" />
<span>{stock.sector || 'N/A'}</span>
</div>
<div className="flex items-center gap-1.5 text-white/70 mt-1">
<Calendar className="w-3 h-3" />
{latestRecommendation?.date ? new Date(latestRecommendation.date).toLocaleDateString('en-IN', {
month: 'short',
day: 'numeric',
year: 'numeric',
}) : 'N/A'}
</div>
</div>
</div>
</div>
{/* Analysis Details */}
<div className="p-6">
{analysis ? (
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Decision</h3>
<DecisionBadge decision={analysis.decision} />
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Confidence</h3>
<ConfidenceBadge confidence={analysis.confidence} />
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Risk Level</h3>
<RiskBadge risk={analysis.risk} />
</div>
{/* Analysis Details - Inline */}
{analysis && (
<div className="p-3 flex items-center gap-4 bg-gray-50/50 dark:bg-slate-700/50">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Decision:</span>
<DecisionBadge decision={analysis.decision} size="small" />
</div>
) : (
<div className="flex items-center gap-3 text-gray-500">
<Info className="w-5 h-5" />
<span>No analysis available for this stock yet.</span>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Confidence:</span>
<ConfidenceBadge confidence={analysis.confidence} />
</div>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Risk:</span>
<RiskBadge risk={analysis.risk} />
</div>
</div>
)}
</section>
{/* Quick Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-gray-900">{history.length}</div>
<div className="text-sm text-gray-500">Total Analyses</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-green-600">
{history.filter(h => h.decision === 'BUY').length}
{/* Price Chart with Predictions */}
{priceHistory.length > 0 && (
<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="flex items-center gap-2">
<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>
</div>
</div>
<div className="text-sm text-gray-500">Buy Signals</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-amber-600">
{history.filter(h => h.decision === 'HOLD').length}
<div className="p-4 bg-white dark:bg-slate-800">
<StockPriceChart
priceHistory={priceHistory}
predictions={predictionPoints}
symbol={symbol || ''}
/>
</div>
<div className="text-sm text-gray-500">Hold Signals</div>
</section>
)}
{/* AI Analysis Panel */}
{analysis && getRawAnalysis(symbol || '') && (
<AIAnalysisPanel
analysis={getRawAnalysis(symbol || '') || ''}
decision={analysis.decision}
/>
)}
{/* Compact Stats Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="card p-2.5 text-center">
<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>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-red-600">
{history.filter(h => h.decision === 'SELL').length}
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-green-600 dark:text-green-400">
{history.filter((h: { decision: string }) => h.decision === 'BUY').length}
</div>
<div className="text-sm text-gray-500">Sell Signals</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Buy</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Hold</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{history.filter((h: { decision: string }) => h.decision === 'SELL').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Sell</div>
</div>
</div>
{/* Analysis History */}
<section className="card">
<div className="p-6 border-b border-gray-100">
<h2 className="section-title">Recommendation History</h2>
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2>
</div>
{history.length > 0 ? (
<div className="divide-y divide-gray-100">
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[250px] overflow-y-auto">
{history.map((entry, idx) => (
<div key={idx} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="text-sm text-gray-500">
{new Date(entry.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<div key={idx} className="px-3 py-2 flex items-center justify-between">
<div className="text-xs text-gray-500 dark:text-gray-400">
{new Date(entry.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</div>
<DecisionBadge decision={entry.decision} />
<DecisionBadge decision={entry.decision} size="small" />
</div>
))}
</div>
) : (
<div className="p-12 text-center">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-700 mb-2">No History Yet</h3>
<p className="text-gray-500">Recommendation history will appear here as we analyze this stock daily.</p>
<div className="p-6 text-center">
<Calendar className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">No history yet</p>
</div>
)}
</section>
{/* Top Pick / Avoid Status */}
{/* Top Pick / Avoid Status - Compact */}
{latestRecommendation && (
<>
{latestRecommendation.top_picks.some(p => p.symbol === symbol) && (
<section className="card bg-gradient-to-r from-green-50 to-emerald-50 border-green-200">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-green-800 mb-2">Top Pick</h3>
<p className="text-green-700">
{latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason}
</p>
</div>
<section className="card p-3 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<div className="flex items-center gap-3">
<TrendingUp className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
<div>
<span className="font-semibold text-green-800 dark:text-green-300 text-sm">Top Pick: </span>
<span className="text-sm text-green-700 dark:text-green-400">
{latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason}
</span>
</div>
</div>
</section>
)}
{latestRecommendation.stocks_to_avoid.some(s => s.symbol === symbol) && (
<section className="card bg-gradient-to-r from-red-50 to-rose-50 border-red-200">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-red-100 flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-red-800 mb-2">Stock to Avoid</h3>
<p className="text-red-700">
{latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason}
</p>
</div>
<section className="card p-3 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" />
<div>
<span className="font-semibold text-red-800 dark:text-red-300 text-sm">Avoid: </span>
<span className="text-sm text-red-700 dark:text-red-400">
{latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason}
</span>
</div>
</div>
</section>
@ -215,17 +235,10 @@ export default function StockDetail() {
</>
)}
{/* Disclaimer */}
<section className="card p-6 bg-gray-50">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-gray-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-gray-600">
<strong>Disclaimer:</strong> This AI-generated recommendation is for educational purposes only.
It should not be considered as financial advice. Always do your own research and consult with
a qualified financial advisor before making investment decisions.
</p>
</div>
</section>
{/* Compact Disclaimer */}
<p className="text-[10px] text-gray-400 dark:text-gray-500 text-center">
AI-generated recommendation for educational purposes only. Not financial advice.
</p>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Search, Filter, ChevronRight, Building2 } from 'lucide-react';
import { Search, Building2 } from 'lucide-react';
import { NIFTY_50_STOCKS } from '../types';
import { getLatestRecommendation } from '../data/recommendations';
import { DecisionBadge, ConfidenceBadge } from '../components/StockCard';
@ -33,97 +33,78 @@ export default function Stocks() {
};
return (
<div className="space-y-8">
{/* Header */}
<section className="text-center py-8">
<h1 className="text-4xl font-display font-bold text-gray-900 mb-4">
All <span className="gradient-text">Nifty 50 Stocks</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Browse all 50 stocks in the Nifty index with their latest AI recommendations.
</p>
</section>
{/* Search and Filter */}
<div className="card p-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Search by symbol or company name..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-nifty-500 focus:ring-2 focus:ring-nifty-500/20 outline-none transition-all"
/>
</div>
{/* Sector Filter */}
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<select
value={sectorFilter}
onChange={(e) => setSectorFilter(e.target.value)}
className="px-4 py-2.5 rounded-lg border border-gray-200 focus:border-nifty-500 focus:ring-2 focus:ring-nifty-500/20 outline-none bg-white"
>
{sectors.map((sector) => (
<option key={sector} value={sector}>
{sector === 'ALL' ? 'All Sectors' : sector}
</option>
))}
</select>
<div className="space-y-4">
{/* Combined Header + Search */}
<div className="card p-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 mb-3">
<div>
<h1 className="text-xl font-display font-bold text-gray-900">
All <span className="gradient-text">Nifty 50 Stocks</span>
</h1>
<p className="text-sm text-gray-500">
{filteredStocks.length} of {NIFTY_50_STOCKS.length} stocks
</p>
</div>
</div>
<p className="text-sm text-gray-500 mt-4">
Showing {filteredStocks.length} of {NIFTY_50_STOCKS.length} stocks
</p>
{/* Search and Filter - inline */}
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search symbol or company..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm rounded-md border border-gray-200 focus:border-nifty-500 focus:ring-1 focus:ring-nifty-500/20 outline-none"
/>
</div>
<select
value={sectorFilter}
onChange={(e) => setSectorFilter(e.target.value)}
className="px-3 py-1.5 text-sm rounded-md border border-gray-200 focus:border-nifty-500 focus:ring-1 focus:ring-nifty-500/20 outline-none bg-white"
>
{sectors.map((sector) => (
<option key={sector} value={sector}>
{sector === 'ALL' ? 'All Sectors' : sector}
</option>
))}
</select>
</div>
</div>
{/* Stocks Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Compact Stocks Grid */}
<div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
{filteredStocks.map((stock) => {
const analysis = getStockAnalysis(stock.symbol);
return (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="card-hover p-4 group"
className="card-hover p-3 group"
>
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="font-bold text-lg text-gray-900">{stock.symbol}</h3>
<p className="text-sm text-gray-500">{stock.company_name}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors" />
<div className="flex items-center justify-between mb-1">
<h3 className="font-semibold text-sm text-gray-900">{stock.symbol}</h3>
{analysis && <DecisionBadge decision={analysis.decision} size="small" />}
</div>
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">{stock.sector}</span>
</div>
{analysis && (
<div className="pt-3 border-t border-gray-100">
<div className="flex items-center justify-between">
<DecisionBadge decision={analysis.decision} />
<div className="flex items-center gap-2">
<ConfidenceBadge confidence={analysis.confidence} />
</div>
</div>
<p className="text-xs text-gray-500 truncate mb-1.5">{stock.company_name}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Building2 className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500 truncate">{stock.sector}</span>
</div>
)}
{analysis && <ConfidenceBadge confidence={analysis.confidence} />}
</div>
</Link>
);
})}
</div>
{filteredStocks.length === 0 && (
<div className="card p-12 text-center">
<Search className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-700 mb-2">No stocks found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria.</p>
<div className="card p-8 text-center">
<Search className="w-8 h-8 text-gray-300 mx-auto mb-2" />
<h3 className="font-semibold text-gray-700 mb-1">No stocks found</h3>
<p className="text-sm text-gray-500">Try adjusting your search.</p>
</div>
)}
</div>

View File

@ -2,6 +2,54 @@ export type Decision = 'BUY' | 'SELL' | 'HOLD';
export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW';
export type Risk = 'HIGH' | 'MEDIUM' | 'LOW';
// Backtest Types
export interface PricePoint {
date: string;
price: number;
}
export interface BacktestResult {
prediction_correct: boolean;
actual_return_1d: number; // next trading day percentage return
actual_return_1w: number; // percentage
actual_return_1m: number; // percentage
price_at_prediction: number;
current_price: number;
price_history: PricePoint[];
}
export interface AccuracyMetrics {
total_predictions: number;
correct_predictions: number;
success_rate: number;
buy_accuracy: number;
sell_accuracy: number;
hold_accuracy: number;
}
// Date-level statistics for history page
export interface DateStats {
date: string;
avgReturn1d: number; // Average next-day return for all stocks
avgReturn1m: number; // Average 1-month return
totalStocks: number;
correctPredictions: number;
accuracy: number;
buyCount: number;
sellCount: number;
holdCount: number;
}
export interface OverallStats {
totalDays: number;
totalPredictions: number;
avgDailyReturn: number;
avgMonthlyReturn: number;
overallAccuracy: number;
bestDay: { date: string; return: number } | null;
worstDay: { date: string; return: number } | null;
}
export interface StockAnalysis {
symbol: string;
company_name: string;
@ -76,6 +124,50 @@ export interface NiftyStock {
sector?: string;
}
// Nifty50 Index data point
export interface Nifty50IndexPoint {
date: string;
value: number;
return: number; // daily return %
}
// Risk metrics for portfolio analysis
export interface RiskMetrics {
sharpeRatio: number; // (mean return - risk-free) / std dev
maxDrawdown: number; // peak-to-trough decline %
winLossRatio: number; // avg win / avg loss
winRate: number; // % of winning predictions
volatility: number; // std dev of returns
totalTrades: number;
}
// Return distribution bucket
export interface ReturnBucket {
range: string; // e.g., "0% to 1%"
min: number;
max: number;
count: number;
stocks: string[]; // symbols in this bucket
}
// Filter state for History page
export interface FilterState {
decision: 'ALL' | 'BUY' | 'SELL' | 'HOLD';
confidence: 'ALL' | 'HIGH' | 'MEDIUM' | 'LOW';
sector: string;
sortBy: 'symbol' | 'return' | 'accuracy';
sortOrder: 'asc' | 'desc';
}
// Accuracy trend data point
export interface AccuracyTrendPoint {
date: string;
overall: number;
buy: number;
sell: number;
hold: number;
}
export const NIFTY_50_STOCKS: NiftyStock[] = [
{ symbol: 'RELIANCE', company_name: 'Reliance Industries Ltd', sector: 'Energy' },
{ symbol: 'TCS', company_name: 'Tata Consultancy Services Ltd', sector: 'IT' },

233
frontend/test-ui.mjs Normal file
View File

@ -0,0 +1,233 @@
import puppeteer from 'puppeteer';
import { writeFile } from 'fs/promises';
import { join } from 'path';
const SESSION_ID = 'session-20260131-152418';
const SCREENSHOT_DIR = `/home/hemang/Documents/GitHub/TradingAgents/.frontend-dev/screenshots/${SESSION_ID}`;
const BASE_URL = 'http://localhost:5173';
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Session state
const sessionState = {
id: SESSION_ID,
startTime: new Date().toISOString(),
steps: [],
currentStep: 0,
issues: [],
consoleErrors: [],
};
let browser;
let page;
// Helper to take screenshot
async function takeScreenshot(name, description) {
sessionState.currentStep++;
const stepNum = String(sessionState.currentStep).padStart(3, '0');
const screenshotPath = join(SCREENSHOT_DIR, `step-${stepNum}-${name}.png`);
await page.screenshot({
path: screenshotPath,
fullPage: false
});
sessionState.steps.push({
stepNumber: sessionState.currentStep,
name,
description,
screenshotPath,
timestamp: new Date().toISOString()
});
console.log(`Step ${stepNum}: ${description}`);
return screenshotPath;
}
async function runTests() {
try {
// Launch browser
console.log('Launching browser...');
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
page = await browser.newPage();
// Listen to console errors
page.on('console', msg => {
if (msg.type() === 'error') {
sessionState.consoleErrors.push({
text: msg.text(),
timestamp: new Date().toISOString()
});
}
});
// Set viewport to desktop
await page.setViewport({ width: 1920, height: 1080 });
console.log('Starting iterative testing...\n');
// ===== SCENARIO 1: Dashboard Testing =====
console.log('=== SCENARIO 1: Dashboard Testing ===');
// Step 1: Navigate to Dashboard
await page.goto(BASE_URL, { waitUntil: 'networkidle0' });
await takeScreenshot('dashboard-initial', 'Initial dashboard load');
// Step 2: Test Buy filter
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const buyButton = buttons.find(btn => btn.textContent.includes('Buy ('));
if (buyButton) buyButton.click();
});
await wait(500);
await takeScreenshot('dashboard-buy-filter', 'After clicking Buy filter');
// Step 3: Test Hold filter
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const holdButton = buttons.find(btn => btn.textContent.includes('Hold ('));
if (holdButton) holdButton.click();
});
await wait(500);
await takeScreenshot('dashboard-hold-filter', 'After clicking Hold filter');
// Step 4: Test Sell filter
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const sellButton = buttons.find(btn => btn.textContent.includes('Sell ('));
if (sellButton) sellButton.click();
});
await wait(500);
await takeScreenshot('dashboard-sell-filter', 'After clicking Sell filter');
// Step 5: Back to All filter
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const allButton = buttons.find(btn => btn.textContent.includes('All ('));
if (allButton) allButton.click();
});
await wait(500);
await takeScreenshot('dashboard-all-filter', 'Back to All filter');
// ===== SCENARIO 2: Stock Detail =====
console.log('\n=== SCENARIO 2: Stock Detail ===');
// Click on first stock
const firstStock = await page.$('a[href*="/stock/"]');
if (firstStock) {
await firstStock.click();
await wait(1000);
await takeScreenshot('stock-detail', 'Stock detail page loaded');
}
// ===== SCENARIO 3: History Page =====
console.log('\n=== SCENARIO 3: History Page ===');
await page.goto(`${BASE_URL}/history`, { waitUntil: 'networkidle0' });
await takeScreenshot('history-initial', 'History page loaded');
// Select a date
const dateButton = await page.$('button[class*="px-3 py-1.5"]');
if (dateButton) {
await dateButton.click();
await wait(500);
await takeScreenshot('history-date-selected', 'After selecting a date');
}
// ===== SCENARIO 4: All Stocks Page =====
console.log('\n=== SCENARIO 4: All Stocks Page ===');
await page.goto(`${BASE_URL}/stocks`, { waitUntil: 'networkidle0' });
await takeScreenshot('stocks-initial', 'All stocks page loaded');
// Test search
await page.type('input[type="text"]', 'RELIANCE');
await wait(500);
await takeScreenshot('stocks-search', 'After searching for RELIANCE');
// Clear and test another search
await page.evaluate(() => {
const input = document.querySelector('input[type="text"]');
if (input) input.value = '';
});
await page.type('input[type="text"]', 'HDFC');
await wait(500);
await takeScreenshot('stocks-search-hdfc', 'After searching for HDFC');
// ===== SCENARIO 5: Mobile Testing =====
console.log('\n=== SCENARIO 5: Mobile Testing ===');
await page.setViewport({ width: 375, height: 667 });
await page.goto(BASE_URL, { waitUntil: 'networkidle0' });
await takeScreenshot('mobile-dashboard', 'Mobile dashboard view');
// Test mobile menu
const menuButton = await page.$('button[class*="md:hidden"]');
if (menuButton) {
await menuButton.click();
await wait(500);
await takeScreenshot('mobile-menu-open', 'Mobile hamburger menu opened');
// Close menu
await menuButton.click();
await wait(500);
await takeScreenshot('mobile-menu-closed', 'Mobile hamburger menu closed');
}
// Mobile history page
await page.goto(`${BASE_URL}/history`, { waitUntil: 'networkidle0' });
await takeScreenshot('mobile-history', 'Mobile history page');
// Mobile stocks page
await page.goto(`${BASE_URL}/stocks`, { waitUntil: 'networkidle0' });
await takeScreenshot('mobile-stocks', 'Mobile stocks page');
// ===== HOVER STATES TESTING =====
console.log('\n=== Testing Hover States (Desktop) ===');
await page.setViewport({ width: 1920, height: 1080 });
await page.goto(BASE_URL, { waitUntil: 'networkidle0' });
// Hover over stock card
const stockCard = await page.$('a[href*="/stock/"]');
if (stockCard) {
await stockCard.hover();
await wait(300);
await takeScreenshot('hover-stock-card', 'Hovering over stock card');
}
// Hover over navigation
const navLink = await page.$('a[href="/history"]');
if (navLink) {
await navLink.hover();
await wait(300);
await takeScreenshot('hover-nav-link', 'Hovering over navigation link');
}
// ===== FINAL STATE =====
sessionState.endTime = new Date().toISOString();
sessionState.testingComplete = true;
// Save session state
const sessionPath = `/home/hemang/Documents/GitHub/TradingAgents/.frontend-dev/sessions/${SESSION_ID}.json`;
await writeFile(sessionPath, JSON.stringify(sessionState, null, 2));
console.log(`\n=== Testing Complete ===`);
console.log(`Steps completed: ${sessionState.currentStep}`);
console.log(`Console errors: ${sessionState.consoleErrors.length}`);
console.log(`Issues found: ${sessionState.issues.length}`);
console.log(`Session saved to: ${sessionPath}`);
} catch (error) {
console.error('Test error:', error);
} finally {
if (browser) {
await browser.close();
}
}
}
runTests();