Files
investment-sandbox/components/modules/events/EventsDemo.tsx
2026-06-06 21:11:16 +02:00

981 lines
47 KiB
TypeScript

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