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

916 lines
47 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 ScannerMathModal from './ScannerMathModal';
import {
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
BookOpen
} 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;
}
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 [isShieldActive, setIsShieldActive] = 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);
// 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
};
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-955/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">
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 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>📖 Modulerklärung</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 ? '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>
{/* 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-Modus</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-Crashs' },
{ 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">Markt-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) */}
<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">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Sparkles className="text-amber-400 w-5 h-5 animate-pulse" /> 3-Tier Screener Kapazitäts-Grid (Top 5)
</h3>
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
</div>
<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>
</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>
<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)} />
</div>
);
}