add
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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();
|
||||