1065 lines
56 KiB
TypeScript
1065 lines
56 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 ScannerBlueprintModal from './ScannerBlueprintModal';
|
||
import {
|
||
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
|
||
BookOpen, TrendingUp
|
||
} from 'lucide-react';
|
||
|
||
// Predefined mock database for deep-check searches (Removed mock database)
|
||
interface SearchResult {
|
||
ticker: string;
|
||
name: string;
|
||
priceChange: number;
|
||
sentiment: 'GREEN' | 'YELLOW' | 'RED';
|
||
whyDropped: string;
|
||
gjrGarchVol: number;
|
||
reboundScore: number;
|
||
returns: number[];
|
||
currentPrice?: number;
|
||
peakPrice?: number;
|
||
sloanRatio?: number;
|
||
sloanRegime?: 'SAFE' | 'ANOMALY';
|
||
}
|
||
|
||
interface PEADData {
|
||
ticker: string;
|
||
name: string;
|
||
peadSector: string;
|
||
announcementDate: string;
|
||
daysElapsed: number;
|
||
epsActual: number;
|
||
epsConsensus: number;
|
||
surprisePercent: number;
|
||
driftStatus: 'Active Drift' | 'Consolidating';
|
||
}
|
||
|
||
export default function ScannerDemo() {
|
||
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore();
|
||
|
||
// Component local states
|
||
const [scanning, setScanning] = useState(false);
|
||
const [scanProgress, setScanProgress] = useState('');
|
||
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([]);
|
||
|
||
const [scanMode, setScanMode] = useState<'day_crash' | 'ma_drop' | '52w_dist' | 'rsi_oversold'>('day_crash');
|
||
const [marketRegion, setMarketRegion] = useState<'us' | 'eu' | 'crypto'>('us');
|
||
const [scanResults, setScanResults] = useState<any[]>([]);
|
||
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
|
||
const [searchError, setSearchError] = useState(false);
|
||
const [checkingDeep, setCheckingDeep] = useState(false);
|
||
|
||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false);
|
||
const [isShieldActive, setIsShieldActive] = useState(false);
|
||
const [leftPanelTab, setLeftPanelTab] = useState<'screener' | 'pead'>('screener');
|
||
const [peadData, setPeadData] = useState<PEADData[]>([
|
||
{ ticker: 'NVDA', name: 'NVIDIA Corporation', peadSector: 'Technology', announcementDate: '2026-05-20', daysElapsed: 25, epsActual: 6.12, epsConsensus: 5.58, surprisePercent: 9.68, driftStatus: 'Active Drift' },
|
||
{ ticker: 'AAPL', name: 'Apple Inc.', peadSector: 'Technology', announcementDate: '2026-05-02', daysElapsed: 43, epsActual: 1.53, epsConsensus: 1.50, surprisePercent: 2.00, driftStatus: 'Consolidating' },
|
||
{ ticker: 'MSFT', name: 'Microsoft Corporation', peadSector: 'Technology', announcementDate: '2026-04-25', daysElapsed: 50, epsActual: 2.94, epsConsensus: 2.82, surprisePercent: 4.26, driftStatus: 'Consolidating' },
|
||
{ ticker: 'TSLA', name: 'Tesla Inc.', peadSector: 'Consumer Goods', announcementDate: '2026-04-23', daysElapsed: 52, epsActual: 0.45, epsConsensus: 0.51, surprisePercent: -11.76, driftStatus: 'Consolidating' },
|
||
{ ticker: 'JPM', name: 'JPMorgan Chase & Co.', peadSector: 'Financial Services', announcementDate: '2026-04-12', daysElapsed: 63, epsActual: 4.44, epsConsensus: 4.15, surprisePercent: 6.99, driftStatus: 'Consolidating' }
|
||
]);
|
||
|
||
// 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}®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<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }> = {};
|
||
const newPrices: Record<string, { peakPrice: number; currentPrice: number }> = {};
|
||
|
||
results.forEach((result: any) => {
|
||
if (result.error) return;
|
||
|
||
// Calculate dynamic volatility from return series
|
||
const gjrResult = calculateGJRGARCH(result.returns);
|
||
const gjrGarchVol = gjrResult.forecast / 100;
|
||
|
||
// Calculate overreaction ratio
|
||
let dropAbs = Math.abs(result.priceChange);
|
||
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
|
||
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
|
||
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
|
||
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
|
||
|
||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||
|
||
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
|
||
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
|
||
|
||
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
|
||
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
|
||
|
||
const whyDropped = `Wert liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH(1,1) Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||
|
||
newAlerts.push({
|
||
id: `sa_${result.ticker}_${Date.now()}`,
|
||
ticker: result.ticker,
|
||
priceChange: result.priceChange,
|
||
gjrGarchVol,
|
||
overreactionScore,
|
||
status
|
||
});
|
||
|
||
newMetadata[result.ticker] = {
|
||
name: result.name,
|
||
whyDropped,
|
||
sentiment
|
||
};
|
||
|
||
newPrices[result.ticker] = {
|
||
peakPrice: result.peakPrice,
|
||
currentPrice: result.currentPrice
|
||
};
|
||
});
|
||
|
||
setAlertsMetadata(prev => ({ ...prev, ...newMetadata }));
|
||
setAlertsPrices(prev => ({ ...prev, ...newPrices }));
|
||
setActiveAlerts(newAlerts);
|
||
|
||
const peadList: PEADData[] = [];
|
||
results.forEach((r: any) => {
|
||
if (r.error || r.peadSector === 'Cryptocurrency') return;
|
||
peadList.push({
|
||
ticker: r.ticker,
|
||
name: r.name,
|
||
peadSector: r.peadSector || 'Conglomerate',
|
||
announcementDate: r.announcementDate || 'N/A',
|
||
daysElapsed: r.daysElapsed || 0,
|
||
epsActual: r.epsActual || 0,
|
||
epsConsensus: r.epsConsensus || 0,
|
||
surprisePercent: r.surprisePercent || 0,
|
||
driftStatus: r.driftStatus || 'Consolidating'
|
||
});
|
||
});
|
||
peadList.sort((a, b) => Math.abs(b.surprisePercent) - Math.abs(a.surprisePercent));
|
||
setPeadData(peadList);
|
||
|
||
// Update global store alerts for Sandbox module use
|
||
updateScannerAlerts(newAlerts);
|
||
|
||
setScanProgress('');
|
||
} catch (err) {
|
||
console.error(err);
|
||
setScanProgress('Fehler beim Scannen der Live-Daten.');
|
||
} finally {
|
||
setScanning(false);
|
||
}
|
||
};
|
||
|
||
// Trigger scan automatically when scan mode or region toggles change
|
||
React.useEffect(() => {
|
||
handleMarketScan();
|
||
}, [scanMode, marketRegion]);
|
||
|
||
// Perform a manual deep check using real live API
|
||
const handleDeepCheck = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSearchError(false);
|
||
setSearchResult(null);
|
||
const query = searchQuery.trim().toUpperCase();
|
||
if (!query) return;
|
||
|
||
setCheckingDeep(true);
|
||
try {
|
||
const response = await fetch(`/api/finance?ticker=${query}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch');
|
||
}
|
||
const data = await response.json();
|
||
const result = data.results?.[0];
|
||
if (!result || result.error) {
|
||
throw new Error(result?.error || 'Invalid result');
|
||
}
|
||
|
||
const gjrResult = calculateGJRGARCH(result.returns);
|
||
const gjrGarchVol = gjrResult.forecast / 100;
|
||
|
||
const dropAbs = Math.abs(result.priceChange);
|
||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||
if (result.priceChange > -0.03) {
|
||
overreactionScore = Math.round(overreactionScore * 0.5);
|
||
}
|
||
|
||
const sentiment = overreactionScore > 70 ? 'GREEN' : (overreactionScore >= 40 ? 'YELLOW' : 'RED');
|
||
const whyDropped = `Mathematischer Ausbruchspunkt: Der Kurs verzeichnet einen Rückgang von ${Math.round(Math.abs(result.priceChange) * 100)}% ausgehend vom 90-Tage-Hoch von $${result.peakPrice.toFixed(2)}. GJR-GARCH-Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||
|
||
const res: SearchResult = {
|
||
ticker: query,
|
||
name: result.name,
|
||
priceChange: result.priceChange,
|
||
sentiment,
|
||
whyDropped,
|
||
gjrGarchVol,
|
||
reboundScore: overreactionScore,
|
||
returns: result.returns,
|
||
currentPrice: result.currentPrice,
|
||
peakPrice: result.peakPrice,
|
||
sloanRatio: result.sloanRatio,
|
||
sloanRegime: result.sloanRegime
|
||
};
|
||
|
||
setAlertsMetadata(prev => ({
|
||
...prev,
|
||
[query]: { name: result.name, whyDropped, sentiment }
|
||
}));
|
||
|
||
setAlertsPrices(prev => ({
|
||
...prev,
|
||
[query]: { peakPrice: result.peakPrice, currentPrice: result.currentPrice }
|
||
}));
|
||
|
||
setSearchResult(res);
|
||
} catch (err) {
|
||
console.error(err);
|
||
setSearchError(true);
|
||
} finally {
|
||
setCheckingDeep(false);
|
||
}
|
||
};
|
||
|
||
const handleAddToWatchlist = (
|
||
ticker: string,
|
||
priceChange: number,
|
||
sentiment: 'GREEN' | 'YELLOW' | 'RED',
|
||
whyDropped: string,
|
||
peakPrice?: number,
|
||
currentPrice?: number
|
||
) => {
|
||
const realInitial = peakPrice || 100;
|
||
const realCurrent = currentPrice || (realInitial * (1 + priceChange));
|
||
|
||
addToWatchlist({
|
||
ticker,
|
||
priceChange,
|
||
sentiment,
|
||
whyDropped,
|
||
initialPrice: realInitial,
|
||
currentPrice: realCurrent,
|
||
});
|
||
};
|
||
|
||
// Compute GJR-GARCH forecasting series for the math accordion/visual validation
|
||
const gjrGarchMathResult = useMemo(() => {
|
||
const mockReturns = [0.015, -0.022, 0.008, -0.031, 0.014, -0.055, 0.012, -0.008, 0.024, -0.065];
|
||
return calculateGJRGARCH(mockReturns);
|
||
}, []);
|
||
|
||
const mathChartData = useMemo(() => {
|
||
return gjrGarchMathResult.series.map((vol, idx) => ({
|
||
day: `T-${gjrGarchMathResult.series.length - idx - 1}`,
|
||
'GJR-GARCH Vol (%)': parseFloat(vol.toFixed(2)),
|
||
}));
|
||
}, [gjrGarchMathResult]);
|
||
|
||
const categorizedResults = useMemo(() => {
|
||
const mega: any[] = [];
|
||
const mid: any[] = [];
|
||
const small: any[] = [];
|
||
|
||
scanResults.forEach((result: any) => {
|
||
// Calculate dynamic volatility from return series
|
||
const gjrResult = calculateGJRGARCH(result.returns || []);
|
||
const gjrGarchVol = gjrResult.forecast / 100;
|
||
|
||
// Calculate overreaction ratio based on selected mode
|
||
let dropAbs = Math.abs(result.priceChange);
|
||
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
|
||
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
|
||
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
|
||
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
|
||
|
||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||
|
||
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
|
||
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
|
||
|
||
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
|
||
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
|
||
|
||
const whyDropped = `Kurs liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||
|
||
const enriched = {
|
||
...result,
|
||
gjrGarchVol,
|
||
overreactionScore,
|
||
status,
|
||
sentiment,
|
||
whyDropped
|
||
};
|
||
|
||
const mcap = result.marketCap || 0;
|
||
if (mcap >= 100e9) {
|
||
mega.push(enriched);
|
||
} else if (mcap >= 10e9) {
|
||
mid.push(enriched);
|
||
} else {
|
||
small.push(enriched);
|
||
}
|
||
});
|
||
|
||
const sortByMode = (list: any[]) => {
|
||
return list.sort((a, b) => {
|
||
if (scanMode === 'ma_drop') return a.maDeviation - b.maDeviation;
|
||
if (scanMode === '52w_dist') return a.dist52w - b.dist52w;
|
||
if (scanMode === 'rsi_oversold') return a.rsi14 - b.rsi14;
|
||
return a.dayChange - b.dayChange; // day_crash
|
||
});
|
||
};
|
||
|
||
return {
|
||
mega: sortByMode(mega).slice(0, 5),
|
||
mid: sortByMode(mid).slice(0, 5),
|
||
small: sortByMode(small).slice(0, 5)
|
||
};
|
||
}, [scanResults, scanMode]);
|
||
|
||
const renderCategoryTable = (title: string, description: string, assets: any[]) => {
|
||
return (
|
||
<div className="bg-slate-900/40 border border-slate-850 rounded-2xl p-5 space-y-3">
|
||
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
|
||
<div>
|
||
<h4 className="font-bold text-white text-sm">{title}</h4>
|
||
<p className="text-[10px] text-slate-400 font-mono">{description}</p>
|
||
</div>
|
||
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-900 border border-slate-888 text-slate-300">
|
||
{assets.length} Assets
|
||
</span>
|
||
</div>
|
||
|
||
{assets.length === 0 ? (
|
||
<div className="py-6 text-center text-slate-555 text-xs italic">
|
||
Keine Assets in dieser Kategorie unter den Scanner-Ergebnissen.
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto scrollbar-thin">
|
||
<table className="w-full text-left text-xs border-collapse min-w-[750px]">
|
||
<thead>
|
||
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider">
|
||
<th className="py-2.5 px-3">Asset</th>
|
||
<th className="py-2.5 px-3">Preis</th>
|
||
<th className="py-2.5 px-3 text-right">Abweichung</th>
|
||
<th className="py-2.5 px-3 text-right">KGV (T)</th>
|
||
<th className="py-2.5 px-3 text-right">KGV (F)</th>
|
||
<th className="py-2.5 px-3 text-right">PEG</th>
|
||
<th className="py-2.5 px-3 text-right">KBV</th>
|
||
<th className="py-2.5 px-3 text-right">Rendite</th>
|
||
<th className="py-2.5 px-3 text-center">Score</th>
|
||
<th className="py-2.5 px-3 text-center">Rebound</th>
|
||
<th className="py-2.5 px-3 text-center">Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-900/60">
|
||
{assets.map((asset) => {
|
||
const isGreen = asset.sentiment === 'GREEN';
|
||
const isYellow = asset.sentiment === 'YELLOW';
|
||
|
||
// Format deviation based on mode
|
||
let devText = '';
|
||
let devColor = 'text-slate-300';
|
||
if (scanMode === 'day_crash') {
|
||
devText = `${(asset.dayChange * 100).toFixed(2)}%`;
|
||
devColor = asset.dayChange < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||
} else if (scanMode === 'ma_drop') {
|
||
devText = `${(asset.maDeviation * 100).toFixed(2)}%`;
|
||
devColor = asset.maDeviation < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||
} else if (scanMode === '52w_dist') {
|
||
devText = `${(asset.dist52w * 100).toFixed(2)}%`;
|
||
devColor = asset.dist52w < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||
} else if (scanMode === 'rsi_oversold') {
|
||
devText = asset.rsi14.toFixed(1);
|
||
devColor = asset.rsi14 < 30 ? 'text-amber-400 font-bold font-mono' : 'text-slate-400';
|
||
}
|
||
|
||
// Highlight valuation multipliers
|
||
const peColor = asset.trailingPE && asset.trailingPE > 0 && asset.trailingPE < 15 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||
const fpeColor = asset.forwardPE && asset.forwardPE > 0 && asset.forwardPE < 12 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||
const pegColor = asset.peg && asset.peg > 0 && asset.peg < 1.0 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||
const pbColor = asset.priceToBook && asset.priceToBook > 0 && asset.priceToBook < 1.5 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||
const divColor = asset.dividendYield && asset.dividendYield > 3.0 ? 'text-emerald-400 font-semibold' : 'text-slate-450';
|
||
|
||
const tickerCatalyst = selectedCatalysts[asset.ticker] || getDefaultCatalyst(asset.ticker);
|
||
const reboundScore = getReboundScore(asset.overreactionScore, tickerCatalyst);
|
||
const isExpanded = expandedTicker === asset.ticker;
|
||
|
||
return (
|
||
<React.Fragment key={asset.ticker}>
|
||
<tr
|
||
className="hover:bg-slate-900/30 transition-colors group cursor-pointer"
|
||
onClick={() => setExpandedTicker(isExpanded ? null : asset.ticker)}
|
||
>
|
||
<td className="py-3 px-3">
|
||
<div className="flex items-center gap-1.5">
|
||
{isExpanded ? <ChevronUp className="w-3.5 h-3.5 text-slate-500" /> : <ChevronDown className="w-3.5 h-3.5 text-slate-500" />}
|
||
<div className="flex flex-col">
|
||
<span className="font-mono font-bold text-slate-100 text-sm">{asset.ticker}</span>
|
||
<span className="text-[10px] text-slate-500 max-w-[120px] truncate" title={asset.name}>{asset.name}</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="py-3 px-3 font-mono font-semibold text-slate-200">
|
||
${asset.currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${devColor}`}>
|
||
{devText}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${peColor}`}>
|
||
{asset.trailingPE && asset.trailingPE > 0 ? asset.trailingPE.toFixed(1) : 'N/A'}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${fpeColor}`}>
|
||
{asset.forwardPE && asset.forwardPE > 0 ? asset.forwardPE.toFixed(1) : 'N/A'}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${pegColor}`}>
|
||
{asset.peg && asset.peg > 0 ? asset.peg.toFixed(2) : 'N/A'}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${pbColor}`}>
|
||
{asset.priceToBook && asset.priceToBook > 0 ? asset.priceToBook.toFixed(2) : 'N/A'}
|
||
</td>
|
||
<td className={`py-3 px-3 text-right font-mono ${divColor}`}>
|
||
{asset.dividendYield && asset.dividendYield > 0 ? `${asset.dividendYield.toFixed(2)}%` : '0.00%'}
|
||
</td>
|
||
<td className="py-3 px-3 text-center font-mono">
|
||
<span className={`font-bold text-xs ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||
{asset.overreactionScore}%
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-3 text-center font-mono">
|
||
<span className={`font-bold text-xs ${reboundScore > 75 ? 'text-emerald-400' : (reboundScore > 45 ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||
{reboundScore}%
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-3 text-center" onClick={(e) => e.stopPropagation()}>
|
||
{(isGreen || isYellow) ? (
|
||
<button
|
||
onClick={() => {
|
||
handleAddToWatchlist(asset.ticker, asset.priceChange, asset.sentiment, asset.whyDropped, asset.peakPrice, asset.currentPrice);
|
||
}}
|
||
className="bg-slate-900 hover:bg-slate-850 hover:border-emerald-500/50 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[10px] font-bold py-1 px-2.5 rounded-md transition-all active:scale-[0.96] cursor-pointer"
|
||
>
|
||
Track
|
||
</button>
|
||
) : (
|
||
<span className="text-[10px] text-slate-600 font-mono">-</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
{isExpanded && (
|
||
<tr className="bg-slate-950/60 border-t border-slate-900">
|
||
<td colSpan={11} className="p-4" onClick={(e) => e.stopPropagation()}>
|
||
<div className="space-y-4 text-slate-300">
|
||
<div className="flex justify-between items-center border-b border-slate-850 pb-2">
|
||
<h5 className="font-bold text-xs text-amber-400 flex items-center gap-1.5">
|
||
<span>🔍 KI-Überreaktions- & Sentiment-Diagnose für {asset.ticker}</span>
|
||
</h5>
|
||
<span className="text-[9px] text-slate-500 font-mono">Status: {asset.status}</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<label className="text-[9px] text-slate-400 uppercase font-bold font-mono">Identifizierter Drop-Katalysator:</label>
|
||
<select
|
||
value={tickerCatalyst}
|
||
onChange={(e) => {
|
||
setSelectedCatalysts(prev => ({ ...prev, [asset.ticker]: e.target.value }));
|
||
}}
|
||
className="bg-slate-900 border border-slate-800 rounded-lg p-2 text-xs w-full text-slate-200 focus:outline-none focus:border-amber-500/50 cursor-pointer"
|
||
>
|
||
<option value="systemic_selloff">Systemic Selloff (Systemischer Markt-Ausverkauf - Stress: 15%)</option>
|
||
<option value="supply_chain">Supply Chain Disruption (Lieferengpass - Stress: 40%)</option>
|
||
<option value="executive_shift">Executive Shift (Management-Wechsel - Stress: 55%)</option>
|
||
<option value="regulatory_fine">Regulatory Issue / Fine (Regulierungen - Stress: 65%)</option>
|
||
<option value="earnings_miss">Earnings Miss (Gewinnverfehlung - Stress: 75%)</option>
|
||
</select>
|
||
<p className="text-[9px] text-slate-555 italic leading-relaxed">
|
||
*Der Katalysator bestimmt den Stress-Koeffizienten (C_stress), welcher die Erholungsgeschwindigkeit beeinflusst.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[9px] text-slate-400 uppercase font-bold font-mono">Rebound-Wahrscheinlichkeits Herleitung:</label>
|
||
<div className="bg-slate-900/50 border border-slate-850 rounded-lg p-3 space-y-1.5 text-xs font-mono">
|
||
<div className="flex justify-between">
|
||
<span>Outlier Score (GJR-GARCH):</span>
|
||
<span className="text-slate-250 font-bold">{asset.overreactionScore}%</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span>News Stress Damping:</span>
|
||
<span className="text-slate-450 font-semibold">-{getStressCoefficient(tickerCatalyst)}%</span>
|
||
</div>
|
||
<div className="border-t border-slate-800 pt-1.5 flex justify-between text-amber-400 font-bold">
|
||
<span>Rebound-Wahrscheinlichkeit:</span>
|
||
<span>{reboundScore}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-slate-900/30 border border-slate-850/80 rounded-xl p-3.5 space-y-1">
|
||
<div className="text-[9px] uppercase font-bold text-slate-400 font-mono">Diagnose & Katalysator-Analyse</div>
|
||
<p className="text-xs text-slate-450 leading-relaxed">
|
||
{tickerCatalyst === 'systemic_selloff' && `Der Kursrückgang von ${asset.ticker} wird durch ein makroökonomisches Risk-Off-Event getrieben. Es liegen keine unternehmensspezifischen Verschlechterungen vor. Die GJR-GARCH-Prognose deutet auf eine schnelle Rebound-Wahrscheinlichkeit von ${reboundScore}% hin, da der Schock rein liquiditätsgetrieben ist.`}
|
||
{tickerCatalyst === 'supply_chain' && `Lieferkettenengpässe dämpfen die unmittelbare Lieferfähigkeit von ${asset.ticker}. Obwohl die fundamentale Nachfrage stabil bleibt, verzögert sich die Umsatzrealisierung. Dies erhöht das Risiko eines mittelfristigen Margendrucks (Stress-Koeffizient: 40%).`}
|
||
{tickerCatalyst === 'executive_shift' && `Der überraschende Management-Wechsel bei ${asset.ticker} erzeugt kurzfristige Unsicherheit bezüglich der strategischen Neuausrichtung. Der Markt reagiert überproportional negativ auf die unklare Kontinuität (Rebound-Wahrscheinlichkeit bei ${reboundScore}%).`}
|
||
{tickerCatalyst === 'regulatory_fine' && `${asset.ticker} sieht sich behördlichen Untersuchungen oder Strafzahlungen ausgesetzt. Dies belastet die Cashflow-Prognosen direkt. Ein schneller Kurs-Rebound ist unwahrscheinlich, da rechtliche Klärungen zeitaufwendig sind.`}
|
||
{tickerCatalyst === 'earnings_miss' && `Die gemeldeten Quartalszahlen von ${asset.ticker} lagen unter den Konsensschätzungen. Wachstumsraten schwächen sich strukturell ab. Die GJR-GARCH Volatilitätsschätzung zeigt ein hohes Risiko für verbleibenden Abwertungsdruck (Stress-Koeffizient: 75%).`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
|
||
{/* SECTION 1: Overreaction Scan Action */}
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
|
||
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl -z-10" />
|
||
|
||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||
<div className="space-y-1">
|
||
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
|
||
<h2 className="text-xl font-bold text-white flex flex-wrap items-center gap-2">
|
||
<ShieldAlert className="text-amber-400 w-5 h-5" />
|
||
<span>Anomalien-Scanner & Marktverzerrungen</span>
|
||
{isShieldActive ? (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.15)] ml-2 animate-pulse">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
|
||
DEV-ARCHIV AKTIV (0 CALLS)
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.15)] ml-2">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-ping" />
|
||
LIVE-API ENDPUNKT (FMP CORPO)
|
||
</span>
|
||
)}
|
||
</h2>
|
||
<p className="text-slate-400 text-xs max-w-2xl">
|
||
Isolates price crashes > 5% under relative market stability (S&P 500 drifting sideways or rising). Measures asymmetry using GJR-GARCH to separate panic from structural risks.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="w-full md:w-auto flex flex-col sm:flex-row md:flex-row items-stretch sm:items-center gap-3 shrink-0">
|
||
<button
|
||
onClick={() => setIsMathModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-4 py-3 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-amber-400 justify-center"
|
||
>
|
||
<BookOpen className="w-3.5 h-3.5" />
|
||
<span>📖 Quantitative Handbook</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setIsBlueprintModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-4 py-3 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-amber-400 justify-center animate-pulse-slow"
|
||
>
|
||
<Info className="w-3.5 h-3.5" />
|
||
<span>⚙️ Operational Blueprint</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleMarketScan}
|
||
disabled={scanning}
|
||
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 disabled:from-amber-850 disabled:to-orange-900 disabled:text-slate-400 text-slate-950 font-bold py-3 px-6 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 active:scale-[0.98]"
|
||
>
|
||
<RefreshCw className={`w-5 h-5 ${scanning ? 'animate-spin' : ''}`} />
|
||
<span>{scanning ? 'Scanning Market...' : 'Scan Market'}</span>
|
||
</button>
|
||
{scanning && (
|
||
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Core Scan Modes & Region Toggles */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 border-t border-slate-850 pt-5 mt-5">
|
||
{/* Mode Toggles */}
|
||
<div className="space-y-2">
|
||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Screener Mode</span>
|
||
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit">
|
||
{[
|
||
{ id: 'day_crash', label: 'Day Crashes' },
|
||
{ id: 'ma_drop', label: 'MA Drop (SMA50)' },
|
||
{ id: '52w_dist', label: '52W-Distance' },
|
||
{ id: 'rsi_oversold', label: 'RSI-Oversold' }
|
||
].map((m) => (
|
||
<button
|
||
key={m.id}
|
||
onClick={() => setScanMode(m.id as any)}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||
scanMode === m.id
|
||
? 'bg-amber-500 text-slate-950 shadow-md shadow-amber-500/10'
|
||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
|
||
}`}
|
||
>
|
||
{m.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Region Toggles */}
|
||
<div className="space-y-2 md:text-right">
|
||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block md:pr-1">Market Region</span>
|
||
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit md:ml-auto">
|
||
{[
|
||
{ id: 'us', label: 'US Markets' },
|
||
{ id: 'eu', label: 'EU Markets' },
|
||
{ id: 'crypto', label: 'Crypto Assets' }
|
||
].map((r) => (
|
||
<button
|
||
key={r.id}
|
||
onClick={() => setMarketRegion(r.id as any)}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||
marketRegion === r.id
|
||
? 'bg-orange-500 text-slate-950 shadow-md shadow-orange-500/10'
|
||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
|
||
}`}
|
||
>
|
||
{r.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Collapsible Math Accordion */}
|
||
<div className="border-t border-slate-850 pt-4 mt-6">
|
||
<button
|
||
onClick={() => setShowMathAccordion(!showMathAccordion)}
|
||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-amber-400 transition-colors focus:outline-none"
|
||
>
|
||
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
|
||
<span className="font-semibold uppercase tracking-wider">GJR-GARCH(1,1) Volatilitätsmodellierung & Leverage-Effekt</span>
|
||
</button>
|
||
|
||
{showMathAccordion && (
|
||
<div className="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300">
|
||
<div className="space-y-3">
|
||
<p>
|
||
Das <strong>GJR-GARCH(1,1)</strong>-Modell erfasst die Asymmetrie im Volatilitätsprozess von Renditen. Es besitzt einen zusätzlichen Term (<InlineMath math="\gamma" />), der aktiviert wird, wenn der gestrige Schock negativ war.
|
||
</p>
|
||
<div className="py-2 overflow-x-auto text-slate-200">
|
||
<BlockMath math="\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \gamma \epsilon_{t-1}^2 I_{t-1} + \beta \sigma_{t-1}^2" />
|
||
</div>
|
||
<p>
|
||
Die Indikatorvariable <InlineMath math="I_{t-1}" /> nimmt den Wert 1 bei einem Kurssturz an:
|
||
</p>
|
||
<div className="py-2 overflow-x-auto text-slate-200">
|
||
<BlockMath math="I_{t-1} = \begin{cases} 1 & \text{falls } \epsilon_{t-1} < 0 \\ 0 & \text{sonst} \end{cases}" />
|
||
</div>
|
||
<p className="text-slate-400">
|
||
Sind die Volatilitätsschocks asymmetrisch (<InlineMath math="\gamma > 0" />), führt ein Kurssturz (Bad News) zu einer signifikant höheren Volatilität als ein gleich großer Kursgewinn (Good News).
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="text-xs text-slate-400 font-semibold mb-2">Simulierter GJR-GARCH Volatilitätsprozess nach Schocks</div>
|
||
<div className="h-44 w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<LineChart data={mathChartData}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
||
<XAxis dataKey="day" stroke="#64748b" fontSize={9} />
|
||
<YAxis stroke="#64748b" fontSize={9} unit="%" />
|
||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
|
||
<Line type="monotone" dataKey="GJR-GARCH Vol (%)" stroke="#f59e0b" strokeWidth={2} dot={false} />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<div className="text-[10px] text-slate-500 font-mono text-center">
|
||
Spike am Tag T-4 repräsentiert die asymmetrische Reaktion auf einen Schock von -6.5%.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
|
||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||
|
||
{/* Left 2 Columns: 3-Tier Capacity Grid (Top 5 per tier) or PEAD Drift Radar */}
|
||
<div className="xl:col-span-2 space-y-6">
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||
<div className="flex justify-between items-center border-b border-slate-850 pb-3">
|
||
<div className="flex items-center gap-4">
|
||
<button
|
||
onClick={() => setLeftPanelTab('screener')}
|
||
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'screener' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||
>
|
||
<Sparkles className="text-amber-400 w-4 h-4 animate-pulse" /> 3-Tier Screener Capacity Grid
|
||
</button>
|
||
<button
|
||
onClick={() => setLeftPanelTab('pead')}
|
||
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'pead' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||
>
|
||
<TrendingUp className="text-amber-400 w-4 h-4" /> 🚀 PEAD Drift Radar
|
||
</button>
|
||
</div>
|
||
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
|
||
</div>
|
||
|
||
{leftPanelTab === 'screener' ? (
|
||
<div className="space-y-6">
|
||
{/* Category A: Mega Caps */}
|
||
{renderCategoryTable(
|
||
"Kategorie A: Mega Caps (> 100B USD)",
|
||
"Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung",
|
||
categorizedResults.mega
|
||
)}
|
||
|
||
{/* Category B: Mid Caps */}
|
||
{renderCategoryTable(
|
||
"Kategorie B: Mid Caps (10B - 100B USD)",
|
||
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
|
||
categorizedResults.mid
|
||
)}
|
||
|
||
{/* Category C: Small Caps */}
|
||
{renderCategoryTable(
|
||
"Kategorie C: Small Caps (< 10B USD)",
|
||
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
|
||
categorizedResults.small
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
<div className="text-xs text-slate-400 leading-relaxed font-sans">
|
||
Monitors Post-Earnings Announcement Drift (PEAD) anomaly using standardized unanticipated earnings momentum.
|
||
</div>
|
||
|
||
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||
<table className="w-full text-left text-xs border-collapse min-w-[700px] font-mono">
|
||
<thead>
|
||
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider bg-slate-900/40">
|
||
<th className="py-2.5 px-3">Asset</th>
|
||
<th className="py-2.5 px-3">Sector</th>
|
||
<th className="py-2.5 px-3">Announcement Date</th>
|
||
<th className="py-2.5 px-3 text-right">Reported vs Consensus</th>
|
||
<th className="py-2.5 px-3 text-right">Surprise %</th>
|
||
<th className="py-2.5 px-3 text-center">Drift Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-900/60">
|
||
{peadData.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={6} className="py-6 text-center text-slate-500 italic text-xs">
|
||
No equity assets available in scan history.
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
peadData.map((pead) => {
|
||
const isPositive = pead.surprisePercent >= 0;
|
||
const isActiveDrift = pead.driftStatus === 'Active Drift';
|
||
|
||
return (
|
||
<tr key={pead.ticker} className="hover:bg-slate-850/10 transition-colors">
|
||
<td className="py-2.5 px-3 font-bold text-slate-200">
|
||
<span className="text-amber-400 block font-bold">{pead.ticker}</span>
|
||
<span className="text-[10px] text-slate-500 block font-normal">{pead.name}</span>
|
||
</td>
|
||
<td className="py-2.5 px-3 text-slate-400 font-sans text-xs">
|
||
{pead.peadSector}
|
||
</td>
|
||
<td className="py-2.5 px-3 text-slate-300 font-mono">
|
||
<span>{pead.announcementDate}</span>
|
||
<span className="text-[10px] text-slate-500 block">({pead.daysElapsed} days elapsed)</span>
|
||
</td>
|
||
<td className="py-2.5 px-3 text-right text-slate-300">
|
||
<span>{pead.epsActual.toFixed(2)}</span>
|
||
<span className="text-[10px] text-slate-500 block font-normal">vs {pead.epsConsensus.toFixed(2)}</span>
|
||
</td>
|
||
<td className={`py-2.5 px-3 text-right font-bold ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||
{isPositive ? '+' : ''}{pead.surprisePercent.toFixed(2)}%
|
||
</td>
|
||
<td className="py-2.5 px-3 text-center">
|
||
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[9px] font-bold border ${isActiveDrift ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20 shadow-[0_0_8px_rgba(16,185,129,0.1)]' : 'bg-slate-900 text-slate-400 border-slate-800'}`}>
|
||
{pead.driftStatus}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column: Deep-Check & Search Mask */}
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||
<div>
|
||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
|
||
<Search className="text-amber-400 w-5 h-5" /> Deep-Check Suchmaske
|
||
</h3>
|
||
<p className="text-xs text-slate-400 mt-2 leading-relaxed">
|
||
Suchen Sie gezielt nach Werten, um Anomalien-Analysen abzurufen. (z.B. RACE, KO, TSLA, SMCI, NFLX)
|
||
</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleDeepCheck} className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
required
|
||
placeholder="z.B. RACE"
|
||
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono focus:outline-none focus:border-amber-500 text-sm uppercase"
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={checkingDeep}
|
||
className="bg-slate-800 hover:bg-slate-700 text-amber-400 hover:text-amber-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm disabled:opacity-50"
|
||
>
|
||
{checkingDeep ? 'Prüft...' : 'Prüfen'}
|
||
</button>
|
||
</form>
|
||
|
||
{searchResult && (
|
||
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-4 animate-fade-in">
|
||
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
|
||
<div>
|
||
<h4 className="font-bold text-slate-100 font-mono text-sm">{searchResult.ticker}</h4>
|
||
<span className="text-[10px] text-slate-500">{searchResult.name}</span>
|
||
</div>
|
||
<div>
|
||
{searchResult.sentiment === 'GREEN' && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px] font-bold border border-emerald-500/25">GREEN</span>}
|
||
{searchResult.sentiment === 'YELLOW' && <span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-400 text-[10px] font-bold border border-yellow-500/25">YELLOW</span>}
|
||
{searchResult.sentiment === 'RED' && <span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[10px] font-bold border border-rose-500/25">RED</span>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-xs">
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-400">Aktueller Sturz:</span>
|
||
<span className="text-rose-400 font-mono font-bold">{(searchResult.priceChange * 100).toFixed(1)}%</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-400">GJR-GARCH Volatilität:</span>
|
||
<span className="text-cyan-400 font-mono font-bold">{(searchResult.gjrGarchVol * 100).toFixed(1)}%</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-400">Rebound Score:</span>
|
||
<span className={`font-mono font-bold ${searchResult.sentiment === 'GREEN' ? 'text-emerald-400' : (searchResult.sentiment === 'YELLOW' ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||
{searchResult.reboundScore}/100
|
||
</span>
|
||
</div>
|
||
|
||
{searchResult.sloanRatio !== undefined && (
|
||
<div className="flex justify-between items-center">
|
||
<span className="text-slate-400">Sloan Accrual Ratio:</span>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="font-mono font-bold text-slate-300">
|
||
{searchResult.sloanRatio.toFixed(2)}%
|
||
</span>
|
||
{searchResult.sloanRegime === 'SAFE' ? (
|
||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||
SAFE
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/20 animate-pulse">
|
||
ANOMALY
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-2 border-t border-slate-900">
|
||
<span className="text-[10px] text-slate-400 uppercase font-semibold block mb-1">KI-Kommentar:</span>
|
||
<p className="italic text-slate-300 leading-relaxed text-[11px]">{searchResult.whyDropped}</p>
|
||
</div>
|
||
|
||
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
|
||
<button
|
||
onClick={() => handleAddToWatchlist(
|
||
searchResult.ticker,
|
||
searchResult.priceChange,
|
||
searchResult.sentiment,
|
||
searchResult.whyDropped,
|
||
searchResult.peakPrice,
|
||
searchResult.currentPrice
|
||
)}
|
||
className="w-full bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 mt-2"
|
||
>
|
||
<Plus className="w-4 h-4" /> Watchlist hinzufügen
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* SECTION 3: Hot Watchlist & Rebound-Tracker (48 Hours) */}
|
||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
|
||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-800 pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Flame className="text-orange-400 w-6 h-6 animate-pulse" />
|
||
<h3 className="text-lg font-bold text-white">Hot Rebound Watchlist & Tracker (48h)</h3>
|
||
</div>
|
||
|
||
{watchlist.length > 0 && (
|
||
<button
|
||
onClick={simulateWatchlistTick}
|
||
className="bg-slate-950 hover:bg-slate-900 text-orange-400 hover:text-orange-350 border border-slate-800 text-xs font-bold py-2 px-4 rounded-xl flex items-center gap-1.5 transition-colors active:scale-[0.97]"
|
||
title="Simuliert das Fortschreiten der Zeit um 4 Stunden"
|
||
>
|
||
<Play className="w-4 h-4 fill-orange-400" />
|
||
<span>Simuliere +4 Std. Kursänderung</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{watchlist.length === 0 ? (
|
||
<div className="p-12 text-center text-slate-500 text-sm border border-dashed border-slate-800 rounded-xl bg-slate-950/20">
|
||
Bislang keine Assets auf der Rebound-Watchlist. Nutzen Sie die Markt-Scanergebnisse oben, um Werte hinzuzufügen.
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{watchlist.map((item) => {
|
||
const perf = item.reboundPerformance;
|
||
const isPositive = perf >= 0;
|
||
const progressPct = (item.hoursTracked / 48) * 100;
|
||
|
||
return (
|
||
<div key={item.id} className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-3 relative overflow-hidden">
|
||
<div className="absolute top-0 right-0 p-1.5">
|
||
<button
|
||
onClick={() => removeFromWatchlist(item.id)}
|
||
className="text-slate-500 hover:text-rose-400 transition-colors p-1"
|
||
title="Aus der Watchlist entfernen"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-start pr-6">
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-mono font-bold text-slate-200 text-base">{item.ticker}</span>
|
||
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold ${item.sentiment === 'GREEN' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'}`}>
|
||
{item.sentiment}
|
||
</span>
|
||
</div>
|
||
<span className="text-[10px] text-slate-500">Gezogen bei: <span className="font-mono">{(item.priceChange * 100).toFixed(1)}%</span></span>
|
||
</div>
|
||
|
||
<div className="text-right">
|
||
<div className="text-[9px] text-slate-400 uppercase">Rebound Performance</div>
|
||
<div className={`font-mono text-base font-extrabold flex items-center justify-end gap-0.5 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||
{isPositive ? '+' : ''}{perf.toFixed(2)}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Progress timeline */}
|
||
<div className="space-y-1 text-[10px] text-slate-400">
|
||
<div className="flex justify-between font-mono">
|
||
<span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5 text-slate-500" /> Tracking-Dauer: {item.hoursTracked}/48 Std.</span>
|
||
<span>{Math.round(progressPct)}% abgeschlossen</span>
|
||
</div>
|
||
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
|
||
<div
|
||
className="bg-gradient-to-r from-orange-400 to-amber-500 h-full rounded-full transition-all duration-500"
|
||
style={{ width: `${progressPct}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="text-[10px] text-slate-500 italic truncate" title={item.whyDropped}>
|
||
Hintergrund: {item.whyDropped}
|
||
</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<ScannerMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||
<ScannerBlueprintModal isOpen={isBlueprintModalOpen} onClose={() => setIsBlueprintModalOpen(false)} />
|
||
</div>
|
||
);
|
||
}
|