Files
investment-sandbox/components/modules/scanner/ScannerDemo.tsx
2026-06-06 21:11:16 +02:00

539 lines
27 KiB
TypeScript

'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore, ScannerAlert, WatchlistItem } from '@/lib/store';
import { calculateGJRGARCH } from '@/lib/math/statistics';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play
} from 'lucide-react';
// Predefined mock database for deep-check searches
interface SearchResult {
ticker: string;
name: string;
priceChange: number;
sentiment: 'GREEN' | 'YELLOW' | 'RED';
whyDropped: string;
gjrGarchVol: number;
reboundScore: number;
returns: number[];
}
const mockSearchDatabase: Record<string, SearchResult> = {
'RACE': {
ticker: 'RACE',
name: 'Ferrari N.V.',
priceChange: -0.065,
sentiment: 'GREEN',
whyDropped: 'Emotionaler Abverkauf nach viralem Video von Cristiano Ronaldo, der sich über Autoprobleme beschwert. Keine fundamentalen Schäden.',
gjrGarchVol: 0.048,
reboundScore: 88,
returns: [0.01, -0.005, 0.012, -0.008, 0.003, -0.065]
},
'KO': {
ticker: 'KO',
name: 'The Coca-Cola Co.',
priceChange: -0.052,
sentiment: 'GREEN',
whyDropped: 'Berühmter Influencer entfernt Coca-Cola Flasche während Pressekonferenz. Reine Social-Media-Hype Reaktion.',
gjrGarchVol: 0.021,
reboundScore: 82,
returns: [0.002, 0.005, -0.003, 0.001, -0.002, -0.052]
},
'TSLA': {
ticker: 'TSLA',
name: 'Tesla Inc.',
priceChange: -0.084,
sentiment: 'YELLOW',
whyDropped: 'Auslieferungszahlen leicht unter Analystenschätzungen. Margenentwicklung bleibt jedoch stabil.',
gjrGarchVol: 0.062,
reboundScore: 65,
returns: [-0.012, 0.008, -0.025, 0.015, -0.005, -0.084]
},
'SMCI': {
ticker: 'SMCI',
name: 'Super Micro Computer',
priceChange: -0.124,
sentiment: 'RED',
whyDropped: 'Hindenburg Research Short-Seller-Report bezüglich mutmaßlicher Bilanzmanipulationen veröffentlicht.',
gjrGarchVol: 0.085,
reboundScore: 24,
returns: [0.035, -0.018, 0.042, -0.051, 0.012, -0.124]
},
'BA': {
ticker: 'BA',
name: 'Boeing Co.',
priceChange: -0.071,
sentiment: 'RED',
whyDropped: 'FAA verhängt vorübergehendes Flugverbot nach erneutem technischen Zwischenfall mit Rumpftür.',
gjrGarchVol: 0.058,
reboundScore: 18,
returns: [-0.005, -0.012, 0.005, -0.021, -0.008, -0.071]
},
'NFLX': {
ticker: 'NFLX',
name: 'Netflix Inc.',
priceChange: -0.058,
sentiment: 'GREEN',
whyDropped: 'Gerüchte über angebliche Preissenkungen in Schwellenländern belasten Kurs kurzfristig.',
gjrGarchVol: 0.038,
reboundScore: 78,
returns: [0.015, -0.002, 0.008, 0.005, -0.011, -0.058]
}
};
export default function ScannerDemo() {
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick } = useSandboxStore();
// Component local states
const [scanning, setScanning] = useState(false);
const [scanProgress, setScanProgress] = useState('');
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([
{ id: 'sa1', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa2', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
]);
const [searchQuery, setSearchQuery] = useState('');
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
const [searchError, setSearchError] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
// Run market scan simulator
const handleMarketScan = () => {
setScanning(true);
setScanProgress('Verbinde mit Börsenfeeds...');
setTimeout(() => {
setScanProgress('Berechne historische Volatilitätsmatrizen...');
setTimeout(() => {
setScanProgress('Filtere abnormale Abweichungen (Asset > -5%, Index stabil)...');
setTimeout(() => {
// Scan isolated anomalies
setActiveAlerts([
{ id: 'sa3', ticker: 'RACE', priceChange: -0.065, gjrGarchVol: 0.048, overreactionScore: 88, status: 'UNDEREVALUATED' },
{ id: 'sa4', ticker: 'KO', priceChange: -0.052, gjrGarchVol: 0.021, overreactionScore: 82, status: 'UNDEREVALUATED' },
{ id: 'sa5', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa6', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
{ id: 'sa7', ticker: 'BA', priceChange: -0.071, gjrGarchVol: 0.058, overreactionScore: 18, status: 'OVERVALUATED' },
]);
setScanning(false);
setScanProgress('');
}, 600);
}, 500);
}, 400);
};
// Perform a manual deep check
const handleDeepCheck = (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
setSearchResult(null);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
if (mockSearchDatabase[query]) {
setSearchResult(mockSearchDatabase[query]);
} else {
// Simulate dynamic result for unknown assets
const simulatedVol = 0.03 + Math.random() * 0.04;
const simulatedScore = Math.floor(40 + Math.random() * 50);
const isNegative = Math.random() > 0.4;
const simulatedChange = -0.05 - Math.random() * 0.06;
const res: SearchResult = {
ticker: query,
name: `${query} Corp.`,
priceChange: simulatedChange,
sentiment: isNegative ? (simulatedScore > 75 ? 'GREEN' : 'YELLOW') : 'RED',
whyDropped: 'Simulierte Marktabweichung basierend auf automatischem Sentiment-Scanning der Finanzberichte.',
gjrGarchVol: simulatedVol,
reboundScore: simulatedScore,
returns: [0.005, -0.008, 0.012, -0.015, 0.004, simulatedChange]
};
setSearchResult(res);
}
};
const handleAddToWatchlist = (ticker: string, priceChange: number, sentiment: 'GREEN' | 'YELLOW' | 'RED', whyDropped: string) => {
// Determine a mock initial price based on ticker
let initialPrice = 150;
if (ticker === 'RACE') initialPrice = 380;
if (ticker === 'KO') initialPrice = 60;
if (ticker === 'TSLA') initialPrice = 175;
if (ticker === 'NFLX') initialPrice = 610;
addToWatchlist({
ticker,
priceChange,
sentiment,
whyDropped,
initialPrice,
currentPrice: initialPrice * (1 + priceChange), // current price after drop
});
};
// Compute GJR-GARCH forecasting series for the math accordion/visual validation
const gjrGarchMathResult = useMemo(() => {
const mockReturns = [0.015, -0.022, 0.008, -0.031, 0.014, -0.055, 0.012, -0.008, 0.024, -0.065];
return calculateGJRGARCH(mockReturns);
}, []);
const mathChartData = useMemo(() => {
return gjrGarchMathResult.series.map((vol, idx) => ({
day: `T-${gjrGarchMathResult.series.length - idx - 1}`,
'GJR-GARCH Vol (%)': parseFloat(vol.toFixed(2)),
}));
}, [gjrGarchMathResult]);
return (
<div className="space-y-6">
{/* SECTION 1: Overreaction Scan Action */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div className="space-y-1">
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<ShieldAlert className="text-amber-400 w-5 h-5" /> Anomalien-Scanner & Marktverzerrungen
</h2>
<p className="text-slate-400 text-xs max-w-2xl">
Isoliert Kursstürze &gt; 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
</p>
</div>
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
<button
onClick={handleMarketScan}
disabled={scanning}
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 disabled:from-amber-850 disabled:to-orange-900 disabled:text-slate-400 text-slate-950 font-bold py-3 px-6 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 active:scale-[0.98]"
>
<RefreshCw className={`w-5 h-5 ${scanning ? 'animate-spin' : ''}`} />
<span>{scanning ? 'Scanne Markt...' : 'Markt scannen'}</span>
</button>
{scanning && (
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
)}
</div>
</div>
{/* Collapsible Math Accordion */}
<div className="border-t border-slate-850 pt-4 mt-6">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-amber-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">GJR-GARCH(1,1) Volatilitätsmodellierung & Leverage-Effekt</span>
</button>
{showMathAccordion && (
<div className="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300">
<div className="space-y-3">
<p>
Das <strong>GJR-GARCH(1,1)</strong>-Modell erfasst die Asymmetrie im Volatilitätsprozess von Renditen. Es besitzt einen zusätzlichen Term (<InlineMath math="\gamma" />), der aktiviert wird, wenn der gestrige Schock negativ war.
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \gamma \epsilon_{t-1}^2 I_{t-1} + \beta \sigma_{t-1}^2" />
</div>
<p>
Die Indikatorvariable <InlineMath math="I_{t-1}" /> nimmt den Wert 1 bei einem Kurssturz an:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="I_{t-1} = \begin{cases} 1 & \text{falls } \epsilon_{t-1} < 0 \\ 0 & \text{sonst} \end{cases}" />
</div>
<p className="text-slate-400">
Sind die Volatilitätsschocks asymmetrisch (<InlineMath math="\gamma > 0" />), führt ein Kurssturz (Bad News) zu einer signifikant höheren Volatilität als ein gleich großer Kursgewinn (Good News).
</p>
</div>
<div className="space-y-2">
<div className="text-xs text-slate-400 font-semibold mb-2">Simulierter GJR-GARCH Volatilitätsprozess nach Schocks</div>
<div className="h-44 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={mathChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="day" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} unit="%" />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Line type="monotone" dataKey="GJR-GARCH Vol (%)" stroke="#f59e0b" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
<div className="text-[10px] text-slate-500 font-mono text-center">
Spike am Tag T-4 repräsentiert die asymmetrische Reaktion auf einen Schock von -6.5%.
</div>
</div>
</div>
)}
</div>
</div>
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Scanned anomalies details */}
<div className="xl:col-span-2 space-y-6">
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Sparkles className="text-amber-400 w-5 h-5" /> Gefundene Anomalien (Sentiment-Ampel)
</h3>
<div className="space-y-4">
{activeAlerts.map((alert) => {
// Fetch associated info from mockDB if available, else generic mock
const dbInfo = mockSearchDatabase[alert.ticker] || {
name: `${alert.ticker} Corp.`,
sentiment: alert.overreactionScore > 75 ? 'GREEN' : (alert.overreactionScore > 40 ? 'YELLOW' : 'RED'),
whyDropped: 'Kurzfristige Eindeckungen und Gewinnmitnahmen an den Terminmärkten belasten das Sentiment.'
};
const isGreen = dbInfo.sentiment === 'GREEN';
const isYellow = dbInfo.sentiment === 'YELLOW';
const isRed = dbInfo.sentiment === 'RED';
return (
<div key={alert.id} className="p-5 bg-slate-950/40 border border-slate-850 rounded-xl space-y-4 relative group">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 border-b border-slate-900 pb-3">
<div>
<div className="flex items-center gap-2.5">
<span className="font-mono font-bold text-lg text-slate-100">{alert.ticker}</span>
<span className="text-slate-400 text-xs">({dbInfo.name})</span>
</div>
<div className="text-[10px] text-slate-400 mt-1">
Kurssturz: <span className="text-rose-400 font-bold font-mono">{(alert.priceChange * 100).toFixed(1)}%</span>
<span className="mx-2">|</span>
GJR-GARCH Vol: <span className="text-cyan-400 font-bold font-mono">{(alert.gjrGarchVol * 100).toFixed(1)}%</span>
</div>
</div>
{/* Traffic Light Sentiment Badge */}
<div className="flex items-center gap-2">
{isGreen && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/25 flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5" /> EMOTIONALER OVERREACTION (KAUF)
</span>
)}
{isYellow && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-yellow-500/10 text-yellow-400 border border-yellow-500/25 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> UNSICHERHEIT (HALTEN)
</span>
)}
{isRed && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/25 flex items-center gap-1">
<XCircle className="w-3.5 h-3.5" /> FUNDAMENTALER SCHADEN (MEIDEN)
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Analysis Block */}
<div className="md:col-span-2 space-y-1">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<Info className="w-3 h-3 text-amber-400" /> KI-Ursachenanalyse:
</div>
<p className="text-xs text-slate-300 leading-relaxed italic">
&bdquo;{dbInfo.whyDropped}&ldquo;
</p>
</div>
{/* Actions & Score */}
<div className="flex flex-row md:flex-col items-center md:items-end justify-between md:justify-center gap-3 md:border-l md:border-slate-900 md:pl-4">
<div className="text-left md:text-right">
<span className="text-[9px] text-slate-400 uppercase tracking-widest block">Rebound Score</span>
<span className={`font-mono text-xl font-extrabold ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
{alert.overreactionScore}%
</span>
</div>
{(isGreen || isYellow) && (
<button
onClick={() => handleAddToWatchlist(alert.ticker, alert.priceChange, dbInfo.sentiment, dbInfo.whyDropped)}
className="bg-slate-900 hover:bg-slate-850 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[11px] font-semibold py-1.5 px-3 rounded-lg flex items-center gap-1 transition-colors active:scale-[0.98]"
>
<Plus className="w-3.5 h-3.5" /> Tracken
</button>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Right Column: Deep-Check & Search Mask */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<Search className="text-amber-400 w-5 h-5" /> Deep-Check Suchmaske
</h3>
<p className="text-xs text-slate-400 mt-2 leading-relaxed">
Suchen Sie gezielt nach Werten, um Anomalien-Analysen abzurufen. (z.B. RACE, KO, TSLA, SMCI, NFLX)
</p>
</div>
<form onSubmit={handleDeepCheck} className="flex gap-2">
<input
type="text"
required
placeholder="z.B. RACE"
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono focus:outline-none focus:border-amber-500 text-sm uppercase"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-amber-400 hover:text-amber-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
>
Prüfen
</button>
</form>
{searchResult && (
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-4 animate-fade-in">
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
<div>
<h4 className="font-bold text-slate-100 font-mono text-sm">{searchResult.ticker}</h4>
<span className="text-[10px] text-slate-500">{searchResult.name}</span>
</div>
<div>
{searchResult.sentiment === 'GREEN' && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px] font-bold border border-emerald-500/25">GREEN</span>}
{searchResult.sentiment === 'YELLOW' && <span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-400 text-[10px] font-bold border border-yellow-500/25">YELLOW</span>}
{searchResult.sentiment === 'RED' && <span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[10px] font-bold border border-rose-500/25">RED</span>}
</div>
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-slate-400">Aktueller Sturz:</span>
<span className="text-rose-400 font-mono font-bold">{(searchResult.priceChange * 100).toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">GJR-GARCH Volatilität:</span>
<span className="text-cyan-400 font-mono font-bold">{(searchResult.gjrGarchVol * 100).toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Rebound Score:</span>
<span className={`font-mono font-bold ${searchResult.sentiment === 'GREEN' ? 'text-emerald-400' : (searchResult.sentiment === 'YELLOW' ? 'text-yellow-400' : 'text-rose-400')}`}>
{searchResult.reboundScore}/100
</span>
</div>
<div className="pt-2 border-t border-slate-900">
<span className="text-[10px] text-slate-400 uppercase font-semibold block mb-1">KI-Kommentar:</span>
<p className="italic text-slate-300 leading-relaxed text-[11px]">{searchResult.whyDropped}</p>
</div>
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
<button
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
className="w-full bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 mt-2"
>
<Plus className="w-4 h-4" /> Watchlist hinzufügen
</button>
)}
</div>
</div>
)}
</div>
</div>
{/* SECTION 3: Hot Watchlist & Rebound-Tracker (48 Hours) */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-800 pb-3">
<div className="flex items-center gap-2">
<Flame className="text-orange-400 w-6 h-6 animate-pulse" />
<h3 className="text-lg font-bold text-white">Hot Rebound Watchlist &amp; Tracker (48h)</h3>
</div>
{watchlist.length > 0 && (
<button
onClick={simulateWatchlistTick}
className="bg-slate-950 hover:bg-slate-900 text-orange-400 hover:text-orange-350 border border-slate-800 text-xs font-bold py-2 px-4 rounded-xl flex items-center gap-1.5 transition-colors active:scale-[0.97]"
title="Simuliert das Fortschreiten der Zeit um 4 Stunden"
>
<Play className="w-4 h-4 fill-orange-400" />
<span>Simuliere +4 Std. Kursänderung</span>
</button>
)}
</div>
{watchlist.length === 0 ? (
<div className="p-12 text-center text-slate-500 text-sm border border-dashed border-slate-800 rounded-xl bg-slate-950/20">
Bislang keine Assets auf der Rebound-Watchlist. Nutzen Sie die Markt-Scanergebnisse oben, um Werte hinzuzufügen.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{watchlist.map((item) => {
const perf = item.reboundPerformance;
const isPositive = perf >= 0;
const progressPct = (item.hoursTracked / 48) * 100;
return (
<div key={item.id} className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-3 relative overflow-hidden">
<div className="absolute top-0 right-0 p-1.5">
<button
onClick={() => removeFromWatchlist(item.id)}
className="text-slate-500 hover:text-rose-400 transition-colors p-1"
title="Aus der Watchlist entfernen"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex justify-between items-start pr-6">
<div>
<div className="flex items-center gap-2">
<span className="font-mono font-bold text-slate-200 text-base">{item.ticker}</span>
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold ${item.sentiment === 'GREEN' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'}`}>
{item.sentiment}
</span>
</div>
<span className="text-[10px] text-slate-500">Gezogen bei: <span className="font-mono">{(item.priceChange * 100).toFixed(1)}%</span></span>
</div>
<div className="text-right">
<div className="text-[9px] text-slate-400 uppercase">Rebound Performance</div>
<div className={`font-mono text-base font-extrabold flex items-center justify-end gap-0.5 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? '+' : ''}{perf.toFixed(2)}%
</div>
</div>
</div>
{/* Progress timeline */}
<div className="space-y-1 text-[10px] text-slate-400">
<div className="flex justify-between font-mono">
<span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5 text-slate-500" /> Tracking-Dauer: {item.hoursTracked}/48 Std.</span>
<span>{Math.round(progressPct)}% abgeschlossen</span>
</div>
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
<div
className="bg-gradient-to-r from-orange-400 to-amber-500 h-full rounded-full transition-all duration-500"
style={{ width: `${progressPct}%` }}
/>
</div>
</div>
<p className="text-[10px] text-slate-500 italic truncate" title={item.whyDropped}>
Hintergrund: {item.whyDropped}
</p>
</div>
);
})}
</div>
)}
</div>
</div>
);
}