Initial project structure: MarketScanner - Fear-to-Fortune Trading Intelligence

Features:
- FastAPI backend with stocks, news, signals, watchlist, analytics endpoints
- React frontend with TailwindCSS dark mode trading dashboard
- Celery workers for news fetching, sentiment analysis, pattern detection
- TimescaleDB schema for time-series stock data
- Docker Compose setup for all services
- OpenAI integration for sentiment analysis
This commit is contained in:
mindesbunister
2026-01-08 14:15:51 +01:00
commit 074787f067
58 changed files with 4864 additions and 0 deletions

34
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build arguments
ARG VITE_API_URL=http://localhost:8000
ENV VITE_API_URL=$VITE_API_URL
# Build the app
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

14
frontend/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="MarketScanner - Fear-to-Fortune Trading Intelligence" />
<title>MarketScanner | Buy the Fear</title>
</head>
<body class="bg-gray-950 text-gray-100">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

35
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,35 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Handle SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy (optional, for same-origin requests)
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}

43
frontend/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "marketscanner-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@tanstack/react-query": "^5.17.19",
"axios": "^1.6.7",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"framer-motion": "^11.0.3",
"lightweight-charts": "^4.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.21.3",
"recharts": "^2.12.0",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

25
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import Signals from './pages/Signals'
import Stocks from './pages/Stocks'
import News from './pages/News'
import Watchlist from './pages/Watchlist'
import Analytics from './pages/Analytics'
function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/signals" element={<Signals />} />
<Route path="/stocks" element={<Stocks />} />
<Route path="/news" element={<News />} />
<Route path="/watchlist" element={<Watchlist />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Layout>
)
}
export default App

View File

@@ -0,0 +1,172 @@
import { ReactNode, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { motion } from 'framer-motion'
import {
HomeIcon,
BellAlertIcon,
ChartBarIcon,
NewspaperIcon,
StarIcon,
ChartPieIcon,
Bars3Icon,
XMarkIcon,
} from '@heroicons/react/24/outline'
import clsx from 'clsx'
interface LayoutProps {
children: ReactNode
}
const navigation = [
{ name: 'Dashboard', href: '/', icon: HomeIcon },
{ name: 'Buy Signals', href: '/signals', icon: BellAlertIcon },
{ name: 'Stocks', href: '/stocks', icon: ChartBarIcon },
{ name: 'News', href: '/news', icon: NewspaperIcon },
{ name: 'Watchlist', href: '/watchlist', icon: StarIcon },
{ name: 'Analytics', href: '/analytics', icon: ChartPieIcon },
]
export default function Layout({ children }: LayoutProps) {
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */}
<aside
className={clsx(
'fixed inset-y-0 left-0 z-50 w-64 bg-gray-900/95 backdrop-blur-xl border-r border-gray-800 transform transition-transform duration-300 lg:translate-x-0',
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
)}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-800">
<Link to="/" className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
<ChartBarIcon className="w-6 h-6 text-white" />
</div>
<div>
<span className="font-bold text-lg gradient-text">MarketScanner</span>
<p className="text-xs text-gray-500">Buy the Fear</p>
</div>
</Link>
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden p-1 rounded-lg hover:bg-gray-800"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
{navigation.map((item) => {
const isActive = location.pathname === item.href
return (
<Link
key={item.name}
to={item.href}
onClick={() => setSidebarOpen(false)}
className={clsx(
'flex items-center px-3 py-2.5 rounded-lg transition-all duration-200',
isActive
? 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
)}
>
<item.icon className={clsx('w-5 h-5 mr-3', isActive && 'text-green-400')} />
{item.name}
{item.name === 'Buy Signals' && (
<span className="ml-auto badge-success">3</span>
)}
</Link>
)
})}
</nav>
{/* Bottom section */}
<div className="p-4 border-t border-gray-800">
<div className="card p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-500">Market Sentiment</span>
<span className="badge-danger">Fear</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-red-500 to-orange-500"
initial={{ width: 0 }}
animate={{ width: '35%' }}
transition={{ duration: 1, ease: 'easeOut' }}
/>
</div>
<div className="flex justify-between mt-1 text-xs text-gray-600">
<span>Extreme Fear</span>
<span>Greed</span>
</div>
</div>
</div>
</div>
</aside>
{/* Main content */}
<div className="lg:pl-64">
{/* Top bar */}
<header className="sticky top-0 z-30 h-16 bg-gray-900/80 backdrop-blur-xl border-b border-gray-800">
<div className="flex items-center justify-between h-full px-4">
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-2 rounded-lg hover:bg-gray-800"
>
<Bars3Icon className="w-6 h-6" />
</button>
<div className="flex items-center space-x-4">
{/* Search */}
<div className="hidden md:block">
<input
type="text"
placeholder="Search stocks..."
className="input w-64"
/>
</div>
</div>
<div className="flex items-center space-x-4">
{/* Market status */}
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span className="text-sm text-gray-400">Market Open</span>
</div>
{/* Time */}
<div className="hidden sm:block text-sm text-gray-500">
{new Date().toLocaleTimeString()}
</div>
</div>
</div>
</header>
{/* Page content */}
<main className="p-4 md:p-6 lg:p-8">
<motion.div
key={location.pathname}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</main>
</div>
</div>
)
}

88
frontend/src/index.css Normal file
View File

@@ -0,0 +1,88 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom scrollbar for dark mode */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a2e;
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Custom styles */
@layer components {
.card {
@apply bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl;
}
.card-hover {
@apply card transition-all duration-300 hover:border-gray-700 hover:shadow-lg hover:shadow-green-500/5;
}
.btn-primary {
@apply px-4 py-2 bg-green-600 hover:bg-green-500 text-white font-medium rounded-lg transition-colors;
}
.btn-secondary {
@apply px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white font-medium rounded-lg transition-colors;
}
.btn-danger {
@apply px-4 py-2 bg-red-600 hover:bg-red-500 text-white font-medium rounded-lg transition-colors;
}
.input {
@apply w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:outline-none focus:border-green-500 text-white placeholder-gray-500;
}
.badge {
@apply px-2 py-0.5 text-xs font-medium rounded-full;
}
.badge-success {
@apply badge bg-green-500/20 text-green-400 border border-green-500/30;
}
.badge-danger {
@apply badge bg-red-500/20 text-red-400 border border-red-500/30;
}
.badge-warning {
@apply badge bg-yellow-500/20 text-yellow-400 border border-yellow-500/30;
}
.badge-info {
@apply badge bg-blue-500/20 text-blue-400 border border-blue-500/30;
}
.gradient-text {
@apply bg-gradient-to-r from-green-400 via-emerald-400 to-teal-400 bg-clip-text text-transparent;
}
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
/* Number styling */
.font-mono {
font-variant-numeric: tabular-nums;
}

33
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,33 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'
import App from './App'
import './index.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30000, // 30 seconds
retry: 2,
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
<Toaster
position="top-right"
toastOptions={{
className: 'bg-gray-800 text-white',
duration: 4000,
}}
/>
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
)

View File

@@ -0,0 +1,231 @@
import { motion } from 'framer-motion'
import {
ChartPieIcon,
ArrowTrendingUpIcon,
ClockIcon,
CheckCircleIcon,
} from '@heroicons/react/24/outline'
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
AreaChart,
Area,
} from 'recharts'
// Mock data
const performanceData = [
{ date: 'Jan', signals: 5, successful: 4, return: 12.5 },
{ date: 'Feb', signals: 8, successful: 6, return: 18.2 },
{ date: 'Mar', signals: 3, successful: 2, return: 8.5 },
{ date: 'Apr', signals: 6, successful: 5, return: 15.3 },
{ date: 'May', signals: 4, successful: 4, return: 22.1 },
{ date: 'Jun', signals: 7, successful: 5, return: 14.8 },
]
const sentimentTrend = [
{ date: 'Mon', sentiment: -15 },
{ date: 'Tue', sentiment: -25 },
{ date: 'Wed', sentiment: -45 },
{ date: 'Thu', sentiment: -38 },
{ date: 'Fri', sentiment: -52 },
{ date: 'Sat', sentiment: -48 },
{ date: 'Sun', sentiment: -35 },
]
const topPatterns = [
{ type: 'Earnings Miss Recovery', avgRecovery: 28.5, avgDays: 45, successRate: 82 },
{ type: 'Scandal/PR Crisis', avgRecovery: 35.2, avgDays: 60, successRate: 75 },
{ type: 'Product Recall', avgRecovery: 22.8, avgDays: 35, successRate: 78 },
{ type: 'Sector Rotation', avgRecovery: 18.5, avgDays: 25, successRate: 85 },
{ type: 'Market Correction', avgRecovery: 42.3, avgDays: 90, successRate: 72 },
]
const stats = [
{ label: 'Total Signals', value: '33', icon: ChartPieIcon },
{ label: 'Success Rate', value: '79%', icon: CheckCircleIcon },
{ label: 'Avg Return', value: '+15.2%', icon: ArrowTrendingUpIcon },
{ label: 'Avg Hold Time', value: '42 days', icon: ClockIcon },
]
export default function Analytics() {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold gradient-text">Analytics</h1>
<p className="text-gray-500 mt-1">Performance metrics and pattern analysis</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="card p-5"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">{stat.label}</p>
<p className="text-2xl font-bold mt-1 gradient-text">{stat.value}</p>
</div>
<stat.icon className="w-8 h-8 text-green-400/50" />
</div>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Signal Performance Chart */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="card p-5"
>
<h2 className="text-lg font-semibold mb-4">Signal Performance</h2>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={performanceData}>
<defs>
<linearGradient id="returnGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="date" stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip
contentStyle={{
backgroundColor: '#1f2937',
border: '1px solid #374151',
borderRadius: '8px',
}}
/>
<Area
type="monotone"
dataKey="return"
stroke="#22c55e"
fill="url(#returnGradient)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
</motion.div>
{/* Sentiment Trend */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="card p-5"
>
<h2 className="text-lg font-semibold mb-4">Market Sentiment (7 Days)</h2>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={sentimentTrend}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="date" stroke="#6b7280" />
<YAxis stroke="#6b7280" domain={[-100, 100]} />
<Tooltip
contentStyle={{
backgroundColor: '#1f2937',
border: '1px solid #374151',
borderRadius: '8px',
}}
/>
<Line
type="monotone"
dataKey="sentiment"
stroke="#ef4444"
strokeWidth={2}
dot={{ fill: '#ef4444', strokeWidth: 2 }}
/>
{/* Zero line */}
<Line
type="monotone"
dataKey={() => 0}
stroke="#6b7280"
strokeDasharray="5 5"
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</motion.div>
</div>
{/* Top Patterns */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="card p-5"
>
<h2 className="text-lg font-semibold mb-4">Top Historical Patterns</h2>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pattern Type</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Avg Recovery</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Avg Days</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Success Rate</th>
</tr>
</thead>
<tbody>
{topPatterns.map((pattern, index) => (
<motion.tr
key={pattern.type}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 + index * 0.1 }}
className="border-b border-gray-800/50"
>
<td className="px-4 py-4 font-medium">{pattern.type}</td>
<td className="px-4 py-4 text-right text-green-400 font-mono">
+{pattern.avgRecovery}%
</td>
<td className="px-4 py-4 text-right text-gray-400 font-mono">
{pattern.avgDays}
</td>
<td className="px-4 py-4 text-right">
<div className="flex items-center justify-end">
<div className="w-16 h-2 bg-gray-700 rounded-full overflow-hidden mr-2">
<div
className="h-full bg-green-500"
style={{ width: `${pattern.successRate}%` }}
/>
</div>
<span className="text-green-400 font-medium">{pattern.successRate}%</span>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</motion.div>
{/* Insight */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.9 }}
className="card p-5 border-green-500/30 bg-green-500/5"
>
<h3 className="font-semibold text-green-400 mb-2">💡 Key Insight</h3>
<p className="text-gray-300">
Based on historical data, <strong>Earnings Miss Recovery</strong> patterns have the highest
success rate (82%), while <strong>Market Correction</strong> events offer the highest
average return (+42.3%) but require longer hold times. Consider your risk tolerance
and time horizon when acting on signals.
</p>
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,242 @@
import { motion } from 'framer-motion'
import {
ArrowTrendingUpIcon,
ArrowTrendingDownIcon,
BellAlertIcon,
NewspaperIcon,
ChartBarIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline'
// Mock data - will be replaced with API calls
const stats = [
{ name: 'Active Signals', value: '3', change: '+2', trend: 'up', icon: BellAlertIcon },
{ name: 'Stocks Tracked', value: '47', change: '+5', trend: 'up', icon: ChartBarIcon },
{ name: 'News Today', value: '156', change: '+23', trend: 'up', icon: NewspaperIcon },
{ name: 'Panic Events', value: '2', change: '+1', trend: 'down', icon: ExclamationTriangleIcon },
]
const topSignals = [
{ symbol: 'NVDA', confidence: 87, price: 485.23, drawdown: -18.5, expectedRecovery: 35 },
{ symbol: 'META', confidence: 76, price: 345.67, drawdown: -12.3, expectedRecovery: 25 },
{ symbol: 'TSLA', confidence: 71, price: 178.90, drawdown: -25.8, expectedRecovery: 45 },
]
const recentNews = [
{ title: 'NVIDIA faces supply chain concerns amid AI boom', sentiment: -45, time: '2h ago' },
{ title: 'Meta announces layoffs in Reality Labs division', sentiment: -62, time: '4h ago' },
{ title: 'Fed signals potential rate cuts in 2024', sentiment: 35, time: '5h ago' },
{ title: 'Tesla recalls 2M vehicles over autopilot issues', sentiment: -78, time: '6h ago' },
]
const sectorHeatmap = [
{ sector: 'Technology', sentiment: -25, change: -5.2 },
{ sector: 'Healthcare', sentiment: 15, change: 2.1 },
{ sector: 'Energy', sentiment: -45, change: -8.3 },
{ sector: 'Financials', sentiment: 5, change: 0.8 },
{ sector: 'Consumer', sentiment: -35, change: -4.5 },
{ sector: 'Defense', sentiment: 55, change: 12.3 },
]
export default function Dashboard() {
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold gradient-text">Dashboard</h1>
<p className="text-gray-500 mt-1">Fear-to-Fortune Trading Intelligence</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<motion.div
key={stat.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="card-hover p-5"
>
<div className="flex items-start justify-between">
<div>
<p className="text-sm text-gray-500">{stat.name}</p>
<p className="text-3xl font-bold mt-1">{stat.value}</p>
</div>
<div className={`p-2 rounded-lg ${stat.trend === 'up' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
<stat.icon className={`w-5 h-5 ${stat.trend === 'up' ? 'text-green-400' : 'text-red-400'}`} />
</div>
</div>
<div className="flex items-center mt-3">
{stat.trend === 'up' ? (
<ArrowTrendingUpIcon className="w-4 h-4 text-green-400 mr-1" />
) : (
<ArrowTrendingDownIcon className="w-4 h-4 text-red-400 mr-1" />
)}
<span className={stat.trend === 'up' ? 'text-green-400' : 'text-red-400'}>
{stat.change}
</span>
<span className="text-gray-500 text-sm ml-2">from yesterday</span>
</div>
</motion.div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Top Buy Signals */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="lg:col-span-2 card p-5"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center">
<BellAlertIcon className="w-5 h-5 mr-2 text-green-400" />
Top Buy Signals
</h2>
<a href="/signals" className="text-sm text-green-400 hover:text-green-300">
View all
</a>
</div>
<div className="space-y-3">
{topSignals.map((signal, index) => (
<motion.div
key={signal.symbol}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 + index * 0.1 }}
className="flex items-center justify-between p-4 bg-gray-800/50 rounded-lg hover:bg-gray-800 transition-colors"
>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-green-500/20 to-emerald-500/20 rounded-xl flex items-center justify-center border border-green-500/30">
<span className="font-bold text-green-400">{signal.symbol.slice(0, 2)}</span>
</div>
<div>
<p className="font-semibold">${signal.symbol}</p>
<p className="text-sm text-gray-500">${signal.price.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<p className="text-sm text-gray-500">Drawdown</p>
<p className="text-red-400 font-medium">{signal.drawdown}%</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Expected</p>
<p className="text-green-400 font-medium">+{signal.expectedRecovery}%</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Confidence</p>
<div className="flex items-center">
<div className="w-16 h-2 bg-gray-700 rounded-full overflow-hidden mr-2">
<div
className="h-full bg-gradient-to-r from-green-500 to-emerald-400"
style={{ width: `${signal.confidence}%` }}
/>
</div>
<span className="text-green-400 font-bold">{signal.confidence}%</span>
</div>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Sector Heatmap */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="card p-5"
>
<h2 className="text-lg font-semibold mb-4">Sector Sentiment</h2>
<div className="space-y-2">
{sectorHeatmap.map((sector) => (
<div
key={sector.sector}
className="flex items-center justify-between p-3 rounded-lg"
style={{
background: sector.sentiment > 0
? `rgba(34, 197, 94, ${Math.abs(sector.sentiment) / 200})`
: `rgba(239, 68, 68, ${Math.abs(sector.sentiment) / 200})`,
}}
>
<span className="font-medium">{sector.sector}</span>
<div className="flex items-center space-x-3">
<span className={sector.change > 0 ? 'text-green-400' : 'text-red-400'}>
{sector.change > 0 ? '+' : ''}{sector.change}%
</span>
<span className="text-sm text-gray-400">
({sector.sentiment})
</span>
</div>
</div>
))}
</div>
</motion.div>
</div>
{/* Recent Panic News */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="card p-5"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center">
<NewspaperIcon className="w-5 h-5 mr-2 text-red-400" />
Recent Panic News
</h2>
<a href="/news" className="text-sm text-green-400 hover:text-green-300">
View all
</a>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{recentNews.map((news, index) => (
<motion.div
key={index}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 + index * 0.1 }}
className="p-4 bg-gray-800/50 rounded-lg hover:bg-gray-800 transition-colors cursor-pointer"
>
<div className="flex items-start justify-between">
<p className="text-sm flex-1 mr-3">{news.title}</p>
<span
className={`badge ${
news.sentiment < -30
? 'badge-danger'
: news.sentiment > 30
? 'badge-success'
: 'badge-warning'
}`}
>
{news.sentiment}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">{news.time}</p>
</motion.div>
))}
</div>
</motion.div>
{/* Quote */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.9 }}
className="text-center py-6"
>
<blockquote className="text-lg italic text-gray-500">
"Buy when there's blood in the streets, even if the blood is your own."
</blockquote>
<cite className="text-sm text-gray-600"> Baron Rothschild</cite>
</motion.div>
</div>
)
}

184
frontend/src/pages/News.tsx Normal file
View File

@@ -0,0 +1,184 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { NewspaperIcon, FunnelIcon } from '@heroicons/react/24/outline'
// Mock data
const news = [
{
id: '1',
title: 'NVIDIA faces supply chain concerns as AI demand continues to surge',
source: 'Reuters',
sentiment: -45,
stocks: ['NVDA', 'AMD'],
time: '2 hours ago',
summary: 'NVIDIA is struggling to meet AI chip demand as supply chain issues persist...',
},
{
id: '2',
title: 'Tesla recalls 2 million vehicles over autopilot safety issues',
source: 'Bloomberg',
sentiment: -78,
stocks: ['TSLA'],
time: '4 hours ago',
summary: 'The recall affects nearly all Tesla vehicles sold in the US...',
},
{
id: '3',
title: 'Meta announces 10,000 layoffs in Reality Labs division',
source: 'CNBC',
sentiment: -62,
stocks: ['META'],
time: '5 hours ago',
summary: 'Meta is cutting costs as metaverse investments continue to drain resources...',
},
{
id: '4',
title: 'Federal Reserve signals potential rate cuts in 2024',
source: 'Wall Street Journal',
sentiment: 45,
stocks: ['SPY', 'QQQ'],
time: '6 hours ago',
summary: 'Fed officials indicate inflation has cooled enough to consider easing...',
},
{
id: '5',
title: 'Lockheed Martin secures $20B defense contract',
source: 'Defense News',
sentiment: 72,
stocks: ['LMT', 'RTX'],
time: '8 hours ago',
summary: 'The Pentagon awards major contract for next-generation fighter jets...',
},
{
id: '6',
title: 'Apple iPhone sales decline in China amid competition',
source: 'Financial Times',
sentiment: -35,
stocks: ['AAPL'],
time: '10 hours ago',
summary: 'Huawei and other local brands continue to gain market share...',
},
]
const sentimentFilters = ['All', 'Panic', 'Negative', 'Neutral', 'Positive']
export default function News() {
const [selectedFilter, setSelectedFilter] = useState('All')
const filteredNews = news.filter(item => {
if (selectedFilter === 'All') return true
if (selectedFilter === 'Panic') return item.sentiment <= -50
if (selectedFilter === 'Negative') return item.sentiment < -20 && item.sentiment > -50
if (selectedFilter === 'Neutral') return item.sentiment >= -20 && item.sentiment <= 20
if (selectedFilter === 'Positive') return item.sentiment > 20
return true
})
const getSentimentColor = (sentiment: number) => {
if (sentiment <= -50) return 'text-red-500'
if (sentiment < -20) return 'text-orange-400'
if (sentiment <= 20) return 'text-gray-400'
return 'text-green-400'
}
const getSentimentBadge = (sentiment: number) => {
if (sentiment <= -50) return 'badge-danger'
if (sentiment < -20) return 'badge-warning'
if (sentiment <= 20) return 'badge-info'
return 'badge-success'
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold gradient-text">News Feed</h1>
<p className="text-gray-500 mt-1">Real-time financial news with sentiment analysis</p>
</div>
</div>
{/* Filters */}
<div className="flex items-center space-x-4">
<FunnelIcon className="w-5 h-5 text-gray-500" />
<div className="flex space-x-2">
{sentimentFilters.map((filter) => (
<button
key={filter}
onClick={() => setSelectedFilter(filter)}
className={`px-4 py-2 rounded-lg transition-colors ${
selectedFilter === filter
? filter === 'Panic'
? 'bg-red-500/20 text-red-400 border border-red-500/30'
: 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'bg-gray-800 text-gray-400 hover:text-white'
}`}
>
{filter}
{filter === 'Panic' && <span className="ml-1">🔥</span>}
</button>
))}
</div>
</div>
{/* News Cards */}
<div className="space-y-4">
{filteredNews.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="card-hover p-5 cursor-pointer"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<span className="text-sm text-gray-500">{item.source}</span>
<span className="text-gray-600"></span>
<span className="text-sm text-gray-500">{item.time}</span>
<span className={getSentimentBadge(item.sentiment)}>
{item.sentiment <= -50 ? '🔴 Panic' : item.sentiment > 20 ? '🟢 Positive' : 'Neutral'}
</span>
</div>
<h3 className="text-lg font-semibold hover:text-green-400 transition-colors">
{item.title}
</h3>
<p className="text-gray-500 mt-2 text-sm">{item.summary}</p>
<div className="flex items-center space-x-2 mt-3">
{item.stocks.map((stock) => (
<span
key={stock}
className="px-2 py-1 bg-gray-800 rounded text-sm text-green-400 font-medium"
>
${stock}
</span>
))}
</div>
</div>
<div className="ml-6 text-center">
<div className={`text-3xl font-bold ${getSentimentColor(item.sentiment)}`}>
{item.sentiment}
</div>
<div className="text-xs text-gray-500 mt-1">Sentiment</div>
</div>
</div>
</motion.div>
))}
</div>
{filteredNews.length === 0 && (
<div className="card p-12 text-center">
<NewspaperIcon className="w-16 h-16 mx-auto text-gray-600" />
<h3 className="text-xl font-semibold mt-4">No News Found</h3>
<p className="text-gray-500 mt-2">
No news articles match your current filters.
</p>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,185 @@
import { motion } from 'framer-motion'
import { BellAlertIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'
// Mock data
const signals = [
{
id: '1',
symbol: 'NVDA',
name: 'NVIDIA Corporation',
confidence: 87,
price: 485.23,
drawdown: -18.5,
expectedRecovery: 35,
expectedDays: 45,
sentiment: -65,
status: 'active',
createdAt: '2024-01-15T10:30:00Z',
reason: 'Panic selling due to supply chain concerns. Historical pattern shows 85% recovery rate within 60 days.',
},
{
id: '2',
symbol: 'META',
name: 'Meta Platforms Inc.',
confidence: 76,
price: 345.67,
drawdown: -12.3,
expectedRecovery: 25,
expectedDays: 30,
sentiment: -48,
status: 'active',
createdAt: '2024-01-15T09:15:00Z',
reason: 'Reality Labs layoff announcement. Similar events historically recovered within 45 days.',
},
{
id: '3',
symbol: 'TSLA',
name: 'Tesla Inc.',
confidence: 71,
price: 178.90,
drawdown: -25.8,
expectedRecovery: 45,
expectedDays: 60,
sentiment: -72,
status: 'active',
createdAt: '2024-01-15T08:00:00Z',
reason: 'Autopilot recall news. Tesla has recovered from similar negative news 78% of the time.',
},
]
export default function Signals() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold gradient-text">Buy Signals</h1>
<p className="text-gray-500 mt-1">Opportunities identified by panic pattern matching</p>
</div>
<div className="flex space-x-2">
<button className="btn-secondary">Filter</button>
<button className="btn-primary">Refresh</button>
</div>
</div>
{/* Signal Cards */}
<div className="space-y-4">
{signals.map((signal, index) => (
<motion.div
key={signal.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="card p-6"
>
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
{/* Left: Stock Info */}
<div className="flex items-start space-x-4">
<div className="w-14 h-14 bg-gradient-to-br from-green-500/20 to-emerald-500/20 rounded-xl flex items-center justify-center border border-green-500/30 animate-glow">
<BellAlertIcon className="w-7 h-7 text-green-400" />
</div>
<div>
<div className="flex items-center space-x-2">
<h3 className="text-xl font-bold">${signal.symbol}</h3>
<span className="badge-success">Active</span>
</div>
<p className="text-sm text-gray-500">{signal.name}</p>
<p className="text-sm text-gray-400 mt-1">
Signal generated {new Date(signal.createdAt).toLocaleString()}
</p>
</div>
</div>
{/* Middle: Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6">
<div>
<p className="text-xs text-gray-500 uppercase">Price</p>
<p className="text-xl font-bold">${signal.price.toFixed(2)}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase">Drawdown</p>
<p className="text-xl font-bold text-red-400">{signal.drawdown}%</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase">Expected</p>
<p className="text-xl font-bold text-green-400">+{signal.expectedRecovery}%</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase">Timeframe</p>
<p className="text-xl font-bold">{signal.expectedDays}d</p>
</div>
</div>
{/* Right: Confidence & Actions */}
<div className="flex items-center space-x-6">
<div className="text-center">
<div className="relative w-20 h-20">
<svg className="w-20 h-20 transform -rotate-90">
<circle
cx="40"
cy="40"
r="36"
fill="none"
stroke="#374151"
strokeWidth="8"
/>
<circle
cx="40"
cy="40"
r="36"
fill="none"
stroke="url(#gradient)"
strokeWidth="8"
strokeDasharray={`${signal.confidence * 2.26} 226`}
strokeLinecap="round"
/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22c55e" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
</defs>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-bold text-green-400">{signal.confidence}%</span>
</div>
</div>
<p className="text-xs text-gray-500 mt-1">Confidence</p>
</div>
<div className="flex flex-col space-y-2">
<button className="btn-primary flex items-center">
<CheckIcon className="w-4 h-4 mr-1" />
Buy
</button>
<button className="btn-secondary flex items-center">
<XMarkIcon className="w-4 h-4 mr-1" />
Dismiss
</button>
</div>
</div>
</div>
{/* Reason */}
<div className="mt-4 p-4 bg-gray-800/50 rounded-lg">
<p className="text-sm text-gray-400">
<span className="text-gray-500 font-medium">Analysis: </span>
{signal.reason}
</p>
</div>
</motion.div>
))}
</div>
{/* Empty State */}
{signals.length === 0 && (
<div className="card p-12 text-center">
<BellAlertIcon className="w-16 h-16 mx-auto text-gray-600" />
<h3 className="text-xl font-semibold mt-4">No Active Signals</h3>
<p className="text-gray-500 mt-2">
When panic creates opportunities, they'll appear here.
</p>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,136 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline'
// Mock data
const stocks = [
{ symbol: 'AAPL', name: 'Apple Inc.', sector: 'Technology', price: 185.23, change: 1.25, sentiment: 15 },
{ symbol: 'MSFT', name: 'Microsoft Corporation', sector: 'Technology', price: 378.45, change: -0.85, sentiment: 8 },
{ symbol: 'NVDA', name: 'NVIDIA Corporation', sector: 'Technology', price: 485.23, change: -3.45, sentiment: -45 },
{ symbol: 'GOOGL', name: 'Alphabet Inc.', sector: 'Technology', price: 142.67, change: 0.55, sentiment: 5 },
{ symbol: 'META', name: 'Meta Platforms Inc.', sector: 'Technology', price: 345.67, change: -2.15, sentiment: -35 },
{ symbol: 'TSLA', name: 'Tesla Inc.', sector: 'Consumer Discretionary', price: 178.90, change: -5.25, sentiment: -65 },
{ symbol: 'LMT', name: 'Lockheed Martin', sector: 'Defense', price: 458.32, change: 4.55, sentiment: 55 },
{ symbol: 'RTX', name: 'RTX Corporation', sector: 'Defense', price: 92.45, change: 2.15, sentiment: 42 },
{ symbol: 'XOM', name: 'Exxon Mobil', sector: 'Energy', price: 102.34, change: -1.85, sentiment: -25 },
{ symbol: 'JPM', name: 'JPMorgan Chase', sector: 'Financials', price: 172.56, change: 0.95, sentiment: 12 },
]
const sectors = ['All', 'Technology', 'Defense', 'Energy', 'Financials', 'Healthcare', 'Consumer']
export default function Stocks() {
const [search, setSearch] = useState('')
const [selectedSector, setSelectedSector] = useState('All')
const filteredStocks = stocks.filter(stock => {
const matchesSearch = stock.symbol.toLowerCase().includes(search.toLowerCase()) ||
stock.name.toLowerCase().includes(search.toLowerCase())
const matchesSector = selectedSector === 'All' || stock.sector === selectedSector
return matchesSearch && matchesSector
})
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold gradient-text">Stocks</h1>
<p className="text-gray-500 mt-1">Monitor and track stocks across all sectors</p>
</div>
<button className="btn-primary flex items-center">
<PlusIcon className="w-4 h-4 mr-2" />
Add Stock
</button>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
type="text"
placeholder="Search by symbol or name..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="input pl-10"
/>
</div>
<div className="flex space-x-2 overflow-x-auto pb-2">
{sectors.map((sector) => (
<button
key={sector}
onClick={() => setSelectedSector(sector)}
className={`px-4 py-2 rounded-lg whitespace-nowrap transition-colors ${
selectedSector === sector
? 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'bg-gray-800 text-gray-400 hover:text-white'
}`}
>
{sector}
</button>
))}
</div>
</div>
{/* Stocks Table */}
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-800">
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">Symbol</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase">Sector</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase">Price</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase">Change</th>
<th className="px-6 py-4 text-center text-xs font-medium text-gray-500 uppercase">Sentiment</th>
<th className="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody>
{filteredStocks.map((stock, index) => (
<motion.tr
key={stock.symbol}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: index * 0.05 }}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="px-6 py-4">
<span className="font-bold text-green-400">${stock.symbol}</span>
</td>
<td className="px-6 py-4 text-gray-300">{stock.name}</td>
<td className="px-6 py-4">
<span className="badge-info">{stock.sector}</span>
</td>
<td className="px-6 py-4 text-right font-mono">${stock.price.toFixed(2)}</td>
<td className={`px-6 py-4 text-right font-mono ${stock.change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{stock.change >= 0 ? '+' : ''}{stock.change.toFixed(2)}%
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-center">
<div className="w-16 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full ${stock.sentiment >= 0 ? 'bg-green-500' : 'bg-red-500'}`}
style={{
width: `${Math.abs(stock.sentiment)}%`,
marginLeft: stock.sentiment >= 0 ? '50%' : `${50 - Math.abs(stock.sentiment)}%`,
}}
/>
</div>
<span className={`ml-2 text-sm ${stock.sentiment >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{stock.sentiment}
</span>
</div>
</td>
<td className="px-6 py-4 text-right">
<button className="text-sm text-green-400 hover:text-green-300">View</button>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,193 @@
import { motion } from 'framer-motion'
import { StarIcon, TrashIcon, BellIcon } from '@heroicons/react/24/outline'
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'
// Mock data
const watchlist = [
{
id: '1',
symbol: 'NVDA',
name: 'NVIDIA Corporation',
price: 485.23,
change: -3.45,
sentiment: -45,
alertThreshold: -50,
priority: 1,
notes: 'Watching for panic below $450',
},
{
id: '2',
symbol: 'TSLA',
name: 'Tesla Inc.',
price: 178.90,
change: -5.25,
sentiment: -65,
alertThreshold: -60,
priority: 1,
notes: 'Autopilot recall - monitoring sentiment',
},
{
id: '3',
symbol: 'META',
name: 'Meta Platforms Inc.',
price: 345.67,
change: -2.15,
sentiment: -35,
alertThreshold: -50,
priority: 2,
notes: 'Reality Labs concerns',
},
{
id: '4',
symbol: 'BA',
name: 'Boeing Company',
price: 215.34,
change: -1.85,
sentiment: -28,
alertThreshold: -40,
priority: 2,
notes: 'Safety issues being monitored',
},
{
id: '5',
symbol: 'LMT',
name: 'Lockheed Martin',
price: 458.32,
change: 4.55,
sentiment: 55,
alertThreshold: -30,
priority: 3,
notes: 'Defense sector strength',
},
]
export default function Watchlist() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold gradient-text">Watchlist</h1>
<p className="text-gray-500 mt-1">Stocks you're monitoring for opportunities</p>
</div>
<button className="btn-primary flex items-center">
<StarIcon className="w-4 h-4 mr-2" />
Add to Watchlist
</button>
</div>
{/* Priority Legend */}
<div className="flex items-center space-x-4 text-sm">
<span className="text-gray-500">Priority:</span>
<span className="flex items-center">
<span className="w-3 h-3 bg-red-500 rounded-full mr-2" />
High
</span>
<span className="flex items-center">
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-2" />
Medium
</span>
<span className="flex items-center">
<span className="w-3 h-3 bg-blue-500 rounded-full mr-2" />
Low
</span>
</div>
{/* Watchlist Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{watchlist.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.1 }}
className="card p-5"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div className={`w-2 h-2 rounded-full ${
item.priority === 1 ? 'bg-red-500' :
item.priority === 2 ? 'bg-yellow-500' : 'bg-blue-500'
}`} />
<div>
<div className="flex items-center space-x-2">
<h3 className="font-bold text-lg">${item.symbol}</h3>
<StarIconSolid className="w-4 h-4 text-yellow-500" />
</div>
<p className="text-sm text-gray-500">{item.name}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
<BellIcon className="w-5 h-5 text-gray-400" />
</button>
<button className="p-2 hover:bg-red-500/20 rounded-lg transition-colors">
<TrashIcon className="w-5 h-5 text-red-400" />
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500 uppercase">Price</p>
<p className="text-lg font-bold">${item.price.toFixed(2)}</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase">Change</p>
<p className={`text-lg font-bold ${item.change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{item.change >= 0 ? '+' : ''}{item.change.toFixed(2)}%
</p>
</div>
<div>
<p className="text-xs text-gray-500 uppercase">Sentiment</p>
<p className={`text-lg font-bold ${item.sentiment >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{item.sentiment}
</p>
</div>
</div>
{/* Alert Threshold */}
<div className="mb-4">
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-500">Panic Alert Threshold</span>
<span className="text-red-400">{item.alertThreshold}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
item.sentiment <= item.alertThreshold ? 'bg-red-500 animate-pulse' : 'bg-gray-600'
}`}
style={{ width: `${Math.abs(item.sentiment)}%` }}
/>
</div>
{item.sentiment <= item.alertThreshold && (
<p className="text-xs text-red-400 mt-1">⚠️ Below threshold - Watch for opportunity!</p>
)}
</div>
{/* Notes */}
{item.notes && (
<div className="p-3 bg-gray-800/50 rounded-lg">
<p className="text-sm text-gray-400">
<span className="text-gray-500">Notes: </span>
{item.notes}
</p>
</div>
)}
</motion.div>
))}
</div>
{watchlist.length === 0 && (
<div className="card p-12 text-center">
<StarIcon className="w-16 h-16 mx-auto text-gray-600" />
<h3 className="text-xl font-semibold mt-4">Your Watchlist is Empty</h3>
<p className="text-gray-500 mt-2">
Add stocks you want to monitor for panic-buying opportunities.
</p>
<button className="btn-primary mt-4">Add Your First Stock</button>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,79 @@
import axios from 'axios'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
export const api = axios.create({
baseURL: `${API_URL}/api/v1`,
headers: {
'Content-Type': 'application/json',
},
})
// Stocks
export const stocksApi = {
list: (params?: { sector?: string; search?: string; skip?: number; limit?: number }) =>
api.get('/stocks/', { params }),
get: (symbol: string) => api.get(`/stocks/${symbol}`),
create: (data: { symbol: string; name: string; sector?: string; industry?: string }) =>
api.post('/stocks/', data),
delete: (symbol: string) => api.delete(`/stocks/${symbol}`),
getSectors: () => api.get('/stocks/sectors'),
getIndustries: (sector?: string) => api.get('/stocks/industries', { params: { sector } }),
}
// News
export const newsApi = {
list: (params?: { source?: string; sentiment?: string; hours?: number; skip?: number; limit?: number }) =>
api.get('/news/', { params }),
getForStock: (symbol: string, params?: { hours?: number }) =>
api.get(`/news/stock/${symbol}`, { params }),
getPanicNews: (params?: { threshold?: number; hours?: number; limit?: number }) =>
api.get('/news/panic', { params }),
get: (id: string) => api.get(`/news/${id}`),
}
// Signals
export const signalsApi = {
list: (params?: { status?: string; min_confidence?: number; skip?: number; limit?: number }) =>
api.get('/signals/', { params }),
getTop: (limit?: number) => api.get('/signals/top', { params: { limit } }),
get: (id: string) => api.get(`/signals/${id}`),
trigger: (id: string) => api.post(`/signals/${id}/trigger`),
dismiss: (id: string) => api.post(`/signals/${id}/dismiss`),
}
// Watchlist
export const watchlistApi = {
list: (priority?: number) => api.get('/watchlist/', { params: { priority } }),
add: (data: {
symbol: string
panic_alert_threshold?: number
price_alert_low?: number
price_alert_high?: number
priority?: number
notes?: string
}) => api.post('/watchlist/', data),
update: (id: string, data: Partial<{
panic_alert_threshold: number
price_alert_low: number
price_alert_high: number
priority: number
notes: string
is_active: boolean
}>) => api.put(`/watchlist/${id}`, data),
remove: (id: string) => api.delete(`/watchlist/${id}`),
removeBySymbol: (symbol: string) => api.delete(`/watchlist/symbol/${symbol}`),
}
// Analytics
export const analyticsApi = {
getDashboard: () => api.get('/analytics/dashboard'),
getSentimentTrend: (days?: number) => api.get('/analytics/sentiment/trend', { params: { days } }),
getSectorPanic: () => api.get('/analytics/sector/panic'),
getTopPatterns: (limit?: number) => api.get('/analytics/patterns/top', { params: { limit } }),
getRecentPanicEvents: (days?: number, limit?: number) =>
api.get('/analytics/panic-events/recent', { params: { days, limit } }),
getPerformance: (days?: number) => api.get('/analytics/performance', { params: { days } }),
}
export default api

View File

@@ -0,0 +1,54 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Custom trading colors
panic: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
greed: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
neutral: {
850: '#1a1a2e',
950: '#0f0f1a',
},
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'glow': 'glow 2s ease-in-out infinite alternate',
},
keyframes: {
glow: {
'0%': { boxShadow: '0 0 5px rgb(34 197 94 / 0.5), 0 0 10px rgb(34 197 94 / 0.3)' },
'100%': { boxShadow: '0 0 10px rgb(34 197 94 / 0.8), 0 0 20px rgb(34 197 94 / 0.5)' },
},
},
},
},
plugins: [],
}

24
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

16
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})