Closes #ISSUE-008 - Overreaction Scanner Overhaul: GJR-GARCH rebound gauge, catalyst drawers, and Category C small-caps
This commit is contained in:
@@ -51,6 +51,37 @@ export default function ScannerDemo() {
|
||||
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);
|
||||
@@ -58,7 +89,7 @@ export default function ScannerDemo() {
|
||||
|
||||
try {
|
||||
setScanProgress(`Rufe die ${marketRegion.toUpperCase()} Marktdaten ab...`);
|
||||
const response = await fetch(`/api/finance?mode=${scanMode}®ion=${marketRegion}`);
|
||||
const response = await fetch(`/api/scanner?mode=${scanMode}®ion=${marketRegion}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch scanner tickers');
|
||||
}
|
||||
@@ -307,24 +338,24 @@ export default function ScannerDemo() {
|
||||
|
||||
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="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-800 text-slate-300">
|
||||
<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-500 text-xs italic">
|
||||
<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-[700px]">
|
||||
<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>
|
||||
@@ -336,6 +367,7 @@ export default function ScannerDemo() {
|
||||
<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>
|
||||
@@ -368,55 +400,137 @@ export default function ScannerDemo() {
|
||||
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 (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user