'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 } 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'; } 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([]); 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([]); const [searchQuery, setSearchQuery] = useState(''); const [searchResult, setSearchResult] = useState(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); // Cache for metadata and prices retrieved dynamically const [alertsMetadata, setAlertsMetadata] = useState>({}); const [alertsPrices, setAlertsPrices] = useState>({}); const [expandedTicker, setExpandedTicker] = useState(null); const [selectedCatalysts, setSelectedCatalysts] = useState>({}); 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}®ion=${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 = {}; const newPrices: Record = {}; 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, 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 (

{title}

{description}

{assets.length} Assets
{assets.length === 0 ? (
Keine Assets in dieser Kategorie unter den Scanner-Ergebnissen.
) : (
{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 ( setExpandedTicker(isExpanded ? null : asset.ticker)} > {isExpanded && ( )} ); })}
Asset Preis Abweichung KGV (T) KGV (F) PEG KBV Rendite Score Rebound Aktion
{isExpanded ? : }
{asset.ticker} {asset.name}
${asset.currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {devText} {asset.trailingPE && asset.trailingPE > 0 ? asset.trailingPE.toFixed(1) : 'N/A'} {asset.forwardPE && asset.forwardPE > 0 ? asset.forwardPE.toFixed(1) : 'N/A'} {asset.peg && asset.peg > 0 ? asset.peg.toFixed(2) : 'N/A'} {asset.priceToBook && asset.priceToBook > 0 ? asset.priceToBook.toFixed(2) : 'N/A'} {asset.dividendYield && asset.dividendYield > 0 ? `${asset.dividendYield.toFixed(2)}%` : '0.00%'} {asset.overreactionScore}% 75 ? 'text-emerald-400' : (reboundScore > 45 ? 'text-yellow-400' : 'text-rose-400')}`}> {reboundScore}% e.stopPropagation()}> {(isGreen || isYellow) ? ( ) : ( - )}
e.stopPropagation()}>
🔍 KI-Überreaktions- & Sentiment-Diagnose für {asset.ticker}
Status: {asset.status}

*Der Katalysator bestimmt den Stress-Koeffizienten (C_stress), welcher die Erholungsgeschwindigkeit beeinflusst.

Outlier Score (GJR-GARCH): {asset.overreactionScore}%
News Stress Damping: -{getStressCoefficient(tickerCatalyst)}%
Rebound-Wahrscheinlichkeit: {reboundScore}%
Diagnose & Katalysator-Analyse

{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%).`}

)}
); }; return (
{/* SECTION 1: Overreaction Scan Action */}
Market Scanner Engine

Anomalien-Scanner & Marktverzerrungen {isShieldActive ? ( DEV-ARCHIV AKTIV (0 CALLS) ) : ( LIVE-API ENDPUNKT (FMP CORPO) )}

Isolates price crashes > 5% under relative market stability (S&P 500 drifting sideways or rising). Measures asymmetry using GJR-GARCH to separate panic from structural risks.

{scanning && ( {scanProgress} )}
{/* Core Scan Modes & Region Toggles */}
{/* Mode Toggles */}
Screener Mode
{[ { 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) => ( ))}
{/* Region Toggles */}
Market Region
{[ { id: 'us', label: 'US Markets' }, { id: 'eu', label: 'EU Markets' }, { id: 'crypto', label: 'Crypto Assets' } ].map((r) => ( ))}
{/* Collapsible Math Accordion */}
{showMathAccordion && (

Das GJR-GARCH(1,1)-Modell erfasst die Asymmetrie im Volatilitätsprozess von Renditen. Es besitzt einen zusätzlichen Term (), der aktiviert wird, wenn der gestrige Schock negativ war.

Die Indikatorvariable nimmt den Wert 1 bei einem Kurssturz an:

Sind die Volatilitätsschocks asymmetrisch (), führt ein Kurssturz (Bad News) zu einer signifikant höheren Volatilität als ein gleich großer Kursgewinn (Good News).

Simulierter GJR-GARCH Volatilitätsprozess nach Schocks
Spike am Tag T-4 repräsentiert die asymmetrische Reaktion auf einen Schock von -6.5%.
)}
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
{/* Left 2 Columns: 3-Tier Capacity Grid (Top 5 per tier) */}

3-Tier Screener Kapazitäts-Grid (Top 5)

Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}
{/* 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 )}
{/* Right Column: Deep-Check & Search Mask */}

Deep-Check Suchmaske

Suchen Sie gezielt nach Werten, um Anomalien-Analysen abzurufen. (z.B. RACE, KO, TSLA, SMCI, NFLX)

setSearchQuery(e.target.value)} />
{searchResult && (

{searchResult.ticker}

{searchResult.name}
{searchResult.sentiment === 'GREEN' && GREEN} {searchResult.sentiment === 'YELLOW' && YELLOW} {searchResult.sentiment === 'RED' && RED}
Aktueller Sturz: {(searchResult.priceChange * 100).toFixed(1)}%
GJR-GARCH Volatilität: {(searchResult.gjrGarchVol * 100).toFixed(1)}%
Rebound Score: {searchResult.reboundScore}/100
{searchResult.sloanRatio !== undefined && (
Sloan Accrual Ratio:
{searchResult.sloanRatio.toFixed(2)}% {searchResult.sloanRegime === 'SAFE' ? ( SAFE ) : ( ANOMALY )}
)}
KI-Kommentar:

{searchResult.whyDropped}

{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && ( )}
)}
{/* SECTION 3: Hot Watchlist & Rebound-Tracker (48 Hours) */}

Hot Rebound Watchlist & Tracker (48h)

{watchlist.length > 0 && ( )}
{watchlist.length === 0 ? (
Bislang keine Assets auf der Rebound-Watchlist. Nutzen Sie die Markt-Scanergebnisse oben, um Werte hinzuzufügen.
) : (
{watchlist.map((item) => { const perf = item.reboundPerformance; const isPositive = perf >= 0; const progressPct = (item.hoursTracked / 48) * 100; return (
{item.ticker} {item.sentiment}
Gezogen bei: {(item.priceChange * 100).toFixed(1)}%
Rebound Performance
{isPositive ? '+' : ''}{perf.toFixed(2)}%
{/* Progress timeline */}
Tracking-Dauer: {item.hoursTracked}/48 Std. {Math.round(progressPct)}% abgeschlossen

Hintergrund: {item.whyDropped}

); })}
)}
setIsMathModalOpen(false)} /> setIsBlueprintModalOpen(false)} />
); }