feat: complete core 5 elements and risk layer architecture

This commit is contained in:
Antigravity Agent
2026-06-06 21:11:16 +02:00
commit 96f7643f8a
29 changed files with 12336 additions and 0 deletions

View File

@@ -0,0 +1,487 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore } from '@/lib/store';
import { predictCryptoTrend, calculateBetaPosterior } from '@/lib/math/statistics';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Cpu, Search, RefreshCw, BarChart2, TrendingUp, AlertCircle, Info,
ChevronDown, ChevronUp, ArrowUpRight, ArrowDownRight, Compass, ShieldAlert, Sparkles
} from 'lucide-react';
interface CoinData {
ticker: string;
name: string;
price: string;
change24h: number;
fundingRate: number; // in %
openInterestChange: number; // in %
longShortRatio: number;
whaleInflow: number; // net flows
exchangeReserves: number; // in %
liqLong: string;
liqShort: string;
}
const defaultCoins: Record<string, CoinData> = {
'BTC': {
ticker: 'BTC',
name: 'Bitcoin',
price: '$69,450',
change24h: 2.4,
fundingRate: -0.015,
openInterestChange: 8.2,
longShortRatio: 0.92,
whaleInflow: 480,
exchangeReserves: -1.4,
liqLong: '$68,200',
liqShort: '$70,500'
},
'ETH': {
ticker: 'ETH',
name: 'Ethereum',
price: '$3,820',
change24h: -1.2,
fundingRate: 0.045,
openInterestChange: -3.5,
longShortRatio: 1.34,
whaleInflow: -120,
exchangeReserves: 0.8,
liqLong: '$3,710',
liqShort: '$3,920'
},
'SOL': {
ticker: 'SOL',
name: 'Solana',
price: '$184.20',
change24h: 5.8,
fundingRate: 0.082,
openInterestChange: 14.5,
longShortRatio: 1.62,
whaleInflow: 1250,
exchangeReserves: -2.8,
liqLong: '$176.00',
liqShort: '$192.50'
}
};
export default function CryptoDemo() {
const { alphaSuccess, betaFailure, addModelTrial } = useSandboxStore();
const [activeTicker, setActiveTicker] = useState<string>('BTC');
const [searchQuery, setSearchQuery] = useState('');
const [customCoins, setCustomCoins] = useState<Record<string, CoinData>>({});
const [searchError, setSearchError] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
const [simulatedTrialLogged, setSimulatedTrialLogged] = useState(false);
const [lastTrialSuccess, setLastTrialSuccess] = useState(false);
// Active Coin data retrieval
const activeCoin = useMemo(() => {
return customCoins[activeTicker] || defaultCoins[activeTicker] || defaultCoins['BTC'];
}, [activeTicker, customCoins]);
// Compute live Random Forest baseline predictions
const mlPredictions = useMemo(() => {
const inputs = {
fundingRate: activeCoin.fundingRate,
openInterestChange: activeCoin.openInterestChange,
longShortRatio: activeCoin.longShortRatio,
whaleInflow: activeCoin.whaleInflow
};
return predictCryptoTrend(inputs);
}, [activeCoin]);
// Apply Bayesian online learning error-correction posterior update
const correctedPredictions = useMemo(() => {
// Correct short term probability
const shortTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.shortTermProb,
12 // pseudo weight/confidence scale
);
// Correct medium term probability
const mediumTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.mediumTermProb,
12
);
return {
shortTerm: shortTermCorrected,
mediumTerm: mediumTermCorrected
};
}, [mlPredictions, alphaSuccess, betaFailure]);
// Perform search check for Altcoins
const handleAltcoinSearch = (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
if (defaultCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
if (customCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
// Generate simulated data for Altcoin
const isBull = Math.random() > 0.45;
const simulatedChange = isBull ? 3 + Math.random() * 8 : -2 - Math.random() * 6;
const simulatedPrice = isBull ? 2 + Math.random() * 10 : 0.2 + Math.random() * 3;
const newCoin: CoinData = {
ticker: query,
name: `${query} Token`,
price: `$${simulatedPrice.toFixed(4)}`,
change24h: parseFloat(simulatedChange.toFixed(2)),
fundingRate: parseFloat((Math.random() * 0.12 - 0.04).toFixed(3)),
openInterestChange: parseFloat((Math.random() * 30 - 10).toFixed(1)),
longShortRatio: parseFloat((0.8 + Math.random() * 1.1).toFixed(2)),
whaleInflow: Math.floor(Math.random() * 1500 - 400),
exchangeReserves: parseFloat((Math.random() * 4 - 2).toFixed(1)),
liqLong: `$${(simulatedPrice * 0.9).toFixed(4)}`,
liqShort: `$${(simulatedPrice * 1.1).toFixed(4)}`
};
setCustomCoins(prev => ({ ...prev, [query]: newCoin }));
setActiveTicker(query);
setSearchQuery('');
};
const handleSimulateTrial = (success: boolean) => {
addModelTrial(success);
setLastTrialSuccess(success);
setSimulatedTrialLogged(true);
setTimeout(() => setSimulatedTrialLogged(false), 2500);
};
const totalTrials = alphaSuccess + betaFailure;
const priorAccuracy = (alphaSuccess / (totalTrials || 1)) * 100;
return (
<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-cyan-500/10 rounded-full blur-3xl -z-10" />
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 border-b border-slate-800 pb-4 mb-6">
<div>
<span className="text-cyan-400 text-xs font-semibold uppercase tracking-wider">Element 4</span>
<h2 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-sky-200 bg-clip-text text-transparent">
Predictive Krypto-Modelle & Bayes Self-Correction
</h2>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-2 flex items-center gap-3">
<Cpu className="text-cyan-400 w-5 h-5 animate-spin-slow" />
<div>
<p className="text-slate-400 text-xs">A-Priori Genauigkeit</p>
<p className="font-mono text-sm font-bold text-cyan-400">
{priorAccuracy.toFixed(1)}% (n={totalTrials})
</p>
</div>
</div>
</div>
{/* SECTION 1: Top 3 Cards & Search Mask */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
{/* Status Cards BTC, ETH, SOL */}
{['BTC', 'ETH', 'SOL'].map((tick) => {
const coin = defaultCoins[tick];
const isActive = activeTicker === tick;
const isUp = coin.change24h >= 0;
return (
<div
key={tick}
onClick={() => setActiveTicker(tick)}
className={`p-4 rounded-xl border cursor-pointer transition-all hover:bg-slate-850 flex items-center justify-between relative overflow-hidden ${isActive ? 'border-cyan-500/40 bg-cyan-500/5 shadow-md shadow-cyan-500/5' : 'border-slate-850 bg-slate-950/20'}`}
>
<div>
<div className="text-xs text-slate-400 font-semibold">{coin.name}</div>
<div className="text-xl font-extrabold font-mono mt-1 text-slate-100">{coin.price}</div>
<div className={`text-[10px] font-bold font-mono mt-0.5 flex items-center gap-0.5 ${isUp ? 'text-emerald-400' : 'text-rose-400'}`}>
{isUp ? <ArrowUpRight className="w-3.5 h-3.5" /> : <ArrowDownRight className="w-3.5 h-3.5" />}
<span>{isUp ? '+' : ''}{coin.change24h}%</span>
</div>
</div>
<div className="text-right">
<span className="text-2xl font-bold font-mono text-slate-800">{tick}</span>
</div>
</div>
);
})}
{/* Custom Search bar */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/20 flex flex-col justify-center gap-2">
<form onSubmit={handleAltcoinSearch} className="flex gap-2">
<input
type="text"
required
placeholder="Altcoin Ticker (z.B. LINK)"
className="bg-slate-900 border border-slate-800 rounded-lg p-2 flex-1 text-slate-100 font-mono text-xs uppercase focus:outline-none focus:border-cyan-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-cyan-400 hover:text-cyan-350 font-bold px-3 py-2 border border-slate-700 rounded-lg transition-colors text-xs"
>
<Search className="w-4 h-4" />
</button>
</form>
{Object.keys(customCoins).length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-1 overflow-x-auto max-h-[32px]">
{Object.keys(customCoins).map(tick => (
<button
key={tick}
onClick={() => setActiveTicker(tick)}
className={`px-2 py-0.5 rounded font-mono text-[9px] border ${activeTicker === tick ? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30' : 'bg-slate-900 text-slate-500 border-slate-800'}`}
>
{tick}
</button>
))}
</div>
)}
</div>
</div>
{/* SECTION 2: Derivatives & On-Chain Metrics Ledger */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column: Metrics Widgets */}
<div className="xl:col-span-2 space-y-6">
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<BarChart2 className="text-cyan-400 w-5 h-5" /> On-Chain &amp; Derivate-Indikatoren ({activeCoin.ticker})
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Funding & Open Interest Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Ref-Zinssatz &amp; Kontrakte (OI)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Daily Funding Rate:</span>
<span className={`font-bold ${activeCoin.fundingRate < 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.fundingRate > 0 ? '+' : ''}{activeCoin.fundingRate.toFixed(3)}%
</span>
</div>
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Open Interest (24h Δ):</span>
<span className={`font-bold ${activeCoin.openInterestChange >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.openInterestChange > 0 ? '+' : ''}{activeCoin.openInterestChange.toFixed(1)}%
</span>
</div>
</div>
</div>
{/* Long/Short & Liquidation Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Positionierung &amp; Liquidationen</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Long / Short Ratio:</span>
<span className="text-slate-200 font-bold">{activeCoin.longShortRatio}</span>
</div>
<div className="flex justify-between items-center text-xs font-mono">
<span className="text-slate-400">Liq-Cluster:</span>
<span className="text-rose-400">Long: {activeCoin.liqLong} | Short: {activeCoin.liqShort}</span>
</div>
</div>
</div>
{/* Whale Flows Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Whale-Ströme (Nettozufluss)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Netto Inflow (Wallets):</span>
<span className={`font-bold ${activeCoin.whaleInflow >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.whaleInflow > 0 ? '+' : ''}{activeCoin.whaleInflow} {activeCoin.ticker}
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Positive Werte signalisieren, dass Großinvestoren Bestände von Börsen auf private Wallets (Akkumulation) abziehen.
</div>
</div>
</div>
{/* Exchange Reserves Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Börsenreserven (Spot)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Reservenänderung (7d):</span>
<span className={`font-bold ${activeCoin.exchangeReserves <= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.exchangeReserves > 0 ? '+' : ''}{activeCoin.exchangeReserves}%
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Sinkende Reserven an den Spot-Börsen reduzieren den verfügbaren Verkaufsdruck und begünstigen Squeezes.
</div>
</div>
</div>
</div>
</div>
{/* Right Column: Predictive Gauges & Correction Calibration */}
<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">
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<Compass className="text-cyan-400 w-5 h-5" /> Vorhersage-Wahrscheinlichkeiten
</h3>
{/* Gauges / Progress Bars */}
<div className="space-y-4">
{/* 24h Gauge */}
<div className="space-y-2">
<div className="flex justify-between text-xs font-semibold">
<span className="text-slate-300">24h Volatility Squeeze (Short-Term)</span>
<span className="text-cyan-400 font-mono">{(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
{/* ML Baseline Overlay */}
<div
className="bg-cyan-500 h-full rounded-l transition-all duration-500 opacity-30"
style={{ width: `${mlPredictions.shortTermProb * 100}%` }}
/>
{/* Corrected Posterior Marker */}
<div
className="bg-cyan-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(34,211,238,0.5)]"
style={{ width: `${correctedPredictions.shortTerm * 100}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
<span>ML-Signal: {(mlPredictions.shortTermProb * 100).toFixed(0)}%</span>
<span className="text-cyan-400">Bayes-Korrigiert: {(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
</div>
</div>
{/* 14d Gauge */}
<div className="space-y-2">
<div className="flex justify-between text-xs font-semibold">
<span className="text-slate-300">14d Struktureller Bullish-Trend (Medium-Term)</span>
<span className="text-teal-400 font-mono">{(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
<div
className="bg-teal-500 h-full rounded-l transition-all duration-500 opacity-30"
style={{ width: `${mlPredictions.mediumTermProb * 100}%` }}
/>
<div
className="bg-teal-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(45,212,191,0.5)]"
style={{ width: `${correctedPredictions.mediumTerm * 100}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
<span>ML-Signal: {(mlPredictions.mediumTermProb * 100).toFixed(0)}%</span>
<span className="text-teal-400">Bayes-Korrigiert: {(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
</div>
</div>
</div>
{/* Model Calibration Log & Simulation */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold text-slate-300 uppercase">Bayes Modell-Kalibrierung</h4>
<span className="text-[10px] text-slate-500 font-mono">n = {totalTrials} Trials</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs font-mono pb-2 border-b border-slate-900">
<div className="text-slate-400">Erfolge (&alpha;):</div>
<div className="text-emerald-400 font-bold">{alphaSuccess}</div>
<div className="text-slate-400">Fehlalarme (&beta;):</div>
<div className="text-rose-400 font-bold">{betaFailure}</div>
</div>
{/* Trial simulator */}
<div className="space-y-2">
<p className="text-[10px] text-slate-400">Modell-Drift simulieren: Fügen Sie richtige/falsche Outcomes hinzu, um die Beta-Verteilung anzupassen.</p>
<div className="flex gap-2">
<button
onClick={() => handleSimulateTrial(true)}
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 Erfolg
</button>
<button
onClick={() => handleSimulateTrial(false)}
className="flex-1 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 Fehlalarm
</button>
</div>
{simulatedTrialLogged && (
<div className="text-[10px] text-cyan-400 font-mono text-center animate-pulse">
Trial geloggt! Bayes-Prior wurde auf {lastTrialSuccess ? 'Erfolg' : 'Fehlalarm'} aktualisiert.
</div>
)}
</div>
</div>
</div>
</div>
{/* SECTION 3: Mathematical LaTeX 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-cyan-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">Mathematische Formulierung (Beta-Update &amp; Random Forest)</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-bold text-cyan-400 mb-1">1. Bayesianische Beta-Konjugierte Fehlerkorrektur</h4>
<p className="mb-2">
Wir modellieren das Vertrauensintervall über die Fehlerraten des Modells mittels einer Beta-Verteilung. Der A-Priori Fehlerzustand wird durch die Parameter <InlineMath math="\alpha" /> (Erfolge) und <InlineMath math="\beta" /> (Fehlalarme) dargestellt:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P \sim \text{Beta}(\alpha, \beta) \quad \text{mit Erwartungswert } \mathbb{E}[P] = \frac{\alpha}{\alpha + \beta}" />
</div>
<p className="mb-2">
Bei einem neuen ML-Signal <InlineMath math="P_{\text{ML}}" /> führen wir ein konjugiertes Bayes-Update mit einem Vertrauensgewicht <InlineMath math="w" /> aus:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\alpha_{\text{post}} = \alpha + w \cdot P_{\text{ML}}, \quad \beta_{\text{post}} = \beta + w \cdot (1 - P_{\text{ML}})" />
</div>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P_{\text{Posterior}} = \frac{\alpha_{\text{post}}}{\alpha_{\text{post}} + \beta_{\text{post}}}" />
</div>
<p className="text-slate-400">
Ist das Modell historisch sehr instabil (hohes <InlineMath math="\beta" />), korrigiert der Bayesianische Term ein übermütiges ML-Signal nach unten, was die Robustheit des Gesamtsystems schützt.
</p>
</div>
<div className="border-t border-slate-900 pt-3">
<h4 className="font-bold text-cyan-400 mb-1">2. Random Forest Nicht-Lineares Signalmapping</h4>
<p className="mb-2">
Der Random Forest simuliert ein Ensemble von 10 schwachen Entscheidungsbäumen. Jeder Baum spaltet die Daten nach Schwellenwerten (z.B. &bdquo;Funding-Rate &lt; -0.04%&ldquo; und &bdquo;Open Interest &gt; 10%&ldquo;) auf:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\text{ML}_{\text{prob}} = \frac{1}{M} \sum_{m=1}^{M} T_m(\mathbf{x})" />
</div>
<p className="text-slate-400">
wobei <InlineMath math="T_m(\mathbf{x})" /> der prognostizierte Ausgabewert des <InlineMath math="m" />-ten Entscheidungsbaumes für den Featurevektor <InlineMath math="\mathbf{x}" /> ist.
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,980 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore } from '@/lib/store';
import { calculateEventROC, calculateEventSurvival, runEventLMM } from '@/lib/math/statistics';
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
LineChart,
Line,
ReferenceLine,
Legend
} from 'recharts';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Activity,
BarChart4,
Compass,
GitMerge,
Plus,
Trash2,
Calendar,
Sparkles,
AlertCircle,
ChevronDown,
ChevronUp,
BookOpen,
RefreshCw,
Info,
Check,
TrendingUp,
TrendingDown,
Sliders,
Database
} from 'lucide-react';
// Predefined archetypes for Event Creation
const ARCHETYPES: Record<string, { name: string; defaultScores: Record<string, number> }> = {
'FED Zinsentscheid': {
name: 'FED Zinsentscheid',
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -1, Bitcoin: 2 }
},
'US Wahlen (Präsidentschaft)': {
name: 'US Wahlen',
defaultScores: { Apple: 2, NASDAQ: 1, Gold: 3, Bitcoin: 2 }
},
'SpaceX IPO (Gerüchte)': {
name: 'SpaceX IPO',
defaultScores: { Apple: 0, NASDAQ: 2, Gold: -1, Bitcoin: 1 }
},
'CPI Inflationsdaten': {
name: 'CPI Inflationsdaten',
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -2, Bitcoin: 1 }
},
'US Non-Farm Payrolls': {
name: 'US Non-Farm Payrolls',
defaultScores: { Apple: 0, NASDAQ: 1, Gold: -1, Bitcoin: 0 }
},
'EZB Pressekonferenz': {
name: 'EZB Pressekonferenz',
defaultScores: { Apple: -1, NASDAQ: -1, Gold: 2, Bitcoin: 1 }
}
};
const ASSETS = ['Apple', 'NASDAQ', 'Gold', 'Bitcoin'];
export default function EventsDemo() {
const {
selectedModel,
setSelectedModel,
eventsMatrix,
calendarProposals,
lmmObservations,
addEventToMatrix,
updateMatrixCell,
runEndogenousLMMCalibration
} = useSandboxStore();
// Local State
const [tauPre, setTauPre] = useState<number>(7);
const [tauPost, setTauPost] = useState<number>(3);
const [showMath, setShowMath] = useState<boolean>(false);
const [selectedSurvivalAsset, setSelectedSurvivalAsset] = useState<string>('Apple');
// Custom Event Form State
const [customName, setCustomName] = useState<string>('');
const [customDate, setCustomDate] = useState<string>('2026-06-15');
const [selectedArchetype, setSelectedArchetype] = useState<string>('Custom');
// Calibration feedback states
const [isCalibrating, setIsCalibrating] = useState<boolean>(false);
const [calibrationSuccess, setCalibrationSuccess] = useState<boolean>(false);
const [lastCalibrationTime, setLastCalibrationTime] = useState<string | null>(null);
// Current baseline date for relative time calculations
const CURRENT_DATE_STR = '2026-06-06';
// Helper to calculate time kernel weight
const getWeightAndDays = (eventDateStr: string) => {
const eventDate = new Date(eventDateStr);
const currentDate = new Date(CURRENT_DATE_STR);
const diffTime = eventDate.getTime() - currentDate.getTime();
const d = diffTime / (1000 * 60 * 60 * 24); // days relative to today
let weight = 0;
if (d >= 0) {
weight = Math.exp(-d / tauPre);
} else {
const daysSince = -d;
weight = 1 / (1 + Math.log(1 + daysSince / tauPost));
}
return {
d: Math.round(d),
weight: Math.round(weight * 100) / 100
};
};
// 1. Time Weighted Net Impact Scores & Final Action Signals
const actionSignals = useMemo(() => {
const totals: Record<string, number> = { Apple: 0, NASDAQ: 0, Gold: 0, Bitcoin: 0 };
eventsMatrix.forEach((ev) => {
const { weight } = getWeightAndDays(ev.date);
ASSETS.forEach((asset) => {
const score = ev.scores[asset] || 0;
totals[asset] += score * weight;
});
});
const signals: Record<string, { netScore: number; signal: string; colorClass: string; textClass: string; glowClass: string }> = {};
ASSETS.forEach((asset) => {
const netScore = Math.round(totals[asset] * 100) / 100;
let signal = 'NEUTRAL / HOLD';
let colorClass = 'bg-slate-800/40 border-slate-700/60 text-slate-400';
let textClass = 'text-slate-400';
let glowClass = 'shadow-slate-500/5';
if (netScore > 1.5) {
signal = 'STRONG BUY';
colorClass = 'bg-emerald-950/40 border-emerald-800/80 text-emerald-400';
textClass = 'text-emerald-400 font-bold';
glowClass = 'shadow-emerald-500/10 shadow-[0_0_15px_rgba(16,185,129,0.15)]';
} else if (netScore > 0.4) {
signal = 'ACCUMULATE';
colorClass = 'bg-teal-950/30 border-teal-800/50 text-teal-400';
textClass = 'text-teal-300 font-semibold';
glowClass = 'shadow-teal-500/5 shadow-[0_0_10px_rgba(20,184,166,0.1)]';
} else if (netScore < -1.5) {
signal = 'STRONG SELL / RISK OFF';
colorClass = 'bg-rose-950/40 border-rose-800/80 text-rose-400';
textClass = 'text-rose-400 font-bold';
glowClass = 'shadow-rose-500/10 shadow-[0_0_15px_rgba(244,63,94,0.15)]';
} else if (netScore < -0.4) {
signal = 'REDUCE / HEDGE';
colorClass = 'bg-amber-950/30 border-amber-800/50 text-amber-400';
textClass = 'text-amber-300 font-semibold';
glowClass = 'shadow-amber-500/5 shadow-[0_0_10px_rgba(245,158,11,0.1)]';
}
signals[asset] = { netScore, signal, colorClass, textClass, glowClass };
});
return signals;
}, [eventsMatrix, tauPre, tauPost]);
// 2. Dynamic Decay Curve Chart Data
const decayCurveData = useMemo(() => {
const pts = [];
// Generate weight for each day relative to event (d = E - T)
for (let d = -30; d <= 30; d++) {
let weight = 0;
if (d >= 0) {
weight = Math.exp(-d / tauPre);
} else {
const daysSince = -d;
weight = 1 / (1 + Math.log(1 + daysSince / tauPost));
}
pts.push({
days: d,
weight: Math.round(weight * 1000) / 1000
});
}
return pts;
}, [tauPre, tauPost]);
// 3. Dynamic ROC Data
const { rocData, optimalThreshold, maxYouden, auc } = useMemo(() => {
const predictions: number[] = [];
const labels: number[] = [];
lmmObservations.forEach((obs) => {
// Find average event score of this asset in matrix to use as indicator bias
const assetScores = eventsMatrix.map(ev => ev.scores[obs.asset] || 0);
const avgScore = assetScores.reduce((sum, s) => sum + s, 0) / (assetScores.length || 1);
// Construct a predictor between 0 and 1
let basePred = obs.eventType === 'BULLISH' ? 0.65 : 0.35;
basePred += avgScore * 0.04 + obs.trend * 0.5 - obs.vix * 0.002;
const finalPred = Math.min(0.99, Math.max(0.01, basePred));
predictions.push(finalPred);
labels.push(obs.returnVal > 0.012 ? 1 : 0); // label 1 if return > 1.2%
});
const res = calculateEventROC(predictions, labels);
// Trapezoidal Area Under Curve (AUC)
let computedAuc = 0;
const sorted = [...res.points].sort((a, b) => a.fpr - b.fpr);
for (let i = 1; i < sorted.length; i++) {
const w = sorted[i].fpr - sorted[i - 1].fpr;
const h = (sorted[i].tpr + sorted[i - 1].tpr) / 2;
computedAuc += w * h;
}
return {
rocData: res.points,
optimalThreshold: res.optimalThreshold,
maxYouden: res.maxYouden,
auc: Math.round(Math.max(0.5, Math.min(0.99, computedAuc)) * 1000) / 1000
};
}, [eventsMatrix, lmmObservations]);
// 4. Dynamic Survival Curve Data for selected asset
const survivalData = useMemo(() => {
const assetScores = eventsMatrix.map(ev => ev.scores[selectedSurvivalAsset] || 0);
const sumScore = assetScores.reduce((sum, s) => sum + s, 0);
const timesLong: number[] = [];
const eventsLong: number[] = [];
const timesShort: number[] = [];
const eventsShort: number[] = [];
// Simulate 15 historical events outcomes per direction
for (let i = 0; i < 15; i++) {
// LONG: Positive scores reduce time-to-event (gain target hit faster)
let tLong = 35 - sumScore * 3.5 + (Math.sin(i * 1.5) * 12);
let evLong = 1;
if (tLong > 60 || sumScore < -1) {
tLong = 60;
evLong = 0; // right censored
}
timesLong.push(Math.round(Math.max(3, tLong)));
eventsLong.push(evLong);
// SHORT: Negative scores reduce time-to-event (loss target hit faster)
let tShort = 35 + sumScore * 3.5 + (Math.cos(i * 1.9) * 12);
let evShort = 1;
if (tShort > 60 || sumScore > 1) {
tShort = 60;
evShort = 0; // right censored
}
timesShort.push(Math.round(Math.max(3, tShort)));
eventsShort.push(evShort);
}
const curveLong = calculateEventSurvival(timesLong, eventsLong, 'LONG');
const curveShort = calculateEventSurvival(timesShort, eventsShort, 'SHORT');
// Merge for chart mapping
const timeMap: Record<number, { time: number; longRate?: number; shortRate?: number }> = {};
for (let t = 0; t <= 60; t += 2) {
timeMap[t] = { time: t };
}
curveLong.forEach(pt => {
const roundedT = Math.round(pt.time / 2) * 2;
if (timeMap[roundedT]) timeMap[roundedT].longRate = pt.survivalRate;
});
curveShort.forEach(pt => {
const roundedT = Math.round(pt.time / 2) * 2;
if (timeMap[roundedT]) timeMap[roundedT].shortRate = pt.survivalRate;
});
const sortedMerged = Object.values(timeMap).sort((a, b) => a.time - b.time);
let lastLong = 1.0;
let lastShort = 1.0;
return sortedMerged.map(pt => {
if (pt.longRate !== undefined) lastLong = pt.longRate;
else pt.longRate = lastLong;
if (pt.shortRate !== undefined) lastShort = pt.shortRate;
else pt.shortRate = lastShort;
return pt;
});
}, [eventsMatrix, selectedSurvivalAsset]);
// 5. Dynamic LMM regression fitting
const lmmResults = useMemo(() => {
return runEventLMM(lmmObservations);
}, [lmmObservations]);
// Custom Event Handler
const handleAddCustomEvent = (e: React.FormEvent) => {
e.preventDefault();
let name = customName.trim();
let scores: Record<string, number> = { Apple: 0, NASDAQ: 0, Gold: 0, Bitcoin: 0 };
if (selectedArchetype !== 'Custom') {
const arch = ARCHETYPES[selectedArchetype];
name = name || arch.name;
scores = { ...arch.defaultScores };
} else {
name = name || 'Benutzerdefiniertes Ereignis';
}
addEventToMatrix(name, customDate, scores);
setCustomName('');
setSelectedArchetype('Custom');
};
// Calibration Action Trigger
const handleTriggerCalibration = () => {
setIsCalibrating(true);
// Simulate complex econometric iterative calibration
setTimeout(() => {
runEndogenousLMMCalibration();
setIsCalibrating(false);
setCalibrationSuccess(true);
setLastCalibrationTime(new Date().toLocaleTimeString());
setTimeout(() => {
setCalibrationSuccess(false);
}, 4000);
}, 1200);
};
return (
<div className="space-y-6 text-slate-100 font-sans">
{/* 1. Header with Model Selector */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-2xl p-6 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-48 h-48 bg-rose-500/10 rounded-full blur-3xl -z-10" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="bg-rose-500/20 text-rose-400 border border-rose-500/30 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wider">
Element 5
</span>
<span className="text-slate-400 text-xs font-mono">Status: Calibrated & Active</span>
</div>
<h1 className="text-2xl md:text-3xl font-extrabold bg-gradient-to-r from-rose-400 via-purple-300 to-indigo-200 bg-clip-text text-transparent">
Advanced Econometric Event-Analysis Matrix
</h1>
<p className="text-slate-400 text-xs mt-1 max-w-2xl leading-relaxed">
Analyzes multi-asset cross-impact networks under logarithmic decay timelines. Evaluates predictive efficiency via ROC, models target boundaries with directional survival, and performs endogenous regressions via LMM feedback.
</p>
</div>
<div className="flex bg-slate-950/80 p-1 rounded-xl border border-slate-800/80 self-stretch md:self-auto justify-between gap-1">
<button
onClick={() => setSelectedModel('ROC')}
className={`flex-1 md:flex-none px-4 py-2 rounded-lg text-xs font-semibold transition-all flex items-center justify-center gap-2 ${
selectedModel === 'ROC'
? 'bg-rose-500 text-slate-950 shadow-[0_0_12px_rgba(244,63,94,0.3)] font-bold'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
}`}
>
<Compass className="w-3.5 h-3.5" /> ROC Analytics
</button>
<button
onClick={() => setSelectedModel('SURVIVAL')}
className={`flex-1 md:flex-none px-4 py-2 rounded-lg text-xs font-semibold transition-all flex items-center justify-center gap-2 ${
selectedModel === 'SURVIVAL'
? 'bg-rose-500 text-slate-950 shadow-[0_0_12px_rgba(244,63,94,0.3)] font-bold'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
}`}
>
<Activity className="w-3.5 h-3.5" /> Survival Curve
</button>
<button
onClick={() => setSelectedModel('LMM')}
className={`flex-1 md:flex-none px-4 py-2 rounded-lg text-xs font-semibold transition-all flex items-center justify-center gap-2 ${
selectedModel === 'LMM'
? 'bg-rose-500 text-slate-950 shadow-[0_0_12px_rgba(244,63,94,0.3)] font-bold'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
}`}
>
<GitMerge className="w-3.5 h-3.5" /> LMM Regression
</button>
</div>
</div>
</div>
{/* 2. Main Dashboard: Clean View Matrix & Action Signals */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Left/Middle Matrix & Settings (2/3 width) */}
<div className="xl:col-span-2 space-y-6">
{/* A. Event-Asset Matrix Table */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-6 shadow-xl relative">
<div className="flex justify-between items-center mb-4">
<h3 className="text-base font-bold flex items-center gap-2 text-rose-300">
<Database className="w-4 h-4 text-rose-400" /> Event Impact Matrix
</h3>
<div className="text-[10px] text-slate-400 font-mono flex items-center gap-2">
<span className="w-2.5 h-2.5 bg-rose-500/20 border border-rose-500/30 rounded inline-block text-center text-rose-400 font-bold leading-none">-</span>
<span>Bearish (-3)</span>
<span className="w-2.5 h-2.5 bg-emerald-500/20 border border-emerald-500/30 rounded inline-block text-center text-emerald-400 font-bold leading-none">+</span>
<span>Bullish (+3)</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-xs">
<thead>
<tr className="border-b border-slate-800/60 text-slate-400 font-semibold">
<th className="py-3 px-3 min-w-[150px]">Makro-Ereignis</th>
<th className="py-3 px-3">Datum</th>
{ASSETS.map(asset => (
<th key={asset} className="py-3 px-3 text-center">{asset}</th>
))}
<th className="py-3 px-3 text-right">Kernel-Gewicht</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/40">
{eventsMatrix.map((ev) => {
const { d, weight } = getWeightAndDays(ev.date);
return (
<tr key={ev.id} className="hover:bg-slate-950/20 transition-colors group">
<td className="py-3 px-3 font-semibold text-slate-200">
{ev.name}
</td>
<td className="py-3 px-3 text-slate-400 font-mono">
{ev.date}
<span className="block text-[10px] text-slate-500">
{d === 0 ? 'Heute' : d > 0 ? `In ${d} Tagen` : `Vor ${-d} Tagen`}
</span>
</td>
{ASSETS.map((asset) => {
const score = ev.scores[asset] || 0;
// Determine color style based on score value
let badgeStyle = 'text-slate-400 bg-slate-950 border-slate-800/60';
if (score > 1.5) badgeStyle = 'text-emerald-400 bg-emerald-950/30 border-emerald-800/50';
else if (score > 0) badgeStyle = 'text-teal-400 bg-teal-950/20 border-teal-900/30';
else if (score < -1.5) badgeStyle = 'text-rose-400 bg-rose-950/30 border-rose-800/50';
else if (score < 0) badgeStyle = 'text-orange-400 bg-orange-950/20 border-orange-900/30';
return (
<td key={asset} className="py-3 px-3 text-center">
<div className="inline-flex items-center gap-1.5 bg-slate-950/40 px-2 py-1 rounded-lg border border-slate-800/50">
<button
onClick={() => updateMatrixCell(ev.id, asset, Math.max(-3, score - 1))}
className="w-4 h-4 flex items-center justify-center rounded bg-slate-900 border border-slate-800 hover:bg-slate-800 hover:border-slate-700 text-slate-400 hover:text-slate-200 text-[10px] transition-all"
>
-
</button>
<span className={`w-8 text-[11px] font-mono text-center px-1 rounded border ${badgeStyle}`}>
{score > 0 ? `+${score}` : score}
</span>
<button
onClick={() => updateMatrixCell(ev.id, asset, Math.min(3, score + 1))}
className="w-4 h-4 flex items-center justify-center rounded bg-slate-900 border border-slate-800 hover:bg-slate-800 hover:border-slate-700 text-slate-400 hover:text-slate-200 text-[10px] transition-all"
>
+
</button>
</div>
</td>
);
})}
<td className="py-3 px-3 text-right font-mono">
<div className="flex items-center justify-end gap-1.5">
<span className="text-purple-400 font-semibold">{Math.round(weight * 100)}%</span>
<span className="text-[10px] text-slate-500">({weight.toFixed(2)})</span>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* B. Add Event Form & Time Kernel Weights config (split) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Form to Add Event */}
<div className="bg-slate-900/50 border border-slate-800/80 rounded-2xl p-5 shadow-lg">
<h4 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Plus className="w-4 h-4 text-rose-400" /> Event hinzufügen
</h4>
<form onSubmit={handleAddCustomEvent} className="space-y-3.5 text-xs">
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Archetyp Vorlage</label>
<select
value={selectedArchetype}
onChange={(e) => {
setSelectedArchetype(e.target.value);
if (e.target.value !== 'Custom') {
setCustomName(ARCHETYPES[e.target.value].name);
} else {
setCustomName('');
}
}}
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-200 focus:outline-none focus:border-rose-500/50"
>
<option value="Custom">Benutzerdefiniert (Scores auf 0)</option>
{Object.keys(ARCHETYPES).map(key => (
<option key={key} value={key}>{key}</option>
))}
</select>
</div>
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Ereignis Name</label>
<input
type="text"
value={customName}
onChange={(e) => setCustomName(e.target.value)}
placeholder="z.B. OPEC Treffen"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-200 focus:outline-none focus:border-rose-500/50 font-sans"
/>
</div>
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Event Datum</label>
<input
type="date"
value={customDate}
onChange={(e) => setCustomDate(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-200 focus:outline-none focus:border-rose-500/50 font-mono"
/>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-rose-500 to-indigo-600 hover:from-rose-600 hover:to-indigo-700 text-slate-950 hover:text-slate-100 font-bold p-2.5 rounded-lg flex items-center justify-center gap-1.5 transition-all shadow-[0_4px_12px_rgba(244,63,94,0.15)]"
>
<Plus className="w-4 h-4" /> In Matrix aufnehmen
</button>
</form>
</div>
{/* Time Decay Kernel Sliders & Live Decay Curve Chart */}
<div className="bg-slate-900/50 border border-slate-800/80 rounded-2xl p-5 shadow-lg flex flex-col justify-between">
<div>
<h4 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Sliders className="w-4 h-4 text-purple-400" /> Time-Kernel Decay Parameters
</h4>
<div className="space-y-4">
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Pre-Event Slope (<InlineMath math="\tau_{pre}" />)</span>
<span className="font-mono text-purple-400 font-bold">{tauPre} Tage</span>
</div>
<input
type="range"
min="1"
max="30"
value={tauPre}
onChange={(e) => setTauPre(Number(e.target.value))}
className="w-full accent-purple-500 bg-slate-950 h-1 rounded-lg appearance-none cursor-pointer"
/>
<span className="text-[10px] text-slate-500 block mt-0.5">Schnellerer Anstieg (kleinerer Wert) nähert sich dem Ereignis.</span>
</div>
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Post-Event Half-Life (<InlineMath math="\tau_{post}" />)</span>
<span className="font-mono text-purple-400 font-bold">{tauPost} Tage</span>
</div>
<input
type="range"
min="1"
max="20"
value={tauPost}
onChange={(e) => setTauPost(Number(e.target.value))}
className="w-full accent-purple-500 bg-slate-950 h-1 rounded-lg appearance-none cursor-pointer"
/>
<span className="text-[10px] text-slate-500 block mt-0.5">Langsameres Abklingen logarithmisch nach dem Stichtag.</span>
</div>
</div>
</div>
{/* Mini Kernel Chart showing bell curve of relevance */}
<div className="h-20 w-full mt-4 bg-slate-950/40 rounded-lg p-1 border border-slate-900">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={decayCurveData} margin={{ top: 0, right: 5, left: 5, bottom: 0 }}>
<Tooltip
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '6px', fontSize: '10px' }}
labelFormatter={(label) => `Relative Tage: ${label}`}
/>
<Area type="monotone" dataKey="weight" name="Gewicht" stroke="#a855f7" fill="rgba(168, 85, 247, 0.15)" strokeWidth={1.5} dot={false} />
<ReferenceLine x={0} stroke="#f43f5e" strokeDasharray="3 3" label={{ value: 'Event', fill: '#f43f5e', fontSize: 8, position: 'top' }} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
{/* Right Sidebar: Suggestions & Final Action Signals (1/3 width) */}
<div className="space-y-6">
{/* Calendar Inbox Panel */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl">
<div className="flex items-center gap-2 mb-3">
<Calendar className="w-4 h-4 text-indigo-400" />
<h3 className="text-sm font-bold text-slate-200">Wirtschaftskalender Vorschläge</h3>
</div>
{calendarProposals.length === 0 ? (
<div className="bg-slate-950/40 border border-slate-850 p-4 rounded-xl text-center text-slate-500 text-xs py-8">
Keine ausstehenden Vorschläge im Posteingang.
</div>
) : (
<div className="space-y-3 max-h-[200px] overflow-y-auto pr-1">
{calendarProposals.map((cp) => (
<div key={cp.id} className="bg-slate-950/50 border border-slate-800/80 rounded-xl p-3 flex justify-between items-center text-xs group hover:border-indigo-500/30 transition-all">
<div>
<div className="font-semibold text-slate-200">{cp.name}</div>
<div className="text-[10px] text-slate-500 flex items-center gap-1.5 mt-0.5 font-mono">
<span>{cp.date}</span>
<span></span>
<span className="text-indigo-400">{cp.archetype}</span>
</div>
</div>
<button
onClick={() => addEventToMatrix(cp.name, cp.date, cp.defaultScores)}
className="bg-indigo-600 hover:bg-indigo-500 text-slate-950 hover:text-slate-100 font-bold p-1.5 rounded-lg flex items-center justify-center transition-all"
title="In Matrix aufnehmen"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
{/* Action Signals Dashboard */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl">
<h3 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-400" /> Aggregated Trade Signals
</h3>
<div className="space-y-3">
{ASSETS.map((asset) => {
const { netScore, signal, colorClass, textClass, glowClass } = actionSignals[asset];
return (
<div
key={asset}
className={`border rounded-xl p-3.5 transition-all flex flex-col justify-between gap-1 bg-gradient-to-br from-slate-950/80 to-slate-950/40 hover:scale-[1.01] ${glowClass} border-slate-800/80`}
>
<div className="flex justify-between items-center">
<span className="font-bold text-slate-200 text-xs">{asset}</span>
<span className="text-[10px] font-mono text-slate-400">
Weighted Score: <span className={textClass}>{netScore > 0 ? `+${netScore}` : netScore}</span>
</span>
</div>
<div className="flex justify-between items-center mt-1">
<div className={`text-[10px] px-2 py-0.5 rounded border ${colorClass} font-semibold uppercase tracking-wider`}>
{signal}
</div>
<div className="text-[10px] text-slate-500 flex items-center gap-1 font-mono">
{netScore > 0.4 ? (
<TrendingUp className="w-3 h-3 text-emerald-400" />
) : netScore < -0.4 ? (
<TrendingDown className="w-3 h-3 text-rose-400" />
) : (
<AlertCircle className="w-3 h-3 text-slate-400" />
)}
<span>{netScore > 0 ? 'Bullish Drift' : netScore < 0 ? 'Bearish Risk' : 'Stationary'}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* LMM Feedback Trigger */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl text-center space-y-3 relative overflow-hidden">
<h3 className="text-sm font-bold text-slate-200">Endogenous Calibration</h3>
<p className="text-[11px] text-slate-400 leading-relaxed">
Updates manual matrix scores with optimal significant coefficients estimated from historical Linear Mixed Models, calibrating feedback parameters dynamically.
</p>
{calibrationSuccess && (
<div className="bg-emerald-950/30 border border-emerald-800/80 text-emerald-400 text-[10px] rounded-lg p-2 flex items-center gap-1.5 justify-center font-semibold">
<Check className="w-3.5 h-3.5" /> Kalibrierung erfolgreich abgeschlossen!
</div>
)}
<button
disabled={isCalibrating}
onClick={handleTriggerCalibration}
className={`w-full py-2.5 px-4 rounded-lg text-xs font-bold font-mono border transition-all flex items-center justify-center gap-2 ${
isCalibrating
? 'bg-slate-800 border-slate-700 text-slate-400 cursor-not-allowed'
: 'bg-rose-500 hover:bg-rose-600 border-rose-600 text-slate-950 hover:text-slate-100 shadow-[0_0_12px_rgba(244,63,94,0.25)] hover:scale-[1.01]'
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${isCalibrating ? 'animate-spin' : ''}`} />
{isCalibrating ? 'Calibrating LMM...' : 'Trigger LMM Calibration'}
</button>
{lastCalibrationTime && (
<div className="text-[9px] text-slate-500 font-mono">
Letzter Durchlauf: {lastCalibrationTime} ({lmmObservations.length} Obs.)
</div>
)}
</div>
</div>
</div>
{/* 3. Bottom: Econometric Charts & Show Math Panel */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
{/* Model Tabs Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-slate-800 pb-3 gap-2">
<div className="flex items-center gap-2">
<BarChart4 className="text-rose-400 w-5 h-5" />
<h3 className="text-base font-bold text-slate-100 uppercase tracking-wider">
{selectedModel === 'ROC' && 'ROC Model Diagnostics'}
{selectedModel === 'SURVIVAL' && 'Survival Analysis (Time-to-Event)'}
{selectedModel === 'LMM' && 'LMM Panel Regression Summary'}
</h3>
</div>
<button
onClick={() => setShowMath(!showMath)}
className="flex items-center gap-1.5 px-3 py-1 rounded bg-slate-950 border border-slate-800 hover:border-slate-700 text-[10px] text-slate-400 hover:text-slate-200 transition-all font-semibold uppercase tracking-wider"
>
<BookOpen className="w-3.5 h-3.5" />
{showMath ? 'Formeln verbergen' : 'Show Math (LaTeX)'}
</button>
</div>
{/* Collapsible LaTeX equations */}
{showMath && (
<div className="bg-slate-950/40 border border-slate-850 rounded-xl p-5 text-xs text-slate-300 leading-relaxed grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2 border-r border-slate-850/60 pr-4">
<h4 className="font-semibold text-rose-300">ROC Model Diagnostics</h4>
<p className="text-[10px] text-slate-400">
Sensitivity (TPR) maps positive asset breakouts, while Specificity (1-FPR) maps false alerts.
</p>
<div className="overflow-x-auto py-1">
<BlockMath math="\text{TPR} = \frac{\text{TP}}{\text{TP} + \text{FN}}" />
<BlockMath math="\text{FPR} = \frac{\text{FP}}{\text{FP} + \text{TN}}" />
<BlockMath math="J = \text{TPR} + \text{TNR} - 1" />
</div>
</div>
<div className="space-y-2 border-r border-slate-850/60 pr-4">
<h4 className="font-semibold text-indigo-300">Kaplan-Meier Survival</h4>
<p className="text-[10px] text-slate-400">
Calculates probability of NOT hitting target thresholds over 60 days. Events beyond 60 days are mathematically censored.
</p>
<div className="overflow-x-auto py-1">
<BlockMath math="\hat{S}(t) = \prod_{t_i \le t} \left(1 - \frac{d_i}{n_i}\right)" />
<BlockMath math="h(t | X) = h_0(t) e^{\beta X}" />
</div>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-purple-300">Linear Mixed Model (LMM)</h4>
<p className="text-[10px] text-slate-400">
Estimates pure event returns controlling for systemic covariates. Assets are modeled as random effect intercept adjustments.
</p>
<div className="overflow-x-auto py-1">
<BlockMath math="R_{it} = \beta_0 + \beta_1 \text{Event}_{it} + \beta_2 \text{VIX}_t + \beta_3 \text{Trend}_{it} + b_i + \epsilon_{it}" />
<BlockMath math="b_i \sim N(0, \sigma_b^2), \quad \epsilon_{it} \sim N(0, \sigma^2)" />
</div>
</div>
</div>
)}
{/* Tab Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center">
{/* Left Panel: Description & Stats Cards (1/3 width) */}
<div className="space-y-4">
{selectedModel === 'ROC' && (
<div className="space-y-3 text-xs">
<p className="text-slate-400 leading-relaxed">
The Receiver Operating Characteristic (ROC) curve evaluates classifier strength on binary events (e.g. Return &gt; +3.0% within 14 days). An AUC of 0.5 denotes a random baseline, while 1.0 represents a perfect oracle.
</p>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl">
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Area Under Curve (AUC)</span>
<span className="text-lg font-bold font-mono text-rose-400">{auc}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl">
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Max Youden Index (J)</span>
<span className="text-lg font-bold font-mono text-rose-400">{maxYouden}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl col-span-2">
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Optimal Youden Threshold</span>
<span className="text-sm font-bold font-mono text-slate-200">
Score &ge; {optimalThreshold}
</span>
</div>
</div>
</div>
)}
{selectedModel === 'SURVIVAL' && (
<div className="space-y-3 text-xs">
<p className="text-slate-400 leading-relaxed">
Kaplan-Meier survival curves map time-to-rebound (Long target: +5%) and time-to-drawdown (Short target: -5%). Separation of long and short tracks prevents arithmetic zero-sum cancellation.
</p>
<div>
<label className="block text-slate-500 mb-1 text-[10px] uppercase font-semibold">Fokus Asset</label>
<select
value={selectedSurvivalAsset}
onChange={(e) => setSelectedSurvivalAsset(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-200 focus:outline-none focus:border-rose-500/50"
>
{ASSETS.map(asset => (
<option key={asset} value={asset}>{asset}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-3 mt-2">
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl">
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Right Censoring</span>
<span className="text-sm font-bold text-slate-200 font-mono">60 Tage</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl">
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Observation Count</span>
<span className="text-sm font-bold text-slate-200 font-mono">30 Event Runs</span>
</div>
</div>
</div>
)}
{selectedModel === 'LMM' && (
<div className="space-y-3 text-xs">
<p className="text-slate-400 leading-relaxed">
Linear Mixed Model estimates the true impact of events on returns, isolating asset-level intercepts as random deviations. Standard Errors, t-stats, and p-values determine significance.
</p>
<div className="grid grid-cols-3 gap-2">
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">AIC</span>
<span className="text-xs font-bold text-slate-300 font-mono">{lmmResults.aic}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">BIC</span>
<span className="text-xs font-bold text-slate-300 font-mono">{lmmResults.bic}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Adj. R²</span>
<span className="text-xs font-bold text-purple-400 font-mono">{(lmmResults.rSquared * 100).toFixed(1)}%</span>
</div>
</div>
</div>
)}
</div>
{/* Right Panel: Charts / Regression Table (2/3 width) */}
<div className="lg:col-span-2 h-72 w-full flex items-center justify-center bg-slate-950/40 border border-slate-850 rounded-xl p-4">
{selectedModel === 'ROC' && (
<div className="w-full h-full">
<div className="text-[10px] font-mono text-slate-400 mb-2 text-center flex items-center justify-center gap-1.5">
<span>Modell-Klassifikationstrennung (FPR vs TPR)</span>
</div>
<ResponsiveContainer width="100%" height="90%">
<AreaChart data={rocData} margin={{ top: 10, right: 10, left: -25, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="fpr" stroke="#64748b" fontSize={9} tickFormatter={(v) => v.toFixed(1)} />
<YAxis stroke="#64748b" fontSize={9} domain={[0, 1.05]} />
<Tooltip
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '8px', fontSize: '10px' }}
formatter={(value: any) => [parseFloat(value).toFixed(2), 'True Positive Rate']}
/>
<Area type="monotone" dataKey="tpr" name="True Positive Rate" stroke="#f43f5e" fill="rgba(244, 63, 94, 0.12)" strokeWidth={2} />
{/* Diagonal baseline */}
<Line type="monotone" dataKey="fpr" stroke="#334155" strokeDasharray="4 4" dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
)}
{selectedModel === 'SURVIVAL' && (
<div className="w-full h-full flex flex-col justify-between">
<div className="text-[10px] font-mono text-slate-400 text-center flex items-center justify-center gap-4">
<span className="flex items-center gap-1.5"><span className="w-2 h-2 rounded-full bg-emerald-400 inline-block"></span> LONG Rebound (+5%)</span>
<span className="flex items-center gap-1.5"><span className="w-2 h-2 rounded-full bg-rose-400 inline-block"></span> SHORT Drawdown (-5%)</span>
</div>
<div className="flex-1 w-full mt-2">
<ResponsiveContainer width="100%" height="95%">
<LineChart data={survivalData} margin={{ top: 5, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} domain={[0, 1.05]} tickFormatter={(v) => `${(v * 100).toFixed(0)}%`} />
<Tooltip
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '8px', fontSize: '10px' }}
formatter={(v: any) => `${(parseFloat(v) * 100).toFixed(1)}%`}
/>
<Line type="stepAfter" dataKey="longRate" name="LONG Rebound" stroke="#10b981" strokeWidth={2} dot={false} />
<Line type="stepAfter" dataKey="shortRate" name="SHORT Drawdown" stroke="#f43f5e" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
{selectedModel === 'LMM' && (
<div className="w-full h-full overflow-y-auto pr-1">
<div className="flex justify-between items-center mb-2">
<span className="text-[10px] font-mono text-slate-400">Fixed Effects Coefficients</span>
<span className="text-[9px] font-mono text-slate-500">Signif. codes: 0 &lsquo;***&rsquo; 0.001 &lsquo;**&rsquo; 0.01 &lsquo;*&rsquo; 0.05</span>
</div>
<table className="w-full text-left text-[10px] font-mono text-slate-300">
<thead>
<tr className="border-b border-slate-800 text-slate-500 font-semibold">
<th className="py-1">Parameter</th>
<th className="py-1 text-right">Estimate</th>
<th className="py-1 text-right">Std. Error</th>
<th className="py-1 text-right">p-value</th>
<th className="py-1 text-center">Sig.</th>
<th className="py-1 text-right">95% Conf. Interval</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-850">
{lmmResults.fixedEffects.map((coeff) => (
<tr key={coeff.name} className="hover:bg-slate-900/40">
<td className="py-1.5 font-bold text-slate-400">{coeff.name}</td>
<td className="py-1.5 text-right text-emerald-400">{coeff.estimate.toFixed(4)}</td>
<td className="py-1.5 text-right">{coeff.se.toFixed(4)}</td>
<td className="py-1.5 text-right font-semibold">{coeff.pVal.toFixed(5)}</td>
<td className="py-1.5 text-center text-purple-400 font-bold">{coeff.sig}</td>
<td className="py-1.5 text-right text-slate-500">
[{coeff.ciLower.toFixed(4)}, {coeff.ciUpper.toFixed(4)}]
</td>
</tr>
))}
</tbody>
</table>
<div className="border-t border-slate-800/80 mt-3 pt-2 text-[9px] text-slate-400 flex flex-wrap gap-x-6 gap-y-1">
<span><strong className="text-slate-300">Random Effects Asset (intercepts):</strong></span>
{lmmResults.randomEffects.map((re) => (
<span key={re.asset}>
{re.asset}: <span className="text-cyan-400 font-semibold">{re.intercept > 0 ? `+${re.intercept.toFixed(4)}` : re.intercept.toFixed(4)}</span>
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,448 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore, InsiderTrade, CongressTrade, WhaleTrade } from '@/lib/store';
import { calculateRollingZScore, detectInsiderClusters, coupleBayesianRebound } from '@/lib/math/statistics';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Shield, User, ArrowDownRight, ArrowUpRight, DollarSign, Calendar, Landmark,
ChevronDown, ChevronUp, Search, Radio, Building2, AlertTriangle, Layers, Percent
} from 'lucide-react';
export default function InsiderDemo() {
const {
insiderTrades,
congressTrades,
whaleTrades,
insiderVolumes,
priorProbability
} = useSandboxStore();
// Component local UI states
const [activeSegment, setActiveSegment] = useState<'executives' | 'congress' | 'whales'>('executives');
const [searchQuery, setSearchQuery] = useState('');
const [selectedTicker, setSelectedTicker] = useState<string | null>(null);
const [scanning, setScanning] = useState(false);
const [scanResults, setScanResults] = useState<{
ticker: string;
zScore: number;
clusterCount: number;
multiplier: number;
isAnomaly: boolean;
coupledRebound: number;
}[] | null>(null);
const [showMathAccordion, setShowMathAccordion] = useState(false);
// Run Global Flow Scan
const handleGlobalFlowScan = () => {
setScanning(true);
setTimeout(() => {
const results = Object.keys(insiderVolumes).map((ticker) => {
const volumes = insiderVolumes[ticker];
const zResult = calculateRollingZScore(volumes);
// Filter trades for this ticker to detect clusters
const tickerTrades = insiderTrades.filter(t => t.ticker === ticker);
const clusterResult = detectInsiderClusters(tickerTrades);
// Bayesian coupling
const coupled = coupleBayesianRebound(priorProbability, zResult.latest);
return {
ticker,
zScore: parseFloat(zResult.latest.toFixed(2)),
clusterCount: clusterResult.count,
multiplier: parseFloat(clusterResult.multiplier.toFixed(2)),
isAnomaly: zResult.isAnomaly || clusterResult.isCluster,
coupledRebound: coupled,
};
});
// Sort anomalies to the top
results.sort((a, b) => (b.isAnomaly ? 1 : 0) - (a.isAnomaly ? 1 : 0) || b.zScore - a.zScore);
setScanResults(results);
setScanning(false);
}, 1000);
};
// Perform Ticker Lookup
const handleTickerLookup = (e: React.FormEvent) => {
e.preventDefault();
const query = searchQuery.trim().toUpperCase();
if (query) {
setSelectedTicker(query);
} else {
setSelectedTicker(null);
}
};
// Compute stats for selected ticker
const tickerStats = useMemo(() => {
if (!selectedTicker) return null;
const volumes = insiderVolumes[selectedTicker] || [5000, 4800, 5200, 6000, 4500, 5000]; // fallback
const zResult = calculateRollingZScore(volumes);
const tickerTrades = insiderTrades.filter(t => t.ticker === selectedTicker);
const clusterResult = detectInsiderClusters(tickerTrades);
const coupled = coupleBayesianRebound(priorProbability, zResult.latest);
return {
zScore: zResult.latest,
isAnomaly: zResult.isAnomaly,
clusterCount: clusterResult.count,
isCluster: clusterResult.isCluster,
multiplier: clusterResult.multiplier,
coupledRebound: coupled,
volumes,
};
}, [selectedTicker, insiderVolumes, insiderTrades, priorProbability]);
// Volumes chart data for selected ticker
const volumeChartData = useMemo(() => {
if (!tickerStats || !selectedTicker) return [];
return tickerStats.volumes.map((vol, idx) => ({
month: `M-${tickerStats.volumes.length - idx - 1}`,
'Volumen (Shares)': vol,
}));
}, [tickerStats, selectedTicker]);
return (
<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-purple-500/10 rounded-full blur-3xl -z-10" />
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 border-b border-slate-800 pb-4 mb-6">
<div>
<span className="text-purple-400 text-xs font-semibold uppercase tracking-wider">Element 3</span>
<h2 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-indigo-200 bg-clip-text text-transparent">
Institutional & Insider Flow Tracker
</h2>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-2 flex items-center gap-3">
<Shield className="text-purple-400 w-5 h-5" />
<div>
<p className="text-slate-400 text-xs">Bayesianische Kopplung</p>
<p className="font-mono text-sm font-bold text-purple-400">
Prior Rebound: {(priorProbability * 100).toFixed(0)}%
</p>
</div>
</div>
</div>
{/* SECTION 1: Dual-Query Actions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/30 flex flex-col justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-300">Global Flow Outlier Scan</h4>
<p className="text-xs text-slate-400">Filtert den Markt nach statistisch signifikanten Kaufvolumina (Z-Score &gt; 2.0) und konzertierten Käufen (Insider-Cluster).</p>
</div>
<button
onClick={handleGlobalFlowScan}
disabled={scanning}
className="w-full bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 disabled:from-purple-900 text-white font-bold py-2.5 px-4 rounded-xl transition-all shadow-lg shadow-purple-500/10 flex items-center justify-center gap-2"
>
<Radio className={`w-4 h-4 ${scanning ? 'animate-pulse' : ''}`} />
<span>{scanning ? 'Scanne Transaktionen...' : 'Global Flow Scan starten'}</span>
</button>
</div>
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/30 flex flex-col justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-300">Ticker Lookup (Einzelauswertung)</h4>
<p className="text-xs text-slate-400">Gezielte Abfrage von Insider-Z-Scores und Bayesianischen Kopplungs-Updates (z.B. PLTR, RACE, AMZN, AAPL).</p>
</div>
<form onSubmit={handleTickerLookup} className="flex gap-2">
<input
type="text"
required
placeholder="z.B. PLTR"
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono text-sm uppercase focus:outline-none focus:border-purple-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-purple-400 hover:text-purple-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
>
Lookup
</button>
</form>
</div>
</div>
{/* Ticker Stats / Lookup Section */}
{selectedTicker && tickerStats && (
<div className="p-5 rounded-2xl border border-purple-500/30 bg-purple-500/5 grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6 animate-fade-in">
<div className="space-y-3">
<div className="flex justify-between items-center border-b border-slate-800 pb-2">
<h3 className="font-mono font-bold text-lg text-slate-100">{selectedTicker} Einzelauswertung</h3>
{tickerStats.isAnomaly && <span className="px-2 py-0.5 rounded bg-purple-500/20 text-purple-300 text-[10px] font-bold border border-purple-500/30 animate-pulse">FLOW OUTLIER</span>}
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between font-mono">
<span className="text-slate-400">Volumetrischer Z-Score:</span>
<span className={`font-bold ${tickerStats.isAnomaly ? 'text-purple-400' : 'text-slate-300'}`}>
Z = {tickerStats.zScore.toFixed(2)}
</span>
</div>
<div className="flex justify-between font-mono">
<span className="text-slate-400">Konzertiertes Cluster (14 Tage):</span>
<span className={`font-bold ${tickerStats.isCluster ? 'text-emerald-400' : 'text-slate-400'}`}>
{tickerStats.isCluster ? `JA (${tickerStats.clusterCount} Insiders)` : `NEIN (${tickerStats.clusterCount})`}
</span>
</div>
{tickerStats.isCluster && (
<div className="flex justify-between font-mono text-emerald-400">
<span>Cluster Exponent-Multiplikator:</span>
<span>x{tickerStats.multiplier.toFixed(2)}</span>
</div>
)}
<div className="flex justify-between font-mono border-t border-slate-800/80 pt-2 text-sm">
<span className="text-slate-300">Gekoppelte Rebound-Wahrsch.:</span>
<span className="text-purple-400 font-bold flex items-center gap-1">
<Percent className="w-3.5 h-3.5" />
{(tickerStats.coupledRebound * 100).toFixed(0)}%
</span>
</div>
</div>
</div>
{/* Volume chart */}
<div className="lg:col-span-2 h-44 w-full">
<div className="text-[10px] text-slate-400 mb-1 text-center font-mono font-semibold">Insider Handelsvolumen (24 Monate Baseline)</div>
<ResponsiveContainer width="100%" height="90%">
<BarChart data={volumeChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="month" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Bar dataKey="Volumen (Shares)" fill="#8b5cf6" radius={[3, 3, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Global Scan Outlier List */}
{scanResults && (
<div className="p-5 rounded-2xl border border-slate-800 bg-slate-950/20 mb-6 space-y-3 animate-fade-in">
<h3 className="text-sm font-bold text-slate-200 uppercase tracking-wider">Ergebnisse des Global Flow Scans</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{scanResults.map((res) => (
<div
key={res.ticker}
onClick={() => setSelectedTicker(res.ticker)}
className={`p-3 rounded-xl border cursor-pointer hover:bg-slate-900/60 transition-colors ${res.isAnomaly ? 'border-purple-500/40 bg-purple-500/5' : 'border-slate-850 bg-slate-900/40'}`}
>
<div className="flex justify-between items-center">
<span className="font-mono font-bold text-slate-200">{res.ticker}</span>
{res.isAnomaly && <span className="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse" />}
</div>
<div className="text-[10px] text-slate-400 mt-2 space-y-1">
<div>Z-Score: <span className="font-mono text-slate-300 font-bold">{res.zScore}</span></div>
<div>Cluster: <span className="font-mono text-slate-300">{res.clusterCount} Traders</span></div>
<div>Rebound: <span className="font-mono text-purple-400 font-bold">{Math.round(res.coupledRebound * 100)}%</span></div>
</div>
</div>
))}
</div>
</div>
)}
{/* SECTION 2: Smart Money Segment Tabs */}
<div className="space-y-4">
<div className="flex bg-slate-950/80 p-1 rounded-xl border border-slate-800/80 max-w-md">
<button
onClick={() => setActiveSegment('executives')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'executives' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<User className="w-3.5 h-3.5" /> Vorstände (Form 4)
</button>
<button
onClick={() => setActiveSegment('congress')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'congress' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<Landmark className="w-3.5 h-3.5" /> Kongress (Stock Act)
</button>
<button
onClick={() => setActiveSegment('whales')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'whales' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<Building2 className="w-3.5 h-3.5" /> Whales (13F Filings)
</button>
</div>
{/* Ledger displays */}
<div className="border border-slate-800 rounded-xl overflow-hidden bg-slate-950/40">
{activeSegment === 'executives' && (
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Insider Name</th>
<th className="p-3">Position</th>
<th className="p-3">Transaktion</th>
<th className="p-3 font-mono">Stücke</th>
<th className="p-3 text-right">Wert ($)</th>
</tr>
</thead>
<tbody>
{insiderTrades.map((t) => {
const isBuy = t.type === 'BUY';
return (
<tr key={t.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{t.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{t.insiderName}</td>
<td className="p-3 text-slate-400">{t.relation}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
</td>
<td className="p-3 font-mono text-slate-300">{t.shares.toLocaleString()}</td>
<td className={`p-3 font-mono text-right font-bold ${isBuy ? 'text-emerald-400' : 'text-rose-400'}`}>
${t.value.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{activeSegment === 'congress' && (
<div className="space-y-4">
<div className="p-3 bg-amber-500/10 border-b border-slate-800 text-xs text-amber-400 flex items-start gap-2.5">
<AlertTriangle className="w-5 h-5 shrink-0" />
<div>
<span className="font-bold">U.S. Congress Stock Act Offenlegungslags:</span> Abgeordnete müssen Käufe innerhalb von 30 bis 45 Tagen offenlegen. Der Alpha-Lag in der Tabelle visualisiert diese Meldeverzögerung. Käufe können verzögert eingepreist sein.
</div>
</div>
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Abgeordneter</th>
<th className="p-3">Kammer</th>
<th className="p-3">Transaktion</th>
<th className="p-3">Volumen-Spanne</th>
<th className="p-3 font-mono">Handelsdatum</th>
<th className="p-3 font-mono">Meldedatum</th>
<th className="p-3 text-right">Alpha-Lag (Tage)</th>
</tr>
</thead>
<tbody>
{congressTrades.map((c) => {
const isBuy = c.type === 'BUY';
return (
<tr key={c.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{c.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{c.representative}</td>
<td className="p-3 text-slate-400">{c.chamber}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
</td>
<td className="p-3 text-slate-300 font-mono">{c.valueRange}</td>
<td className="p-3 font-mono text-slate-400">{c.transactionDate}</td>
<td className="p-3 font-mono text-slate-400">{c.filingDate}</td>
<td className="p-3 font-mono text-right text-amber-400 font-bold">{c.lagDays} Tage</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{activeSegment === 'whales' && (
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Institution (13F Filers)</th>
<th className="p-3">Art</th>
<th className="p-3 font-mono">Gehandelte Anteile</th>
<th className="p-3 font-mono">Aktueller Bestand</th>
<th className="p-3 font-mono">Meldedatum</th>
<th className="p-3 text-right">Geschätzter Wert ($)</th>
</tr>
</thead>
<tbody>
{whaleTrades.map((w) => {
const isBuy = w.type === 'BUY' || w.type === 'NEW';
return (
<tr key={w.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{w.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{w.institution}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{w.type}
</span>
</td>
<td className="p-3 font-mono text-slate-300">{w.sharesTraded.toLocaleString()}</td>
<td className="p-3 font-mono text-slate-400">{w.sharesHeld.toLocaleString()}</td>
<td className="p-3 font-mono text-slate-400">{w.filingDate}</td>
<td className={`p-3 font-mono text-right font-bold ${isBuy ? 'text-emerald-400' : 'text-rose-400'}`}>
${w.estimatedValue.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
{/* SECTION 3: Mathematical LaTeX 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-purple-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">Mathematische Formulierung (Z-Score &amp; Bayesianische Kopplung)</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-bold text-purple-400 mb-1">1. Volumetrischer Z-Score (Statistische Signifikanz)</h4>
<p className="mb-2">
Der Z-Score gibt an, um wie viele Standardabweichungen das aktuelle Transaktionsvolumen <InlineMath math="X_t" /> vom historischen Durchschnitt <InlineMath math="\mu" /> abweicht:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="Z = \frac{X_t - \mu}{\sigma}" />
</div>
<p className="text-slate-400">
Ein Z-Score &gt; 2.0 wird als Ausreißer (Anomaly Trigger) eingestuft, was einer Wahrscheinlichkeit von unter 2.27% für einen zufälligen Anstieg entspricht (einseitiger Test bei Normalverteilung).
</p>
</div>
<div className="border-t border-slate-900 pt-3">
<h4 className="font-bold text-purple-400 mb-1">2. Bayesianische Kopplung (Rebound-Wahrscheinlichkeit)</h4>
<p className="mb-2">
Wir verknüpfen den Preisdrop-Sentiment (Element 2) mit den Insider-Z-Scores (Element 3), um die A-Posteriori-Wahrscheinlichkeit eines echten Rebounds zu ermitteln:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P(R|Z) = \frac{P(Z|R) \cdot P(R)}{P(Z|R) \cdot P(R) + P(Z|\neg R) \cdot (1 - P(R))}" />
</div>
<p className="text-slate-400">
wobei <InlineMath math="P(R)" /> die Prior-Wahrscheinlichkeit ist. Ist der Z-Score hoch (Käufe), steigt die Likelihood <InlineMath math="P(Z|R)" /> stark an und maximiert den Rebound-Erwartungswert.
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,815 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore, PortfolioHolding, Transaction } from '@/lib/store';
import { calculateEWMA, calculateKellyFraction, calculateAssetCovariance } 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 {
TrendingUp, Wallet, ArrowDownRight, ArrowUpRight, Percent, Plus, FolderSync,
HelpCircle, Settings, Calendar, DollarSign, Tag, Check, AlertCircle, ChevronDown, ChevronUp, Sparkles
} from 'lucide-react';
export default function SandboxDemo() {
const {
portfolios,
activePortfolioId,
ewmaLambda,
createPortfolio,
setActivePortfolio,
executeTransaction,
setEwmaLambda,
scannerAlerts,
posteriorProbability
} = useSandboxStore();
// Selected portfolio
const activePortfolio = useMemo(() => {
return portfolios.find(p => p.id === activePortfolioId) || portfolios[0];
}, [portfolios, activePortfolioId]);
const [mounted, setMounted] = useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl min-h-[400px] flex items-center justify-center">
<div className="text-slate-400 text-sm font-mono animate-pulse">Lade Sandbox-Modul...</div>
</div>
);
}
// UI state
const [showNewPortfolioModal, setShowNewPortfolioModal] = useState(false);
const [newPortfolioName, setNewPortfolioName] = useState('');
const [newStartingBalance, setNewStartingBalance] = useState(50000);
const [tradeSymbol, setTradeSymbol] = useState('AAPL');
const [tradeWknOrIsin, setTradeWknOrIsin] = useState('865985');
const [tradeShares, setTradeShares] = useState(10);
const [tradePrice, setTradePrice] = useState(182);
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY');
const [simulateFees, setSimulateFees] = useState(true);
const [isBackfill, setIsBackfill] = useState(false);
const [backfillDate, setBackfillDate] = useState('2026-05-20');
const [hypothesisTag, setHypothesisTag] = useState('Fokus auf KI-Infrastruktur');
const [orderError, setOrderError] = useState<string | null>(null);
const [orderSuccess, setOrderSuccess] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
const [showMsciBenchmark, setShowMsciBenchmark] = useState(true);
// Kelly Position Sizing states
const [kellySource, setKellySource] = useState<'scanner' | 'crypto' | 'econometric' | 'custom'>('custom');
const [customProb, setCustomProb] = useState<number>(0.60);
const [oddsRatio, setOddsRatio] = useState<number>(1.5);
// Compute Net Worth
const netWorth = useMemo(() => {
const assetsVal = activePortfolio.holdings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0);
return Math.round((activePortfolio.cash + assetsVal) * 100) / 100;
}, [activePortfolio]);
// Dynamic winning probability (p) based on selected source
const kellyProbability = useMemo(() => {
if (kellySource === 'scanner') {
const alert = scannerAlerts.find(a => a.ticker.toUpperCase() === tradeSymbol.toUpperCase());
return alert ? alert.overreactionScore / 100 : 0.52;
}
if (kellySource === 'crypto') {
return posteriorProbability; // e.g. 0.72
}
if (kellySource === 'econometric') {
return 0.65; // ROC target probability
}
return customProb;
}, [kellySource, customProb, tradeSymbol, scannerAlerts, posteriorProbability]);
// Check potential cluster risk for the input symbol
const potentialClusterRisk = useMemo(() => {
if (!tradeSymbol) return false;
const holdingsWithWeights = activePortfolio.holdings.map(h => ({
symbol: h.symbol,
weight: (h.shares * h.currentPrice) / (netWorth || 1.0)
}));
const covResult = calculateAssetCovariance(holdingsWithWeights, tradeSymbol);
return covResult.clusterRisk;
}, [activePortfolio.holdings, tradeSymbol, netWorth]);
// Compute Kelly fraction and recommended cash amount
const kellyFraction = useMemo(() => {
const rawKelly = calculateKellyFraction(kellyProbability, oddsRatio);
// Cap at Half-Kelly already done in calculateKellyFraction, but we can scale by 50% if there is cluster risk
return potentialClusterRisk ? rawKelly * 0.5 : rawKelly;
}, [kellyProbability, oddsRatio, potentialClusterRisk]);
const recommendedKellyCash = useMemo(() => {
return activePortfolio.cash * kellyFraction;
}, [activePortfolio.cash, kellyFraction]);
// Compute returns based on active portfolio's historical value series
const portfolioReturns = useMemo(() => {
const vals = activePortfolio.historicalValues;
if (vals.length < 2) return [];
const r: number[] = [];
for (let i = 1; i < vals.length; i++) {
r.push((vals[i].value - vals[i - 1].value) / vals[i - 1].value);
}
return r;
}, [activePortfolio.historicalValues]);
// Calculate EWMA Volatility live
const ewmaResult = useMemo(() => {
return calculateEWMA(portfolioReturns, ewmaLambda);
}, [portfolioReturns, ewmaLambda]);
// Combine data for charting
const chartData = useMemo(() => {
const vals = activePortfolio.historicalValues;
if (vals.length === 0) return [];
// Normalize MSCI World index from the same starting value of the portfolio
const baseValue = vals[0].value;
let msciVal = baseValue;
return vals.map((hv, idx) => {
// Deterministic pseudo-random walk for MSCI World
if (idx > 0) {
const rand = Math.sin(idx * 57.8) * 0.45 + 0.05; // range: -0.4% to +0.5% return
msciVal = msciVal * (1 + rand * 0.015);
}
const vol = ewmaResult.series[idx - 1] || 0;
return {
date: hv.date,
Portfolio: hv.value,
'MSCI World (Benchmark)': Math.round(msciVal),
'EWMA Vol (%)': parseFloat(vol.toFixed(2)),
};
});
}, [activePortfolio.historicalValues, ewmaResult]);
// Total gain/loss
const totalGainLoss = netWorth - activePortfolio.startingBalance;
const totalGainLossPct = (totalGainLoss / activePortfolio.startingBalance) * 100;
const isPositiveOverall = totalGainLoss >= 0;
const handleCreatePortfolio = (e: React.FormEvent) => {
e.preventDefault();
if (!newPortfolioName.trim()) return;
createPortfolio(newPortfolioName, newStartingBalance);
setNewPortfolioName('');
setShowNewPortfolioModal(false);
};
const handleTransactionSubmit = (e: React.FormEvent) => {
e.preventDefault();
setOrderError(null);
setOrderSuccess(false);
if (tradeShares <= 0 || tradePrice <= 0) {
setOrderError('Bitte geben Sie eine gültige Stückzahl und einen Kurs an.');
return;
}
const ok = executeTransaction(
activePortfolio.id,
tradeSymbol,
tradeWknOrIsin,
tradeType,
tradeShares,
tradePrice,
simulateFees,
isBackfill,
backfillDate,
hypothesisTag
);
if (ok) {
setOrderSuccess(true);
setTimeout(() => setOrderSuccess(false), 3000);
} else {
setOrderError(
tradeType === 'BUY'
? 'Unzureichendes Barguthaben (inklusive allfälliger Transaktionsgebühren).'
: 'Unzureichende Anteile im Depot für den Verkauf.'
);
}
};
return (
<div className="space-y-6">
{activePortfolio.riskProfile?.status === 'RED' && (
<div className="bg-rose-950/40 border border-rose-800/80 text-rose-400 text-xs rounded-xl p-4 flex items-center gap-3 shadow-[0_0_15px_rgba(244,63,94,0.15)] animate-pulse">
<AlertCircle className="w-5 h-5 text-rose-400 shrink-0" />
<div className="flex-1">
<span className="font-bold">Kritische Klumpenrisiken (Kovarianz RED):</span> {activePortfolio.riskProfile.message}
</div>
</div>
)}
{/* SECTION 1: Portfolio Selector & Stats Bar */}
<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-emerald-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div className="space-y-1">
<span className="text-emerald-400 text-xs font-semibold uppercase tracking-wider">Strategic Sandbox</span>
<div className="flex items-center gap-3">
<FolderSync className="text-emerald-400 w-6 h-6" />
<select
value={activePortfolioId}
onChange={(e) => setActivePortfolio(e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-slate-100 font-sans font-semibold text-lg focus:outline-none focus:border-emerald-500 cursor-pointer"
>
{portfolios.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<button
onClick={() => setShowNewPortfolioModal(true)}
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-emerald-400 hover:text-emerald-300 transition-colors border border-slate-700"
title="Neues Sandbox-Portfolio erstellen"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<div className="flex flex-wrap gap-4 w-full md:w-auto">
{/* Net Worth Card */}
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[140px]">
<div className="text-[10px] text-slate-400 uppercase font-semibold">Gesamtwert</div>
<div className="font-mono text-xl font-bold text-slate-100 mt-1">
${netWorth.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
{/* Performance Card */}
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[140px]">
<div className="text-[10px] text-slate-400 uppercase font-semibold">GuV (Gesamt)</div>
<div className={`font-mono text-xl font-bold mt-1 flex items-center gap-1 ${isPositiveOverall ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositiveOverall ? <ArrowUpRight className="w-5 h-5" /> : <ArrowDownRight className="w-5 h-5" />}
<span>{isPositiveOverall ? '+' : ''}{totalGainLossPct.toFixed(2)}%</span>
</div>
</div>
{/* Live EWMA Vol Card */}
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[140px] relative group">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<span>EWMA Volatilität</span>
<span className="cursor-help flex items-center" title="Annualisierte Schwankungsbreite basierend auf historischen Renditen.">
<HelpCircle className="w-3.5 h-3.5 text-slate-500 group-hover:text-emerald-400 transition-colors" />
</span>
</div>
<div className="font-mono text-xl font-bold text-teal-400 mt-1">
{ewmaResult.latest.toFixed(2)}%
</div>
</div>
{/* Covariance Risk Traffic Light Card */}
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[150px] relative group">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<span>Kovarianz-Ampel</span>
<span className="cursor-help flex items-center" title="Systemische Portfolio-Klumpenrisiken basierend auf historischen Asset-Kovarianzen.">
<HelpCircle className="w-3.5 h-3.5 text-slate-500 group-hover:text-rose-400 transition-colors" />
</span>
</div>
<div className="flex items-center gap-2 mt-1.5">
<span className={`w-3.5 h-3.5 rounded-full ${
activePortfolio.riskProfile?.status === 'RED' ? 'bg-rose-500 animate-pulse shadow-[0_0_8px_#f43f5e]' :
activePortfolio.riskProfile?.status === 'YELLOW' ? 'bg-amber-500 shadow-[0_0_8px_#f59e0b]' :
'bg-emerald-500 shadow-[0_0_8px_#10b981]'
}`} />
<span className={`font-mono text-sm font-bold ${
activePortfolio.riskProfile?.status === 'RED' ? 'text-rose-400' :
activePortfolio.riskProfile?.status === 'YELLOW' ? 'text-amber-400' :
'text-emerald-400'
}`}>
{activePortfolio.riskProfile?.status || 'GREEN'} RISK
</span>
</div>
</div>
</div>
</div>
{/* Modal for creating a new Portfolio */}
{showNewPortfolioModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 max-w-sm w-full space-y-4 shadow-2xl">
<h3 className="text-lg font-bold text-white">Neues Sandbox-Portfolio</h3>
<form onSubmit={handleCreatePortfolio} className="space-y-4">
<div>
<label className="text-xs text-slate-400 block mb-1">Portfolio Name</label>
<input
type="text"
required
placeholder="z.B. Biotech Risk High Yield"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-slate-100 focus:outline-none focus:border-emerald-500"
value={newPortfolioName}
onChange={(e) => setNewPortfolioName(e.target.value)}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Startkapital ($)</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-slate-100 focus:outline-none focus:border-emerald-500 font-mono"
value={newStartingBalance}
onChange={(e) => setNewStartingBalance(Number(e.target.value))}
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setShowNewPortfolioModal(false)}
className="flex-1 bg-slate-850 hover:bg-slate-800 text-slate-300 font-semibold py-2 rounded-lg transition-colors border border-slate-700"
>
Abbrechen
</button>
<button
type="submit"
className="flex-1 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-slate-950 font-bold py-2 rounded-lg transition-all shadow-lg shadow-emerald-500/20"
>
Erstellen
</button>
</div>
</form>
</div>
</div>
)}
{/* Accordion / Math Button */}
<div className="border-t border-slate-850 pt-4 mt-4">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-emerald-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">Mathematische Spezifikation & EWMA-Volatilitätsmodell</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-semibold text-emerald-400 mb-1.5">1. EWMA Volatilitätsmodell</h4>
<p className="mb-2">
Die Volatilit&auml;t wird mittels des <strong>Exponentially Weighted Moving Average (EWMA)</strong>-Modells ermittelt. J&uuml;ngere Renditen erhalten hierbei ein h&ouml;heres Gewicht als weiter in der Vergangenheit liegende Renditen, gesteuert durch den Zerfallsparameter <InlineMath math="\lambda" /> (Lambda).
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\sigma_t^2 = \lambda \sigma_{t-1}^2 + (1 - \lambda) r_{t-1}^2" />
</div>
<p className="mb-2">
Die t&auml;gliche Volatilit&auml;t <InlineMath math="\sigma_t" /> wird auf ein ganzes Jahr hochgerechnet (Annualisierung) unter der Annahme von 252 Handelstagen:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\sigma_{\text{ann}} = \sqrt{\sigma_t^2 \times 252}" />
</div>
<p className="text-slate-400">
RiskMetrics empfiehlt f&uuml;r t&auml;gliche Finanzdaten einen Lambda-Wert von <InlineMath math="\lambda = 0.94" />.
</p>
</div>
<div className="border-t border-slate-800 pt-3">
<h4 className="font-semibold text-emerald-400 mb-1.5">2. Kelly-Kriterium zur Positionsgrößenbestimmung</h4>
<p className="mb-2">
Die Kelly-Formel bestimmt den optimalen Anteil des Kapitals (<InlineMath math="f^*" />), der in ein Geschäft investiert werden soll, um das exponentielle Wachstum des Kapitals zu maximieren:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="f^* = \frac{p \cdot b - q}{b} = \frac{p \cdot b - (1 - p)}{b}" />
</div>
<p className="mb-2">
Um Risiken durch ungenaue Schätzungen zu verringern, wenden wir das konservative <strong>Half-Kelly</strong>-Sizing an und begrenzen das Ergebnis auf <InlineMath math="0.5 \times f^*" /> (zusätzlich begrenzt auf <InlineMath math="\ge 0" />).
</p>
</div>
<div className="border-t border-slate-800 pt-3">
<h4 className="font-semibold text-rose-400 mb-1.5">3. Covariance & Cluster Risk (Kovarianz-Ampel)</h4>
<p className="mb-2">
Die Kovarianz zwischen Assets wird durch Multiplikation ihrer paarweisen Korrelation mit ihren jeweiligen Standardabweichungen (Volatilitäten) bestimmt:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\text{Cov}(A, B) = \text{Corr}(A, B) \times \sigma_A \times \sigma_B" />
</div>
<p className="text-slate-400">
Ein <strong>Klumpenrisiko (Risk RED)</strong> wird ausgelöst, wenn ein Asset eine Korrelation <InlineMath math="\text{Corr}(A, B) > 0.70" /> zu bestehenden Positionen aufweist und diese Positionen jeweils mehr als 15% des Portfolios ausmachen.
</p>
</div>
</div>
)}
</div>
</div>
{/* SECTION 2: Chart / Analytics & Order Form */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Analytics Performance Plot */}
<div className="xl:col-span-2 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-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<TrendingUp className="text-emerald-400 w-5 h-5" /> Portfolio Wertentwicklung & Benchmark
</h3>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-xs text-slate-400 cursor-pointer">
<input
type="checkbox"
checked={showMsciBenchmark}
onChange={(e) => setShowMsciBenchmark(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4"
/>
<span>MSCI World (Benchmark) anzeigen</span>
</label>
</div>
</div>
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="date" stroke="#64748b" fontSize={10} />
<YAxis stroke="#64748b" fontSize={10} domain={['auto', 'auto']} tickFormatter={(v) => `$${v.toLocaleString()}`} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Legend verticalAlign="top" height={36} />
<Line type="monotone" dataKey="Portfolio" name={activePortfolio.name} stroke="#10b981" strokeWidth={3} dot={false} activeDot={{ r: 6 }} />
{showMsciBenchmark && (
<Line type="monotone" dataKey="MSCI World (Benchmark)" name="MSCI World Index (Normiert)" stroke="#3b82f6" strokeWidth={2} strokeDasharray="4 4" dot={false} />
)}
</LineChart>
</ResponsiveContainer>
</div>
{/* EWMA parameter tuner slider */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/20 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-200">Parameteranpassung EWMA Lambda (&lambda;)</h4>
<p className="text-xs text-slate-400">Zerfallsfaktor steuert die Schock-Sensitivität des Volatilitätsmodells.</p>
</div>
<div className="flex items-center gap-4 w-full sm:w-auto">
<input
type="range"
min="0.80"
max="0.99"
step="0.01"
value={ewmaLambda}
onChange={(e) => setEwmaLambda(parseFloat(e.target.value))}
className="w-40 accent-emerald-400 cursor-pointer"
/>
<span className="font-mono text-emerald-400 text-sm font-bold bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">&lambda; = {ewmaLambda.toFixed(2)}</span>
</div>
</div>
</div>
{/* Right 1 Column: Advanced Order Mask */}
<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 flex flex-col justify-between">
<div className="space-y-6">
<div className="border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Settings className="text-emerald-400 w-5 h-5" /> Order-Maske (Simuliert)
</h3>
</div>
<form onSubmit={handleTransactionSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Ticker / Symbol</label>
<input
type="text"
required
placeholder="AAPL"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeSymbol}
onChange={(e) => setTradeSymbol(e.target.value.toUpperCase())}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">WKN / ISIN</label>
<input
type="text"
placeholder="865985"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeWknOrIsin}
onChange={(e) => setTradeWknOrIsin(e.target.value)}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Stücke</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeShares}
onChange={(e) => setTradeShares(Number(e.target.value))}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Kurs ($)</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradePrice}
onChange={(e) => setTradePrice(Number(e.target.value))}
/>
</div>
</div>
{/* Order direction buttons */}
<div className="flex gap-2 p-1 rounded-xl bg-slate-950 border border-slate-800">
<button
type="button"
onClick={() => setTradeType('BUY')}
className={`flex-1 py-1.5 text-xs font-bold rounded-lg transition-all ${tradeType === 'BUY' ? 'bg-emerald-500 text-slate-950 shadow-md shadow-emerald-500/10' : 'text-slate-400 hover:text-slate-200'}`}
>
Kauf (Long)
</button>
<button
type="button"
onClick={() => setTradeType('SELL')}
className={`flex-1 py-1.5 text-xs font-bold rounded-lg transition-all ${tradeType === 'SELL' ? 'bg-rose-500 text-white shadow-md shadow-rose-500/10' : 'text-slate-400 hover:text-slate-200'}`}
>
Verkauf (Short)
</button>
</div>
{/* Kelly Sizing Risk Recommendation Widget */}
{tradeType === 'BUY' && (
<div className="bg-slate-950/60 border border-slate-850 rounded-xl p-3 text-[11px] space-y-2 relative overflow-hidden">
<div className="flex justify-between items-center">
<span className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<Sparkles className="w-3 h-3 text-emerald-400" /> Risk-Engine Sizing
</span>
<span className="font-bold text-emerald-400 text-[10px] uppercase tracking-wider">Kelly recommendation</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] text-slate-500 block mb-0.5 font-semibold uppercase">Prob Source (p)</label>
<select
value={kellySource}
onChange={(e) => setKellySource(e.target.value as any)}
className="w-full bg-slate-900 border border-slate-800 rounded px-1.5 py-1 text-[10px] text-slate-200 focus:outline-none"
>
<option value="custom">Manueller Regler</option>
<option value="scanner">El. 2 Scanner-Score</option>
<option value="crypto">El. 4 Bayes Posterior</option>
<option value="econometric">El. 5 ROC Breakout</option>
</select>
</div>
<div>
<label className="text-[9px] text-slate-500 block mb-0.5 font-semibold uppercase">Odds-Verhältnis (b)</label>
<input
type="number"
step="0.1"
min="0.1"
value={oddsRatio}
onChange={(e) => setOddsRatio(Number(e.target.value))}
className="w-full bg-slate-900 border border-slate-800 rounded px-1.5 py-1 text-[10px] text-slate-200 font-mono focus:outline-none"
/>
</div>
</div>
{kellySource === 'custom' && (
<div className="space-y-1">
<div className="flex justify-between text-[9px] text-slate-500">
<span>Erfolgswahrscheinlichkeit:</span>
<span className="font-mono text-emerald-400 font-bold">{(kellyProbability * 100).toFixed(0)}%</span>
</div>
<input
type="range"
min="0.1"
max="0.99"
step="0.01"
value={customProb}
onChange={(e) => setCustomProb(Number(e.target.value))}
className="w-full accent-emerald-500 h-1 bg-slate-900 rounded"
/>
</div>
)}
{kellySource !== 'custom' && (
<div className="text-[9px] text-slate-400 flex justify-between bg-slate-900/40 p-1 px-1.5 rounded border border-slate-900">
<span>System-Wahrscheinlichkeit:</span>
<span className="font-mono text-emerald-400 font-bold">{(kellyProbability * 100).toFixed(1)}%</span>
</div>
)}
{potentialClusterRisk && (
<div className="p-2 bg-rose-500/10 text-rose-400 border border-rose-500/20 text-[9px] rounded flex items-start gap-1">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-rose-400" />
<div>
<strong>Klumpenrisiko!</strong> Korrelation &gt; 0.70 zu bestehenden Positionen. Kelly-Empfehlung wurde um 50% halbiert.
</div>
</div>
)}
<div className="bg-slate-900/80 p-2 rounded-lg border border-slate-850 flex justify-between items-center text-xs">
<div>
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Kelly-Anteil:</span>
<span className="font-mono font-bold text-slate-200">{(kellyFraction * 100).toFixed(1)}% des Cashs</span>
</div>
<div className="text-right">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Kaufvolumen:</span>
<span className="font-mono font-bold text-emerald-400">${Math.round(recommendedKellyCash).toLocaleString()}</span>
</div>
</div>
</div>
)}
{/* Hypothesis input */}
<div>
<label className="text-xs text-slate-400 block mb-1 flex items-center gap-1">
<Tag className="w-3 h-3 text-emerald-400" />
<span>Hypothese / What-if Notiz</span>
</label>
<input
type="text"
placeholder="z.B. Ferrari E-Auto Skepsis"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 text-xs focus:outline-none focus:border-emerald-500"
value={hypothesisTag}
onChange={(e) => setHypothesisTag(e.target.value)}
/>
</div>
{/* Fees Toggle */}
<div className="flex items-center justify-between border-t border-slate-850 pt-3 text-xs">
<span className="text-slate-400 flex items-center gap-1">
<DollarSign className="w-3.5 h-3.5 text-slate-500" /> Ordergebühren simulieren
</span>
<input
type="checkbox"
checked={simulateFees}
onChange={(e) => setSimulateFees(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer"
/>
</div>
{/* Backfill Date Picker Toggle */}
<div className="space-y-2 border-t border-slate-850 pt-3 text-xs">
<div className="flex items-center justify-between">
<span className="text-slate-400 flex items-center gap-1">
<Calendar className="w-3.5 h-3.5 text-slate-500" /> Historischer Backfill
</span>
<input
type="checkbox"
checked={isBackfill}
onChange={(e) => setIsBackfill(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer"
/>
</div>
{isBackfill && (
<input
type="date"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 text-xs focus:outline-none focus:border-emerald-500 font-mono"
value={backfillDate}
onChange={(e) => setBackfillDate(e.target.value)}
/>
)}
</div>
{orderError && (
<div className="p-3 rounded-lg bg-rose-500/10 text-rose-400 border border-rose-500/20 text-xs flex items-center gap-2">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{orderError}</span>
</div>
)}
{orderSuccess && (
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs flex items-center gap-2">
<Check className="w-4 h-4 shrink-0" />
<span>Transaktion erfolgreich gebucht!</span>
</div>
)}
<button
type="submit"
className={`w-full font-bold py-2.5 px-4 rounded-lg transition-all active:scale-[0.98] mt-2 shadow-lg ${tradeType === 'BUY' ? 'bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-slate-950 shadow-emerald-500/10' : 'bg-gradient-to-r from-rose-500 to-pink-500 hover:from-rose-600 hover:to-pink-600 text-white shadow-rose-500/10'}`}
>
Order an den Markt senden
</button>
</form>
</div>
</div>
</div>
{/* SECTION 3: Holdings Table & Transactions Log */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Holdings List */}
<div className="xl:col-span-2 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 justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<TrendingUp className="text-emerald-400 w-5 h-5" /> Depotbestände ({activePortfolio.holdings.length})
</h3>
<div className="text-xs text-slate-400 flex items-center gap-4">
<span>Barguthaben: <span className="font-mono text-emerald-400 font-bold">${activePortfolio.cash.toLocaleString()}</span></span>
</div>
</div>
<div className="overflow-x-auto border border-slate-850 rounded-xl bg-slate-950/40">
<table className="w-full border-collapse text-left text-sm">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Asset</th>
<th className="p-3">Stücke</th>
<th className="p-3">Einstand</th>
<th className="p-3">Kurs</th>
<th className="p-3">Hypothese</th>
<th className="p-3 text-right">GuV</th>
</tr>
</thead>
<tbody>
{activePortfolio.holdings.length === 0 ? (
<tr>
<td colSpan={6} className="p-8 text-center text-slate-500">
Keine Bestände in diesem Sandbox-Portfolio. Nutzen Sie die Order-Maske, um Werte hinzuzufügen.
</td>
</tr>
) : (
activePortfolio.holdings.map((hold) => {
const profitLoss = (hold.currentPrice - hold.avgPrice) * hold.shares;
const isPositive = profitLoss >= 0;
return (
<tr key={hold.symbol} className="border-b border-slate-850/50 hover:bg-slate-850/20 transition-colors">
<td className="p-3">
<div className="font-bold text-teal-400 font-mono">{hold.symbol}</div>
{hold.wknOrIsin && <div className="text-[10px] text-slate-500 font-mono">WKN: {hold.wknOrIsin}</div>}
</td>
<td className="p-3 font-mono font-medium">{hold.shares}</td>
<td className="p-3 font-mono text-slate-300">${hold.avgPrice.toFixed(2)}</td>
<td className="p-3 font-mono text-slate-300">${hold.currentPrice.toFixed(2)}</td>
<td className="p-3 text-slate-400 max-w-[200px] truncate text-xs" title={hold.hypothesisTag}>
{hold.hypothesisTag || '-'}
</td>
<td className={`p-3 font-mono text-right flex items-center justify-end gap-1 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
${Math.abs(profitLoss).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Right 1 Column: Transactions History */}
<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="border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Calendar className="text-emerald-400 w-5 h-5" /> Letzte Orderbuch-Einträge
</h3>
</div>
<div className="max-h-60 overflow-y-auto space-y-2 pr-1">
{activePortfolio.transactions.length === 0 ? (
<p className="text-xs text-slate-500 text-center py-8">Bislang keine Transaktionen in diesem Portfolio.</p>
) : (
activePortfolio.transactions.map((tx) => {
const isBuy = tx.type === 'BUY';
return (
<div key={tx.id} className="p-3 bg-slate-950/40 border border-slate-850 rounded-lg space-y-1.5">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
<span className="font-mono font-bold text-slate-200">{tx.symbol}</span>
</div>
<span className="text-[10px] text-slate-500 font-mono">{tx.timestamp}</span>
</div>
<div className="flex justify-between text-xs font-mono text-slate-400">
<span>{tx.shares} Stk @ ${tx.price.toFixed(2)}</span>
<span className="text-[10px] text-slate-500">Gebühr: ${tx.feeApplied.toFixed(2)}</span>
</div>
{tx.hypothesisTag && (
<div className="text-[10px] text-slate-500 flex items-center gap-1 border-t border-slate-900 pt-1">
<Tag className="w-2.5 h-2.5 text-teal-500" />
<span className="italic truncate max-w-[220px]">{tx.hypothesisTag}</span>
</div>
)}
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
}

View 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 &gt; 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">
&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>
);
})}
</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 &amp; 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>
);
}