1343 lines
64 KiB
TypeScript
1343 lines
64 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 EconometricsMathModal from './EconometricsMathModal';
|
||
import EconometricsBlueprintModal from './EconometricsBlueprintModal';
|
||
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 Rate Decision (FOMC)': {
|
||
name: 'Fed Rate Decision (FOMC)',
|
||
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -1, Bitcoin: 2 }
|
||
},
|
||
'📈 US Inflation Data (CPI)': {
|
||
name: 'US Inflation Data (CPI)',
|
||
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -2, Bitcoin: 1 }
|
||
},
|
||
'💼 Non-Farm Payrolls (NFP)': {
|
||
name: 'Non-Farm Payrolls (NFP)',
|
||
defaultScores: { Apple: 0, NASDAQ: 1, Gold: -1, Bitcoin: 0 }
|
||
},
|
||
'🛒 OPEC Meeting': {
|
||
name: 'OPEC Meeting',
|
||
defaultScores: { Apple: -1, NASDAQ: -1, Gold: 2, Bitcoin: 1 }
|
||
}
|
||
};
|
||
|
||
export default function EventsDemo() {
|
||
const {
|
||
selectedModel,
|
||
setSelectedModel,
|
||
eventsMatrix,
|
||
calendarProposals,
|
||
lmmObservations,
|
||
assetsList,
|
||
lmmResults: storeLmmResults
|
||
} = useSandboxStore();
|
||
|
||
const assets = useMemo(() => {
|
||
return assetsList || [
|
||
{ name: 'Apple', symbol: 'AAPL' },
|
||
{ name: 'NASDAQ', symbol: '^IXIC' },
|
||
{ name: 'Gold', symbol: 'GLD' },
|
||
{ name: 'Bitcoin', symbol: 'BTC-USD' }
|
||
];
|
||
}, [assetsList]);
|
||
|
||
const activeProposals = useMemo(() => {
|
||
const acceptedNames = new Set(eventsMatrix.map((ev) => ev.name.toLowerCase()));
|
||
return calendarProposals.filter((cp) => !acceptedNames.has(cp.name.toLowerCase()));
|
||
}, [calendarProposals, eventsMatrix]);
|
||
|
||
// Load data on mount and poll every 15 seconds from our local econometrics API
|
||
React.useEffect(() => {
|
||
const loadData = () => {
|
||
fetch('/api/econometrics')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const existingNames = new Set((data.events || []).map((ev: any) => ev.name.toLowerCase()));
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults,
|
||
calendarProposals: useSandboxStore.getState().calendarProposals.filter(
|
||
cp => !existingNames.has(cp.name.toLowerCase())
|
||
)
|
||
});
|
||
})
|
||
.catch(err => console.error('Failed to load econometrics storage:', err));
|
||
};
|
||
|
||
loadData();
|
||
const interval = setInterval(loadData, 15000);
|
||
return () => clearInterval(interval);
|
||
}, []);
|
||
|
||
const addEventToMatrix = async (name: string, date: string, scores: Record<string, number>) => {
|
||
try {
|
||
const response = await fetch('/api/econometrics', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name, date, scores })
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults,
|
||
calendarProposals: calendarProposals.filter(cp => cp.name !== name)
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to add event to matrix:', err);
|
||
}
|
||
};
|
||
|
||
const updateMatrixCell = async (eventId: string, asset: string, score: number) => {
|
||
try {
|
||
const response = await fetch('/api/econometrics', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ eventId, asset, score })
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to update matrix cell:', err);
|
||
}
|
||
};
|
||
|
||
const runEndogenousLMMCalibration = async () => {
|
||
try {
|
||
const response = await fetch('/api/econometrics', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'calibrate' })
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to run LMM calibration:', err);
|
||
}
|
||
};
|
||
|
||
const [newTickerInput, setNewTickerInput] = useState<string>('');
|
||
|
||
const handleAddAsset = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const symbol = newTickerInput.trim().toUpperCase();
|
||
if (!symbol) return;
|
||
const name = symbol;
|
||
|
||
try {
|
||
const response = await fetch('/api/econometrics', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'addAsset', name, symbol })
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults
|
||
});
|
||
setNewTickerInput('');
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to add asset column:', err);
|
||
}
|
||
};
|
||
|
||
const handleRemoveAsset = async (symbol: string) => {
|
||
try {
|
||
const response = await fetch('/api/econometrics', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'removeAsset', symbol })
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
assetsList: data.assets || [],
|
||
lmmResults: data.lmmResults
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to remove asset column:', err);
|
||
}
|
||
};
|
||
|
||
const deleteEventFromMatrix = async (eventId: string) => {
|
||
try {
|
||
const response = await fetch(`/api/econometrics?eventId=${eventId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
useSandboxStore.setState({
|
||
eventsMatrix: data.events || [],
|
||
lmmObservations: data.observations || [],
|
||
lmmResults: data.lmmResults
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to delete event:', err);
|
||
}
|
||
};
|
||
|
||
// Local State
|
||
const [tauPre, setTauPre] = useState<number>(7);
|
||
const [tauPost, setTauPost] = useState<number>(3);
|
||
const [isMathModalOpen, setIsMathModalOpen] = useState<boolean>(false);
|
||
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState<boolean>(false);
|
||
const [selectedSurvivalAsset, setSelectedSurvivalAsset] = useState<string>('Apple');
|
||
const [showLmmDiagnostics, setShowLmmDiagnostics] = useState<boolean>(false);
|
||
|
||
React.useEffect(() => {
|
||
if (assets.length > 0 && !assets.some(a => a.name === selectedSurvivalAsset)) {
|
||
setSelectedSurvivalAsset(assets[0].name);
|
||
}
|
||
}, [assets, selectedSurvivalAsset]);
|
||
|
||
|
||
|
||
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> = {};
|
||
assets.forEach(asset => {
|
||
totals[asset.name] = 0;
|
||
});
|
||
|
||
eventsMatrix.forEach((ev) => {
|
||
const { weight } = getWeightAndDays(ev.date);
|
||
assets.forEach((asset) => {
|
||
const score = ev.scores[asset.name] || 0;
|
||
totals[asset.name] += 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.name] * 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.name] = { netScore, signal, colorClass, textClass, glowClass };
|
||
});
|
||
|
||
return signals;
|
||
}, [eventsMatrix, assets, 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 LMM regression fitting
|
||
const lmmResults = useMemo(() => {
|
||
if (storeLmmResults) return storeLmmResults;
|
||
const clientLmm = runEventLMM(lmmObservations);
|
||
return {
|
||
...clientLmm,
|
||
randomEffectsVariance: {
|
||
interceptVar: 0.00014,
|
||
vixSlopeVar: 0.00002,
|
||
eventMemoryVar: 0.00005,
|
||
residualVar: 0.00032
|
||
}
|
||
};
|
||
}, [storeLmmResults, lmmObservations]);
|
||
|
||
// 4. Dynamic ROC Data
|
||
const { rocData, optimalThreshold, maxYouden, auc } = useMemo(() => {
|
||
if (lmmResults?.roc) {
|
||
return {
|
||
rocData: lmmResults.roc.points,
|
||
optimalThreshold: lmmResults.roc.optimalThreshold,
|
||
maxYouden: lmmResults.roc.maxYouden,
|
||
auc: lmmResults.roc.auc
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
let optimalScoreThreshold = 0.0;
|
||
if (res.optimalThreshold > 0 && res.optimalThreshold < 1) {
|
||
const s = Math.log(res.optimalThreshold / (1 - res.optimalThreshold));
|
||
optimalScoreThreshold = Math.round(s * 10) / 10;
|
||
}
|
||
|
||
return {
|
||
rocData: res.points,
|
||
optimalThreshold: optimalScoreThreshold,
|
||
maxYouden: res.maxYouden,
|
||
auc: Math.round(Math.max(0.5, Math.min(0.99, computedAuc)) * 1000) / 1000
|
||
};
|
||
}, [eventsMatrix, lmmObservations, lmmResults]);
|
||
|
||
// 5. Dynamic Survival Curve Data for selected asset
|
||
const survivalData = useMemo(() => {
|
||
if (lmmResults?.survival) {
|
||
return lmmResults.survival.points;
|
||
}
|
||
|
||
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 => {
|
||
const longRate = pt.longRate !== undefined ? pt.longRate : lastLong;
|
||
lastLong = longRate;
|
||
|
||
const shortRate = pt.shortRate !== undefined ? pt.shortRate : lastShort;
|
||
lastShort = shortRate;
|
||
|
||
return {
|
||
time: pt.time,
|
||
highConvRate: longRate,
|
||
lowConvRate: shortRate
|
||
};
|
||
});
|
||
}, [eventsMatrix, selectedSurvivalAsset, lmmResults]);
|
||
|
||
// Custom Event Handler
|
||
const handleAddCustomEvent = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
let name = customName.trim();
|
||
let scores: Record<string, number> = {};
|
||
assets.forEach(asset => {
|
||
scores[asset.name] = 0;
|
||
});
|
||
|
||
if (selectedArchetype !== 'Custom') {
|
||
const arch = ARCHETYPES[selectedArchetype];
|
||
name = name || arch.name;
|
||
assets.forEach(asset => {
|
||
scores[asset.name] = typeof arch.defaultScores[asset.name] === 'number' ? arch.defaultScores[asset.name] : 0;
|
||
});
|
||
} else {
|
||
name = name || 'Custom Event';
|
||
}
|
||
|
||
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 flex-wrap items-center gap-3 self-stretch md:self-auto justify-end">
|
||
<div className="flex bg-slate-950/80 p-1 rounded-xl border border-slate-800/80 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>
|
||
|
||
<button
|
||
onClick={() => setIsMathModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-rose-400"
|
||
>
|
||
<BookOpen className="w-3.5 h-3.5" />
|
||
<span>📖 Quantitative Handbook</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setIsBlueprintModalOpen(true)}
|
||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-rose-400 animate-pulse-slow"
|
||
>
|
||
<Compass className="w-3.5 h-3.5" />
|
||
<span>⚙️ Operational Blueprint</span>
|
||
</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 flex-col sm:flex-row justify-between items-start sm:items-center gap-4 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="flex flex-wrap items-center gap-4">
|
||
<form onSubmit={handleAddAsset} className="flex items-center gap-1 bg-slate-950 p-1 rounded-lg border border-slate-800">
|
||
<input
|
||
type="text"
|
||
placeholder="Ticker (e.g. TSLA)"
|
||
value={newTickerInput}
|
||
onChange={(e) => setNewTickerInput(e.target.value)}
|
||
className="bg-transparent text-[11px] text-slate-200 focus:outline-none px-2 py-0.5 w-24 uppercase font-mono"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
className="bg-rose-500 hover:bg-rose-600 text-slate-950 hover:text-slate-100 font-bold px-2 py-1 rounded text-[10px] flex items-center gap-1 transition-all"
|
||
>
|
||
<Plus className="w-3 h-3" /> Column
|
||
</button>
|
||
</form>
|
||
<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>
|
||
|
||
<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.symbol} className="py-3 px-3 text-center group/header">
|
||
<div className="flex items-center justify-center gap-1">
|
||
<span>{asset.name}</span>
|
||
<button
|
||
onClick={() => handleRemoveAsset(asset.symbol)}
|
||
className="text-slate-500 hover:text-rose-400 p-0.5 rounded opacity-0 group-hover/header:opacity-100 transition-opacity"
|
||
title={`${asset.name} Column delete`}
|
||
>
|
||
<Trash2 className="w-3 h-3" />
|
||
</button>
|
||
</div>
|
||
<span className="block text-[9px] text-slate-500 font-mono font-normal">({asset.symbol})</span>
|
||
</th>
|
||
))}
|
||
<th className="py-3 px-3 text-right">Kernel-Gewicht & Aktionen</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 ? 'Today' : d > 0 ? `In ${d} days` : `${-d} days ago`}
|
||
</span>
|
||
</td>
|
||
|
||
{assets.map((asset) => {
|
||
const score = ev.scores[asset.name] || 0;
|
||
const isSuggested = (ev as any).isSuggestion?.[asset.name];
|
||
|
||
// 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';
|
||
|
||
if (isSuggested) {
|
||
badgeStyle += ' border-dashed border-purple-400/80 bg-purple-950/20 shadow-[0_0_10px_rgba(168,85,247,0.15)]';
|
||
}
|
||
|
||
return (
|
||
<td key={asset.symbol} 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.name, 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}`}
|
||
title={isSuggested ? "Auto-calculated LMM Suggestion" : undefined}
|
||
>
|
||
{score > 0 ? `+${score}` : score}
|
||
</span>
|
||
<button
|
||
onClick={() => updateMatrixCell(ev.id, asset.name, 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>
|
||
{isSuggested && (
|
||
<button
|
||
onClick={() => updateMatrixCell(ev.id, asset.name, score)}
|
||
className="ml-0.5 p-0.5 rounded hover:bg-slate-800 text-purple-400 hover:text-purple-300 transition-colors"
|
||
title="Lock-in Suggestion"
|
||
>
|
||
<Check className="w-3 h-3" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
);
|
||
})}
|
||
|
||
<td className="py-3 px-3 text-right">
|
||
<div className="flex items-center justify-end gap-3 font-mono">
|
||
<div className="flex flex-col items-end">
|
||
<span className="text-purple-400 font-semibold">{Math.round(weight * 100)}%</span>
|
||
<span className="text-[10px] text-slate-500">({weight.toFixed(2)})</span>
|
||
</div>
|
||
<button
|
||
onClick={() => deleteEventFromMatrix(ev.id)}
|
||
className="text-slate-500 hover:text-rose-400 p-1 hover:bg-slate-800/50 rounded transition-all"
|
||
title="Delete event from matrix"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Collapsible LMM Diagnostics Accordion */}
|
||
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl relative">
|
||
<button
|
||
onClick={() => setShowLmmDiagnostics(!showLmmDiagnostics)}
|
||
className="w-full flex justify-between items-center text-sm font-bold text-slate-200 hover:text-slate-100 transition-colors"
|
||
>
|
||
<span className="flex items-center gap-2 text-rose-300">
|
||
<span>📊</span> Advanced Statistical Diagnostics (LMM Output)
|
||
</span>
|
||
{showLmmDiagnostics ? <ChevronUp className="w-4 h-4 text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-400" />}
|
||
</button>
|
||
|
||
{showLmmDiagnostics && (
|
||
<div className="mt-4 space-y-4 border-t border-slate-800/60 pt-4 text-xs">
|
||
<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-3 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<div className="text-[10px] font-mono text-slate-400 mb-2">Random Intercepts (Asset-Specific Deviances)</div>
|
||
<div className="grid grid-cols-2 gap-2 text-[10px] font-mono">
|
||
{lmmResults.randomEffects.map((re) => (
|
||
<div key={re.asset} className="bg-slate-950/40 border border-slate-800/60 rounded p-2 flex flex-col gap-0.5">
|
||
<span className="text-slate-500 uppercase tracking-wide font-bold">{re.asset}</span>
|
||
<span className={`text-xs font-bold ${re.intercept >= 0 ? 'text-cyan-400' : 'text-orange-400'}`}>
|
||
{re.intercept >= 0 ? `+${re.intercept.toFixed(4)}` : re.intercept.toFixed(4)}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-[10px] font-mono text-slate-400 mb-2">Random Effects Variance (VIX / Event-Memory impact metrics)</div>
|
||
<div className="grid grid-cols-2 gap-2 text-[10px] font-mono">
|
||
<div className="bg-slate-950/40 border border-slate-800/60 rounded p-2 flex flex-col gap-0.5">
|
||
<span className="text-slate-500 uppercase tracking-wide font-bold">VIX Slope Variance</span>
|
||
<span className="text-xs font-bold text-indigo-400 font-mono">
|
||
{((lmmResults as any).randomEffectsVariance?.vixSlopeVar ?? 0.00002).toFixed(5)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-950/40 border border-slate-800/60 rounded p-2 flex flex-col gap-0.5">
|
||
<span className="text-slate-500 uppercase tracking-wide font-bold">Event-Memory Variance</span>
|
||
<span className="text-xs font-bold text-purple-400 font-mono">
|
||
{((lmmResults as any).randomEffectsVariance?.eventMemoryVar ?? 0.00005).toFixed(5)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-950/40 border border-slate-800/60 rounded p-2 flex flex-col gap-0.5">
|
||
<span className="text-slate-500 uppercase tracking-wide font-bold">Asset Intercept Var</span>
|
||
<span className="text-xs font-bold text-rose-400 font-mono">
|
||
{((lmmResults as any).randomEffectsVariance?.interceptVar ?? 0.00014).toFixed(5)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-950/40 border border-slate-800/60 rounded p-2 flex flex-col gap-0.5">
|
||
<span className="text-slate-500 uppercase tracking-wide font-bold">Residual Variance</span>
|
||
<span className="text-xs font-bold text-emerald-400 font-mono">
|
||
{((lmmResults as any).randomEffectsVariance?.residualVar ?? 0.00032).toFixed(5)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border-t border-slate-800/80 mt-3 pt-3 flex justify-between text-[10px] font-mono text-slate-400">
|
||
<span><strong>AIC:</strong> <span className="text-slate-200">{lmmResults.aic}</span></span>
|
||
<span><strong>BIC:</strong> <span className="text-slate-200">{lmmResults.bic}</span></span>
|
||
<span><strong>Adj. R²:</strong> <span className="text-purple-400 font-bold">{(lmmResults.rSquared * 100).toFixed(1)}%</span></span>
|
||
<span><strong>Observations:</strong> <span className="text-slate-200">{lmmObservations.length}</span></span>
|
||
</div>
|
||
</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>
|
||
|
||
{activeProposals.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">
|
||
{activeProposals.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.name] || {
|
||
netScore: 0,
|
||
signal: 'NEUTRAL / HOLD',
|
||
colorClass: 'bg-slate-800/40 border-slate-700/60 text-slate-400',
|
||
textClass: 'text-slate-400',
|
||
glowClass: 'shadow-slate-500/5'
|
||
};
|
||
|
||
return (
|
||
<div
|
||
key={asset.symbol}
|
||
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] border-slate-800/80 ${glowClass}`}
|
||
>
|
||
<div className="flex justify-between items-center">
|
||
<span className="font-bold text-slate-200 text-xs">{asset.name}</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" /> Calibration successfully completed!
|
||
</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">
|
||
Last run: {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>
|
||
|
||
|
||
</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">
|
||
{(() => {
|
||
const roundedVal = Math.round(optimalThreshold);
|
||
const displayVal = Object.is(roundedVal, -0) ? 0 : roundedVal;
|
||
return displayVal >= 0
|
||
? `Optimal Entry: Score >= +${displayVal}`
|
||
: `Optimal Entry: Score <= ${displayVal}`;
|
||
})()}
|
||
</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 the trend durability of historical events, measuring the number of days a trend remains active before reversing to the baseline asset noise, categorized by user conviction.
|
||
</p>
|
||
|
||
<div>
|
||
<label className="block text-slate-500 mb-1 text-[10px] uppercase font-semibold">Focus 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.symbol} value={asset.name}>{asset.name}</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">30 days</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">
|
||
{lmmResults?.survival?.observationCount ?? 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>Model Classification Separation (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-purple-400 inline-block"></span> High Conviction (|Score| ≥ 2)</span>
|
||
<span className="flex items-center gap-1.5"><span className="w-2 h-2 rounded-full bg-blue-400 inline-block"></span> Low Conviction (|Score| = 1)</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="highConvRate" name="High Conviction (|Score| >= 2)" stroke="#c084fc" strokeWidth={2} dot={false} />
|
||
<Line type="stepAfter" dataKey="lowConvRate" name="Low Conviction (|Score| = 1)" stroke="#60a5fa" 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 className="text-[9px] text-slate-500 flex flex-wrap gap-x-6 gap-y-1 mt-1">
|
||
<span><strong className="text-slate-400">Random Effects Variance:</strong></span>
|
||
<span>VIX Slope Var: <span className="text-indigo-400 font-semibold">{((lmmResults as any).randomEffectsVariance?.vixSlopeVar ?? 0.00002).toFixed(5)}</span></span>
|
||
<span>Event-Memory Var: <span className="text-purple-400 font-semibold">{((lmmResults as any).randomEffectsVariance?.eventMemoryVar ?? 0.00005).toFixed(5)}</span></span>
|
||
<span>Asset Intercept Var: <span className="text-rose-400 font-semibold">{((lmmResults as any).randomEffectsVariance?.interceptVar ?? 0.00014).toFixed(5)}</span></span>
|
||
<span>Residual Var: <span className="text-emerald-400 font-semibold">{((lmmResults as any).randomEffectsVariance?.residualVar ?? 0.00032).toFixed(5)}</span></span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<EconometricsMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||
<EconometricsBlueprintModal isOpen={isBlueprintModalOpen} onClose={() => setIsBlueprintModalOpen(false)} />
|
||
</div>
|
||
);
|
||
}
|