feat: complete core 5 elements and risk layer architecture
This commit is contained in:
538
components/modules/scanner/ScannerDemo.tsx
Normal file
538
components/modules/scanner/ScannerDemo.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
'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 {
|
||||
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
||||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play
|
||||
} from 'lucide-react';
|
||||
|
||||
// Predefined mock database for deep-check searches
|
||||
interface SearchResult {
|
||||
ticker: string;
|
||||
name: string;
|
||||
priceChange: number;
|
||||
sentiment: 'GREEN' | 'YELLOW' | 'RED';
|
||||
whyDropped: string;
|
||||
gjrGarchVol: number;
|
||||
reboundScore: number;
|
||||
returns: 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();
|
||||
|
||||
// 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 [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
|
||||
const [searchError, setSearchError] = useState(false);
|
||||
|
||||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||||
|
||||
// Run market scan simulator
|
||||
const handleMarketScan = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
// Perform a manual deep check
|
||||
const handleDeepCheck = (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;
|
||||
|
||||
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]
|
||||
};
|
||||
setSearchResult(res);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
addToWatchlist({
|
||||
ticker,
|
||||
priceChange,
|
||||
sentiment,
|
||||
whyDropped,
|
||||
initialPrice,
|
||||
currentPrice: initialPrice * (1 + priceChange), // current price after drop
|
||||
});
|
||||
};
|
||||
|
||||
// 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]);
|
||||
|
||||
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 items-center gap-2">
|
||||
<ShieldAlert className="text-amber-400 w-5 h-5" /> Anomalien-Scanner & Marktverzerrungen
|
||||
</h2>
|
||||
<p className="text-slate-400 text-xs max-w-2xl">
|
||||
Isoliert Kursstürze > 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
|
||||
<button
|
||||
onClick={handleMarketScan}
|
||||
disabled={scanning}
|
||||
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 disabled:from-amber-850 disabled:to-orange-900 disabled:text-slate-400 text-slate-950 font-bold py-3 px-6 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 active:scale-[0.98]"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${scanning ? 'animate-spin' : ''}`} />
|
||||
<span>{scanning ? 'Scanne Markt...' : 'Markt scannen'}</span>
|
||||
</button>
|
||||
{scanning && (
|
||||
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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: Scanned anomalies details */}
|
||||
<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="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.'
|
||||
};
|
||||
|
||||
const isGreen = dbInfo.sentiment === 'GREEN';
|
||||
const isYellow = dbInfo.sentiment === 'YELLOW';
|
||||
const isRed = dbInfo.sentiment === 'RED';
|
||||
|
||||
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">
|
||||
„{dbInfo.whyDropped}“
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</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"
|
||||
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"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{searchResult && (
|
||||
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-4 animate-fade-in">
|
||||
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-100 font-mono text-sm">{searchResult.ticker}</h4>
|
||||
<span className="text-[10px] text-slate-500">{searchResult.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
{searchResult.sentiment === 'GREEN' && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px] font-bold border border-emerald-500/25">GREEN</span>}
|
||||
{searchResult.sentiment === 'YELLOW' && <span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-400 text-[10px] font-bold border border-yellow-500/25">YELLOW</span>}
|
||||
{searchResult.sentiment === 'RED' && <span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[10px] font-bold border border-rose-500/25">RED</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Aktueller Sturz:</span>
|
||||
<span className="text-rose-400 font-mono font-bold">{(searchResult.priceChange * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">GJR-GARCH Volatilität:</span>
|
||||
<span className="text-cyan-400 font-mono font-bold">{(searchResult.gjrGarchVol * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Rebound Score:</span>
|
||||
<span className={`font-mono font-bold ${searchResult.sentiment === 'GREEN' ? 'text-emerald-400' : (searchResult.sentiment === 'YELLOW' ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||||
{searchResult.reboundScore}/100
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-900">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-semibold block mb-1">KI-Kommentar:</span>
|
||||
<p className="italic text-slate-300 leading-relaxed text-[11px]">{searchResult.whyDropped}</p>
|
||||
</div>
|
||||
|
||||
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
|
||||
<button
|
||||
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
|
||||
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>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user