Files
investment-sandbox/components/modules/scanner/ScannerDemo.tsx

1086 lines
58 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 ScannerMathModal from './ScannerMathModal';
import ScannerBlueprintModal from './ScannerBlueprintModal';
import {
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
BookOpen, TrendingUp
} from 'lucide-react';
// Predefined mock database for deep-check searches (Removed mock database)
interface SearchResult {
ticker: string;
name: string;
priceChange: number;
sentiment: 'GREEN' | 'YELLOW' | 'RED';
whyDropped: string;
gjrGarchVol: number;
reboundScore: number;
returns: number[];
currentPrice?: number;
peakPrice?: number;
sloanRatio?: number;
sloanRegime?: 'SAFE' | 'ANOMALY';
}
interface PEADData {
ticker: string;
name: string;
peadSector: string;
announcementDate: string;
daysElapsed: number;
epsActual: number;
epsConsensus: number;
surprisePercent: number;
driftStatus: 'Active Drift' | 'Consolidating';
isLiveApi?: boolean;
}
export default function ScannerDemo() {
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore();
// Component local states
const [scanning, setScanning] = useState(false);
const [scanProgress, setScanProgress] = useState('');
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([]);
const [scanMode, setScanMode] = useState<'day_crash' | 'ma_drop' | '52w_dist' | 'rsi_oversold'>('day_crash');
const [marketRegion, setMarketRegion] = useState<'us' | 'eu' | 'crypto'>('us');
const [scanResults, setScanResults] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
const [searchError, setSearchError] = useState(false);
const [checkingDeep, setCheckingDeep] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false);
const [isShieldActive, setIsShieldActive] = useState(false);
const [leftPanelTab, setLeftPanelTab] = useState<'screener' | 'pead'>('screener');
const [peadData, setPeadData] = useState<PEADData[]>([
{ ticker: 'NVDA', name: 'NVIDIA Corporation', peadSector: 'Technology', announcementDate: '2026-05-20', daysElapsed: 25, epsActual: 6.12, epsConsensus: 5.58, surprisePercent: 9.68, driftStatus: 'Active Drift' },
{ ticker: 'AAPL', name: 'Apple Inc.', peadSector: 'Technology', announcementDate: '2026-05-02', daysElapsed: 43, epsActual: 1.53, epsConsensus: 1.50, surprisePercent: 2.00, driftStatus: 'Consolidating' },
{ ticker: 'MSFT', name: 'Microsoft Corporation', peadSector: 'Technology', announcementDate: '2026-04-25', daysElapsed: 50, epsActual: 2.94, epsConsensus: 2.82, surprisePercent: 4.26, driftStatus: 'Consolidating' },
{ ticker: 'TSLA', name: 'Tesla Inc.', peadSector: 'Consumer Goods', announcementDate: '2026-04-23', daysElapsed: 52, epsActual: 0.45, epsConsensus: 0.51, surprisePercent: -11.76, driftStatus: 'Consolidating' },
{ ticker: 'JPM', name: 'JPMorgan Chase & Co.', peadSector: 'Financial Services', announcementDate: '2026-04-12', daysElapsed: 63, epsActual: 4.44, epsConsensus: 4.15, surprisePercent: 6.99, driftStatus: 'Consolidating' }
]);
const [isLivePeadApi, setIsLivePeadApi] = useState(false);
// Cache for metadata and prices retrieved dynamically
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
const [alertsPrices, setAlertsPrices] = useState<Record<string, { peakPrice: number; currentPrice: number }>>({});
const [expandedTicker, setExpandedTicker] = useState<string | null>(null);
const [selectedCatalysts, setSelectedCatalysts] = useState<Record<string, string>>({});
const getDefaultCatalyst = (ticker: string) => {
const isTech = ['AAPL', 'MSFT', 'NVDA', 'TSLA', 'AMD', 'SMCI', 'PLTR'].includes(ticker);
if (isTech) {
if (ticker === 'TSLA' || ticker === 'SMCI') return 'executive_shift';
return 'systemic_selloff';
}
const isSmall = ['SOFI', 'MARA', 'RIOT', 'PLUG', 'FUBO', 'BMEA'].includes(ticker);
if (isSmall) return 'supply_chain';
return 'earnings_miss';
};
const getStressCoefficient = (catalyst: string) => {
switch (catalyst) {
case 'systemic_selloff': return 15;
case 'supply_chain': return 40;
case 'executive_shift': return 55;
case 'regulatory_fine': return 65;
case 'earnings_miss': return 75;
default: return 15;
}
};
const getReboundScore = (overreactionScore: number, catalyst: string) => {
const stress = getStressCoefficient(catalyst);
const score = 0.6 * overreactionScore + 0.4 * (100 - stress);
return Math.round(score);
};
// Run market scan simulator querying real live API
const handleMarketScan = async () => {
setScanning(true);
setScanProgress('Verbinde mit Börsenfeeds...');
try {
setScanProgress(`Rufe die ${marketRegion.toUpperCase()} Marktdaten ab...`);
const response = await fetch(`/api/scanner?mode=${scanMode}&region=${marketRegion}`);
if (!response.ok) {
throw new Error('Failed to fetch scanner tickers');
}
const data = await response.json();
setIsShieldActive(!!data.isShieldActive);
const results = data.results || [];
setScanProgress('Berechne GJR-GARCH Volatilitäten...');
setScanResults(results);
const newAlerts: ScannerAlert[] = [];
const newMetadata: Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }> = {};
const newPrices: Record<string, { peakPrice: number; currentPrice: number }> = {};
results.forEach((result: any) => {
if (result.error) return;
// Calculate dynamic volatility from return series
const gjrResult = calculateGJRGARCH(result.returns);
const gjrGarchVol = gjrResult.forecast / 100;
// Calculate overreaction ratio
let dropAbs = Math.abs(result.priceChange);
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
const ratio = dropAbs / (gjrGarchVol || 0.01);
let overreactionScore = Math.round(ratio * 30 + 30);
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
const whyDropped = `Wert liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH(1,1) Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
newAlerts.push({
id: `sa_${result.ticker}_${Date.now()}`,
ticker: result.ticker,
priceChange: result.priceChange,
gjrGarchVol,
overreactionScore,
status
});
newMetadata[result.ticker] = {
name: result.name,
whyDropped,
sentiment
};
newPrices[result.ticker] = {
peakPrice: result.peakPrice,
currentPrice: result.currentPrice
};
});
setAlertsMetadata(prev => ({ ...prev, ...newMetadata }));
setAlertsPrices(prev => ({ ...prev, ...newPrices }));
setActiveAlerts(newAlerts);
const peadList: PEADData[] = [];
results.forEach((r: any) => {
if (r.error || r.peadSector === 'Cryptocurrency') return;
peadList.push({
ticker: r.ticker,
name: r.name,
peadSector: r.peadSector || 'Conglomerate',
announcementDate: r.announcementDate || 'N/A',
daysElapsed: r.daysElapsed || 0,
epsActual: r.epsActual || 0,
epsConsensus: r.epsConsensus || 0,
surprisePercent: r.surprisePercent || 0,
driftStatus: r.driftStatus || 'Consolidating',
isLiveApi: r.isLiveApi
});
});
peadList.sort((a, b) => Math.abs(b.surprisePercent) - Math.abs(a.surprisePercent));
setPeadData(peadList);
const hasLivePead = peadList.some(item => item.isLiveApi === true);
setIsLivePeadApi(hasLivePead);
// Update global store alerts for Sandbox module use
updateScannerAlerts(newAlerts);
setScanProgress('');
} catch (err) {
console.error(err);
setScanProgress('Fehler beim Scannen der Live-Daten.');
} finally {
setScanning(false);
}
};
// Trigger scan automatically when scan mode or region toggles change
React.useEffect(() => {
handleMarketScan();
}, [scanMode, marketRegion]);
// Perform a manual deep check using real live API
const handleDeepCheck = async (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
setSearchResult(null);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
setCheckingDeep(true);
try {
const response = await fetch(`/api/finance?ticker=${query}`);
if (!response.ok) {
throw new Error('Failed to fetch');
}
const data = await response.json();
const result = data.results?.[0];
if (!result || result.error) {
throw new Error(result?.error || 'Invalid result');
}
const gjrResult = calculateGJRGARCH(result.returns);
const gjrGarchVol = gjrResult.forecast / 100;
const dropAbs = Math.abs(result.priceChange);
const ratio = dropAbs / (gjrGarchVol || 0.01);
let overreactionScore = Math.round(ratio * 30 + 30);
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
if (result.priceChange > -0.03) {
overreactionScore = Math.round(overreactionScore * 0.5);
}
const sentiment = overreactionScore > 70 ? 'GREEN' : (overreactionScore >= 40 ? 'YELLOW' : 'RED');
const whyDropped = `Mathematischer Ausbruchspunkt: Der Kurs verzeichnet einen Rückgang von ${Math.round(Math.abs(result.priceChange) * 100)}% ausgehend vom 90-Tage-Hoch von $${result.peakPrice.toFixed(2)}. GJR-GARCH-Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
const res: SearchResult = {
ticker: query,
name: result.name,
priceChange: result.priceChange,
sentiment,
whyDropped,
gjrGarchVol,
reboundScore: overreactionScore,
returns: result.returns,
currentPrice: result.currentPrice,
peakPrice: result.peakPrice,
sloanRatio: result.sloanRatio,
sloanRegime: result.sloanRegime
};
setAlertsMetadata(prev => ({
...prev,
[query]: { name: result.name, whyDropped, sentiment }
}));
setAlertsPrices(prev => ({
...prev,
[query]: { peakPrice: result.peakPrice, currentPrice: result.currentPrice }
}));
setSearchResult(res);
} catch (err) {
console.error(err);
setSearchError(true);
} finally {
setCheckingDeep(false);
}
};
const handleAddToWatchlist = (
ticker: string,
priceChange: number,
sentiment: 'GREEN' | 'YELLOW' | 'RED',
whyDropped: string,
peakPrice?: number,
currentPrice?: number
) => {
const realInitial = peakPrice || 100;
const realCurrent = currentPrice || (realInitial * (1 + priceChange));
addToWatchlist({
ticker,
priceChange,
sentiment,
whyDropped,
initialPrice: realInitial,
currentPrice: realCurrent,
});
};
// 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]);
const categorizedResults = useMemo(() => {
const mega: any[] = [];
const mid: any[] = [];
const small: any[] = [];
scanResults.forEach((result: any) => {
// Calculate dynamic volatility from return series
const gjrResult = calculateGJRGARCH(result.returns || []);
const gjrGarchVol = gjrResult.forecast / 100;
// Calculate overreaction ratio based on selected mode
let dropAbs = Math.abs(result.priceChange);
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
const ratio = dropAbs / (gjrGarchVol || 0.01);
let overreactionScore = Math.round(ratio * 30 + 30);
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
const whyDropped = `Kurs liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
const enriched = {
...result,
gjrGarchVol,
overreactionScore,
status,
sentiment,
whyDropped
};
const mcap = result.marketCap || 0;
if (mcap >= 100e9) {
mega.push(enriched);
} else if (mcap >= 10e9) {
mid.push(enriched);
} else {
small.push(enriched);
}
});
const sortByMode = (list: any[]) => {
return list.sort((a, b) => {
if (scanMode === 'ma_drop') return a.maDeviation - b.maDeviation;
if (scanMode === '52w_dist') return a.dist52w - b.dist52w;
if (scanMode === 'rsi_oversold') return a.rsi14 - b.rsi14;
return a.dayChange - b.dayChange; // day_crash
});
};
return {
mega: sortByMode(mega).slice(0, 5),
mid: sortByMode(mid).slice(0, 5),
small: sortByMode(small).slice(0, 5)
};
}, [scanResults, scanMode]);
const renderCategoryTable = (title: string, description: string, assets: any[]) => {
return (
<div className="bg-slate-900/40 border border-slate-850 rounded-2xl p-5 space-y-3">
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
<div>
<h4 className="font-bold text-white text-sm">{title}</h4>
<p className="text-[10px] text-slate-400 font-mono">{description}</p>
</div>
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-900 border border-slate-888 text-slate-300">
{assets.length} Assets
</span>
</div>
{assets.length === 0 ? (
<div className="py-6 text-center text-slate-555 text-xs italic">
Keine Assets in dieser Kategorie unter den Scanner-Ergebnissen.
</div>
) : (
<div className="overflow-x-auto scrollbar-thin">
<table className="w-full text-left text-xs border-collapse min-w-[750px]">
<thead>
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider">
<th className="py-2.5 px-3">Asset</th>
<th className="py-2.5 px-3">Preis</th>
<th className="py-2.5 px-3 text-right">Abweichung</th>
<th className="py-2.5 px-3 text-right">KGV (T)</th>
<th className="py-2.5 px-3 text-right">KGV (F)</th>
<th className="py-2.5 px-3 text-right">PEG</th>
<th className="py-2.5 px-3 text-right">KBV</th>
<th className="py-2.5 px-3 text-right">Rendite</th>
<th className="py-2.5 px-3 text-center">Score</th>
<th className="py-2.5 px-3 text-center">Rebound</th>
<th className="py-2.5 px-3 text-center">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-900/60">
{assets.map((asset) => {
const isGreen = asset.sentiment === 'GREEN';
const isYellow = asset.sentiment === 'YELLOW';
// Format deviation based on mode
let devText = '';
let devColor = 'text-slate-300';
if (scanMode === 'day_crash') {
devText = `${(asset.dayChange * 100).toFixed(2)}%`;
devColor = asset.dayChange < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
} else if (scanMode === 'ma_drop') {
devText = `${(asset.maDeviation * 100).toFixed(2)}%`;
devColor = asset.maDeviation < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
} else if (scanMode === '52w_dist') {
devText = `${(asset.dist52w * 100).toFixed(2)}%`;
devColor = asset.dist52w < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
} else if (scanMode === 'rsi_oversold') {
devText = asset.rsi14.toFixed(1);
devColor = asset.rsi14 < 30 ? 'text-amber-400 font-bold font-mono' : 'text-slate-400';
}
// Highlight valuation multipliers
const peColor = asset.trailingPE && asset.trailingPE > 0 && asset.trailingPE < 15 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
const fpeColor = asset.forwardPE && asset.forwardPE > 0 && asset.forwardPE < 12 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
const pegColor = asset.peg && asset.peg > 0 && asset.peg < 1.0 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
const pbColor = asset.priceToBook && asset.priceToBook > 0 && asset.priceToBook < 1.5 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
const divColor = asset.dividendYield && asset.dividendYield > 3.0 ? 'text-emerald-400 font-semibold' : 'text-slate-450';
const tickerCatalyst = selectedCatalysts[asset.ticker] || getDefaultCatalyst(asset.ticker);
const reboundScore = getReboundScore(asset.overreactionScore, tickerCatalyst);
const isExpanded = expandedTicker === asset.ticker;
return (
<React.Fragment key={asset.ticker}>
<tr
className="hover:bg-slate-900/30 transition-colors group cursor-pointer"
onClick={() => setExpandedTicker(isExpanded ? null : asset.ticker)}
>
<td className="py-3 px-3">
<div className="flex items-center gap-1.5">
{isExpanded ? <ChevronUp className="w-3.5 h-3.5 text-slate-500" /> : <ChevronDown className="w-3.5 h-3.5 text-slate-500" />}
<div className="flex flex-col">
<span className="font-mono font-bold text-slate-100 text-sm">{asset.ticker}</span>
<span className="text-[10px] text-slate-500 max-w-[120px] truncate" title={asset.name}>{asset.name}</span>
</div>
</div>
</td>
<td className="py-3 px-3 font-mono font-semibold text-slate-200">
${asset.currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className={`py-3 px-3 text-right font-mono ${devColor}`}>
{devText}
</td>
<td className={`py-3 px-3 text-right font-mono ${peColor}`}>
{asset.trailingPE && asset.trailingPE > 0 ? asset.trailingPE.toFixed(1) : 'N/A'}
</td>
<td className={`py-3 px-3 text-right font-mono ${fpeColor}`}>
{asset.forwardPE && asset.forwardPE > 0 ? asset.forwardPE.toFixed(1) : 'N/A'}
</td>
<td className={`py-3 px-3 text-right font-mono ${pegColor}`}>
{asset.peg && asset.peg > 0 ? asset.peg.toFixed(2) : 'N/A'}
</td>
<td className={`py-3 px-3 text-right font-mono ${pbColor}`}>
{asset.priceToBook && asset.priceToBook > 0 ? asset.priceToBook.toFixed(2) : 'N/A'}
</td>
<td className={`py-3 px-3 text-right font-mono ${divColor}`}>
{asset.dividendYield && asset.dividendYield > 0 ? `${asset.dividendYield.toFixed(2)}%` : '0.00%'}
</td>
<td className="py-3 px-3 text-center font-mono">
<span className={`font-bold text-xs ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
{asset.overreactionScore}%
</span>
</td>
<td className="py-3 px-3 text-center font-mono">
<span className={`font-bold text-xs ${reboundScore > 75 ? 'text-emerald-400' : (reboundScore > 45 ? 'text-yellow-400' : 'text-rose-400')}`}>
{reboundScore}%
</span>
</td>
<td className="py-3 px-3 text-center" onClick={(e) => e.stopPropagation()}>
{(isGreen || isYellow) ? (
<button
onClick={() => {
handleAddToWatchlist(asset.ticker, asset.priceChange, asset.sentiment, asset.whyDropped, asset.peakPrice, asset.currentPrice);
}}
className="bg-slate-900 hover:bg-slate-850 hover:border-emerald-500/50 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[10px] font-bold py-1 px-2.5 rounded-md transition-all active:scale-[0.96] cursor-pointer"
>
Track
</button>
) : (
<span className="text-[10px] text-slate-600 font-mono">-</span>
)}
</td>
</tr>
{isExpanded && (
<tr className="bg-slate-950/60 border-t border-slate-900">
<td colSpan={11} className="p-4" onClick={(e) => e.stopPropagation()}>
<div className="space-y-4 text-slate-300">
<div className="flex justify-between items-center border-b border-slate-850 pb-2">
<h5 className="font-bold text-xs text-amber-400 flex items-center gap-1.5">
<span>🔍 KI-Überreaktions- & Sentiment-Diagnose für {asset.ticker}</span>
</h5>
<span className="text-[9px] text-slate-500 font-mono">Status: {asset.status}</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[9px] text-slate-400 uppercase font-bold font-mono">Identifizierter Drop-Katalysator:</label>
<select
value={tickerCatalyst}
onChange={(e) => {
setSelectedCatalysts(prev => ({ ...prev, [asset.ticker]: e.target.value }));
}}
className="bg-slate-900 border border-slate-800 rounded-lg p-2 text-xs w-full text-slate-200 focus:outline-none focus:border-amber-500/50 cursor-pointer"
>
<option value="systemic_selloff">Systemic Selloff (Systemischer Markt-Ausverkauf - Stress: 15%)</option>
<option value="supply_chain">Supply Chain Disruption (Lieferengpass - Stress: 40%)</option>
<option value="executive_shift">Executive Shift (Management-Wechsel - Stress: 55%)</option>
<option value="regulatory_fine">Regulatory Issue / Fine (Regulierungen - Stress: 65%)</option>
<option value="earnings_miss">Earnings Miss (Gewinnverfehlung - Stress: 75%)</option>
</select>
<p className="text-[9px] text-slate-555 italic leading-relaxed">
*Der Katalysator bestimmt den Stress-Koeffizienten (C_stress), welcher die Erholungsgeschwindigkeit beeinflusst.
</p>
</div>
<div className="space-y-2">
<label className="text-[9px] text-slate-400 uppercase font-bold font-mono">Rebound-Wahrscheinlichkeits Herleitung:</label>
<div className="bg-slate-900/50 border border-slate-850 rounded-lg p-3 space-y-1.5 text-xs font-mono">
<div className="flex justify-between">
<span>Outlier Score (GJR-GARCH):</span>
<span className="text-slate-250 font-bold">{asset.overreactionScore}%</span>
</div>
<div className="flex justify-between">
<span>News Stress Damping:</span>
<span className="text-slate-450 font-semibold">-{getStressCoefficient(tickerCatalyst)}%</span>
</div>
<div className="border-t border-slate-800 pt-1.5 flex justify-between text-amber-400 font-bold">
<span>Rebound-Wahrscheinlichkeit:</span>
<span>{reboundScore}%</span>
</div>
</div>
</div>
</div>
<div className="bg-slate-900/30 border border-slate-850/80 rounded-xl p-3.5 space-y-1">
<div className="text-[9px] uppercase font-bold text-slate-400 font-mono">Diagnose & Katalysator-Analyse</div>
<p className="text-xs text-slate-450 leading-relaxed">
{tickerCatalyst === 'systemic_selloff' && `Der Kursrückgang von ${asset.ticker} wird durch ein makroökonomisches Risk-Off-Event getrieben. Es liegen keine unternehmensspezifischen Verschlechterungen vor. Die GJR-GARCH-Prognose deutet auf eine schnelle Rebound-Wahrscheinlichkeit von ${reboundScore}% hin, da der Schock rein liquiditätsgetrieben ist.`}
{tickerCatalyst === 'supply_chain' && `Lieferkettenengpässe dämpfen die unmittelbare Lieferfähigkeit von ${asset.ticker}. Obwohl die fundamentale Nachfrage stabil bleibt, verzögert sich die Umsatzrealisierung. Dies erhöht das Risiko eines mittelfristigen Margendrucks (Stress-Koeffizient: 40%).`}
{tickerCatalyst === 'executive_shift' && `Der überraschende Management-Wechsel bei ${asset.ticker} erzeugt kurzfristige Unsicherheit bezüglich der strategischen Neuausrichtung. Der Markt reagiert überproportional negativ auf die unklare Kontinuität (Rebound-Wahrscheinlichkeit bei ${reboundScore}%).`}
{tickerCatalyst === 'regulatory_fine' && `${asset.ticker} sieht sich behördlichen Untersuchungen oder Strafzahlungen ausgesetzt. Dies belastet die Cashflow-Prognosen direkt. Ein schneller Kurs-Rebound ist unwahrscheinlich, da rechtliche Klärungen zeitaufwendig sind.`}
{tickerCatalyst === 'earnings_miss' && `Die gemeldeten Quartalszahlen von ${asset.ticker} lagen unter den Konsensschätzungen. Wachstumsraten schwächen sich strukturell ab. Die GJR-GARCH Volatilitätsschätzung zeigt ein hohes Risiko für verbleibenden Abwertungsdruck (Stress-Koeffizient: 75%).`}
</p>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};
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 flex-wrap items-center gap-2">
<ShieldAlert className="text-amber-400 w-5 h-5" />
<span>Anomalien-Scanner & Marktverzerrungen</span>
{isShieldActive ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)] ml-2 animate-pulse">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
DEV-ARCHIV AKTIV (0 CALLS)
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] ml-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping" />
LIVE-API ENDPUNKT (FMP CORPO)
</span>
)}
</h2>
<p className="text-slate-400 text-xs max-w-2xl">
Isolates price crashes &gt; 5% under relative market stability (S&P 500 drifting sideways or rising). Measures asymmetry using GJR-GARCH to separate panic from structural risks.
</p>
</div>
<div className="w-full md:w-auto flex flex-col sm:flex-row md:flex-row items-stretch sm:items-center gap-3 shrink-0">
<button
onClick={() => setIsMathModalOpen(true)}
className="flex items-center gap-1.5 px-4 py-3 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-amber-400 justify-center"
>
<BookOpen className="w-3.5 h-3.5" />
<span>📖 Quantitative Handbook</span>
</button>
<button
onClick={() => setIsBlueprintModalOpen(true)}
className="flex items-center gap-1.5 px-4 py-3 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-amber-400 justify-center animate-pulse-slow"
>
<Info className="w-3.5 h-3.5" />
<span> Operational Blueprint</span>
</button>
<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 ? 'Scanning Market...' : 'Scan Market'}</span>
</button>
{scanning && (
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
)}
</div>
</div>
{/* Core Scan Modes & Region Toggles */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 border-t border-slate-850 pt-5 mt-5">
{/* Mode Toggles */}
<div className="space-y-2">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Screener Mode</span>
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit">
{[
{ id: 'day_crash', label: 'Day Crashes' },
{ id: 'ma_drop', label: 'MA Drop (SMA50)' },
{ id: '52w_dist', label: '52W-Distance' },
{ id: 'rsi_oversold', label: 'RSI-Oversold' }
].map((m) => (
<button
key={m.id}
onClick={() => setScanMode(m.id as any)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
scanMode === m.id
? 'bg-amber-500 text-slate-950 shadow-md shadow-amber-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
}`}
>
{m.label}
</button>
))}
</div>
</div>
{/* Region Toggles */}
<div className="space-y-2 md:text-right">
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block md:pr-1">Market Region</span>
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit md:ml-auto">
{[
{ id: 'us', label: 'US Markets' },
{ id: 'eu', label: 'EU Markets' },
{ id: 'crypto', label: 'Crypto Assets' }
].map((r) => (
<button
key={r.id}
onClick={() => setMarketRegion(r.id as any)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
marketRegion === r.id
? 'bg-orange-500 text-slate-950 shadow-md shadow-orange-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
}`}
>
{r.label}
</button>
))}
</div>
</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: 3-Tier Capacity Grid (Top 5 per tier) or PEAD Drift Radar */}
<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-6">
<div className="flex justify-between items-center border-b border-slate-850 pb-3">
<div className="flex items-center gap-4">
<button
onClick={() => setLeftPanelTab('screener')}
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'screener' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
>
<Sparkles className="text-amber-400 w-4 h-4 animate-pulse" /> 3-Tier Screener Capacity Grid
</button>
<div className="flex items-center gap-2">
<button
onClick={() => setLeftPanelTab('pead')}
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'pead' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
>
<TrendingUp className="text-amber-400 w-4 h-4" /> 🚀 PEAD Drift Radar
</button>
{leftPanelTab === 'pead' && (
isLivePeadApi ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)]">
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-ping" />
🟢 LIVE EPS FEED
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)]">
<span className="w-1 h-1 rounded-full bg-amber-500 animate-pulse" />
ARCHIV-DATEN (API OFFLINE)
</span>
)
)}
</div>
</div>
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
</div>
{leftPanelTab === 'screener' ? (
<div className="space-y-6">
{/* Category A: Mega Caps */}
{renderCategoryTable(
"Kategorie A: Mega Caps (> 100B USD)",
"Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung",
categorizedResults.mega
)}
{/* Category B: Mid Caps */}
{renderCategoryTable(
"Kategorie B: Mid Caps (10B - 100B USD)",
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
categorizedResults.mid
)}
{/* Category C: Small Caps */}
{renderCategoryTable(
"Kategorie C: Small Caps (< 10B USD)",
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
categorizedResults.small
)}
</div>
) : (
<div className="space-y-4">
<div className="text-xs text-slate-400 leading-relaxed font-sans">
Monitors Post-Earnings Announcement Drift (PEAD) anomaly using standardized unanticipated earnings momentum.
</div>
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
<table className="w-full text-left text-xs border-collapse min-w-[700px] font-mono">
<thead>
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider bg-slate-900/40">
<th className="py-2.5 px-3">Asset</th>
<th className="py-2.5 px-3">Sector</th>
<th className="py-2.5 px-3">Announcement Date</th>
<th className="py-2.5 px-3 text-right">Reported vs Consensus</th>
<th className="py-2.5 px-3 text-right">Surprise %</th>
<th className="py-2.5 px-3 text-center">Drift Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-900/60">
{peadData.length === 0 ? (
<tr>
<td colSpan={6} className="py-6 text-center text-slate-500 italic text-xs">
No equity assets available in scan history.
</td>
</tr>
) : (
peadData.map((pead) => {
const isPositive = pead.surprisePercent >= 0;
const isActiveDrift = pead.driftStatus === 'Active Drift';
return (
<tr key={pead.ticker} className="hover:bg-slate-850/10 transition-colors">
<td className="py-2.5 px-3 font-bold text-slate-200">
<span className="text-amber-400 block font-bold">{pead.ticker}</span>
<span className="text-[10px] text-slate-500 block font-normal">{pead.name}</span>
</td>
<td className="py-2.5 px-3 text-slate-400 font-sans text-xs">
{pead.peadSector}
</td>
<td className="py-2.5 px-3 text-slate-300 font-mono">
<span>{pead.announcementDate}</span>
<span className="text-[10px] text-slate-500 block">({pead.daysElapsed} days elapsed)</span>
</td>
<td className="py-2.5 px-3 text-right text-slate-300">
<span>{pead.epsActual.toFixed(2)}</span>
<span className="text-[10px] text-slate-500 block font-normal">vs {pead.epsConsensus.toFixed(2)}</span>
</td>
<td className={`py-2.5 px-3 text-right font-bold ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? '+' : ''}{pead.surprisePercent.toFixed(2)}%
</td>
<td className="py-2.5 px-3 text-center">
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[9px] font-bold border ${isActiveDrift ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20 shadow-[0_0_8px_rgba(16,185,129,0.1)]' : 'bg-slate-900 text-slate-400 border-slate-800'}`}>
{pead.driftStatus}
</span>
</td>
</tr>
);
})
)}
</tbody>
</table>
</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"
disabled={checkingDeep}
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 disabled:opacity-50"
>
{checkingDeep ? 'Prüft...' : '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>
{searchResult.sloanRatio !== undefined && (
<div className="flex justify-between items-center">
<span className="text-slate-400">Sloan Accrual Ratio:</span>
<div className="flex items-center gap-1.5">
<span className="font-mono font-bold text-slate-300">
{searchResult.sloanRatio.toFixed(2)}%
</span>
{searchResult.sloanRegime === 'SAFE' ? (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
SAFE
</span>
) : (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/20 animate-pulse">
ANOMALY
</span>
)}
</div>
</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,
searchResult.peakPrice,
searchResult.currentPrice
)}
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>
<ScannerMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
<ScannerBlueprintModal isOpen={isBlueprintModalOpen} onClose={() => setIsBlueprintModalOpen(false)} />
</div>
);
}