feat: complete core 5 elements and risk layer architecture
This commit is contained in:
487
components/modules/crypto/CryptoDemo.tsx
Normal file
487
components/modules/crypto/CryptoDemo.tsx
Normal 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 & 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 & 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 & 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 (α):</div>
|
||||
<div className="text-emerald-400 font-bold">{alphaSuccess}</div>
|
||||
<div className="text-slate-400">Fehlalarme (β):</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 & 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. „Funding-Rate < -0.04%“ und „Open Interest > 10%“) 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>
|
||||
);
|
||||
}
|
||||
980
components/modules/events/EventsDemo.tsx
Normal file
980
components/modules/events/EventsDemo.tsx
Normal 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 > +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 ≥ {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 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 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>
|
||||
);
|
||||
}
|
||||
448
components/modules/insider/InsiderDemo.tsx
Normal file
448
components/modules/insider/InsiderDemo.tsx
Normal 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 > 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 & 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 > 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>
|
||||
);
|
||||
}
|
||||
815
components/modules/sandbox/SandboxDemo.tsx
Normal file
815
components/modules/sandbox/SandboxDemo.tsx
Normal 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ät wird mittels des <strong>Exponentially Weighted Moving Average (EWMA)</strong>-Modells ermittelt. Jüngere Renditen erhalten hierbei ein hö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ägliche Volatilitä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ür tä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 (λ)</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">λ = {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 > 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>
|
||||
);
|
||||
}
|
||||
538
components/modules/scanner/ScannerDemo.tsx
Normal file
538
components/modules/scanner/ScannerDemo.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useSandboxStore, ScannerAlert, WatchlistItem } from '@/lib/store';
|
||||
import { calculateGJRGARCH } from '@/lib/math/statistics';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { BlockMath, InlineMath } from 'react-katex';
|
||||
import {
|
||||
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
||||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play
|
||||
} from 'lucide-react';
|
||||
|
||||
// Predefined mock database for deep-check searches
|
||||
interface SearchResult {
|
||||
ticker: string;
|
||||
name: string;
|
||||
priceChange: number;
|
||||
sentiment: 'GREEN' | 'YELLOW' | 'RED';
|
||||
whyDropped: string;
|
||||
gjrGarchVol: number;
|
||||
reboundScore: number;
|
||||
returns: number[];
|
||||
}
|
||||
|
||||
const mockSearchDatabase: Record<string, SearchResult> = {
|
||||
'RACE': {
|
||||
ticker: 'RACE',
|
||||
name: 'Ferrari N.V.',
|
||||
priceChange: -0.065,
|
||||
sentiment: 'GREEN',
|
||||
whyDropped: 'Emotionaler Abverkauf nach viralem Video von Cristiano Ronaldo, der sich über Autoprobleme beschwert. Keine fundamentalen Schäden.',
|
||||
gjrGarchVol: 0.048,
|
||||
reboundScore: 88,
|
||||
returns: [0.01, -0.005, 0.012, -0.008, 0.003, -0.065]
|
||||
},
|
||||
'KO': {
|
||||
ticker: 'KO',
|
||||
name: 'The Coca-Cola Co.',
|
||||
priceChange: -0.052,
|
||||
sentiment: 'GREEN',
|
||||
whyDropped: 'Berühmter Influencer entfernt Coca-Cola Flasche während Pressekonferenz. Reine Social-Media-Hype Reaktion.',
|
||||
gjrGarchVol: 0.021,
|
||||
reboundScore: 82,
|
||||
returns: [0.002, 0.005, -0.003, 0.001, -0.002, -0.052]
|
||||
},
|
||||
'TSLA': {
|
||||
ticker: 'TSLA',
|
||||
name: 'Tesla Inc.',
|
||||
priceChange: -0.084,
|
||||
sentiment: 'YELLOW',
|
||||
whyDropped: 'Auslieferungszahlen leicht unter Analystenschätzungen. Margenentwicklung bleibt jedoch stabil.',
|
||||
gjrGarchVol: 0.062,
|
||||
reboundScore: 65,
|
||||
returns: [-0.012, 0.008, -0.025, 0.015, -0.005, -0.084]
|
||||
},
|
||||
'SMCI': {
|
||||
ticker: 'SMCI',
|
||||
name: 'Super Micro Computer',
|
||||
priceChange: -0.124,
|
||||
sentiment: 'RED',
|
||||
whyDropped: 'Hindenburg Research Short-Seller-Report bezüglich mutmaßlicher Bilanzmanipulationen veröffentlicht.',
|
||||
gjrGarchVol: 0.085,
|
||||
reboundScore: 24,
|
||||
returns: [0.035, -0.018, 0.042, -0.051, 0.012, -0.124]
|
||||
},
|
||||
'BA': {
|
||||
ticker: 'BA',
|
||||
name: 'Boeing Co.',
|
||||
priceChange: -0.071,
|
||||
sentiment: 'RED',
|
||||
whyDropped: 'FAA verhängt vorübergehendes Flugverbot nach erneutem technischen Zwischenfall mit Rumpftür.',
|
||||
gjrGarchVol: 0.058,
|
||||
reboundScore: 18,
|
||||
returns: [-0.005, -0.012, 0.005, -0.021, -0.008, -0.071]
|
||||
},
|
||||
'NFLX': {
|
||||
ticker: 'NFLX',
|
||||
name: 'Netflix Inc.',
|
||||
priceChange: -0.058,
|
||||
sentiment: 'GREEN',
|
||||
whyDropped: 'Gerüchte über angebliche Preissenkungen in Schwellenländern belasten Kurs kurzfristig.',
|
||||
gjrGarchVol: 0.038,
|
||||
reboundScore: 78,
|
||||
returns: [0.015, -0.002, 0.008, 0.005, -0.011, -0.058]
|
||||
}
|
||||
};
|
||||
|
||||
export default function ScannerDemo() {
|
||||
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick } = useSandboxStore();
|
||||
|
||||
// Component local states
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanProgress, setScanProgress] = useState('');
|
||||
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([
|
||||
{ id: 'sa1', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
|
||||
{ id: 'sa2', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
|
||||
]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
|
||||
const [searchError, setSearchError] = useState(false);
|
||||
|
||||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||||
|
||||
// Run market scan simulator
|
||||
const handleMarketScan = () => {
|
||||
setScanning(true);
|
||||
setScanProgress('Verbinde mit Börsenfeeds...');
|
||||
|
||||
setTimeout(() => {
|
||||
setScanProgress('Berechne historische Volatilitätsmatrizen...');
|
||||
setTimeout(() => {
|
||||
setScanProgress('Filtere abnormale Abweichungen (Asset > -5%, Index stabil)...');
|
||||
setTimeout(() => {
|
||||
// Scan isolated anomalies
|
||||
setActiveAlerts([
|
||||
{ id: 'sa3', ticker: 'RACE', priceChange: -0.065, gjrGarchVol: 0.048, overreactionScore: 88, status: 'UNDEREVALUATED' },
|
||||
{ id: 'sa4', ticker: 'KO', priceChange: -0.052, gjrGarchVol: 0.021, overreactionScore: 82, status: 'UNDEREVALUATED' },
|
||||
{ id: 'sa5', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
|
||||
{ id: 'sa6', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
|
||||
{ id: 'sa7', ticker: 'BA', priceChange: -0.071, gjrGarchVol: 0.058, overreactionScore: 18, status: 'OVERVALUATED' },
|
||||
]);
|
||||
setScanning(false);
|
||||
setScanProgress('');
|
||||
}, 600);
|
||||
}, 500);
|
||||
}, 400);
|
||||
};
|
||||
|
||||
// Perform a manual deep check
|
||||
const handleDeepCheck = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSearchError(false);
|
||||
setSearchResult(null);
|
||||
const query = searchQuery.trim().toUpperCase();
|
||||
if (!query) return;
|
||||
|
||||
if (mockSearchDatabase[query]) {
|
||||
setSearchResult(mockSearchDatabase[query]);
|
||||
} else {
|
||||
// Simulate dynamic result for unknown assets
|
||||
const simulatedVol = 0.03 + Math.random() * 0.04;
|
||||
const simulatedScore = Math.floor(40 + Math.random() * 50);
|
||||
const isNegative = Math.random() > 0.4;
|
||||
const simulatedChange = -0.05 - Math.random() * 0.06;
|
||||
|
||||
const res: SearchResult = {
|
||||
ticker: query,
|
||||
name: `${query} Corp.`,
|
||||
priceChange: simulatedChange,
|
||||
sentiment: isNegative ? (simulatedScore > 75 ? 'GREEN' : 'YELLOW') : 'RED',
|
||||
whyDropped: 'Simulierte Marktabweichung basierend auf automatischem Sentiment-Scanning der Finanzberichte.',
|
||||
gjrGarchVol: simulatedVol,
|
||||
reboundScore: simulatedScore,
|
||||
returns: [0.005, -0.008, 0.012, -0.015, 0.004, simulatedChange]
|
||||
};
|
||||
setSearchResult(res);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToWatchlist = (ticker: string, priceChange: number, sentiment: 'GREEN' | 'YELLOW' | 'RED', whyDropped: string) => {
|
||||
// Determine a mock initial price based on ticker
|
||||
let initialPrice = 150;
|
||||
if (ticker === 'RACE') initialPrice = 380;
|
||||
if (ticker === 'KO') initialPrice = 60;
|
||||
if (ticker === 'TSLA') initialPrice = 175;
|
||||
if (ticker === 'NFLX') initialPrice = 610;
|
||||
|
||||
addToWatchlist({
|
||||
ticker,
|
||||
priceChange,
|
||||
sentiment,
|
||||
whyDropped,
|
||||
initialPrice,
|
||||
currentPrice: initialPrice * (1 + priceChange), // current price after drop
|
||||
});
|
||||
};
|
||||
|
||||
// Compute GJR-GARCH forecasting series for the math accordion/visual validation
|
||||
const gjrGarchMathResult = useMemo(() => {
|
||||
const mockReturns = [0.015, -0.022, 0.008, -0.031, 0.014, -0.055, 0.012, -0.008, 0.024, -0.065];
|
||||
return calculateGJRGARCH(mockReturns);
|
||||
}, []);
|
||||
|
||||
const mathChartData = useMemo(() => {
|
||||
return gjrGarchMathResult.series.map((vol, idx) => ({
|
||||
day: `T-${gjrGarchMathResult.series.length - idx - 1}`,
|
||||
'GJR-GARCH Vol (%)': parseFloat(vol.toFixed(2)),
|
||||
}));
|
||||
}, [gjrGarchMathResult]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* SECTION 1: Overreaction Scan Action */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl -z-10" />
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||
<div className="space-y-1">
|
||||
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<ShieldAlert className="text-amber-400 w-5 h-5" /> Anomalien-Scanner & Marktverzerrungen
|
||||
</h2>
|
||||
<p className="text-slate-400 text-xs max-w-2xl">
|
||||
Isoliert Kursstürze > 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
|
||||
<button
|
||||
onClick={handleMarketScan}
|
||||
disabled={scanning}
|
||||
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 disabled:from-amber-850 disabled:to-orange-900 disabled:text-slate-400 text-slate-950 font-bold py-3 px-6 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 active:scale-[0.98]"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${scanning ? 'animate-spin' : ''}`} />
|
||||
<span>{scanning ? 'Scanne Markt...' : 'Markt scannen'}</span>
|
||||
</button>
|
||||
{scanning && (
|
||||
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collapsible Math Accordion */}
|
||||
<div className="border-t border-slate-850 pt-4 mt-6">
|
||||
<button
|
||||
onClick={() => setShowMathAccordion(!showMathAccordion)}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-amber-400 transition-colors focus:outline-none"
|
||||
>
|
||||
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
|
||||
<span className="font-semibold uppercase tracking-wider">GJR-GARCH(1,1) Volatilitätsmodellierung & Leverage-Effekt</span>
|
||||
</button>
|
||||
|
||||
{showMathAccordion && (
|
||||
<div className="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300">
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
Das <strong>GJR-GARCH(1,1)</strong>-Modell erfasst die Asymmetrie im Volatilitätsprozess von Renditen. Es besitzt einen zusätzlichen Term (<InlineMath math="\gamma" />), der aktiviert wird, wenn der gestrige Schock negativ war.
|
||||
</p>
|
||||
<div className="py-2 overflow-x-auto text-slate-200">
|
||||
<BlockMath math="\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \gamma \epsilon_{t-1}^2 I_{t-1} + \beta \sigma_{t-1}^2" />
|
||||
</div>
|
||||
<p>
|
||||
Die Indikatorvariable <InlineMath math="I_{t-1}" /> nimmt den Wert 1 bei einem Kurssturz an:
|
||||
</p>
|
||||
<div className="py-2 overflow-x-auto text-slate-200">
|
||||
<BlockMath math="I_{t-1} = \begin{cases} 1 & \text{falls } \epsilon_{t-1} < 0 \\ 0 & \text{sonst} \end{cases}" />
|
||||
</div>
|
||||
<p className="text-slate-400">
|
||||
Sind die Volatilitätsschocks asymmetrisch (<InlineMath math="\gamma > 0" />), führt ein Kurssturz (Bad News) zu einer signifikant höheren Volatilität als ein gleich großer Kursgewinn (Good News).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-slate-400 font-semibold mb-2">Simulierter GJR-GARCH Volatilitätsprozess nach Schocks</div>
|
||||
<div className="h-44 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={mathChartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
||||
<XAxis dataKey="day" stroke="#64748b" fontSize={9} />
|
||||
<YAxis stroke="#64748b" fontSize={9} unit="%" />
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
|
||||
<Line type="monotone" dataKey="GJR-GARCH Vol (%)" stroke="#f59e0b" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 font-mono text-center">
|
||||
Spike am Tag T-4 repräsentiert die asymmetrische Reaktion auf einen Schock von -6.5%.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left 2 Columns: Scanned anomalies details */}
|
||||
<div className="xl:col-span-2 space-y-6">
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Sparkles className="text-amber-400 w-5 h-5" /> Gefundene Anomalien (Sentiment-Ampel)
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{activeAlerts.map((alert) => {
|
||||
// Fetch associated info from mockDB if available, else generic mock
|
||||
const dbInfo = mockSearchDatabase[alert.ticker] || {
|
||||
name: `${alert.ticker} Corp.`,
|
||||
sentiment: alert.overreactionScore > 75 ? 'GREEN' : (alert.overreactionScore > 40 ? 'YELLOW' : 'RED'),
|
||||
whyDropped: 'Kurzfristige Eindeckungen und Gewinnmitnahmen an den Terminmärkten belasten das Sentiment.'
|
||||
};
|
||||
|
||||
const isGreen = dbInfo.sentiment === 'GREEN';
|
||||
const isYellow = dbInfo.sentiment === 'YELLOW';
|
||||
const isRed = dbInfo.sentiment === 'RED';
|
||||
|
||||
return (
|
||||
<div key={alert.id} className="p-5 bg-slate-950/40 border border-slate-850 rounded-xl space-y-4 relative group">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 border-b border-slate-900 pb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="font-mono font-bold text-lg text-slate-100">{alert.ticker}</span>
|
||||
<span className="text-slate-400 text-xs">({dbInfo.name})</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-400 mt-1">
|
||||
Kurssturz: <span className="text-rose-400 font-bold font-mono">{(alert.priceChange * 100).toFixed(1)}%</span>
|
||||
<span className="mx-2">|</span>
|
||||
GJR-GARCH Vol: <span className="text-cyan-400 font-bold font-mono">{(alert.gjrGarchVol * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traffic Light Sentiment Badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
{isGreen && (
|
||||
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/25 flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> EMOTIONALER OVERREACTION (KAUF)
|
||||
</span>
|
||||
)}
|
||||
{isYellow && (
|
||||
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-yellow-500/10 text-yellow-400 border border-yellow-500/25 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> UNSICHERHEIT (HALTEN)
|
||||
</span>
|
||||
)}
|
||||
{isRed && (
|
||||
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/25 flex items-center gap-1">
|
||||
<XCircle className="w-3.5 h-3.5" /> FUNDAMENTALER SCHADEN (MEIDEN)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Analysis Block */}
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
|
||||
<Info className="w-3 h-3 text-amber-400" /> KI-Ursachenanalyse:
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed italic">
|
||||
„{dbInfo.whyDropped}“
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions & Score */}
|
||||
<div className="flex flex-row md:flex-col items-center md:items-end justify-between md:justify-center gap-3 md:border-l md:border-slate-900 md:pl-4">
|
||||
<div className="text-left md:text-right">
|
||||
<span className="text-[9px] text-slate-400 uppercase tracking-widest block">Rebound Score</span>
|
||||
<span className={`font-mono text-xl font-extrabold ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||||
{alert.overreactionScore}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(isGreen || isYellow) && (
|
||||
<button
|
||||
onClick={() => handleAddToWatchlist(alert.ticker, alert.priceChange, dbInfo.sentiment, dbInfo.whyDropped)}
|
||||
className="bg-slate-900 hover:bg-slate-850 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[11px] font-semibold py-1.5 px-3 rounded-lg flex items-center gap-1 transition-colors active:scale-[0.98]"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Tracken
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Deep-Check & Search Mask */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
|
||||
<Search className="text-amber-400 w-5 h-5" /> Deep-Check Suchmaske
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-2 leading-relaxed">
|
||||
Suchen Sie gezielt nach Werten, um Anomalien-Analysen abzurufen. (z.B. RACE, KO, TSLA, SMCI, NFLX)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleDeepCheck} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="z.B. RACE"
|
||||
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono focus:outline-none focus:border-amber-500 text-sm uppercase"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-slate-800 hover:bg-slate-700 text-amber-400 hover:text-amber-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
|
||||
>
|
||||
Prüfen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{searchResult && (
|
||||
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-4 animate-fade-in">
|
||||
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-100 font-mono text-sm">{searchResult.ticker}</h4>
|
||||
<span className="text-[10px] text-slate-500">{searchResult.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
{searchResult.sentiment === 'GREEN' && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px] font-bold border border-emerald-500/25">GREEN</span>}
|
||||
{searchResult.sentiment === 'YELLOW' && <span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-400 text-[10px] font-bold border border-yellow-500/25">YELLOW</span>}
|
||||
{searchResult.sentiment === 'RED' && <span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[10px] font-bold border border-rose-500/25">RED</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Aktueller Sturz:</span>
|
||||
<span className="text-rose-400 font-mono font-bold">{(searchResult.priceChange * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">GJR-GARCH Volatilität:</span>
|
||||
<span className="text-cyan-400 font-mono font-bold">{(searchResult.gjrGarchVol * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Rebound Score:</span>
|
||||
<span className={`font-mono font-bold ${searchResult.sentiment === 'GREEN' ? 'text-emerald-400' : (searchResult.sentiment === 'YELLOW' ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||||
{searchResult.reboundScore}/100
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-900">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-semibold block mb-1">KI-Kommentar:</span>
|
||||
<p className="italic text-slate-300 leading-relaxed text-[11px]">{searchResult.whyDropped}</p>
|
||||
</div>
|
||||
|
||||
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
|
||||
<button
|
||||
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
|
||||
className="w-full bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 mt-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Watchlist hinzufügen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SECTION 3: Hot Watchlist & Rebound-Tracker (48 Hours) */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Flame className="text-orange-400 w-6 h-6 animate-pulse" />
|
||||
<h3 className="text-lg font-bold text-white">Hot Rebound Watchlist & Tracker (48h)</h3>
|
||||
</div>
|
||||
|
||||
{watchlist.length > 0 && (
|
||||
<button
|
||||
onClick={simulateWatchlistTick}
|
||||
className="bg-slate-950 hover:bg-slate-900 text-orange-400 hover:text-orange-350 border border-slate-800 text-xs font-bold py-2 px-4 rounded-xl flex items-center gap-1.5 transition-colors active:scale-[0.97]"
|
||||
title="Simuliert das Fortschreiten der Zeit um 4 Stunden"
|
||||
>
|
||||
<Play className="w-4 h-4 fill-orange-400" />
|
||||
<span>Simuliere +4 Std. Kursänderung</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{watchlist.length === 0 ? (
|
||||
<div className="p-12 text-center text-slate-500 text-sm border border-dashed border-slate-800 rounded-xl bg-slate-950/20">
|
||||
Bislang keine Assets auf der Rebound-Watchlist. Nutzen Sie die Markt-Scanergebnisse oben, um Werte hinzuzufügen.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{watchlist.map((item) => {
|
||||
const perf = item.reboundPerformance;
|
||||
const isPositive = perf >= 0;
|
||||
const progressPct = (item.hoursTracked / 48) * 100;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-3 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-1.5">
|
||||
<button
|
||||
onClick={() => removeFromWatchlist(item.id)}
|
||||
className="text-slate-500 hover:text-rose-400 transition-colors p-1"
|
||||
title="Aus der Watchlist entfernen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-start pr-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono font-bold text-slate-200 text-base">{item.ticker}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold ${item.sentiment === 'GREEN' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'}`}>
|
||||
{item.sentiment}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-500">Gezogen bei: <span className="font-mono">{(item.priceChange * 100).toFixed(1)}%</span></span>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] text-slate-400 uppercase">Rebound Performance</div>
|
||||
<div className={`font-mono text-base font-extrabold flex items-center justify-end gap-0.5 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{isPositive ? '+' : ''}{perf.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress timeline */}
|
||||
<div className="space-y-1 text-[10px] text-slate-400">
|
||||
<div className="flex justify-between font-mono">
|
||||
<span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5 text-slate-500" /> Tracking-Dauer: {item.hoursTracked}/48 Std.</span>
|
||||
<span>{Math.round(progressPct)}% abgeschlossen</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-orange-400 to-amber-500 h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-slate-500 italic truncate" title={item.whyDropped}>
|
||||
Hintergrund: {item.whyDropped}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user