feat(sandbox): deploy Phase 1 and Phase 2 of Portfolio Sandbox including Swamy-Arora GLS solver and stress-test visualization

This commit is contained in:
Antigravity Agent
2026-06-12 12:16:53 +02:00
parent 96f7643f8a
commit 36ac9e8397
17 changed files with 20956 additions and 510 deletions

View File

@@ -6,12 +6,14 @@ 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
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
BookOpen
} from 'lucide-react';
// Predefined mock database for deep-check searches
// Predefined mock database for deep-check searches (Removed mock database)
interface SearchResult {
ticker: string;
name: string;
@@ -21,159 +23,208 @@ interface SearchResult {
gjrGarchVol: number;
reboundScore: number;
returns: number[];
currentPrice?: number;
peakPrice?: number;
}
const mockSearchDatabase: Record<string, SearchResult> = {
'RACE': {
ticker: 'RACE',
name: 'Ferrari N.V.',
priceChange: -0.065,
sentiment: 'GREEN',
whyDropped: 'Emotionaler Abverkauf nach viralem Video von Cristiano Ronaldo, der sich über Autoprobleme beschwert. Keine fundamentalen Schäden.',
gjrGarchVol: 0.048,
reboundScore: 88,
returns: [0.01, -0.005, 0.012, -0.008, 0.003, -0.065]
},
'KO': {
ticker: 'KO',
name: 'The Coca-Cola Co.',
priceChange: -0.052,
sentiment: 'GREEN',
whyDropped: 'Berühmter Influencer entfernt Coca-Cola Flasche während Pressekonferenz. Reine Social-Media-Hype Reaktion.',
gjrGarchVol: 0.021,
reboundScore: 82,
returns: [0.002, 0.005, -0.003, 0.001, -0.002, -0.052]
},
'TSLA': {
ticker: 'TSLA',
name: 'Tesla Inc.',
priceChange: -0.084,
sentiment: 'YELLOW',
whyDropped: 'Auslieferungszahlen leicht unter Analystenschätzungen. Margenentwicklung bleibt jedoch stabil.',
gjrGarchVol: 0.062,
reboundScore: 65,
returns: [-0.012, 0.008, -0.025, 0.015, -0.005, -0.084]
},
'SMCI': {
ticker: 'SMCI',
name: 'Super Micro Computer',
priceChange: -0.124,
sentiment: 'RED',
whyDropped: 'Hindenburg Research Short-Seller-Report bezüglich mutmaßlicher Bilanzmanipulationen veröffentlicht.',
gjrGarchVol: 0.085,
reboundScore: 24,
returns: [0.035, -0.018, 0.042, -0.051, 0.012, -0.124]
},
'BA': {
ticker: 'BA',
name: 'Boeing Co.',
priceChange: -0.071,
sentiment: 'RED',
whyDropped: 'FAA verhängt vorübergehendes Flugverbot nach erneutem technischen Zwischenfall mit Rumpftür.',
gjrGarchVol: 0.058,
reboundScore: 18,
returns: [-0.005, -0.012, 0.005, -0.021, -0.008, -0.071]
},
'NFLX': {
ticker: 'NFLX',
name: 'Netflix Inc.',
priceChange: -0.058,
sentiment: 'GREEN',
whyDropped: 'Gerüchte über angebliche Preissenkungen in Schwellenländern belasten Kurs kurzfristig.',
gjrGarchVol: 0.038,
reboundScore: 78,
returns: [0.015, -0.002, 0.008, 0.005, -0.011, -0.058]
}
};
export default function ScannerDemo() {
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick } = useSandboxStore();
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore();
// Component local states
const [scanning, setScanning] = useState(false);
const [scanProgress, setScanProgress] = useState('');
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([
{ id: 'sa1', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa2', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
]);
const [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);
// 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 }>>({});
// Run market scan simulator
const handleMarketScan = () => {
// Run market scan simulator querying real live API
const handleMarketScan = async () => {
setScanning(true);
setScanProgress('Verbinde mit Börsenfeeds...');
setTimeout(() => {
setScanProgress('Berechne historische Volatilitätsmatrizen...');
setTimeout(() => {
setScanProgress('Filtere abnormale Abweichungen (Asset > -5%, Index stabil)...');
setTimeout(() => {
// Scan isolated anomalies
setActiveAlerts([
{ id: 'sa3', ticker: 'RACE', priceChange: -0.065, gjrGarchVol: 0.048, overreactionScore: 88, status: 'UNDEREVALUATED' },
{ id: 'sa4', ticker: 'KO', priceChange: -0.052, gjrGarchVol: 0.021, overreactionScore: 82, status: 'UNDEREVALUATED' },
{ id: 'sa5', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa6', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
{ id: 'sa7', ticker: 'BA', priceChange: -0.071, gjrGarchVol: 0.058, overreactionScore: 18, status: 'OVERVALUATED' },
]);
setScanning(false);
setScanProgress('');
}, 600);
}, 500);
}, 400);
try {
setScanProgress(`Rufe die ${marketRegion.toUpperCase()} Marktdaten ab...`);
const response = await fetch(`/api/finance?mode=${scanMode}&region=${marketRegion}`);
if (!response.ok) {
throw new Error('Failed to fetch scanner tickers');
}
const data = await response.json();
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);
}
};
// Perform a manual deep check
const handleDeepCheck = (e: React.FormEvent) => {
// 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;
if (mockSearchDatabase[query]) {
setSearchResult(mockSearchDatabase[query]);
} else {
// Simulate dynamic result for unknown assets
const simulatedVol = 0.03 + Math.random() * 0.04;
const simulatedScore = Math.floor(40 + Math.random() * 50);
const isNegative = Math.random() > 0.4;
const simulatedChange = -0.05 - Math.random() * 0.06;
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: `${query} Corp.`,
priceChange: simulatedChange,
sentiment: isNegative ? (simulatedScore > 75 ? 'GREEN' : 'YELLOW') : 'RED',
whyDropped: 'Simulierte Marktabweichung basierend auf automatischem Sentiment-Scanning der Finanzberichte.',
gjrGarchVol: simulatedVol,
reboundScore: simulatedScore,
returns: [0.005, -0.008, 0.012, -0.015, 0.004, simulatedChange]
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) => {
// Determine a mock initial price based on ticker
let initialPrice = 150;
if (ticker === 'RACE') initialPrice = 380;
if (ticker === 'KO') initialPrice = 60;
if (ticker === 'TSLA') initialPrice = 175;
if (ticker === 'NFLX') initialPrice = 610;
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,
currentPrice: initialPrice * (1 + priceChange), // current price after drop
initialPrice: realInitial,
currentPrice: realCurrent,
});
};
@@ -190,6 +241,192 @@ export default function ScannerDemo() {
}));
}, [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-950/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-800 text-slate-300">
{assets.length} Assets
</span>
</div>
{assets.length === 0 ? (
<div className="py-6 text-center text-slate-500 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-[700px]">
<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">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';
return (
<tr key={asset.ticker} className="hover:bg-slate-900/30 transition-colors group">
<td className="py-3 px-3">
<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>
</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">
<span className={`font-mono 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">
{(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>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};
return (
<div className="space-y-6">
@@ -208,7 +445,15 @@ export default function ScannerDemo() {
</p>
</div>
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
<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}
@@ -223,6 +468,58 @@ export default function ScannerDemo() {
</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
@@ -278,94 +575,37 @@ export default function ScannerDemo() {
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Scanned anomalies details */}
{/* 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-4">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Sparkles className="text-amber-400 w-5 h-5" /> Gefundene Anomalien (Sentiment-Ampel)
</h3>
<div className="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-4">
{activeAlerts.map((alert) => {
// Fetch associated info from mockDB if available, else generic mock
const dbInfo = mockSearchDatabase[alert.ticker] || {
name: `${alert.ticker} Corp.`,
sentiment: alert.overreactionScore > 75 ? 'GREEN' : (alert.overreactionScore > 40 ? 'YELLOW' : 'RED'),
whyDropped: 'Kurzfristige Eindeckungen und Gewinnmitnahmen an den Terminmärkten belasten das Sentiment.'
};
<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
)}
const isGreen = dbInfo.sentiment === 'GREEN';
const isYellow = dbInfo.sentiment === 'YELLOW';
const isRed = dbInfo.sentiment === 'RED';
{/* Category B: Mid Caps */}
{renderCategoryTable(
"Kategorie B: Mid Caps (10B - 100B USD)",
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
categorizedResults.mid
)}
return (
<div key={alert.id} className="p-5 bg-slate-950/40 border border-slate-850 rounded-xl space-y-4 relative group">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 border-b border-slate-900 pb-3">
<div>
<div className="flex items-center gap-2.5">
<span className="font-mono font-bold text-lg text-slate-100">{alert.ticker}</span>
<span className="text-slate-400 text-xs">({dbInfo.name})</span>
</div>
<div className="text-[10px] text-slate-400 mt-1">
Kurssturz: <span className="text-rose-400 font-bold font-mono">{(alert.priceChange * 100).toFixed(1)}%</span>
<span className="mx-2">|</span>
GJR-GARCH Vol: <span className="text-cyan-400 font-bold font-mono">{(alert.gjrGarchVol * 100).toFixed(1)}%</span>
</div>
</div>
{/* Traffic Light Sentiment Badge */}
<div className="flex items-center gap-2">
{isGreen && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/25 flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5" /> EMOTIONALER OVERREACTION (KAUF)
</span>
)}
{isYellow && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-yellow-500/10 text-yellow-400 border border-yellow-500/25 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> UNSICHERHEIT (HALTEN)
</span>
)}
{isRed && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/25 flex items-center gap-1">
<XCircle className="w-3.5 h-3.5" /> FUNDAMENTALER SCHADEN (MEIDEN)
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Analysis Block */}
<div className="md:col-span-2 space-y-1">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<Info className="w-3 h-3 text-amber-400" /> KI-Ursachenanalyse:
</div>
<p className="text-xs text-slate-300 leading-relaxed italic">
&bdquo;{dbInfo.whyDropped}&ldquo;
</p>
</div>
{/* Actions & Score */}
<div className="flex flex-row md:flex-col items-center md:items-end justify-between md:justify-center gap-3 md:border-l md:border-slate-900 md:pl-4">
<div className="text-left md:text-right">
<span className="text-[9px] text-slate-400 uppercase tracking-widest block">Rebound Score</span>
<span className={`font-mono text-xl font-extrabold ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
{alert.overreactionScore}%
</span>
</div>
{(isGreen || isYellow) && (
<button
onClick={() => handleAddToWatchlist(alert.ticker, alert.priceChange, dbInfo.sentiment, dbInfo.whyDropped)}
className="bg-slate-900 hover:bg-slate-850 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[11px] font-semibold py-1.5 px-3 rounded-lg flex items-center gap-1 transition-colors active:scale-[0.98]"
>
<Plus className="w-3.5 h-3.5" /> Tracken
</button>
)}
</div>
</div>
</div>
);
})}
{/* Category C: Small Caps */}
{renderCategoryTable(
"Kategorie C: Small Caps (< 10B USD)",
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
categorizedResults.small
)}
</div>
</div>
</div>
@@ -392,9 +632,10 @@ export default function ScannerDemo() {
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-amber-400 hover:text-amber-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
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"
>
Prüfen
{checkingDeep ? 'Prüft...' : 'Prüfen'}
</button>
</form>
@@ -435,7 +676,14 @@ export default function ScannerDemo() {
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
<button
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
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
@@ -533,6 +781,7 @@ export default function ScannerDemo() {
)}
</div>
<ScannerMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
</div>
);
}