feat(sandbox): deploy Phase 1 and Phase 2 of Portfolio Sandbox including Swamy-Arora GLS solver and stress-test visualization
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
} from 'recharts';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { BlockMath, InlineMath } from 'react-katex';
|
||||
import EconometricsMathModal from './EconometricsMathModal';
|
||||
import {
|
||||
Activity,
|
||||
BarChart4,
|
||||
@@ -42,34 +43,24 @@ import {
|
||||
|
||||
// Predefined archetypes for Event Creation
|
||||
const ARCHETYPES: Record<string, { name: string; defaultScores: Record<string, number> }> = {
|
||||
'FED Zinsentscheid': {
|
||||
name: 'FED Zinsentscheid',
|
||||
'🏦 Fed-Zinsentscheid (FOMC)': {
|
||||
name: 'Fed-Zinsentscheid (FOMC)',
|
||||
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',
|
||||
'📈 US-Inflationsdaten (CPI)': {
|
||||
name: 'US-Inflationsdaten (CPI)',
|
||||
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -2, Bitcoin: 1 }
|
||||
},
|
||||
'US Non-Farm Payrolls': {
|
||||
name: 'US Non-Farm Payrolls',
|
||||
'💼 Non-Farm Payrolls (NFP)': {
|
||||
name: 'Non-Farm Payrolls (NFP)',
|
||||
defaultScores: { Apple: 0, NASDAQ: 1, Gold: -1, Bitcoin: 0 }
|
||||
},
|
||||
'EZB Pressekonferenz': {
|
||||
name: 'EZB Pressekonferenz',
|
||||
'🛒 OPEC-Treffen': {
|
||||
name: 'OPEC-Treffen',
|
||||
defaultScores: { Apple: -1, NASDAQ: -1, Gold: 2, Bitcoin: 1 }
|
||||
}
|
||||
};
|
||||
|
||||
const ASSETS = ['Apple', 'NASDAQ', 'Gold', 'Bitcoin'];
|
||||
|
||||
export default function EventsDemo() {
|
||||
const {
|
||||
selectedModel,
|
||||
@@ -77,16 +68,195 @@ export default function EventsDemo() {
|
||||
eventsMatrix,
|
||||
calendarProposals,
|
||||
lmmObservations,
|
||||
addEventToMatrix,
|
||||
updateMatrixCell,
|
||||
runEndogenousLMMCalibration
|
||||
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 [showMath, setShowMath] = useState<boolean>(false);
|
||||
const [isMathModalOpen, setIsMathModalOpen] = 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]);
|
||||
|
||||
|
||||
|
||||
// Custom Event Form State
|
||||
const [customName, setCustomName] = useState<string>('');
|
||||
@@ -123,18 +293,22 @@ export default function EventsDemo() {
|
||||
|
||||
// 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 };
|
||||
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] || 0;
|
||||
totals[asset] += score * weight;
|
||||
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] * 100) / 100;
|
||||
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';
|
||||
@@ -162,11 +336,11 @@ export default function EventsDemo() {
|
||||
glowClass = 'shadow-amber-500/5 shadow-[0_0_10px_rgba(245,158,11,0.1)]';
|
||||
}
|
||||
|
||||
signals[asset] = { netScore, signal, colorClass, textClass, glowClass };
|
||||
signals[asset.name] = { netScore, signal, colorClass, textClass, glowClass };
|
||||
});
|
||||
|
||||
return signals;
|
||||
}, [eventsMatrix, tauPre, tauPost]);
|
||||
}, [eventsMatrix, assets, tauPre, tauPost]);
|
||||
|
||||
// 2. Dynamic Decay Curve Chart Data
|
||||
const decayCurveData = useMemo(() => {
|
||||
@@ -188,8 +362,32 @@ export default function EventsDemo() {
|
||||
return pts;
|
||||
}, [tauPre, tauPost]);
|
||||
|
||||
// 3. Dynamic ROC Data
|
||||
// 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[] = [];
|
||||
|
||||
@@ -218,16 +416,26 @@ export default function EventsDemo() {
|
||||
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: res.optimalThreshold,
|
||||
optimalThreshold: optimalScoreThreshold,
|
||||
maxYouden: res.maxYouden,
|
||||
auc: Math.round(Math.max(0.5, Math.min(0.99, computedAuc)) * 1000) / 1000
|
||||
};
|
||||
}, [eventsMatrix, lmmObservations]);
|
||||
}, [eventsMatrix, lmmObservations, lmmResults]);
|
||||
|
||||
// 4. Dynamic Survival Curve Data for selected asset
|
||||
// 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);
|
||||
|
||||
@@ -284,31 +492,35 @@ export default function EventsDemo() {
|
||||
let lastShort = 1.0;
|
||||
|
||||
return sortedMerged.map(pt => {
|
||||
if (pt.longRate !== undefined) lastLong = pt.longRate;
|
||||
else pt.longRate = lastLong;
|
||||
const longRate = pt.longRate !== undefined ? pt.longRate : lastLong;
|
||||
lastLong = longRate;
|
||||
|
||||
if (pt.shortRate !== undefined) lastShort = pt.shortRate;
|
||||
else pt.shortRate = lastShort;
|
||||
const shortRate = pt.shortRate !== undefined ? pt.shortRate : lastShort;
|
||||
lastShort = shortRate;
|
||||
|
||||
return pt;
|
||||
return {
|
||||
time: pt.time,
|
||||
highConvRate: longRate,
|
||||
lowConvRate: shortRate
|
||||
};
|
||||
});
|
||||
}, [eventsMatrix, selectedSurvivalAsset]);
|
||||
|
||||
// 5. Dynamic LMM regression fitting
|
||||
const lmmResults = useMemo(() => {
|
||||
return runEventLMM(lmmObservations);
|
||||
}, [lmmObservations]);
|
||||
}, [eventsMatrix, selectedSurvivalAsset, lmmResults]);
|
||||
|
||||
// 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 };
|
||||
let scores: Record<string, number> = {};
|
||||
assets.forEach(asset => {
|
||||
scores[asset.name] = 0;
|
||||
});
|
||||
|
||||
if (selectedArchetype !== 'Custom') {
|
||||
const arch = ARCHETYPES[selectedArchetype];
|
||||
name = name || arch.name;
|
||||
scores = { ...arch.defaultScores };
|
||||
assets.forEach(asset => {
|
||||
scores[asset.name] = typeof arch.defaultScores[asset.name] === 'number' ? arch.defaultScores[asset.name] : 0;
|
||||
});
|
||||
} else {
|
||||
name = name || 'Benutzerdefiniertes Ereignis';
|
||||
}
|
||||
@@ -357,36 +569,46 @@ export default function EventsDemo() {
|
||||
</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">
|
||||
<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={() => 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'
|
||||
}`}
|
||||
onClick={() => setIsMathModalOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-slate-950/80 hover:bg-slate-905 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-rose-400"
|
||||
>
|
||||
<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
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>📖 Modulerklärung</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,15 +622,32 @@ export default function EventsDemo() {
|
||||
|
||||
{/* 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">
|
||||
<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="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 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 (z.B. 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>
|
||||
|
||||
@@ -418,10 +657,22 @@ export default function EventsDemo() {
|
||||
<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>
|
||||
{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} Spalte löschen`}
|
||||
>
|
||||
<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</th>
|
||||
<th className="py-3 px-3 text-right">Kernel-Gewicht & Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/40">
|
||||
@@ -440,8 +691,9 @@ export default function EventsDemo() {
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{ASSETS.map((asset) => {
|
||||
const score = ev.scores[asset] || 0;
|
||||
{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';
|
||||
@@ -450,33 +702,58 @@ export default function EventsDemo() {
|
||||
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} className="py-3 px-3 text-center">
|
||||
<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, Math.max(-3, score - 1))}
|
||||
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}`}>
|
||||
<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, Math.min(3, score + 1))}
|
||||
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 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>
|
||||
<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="Event aus Matrix löschen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -487,6 +764,106 @@ export default function EventsDemo() {
|
||||
</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">
|
||||
|
||||
@@ -618,13 +995,13 @@ export default function EventsDemo() {
|
||||
<h3 className="text-sm font-bold text-slate-200">Wirtschaftskalender Vorschläge</h3>
|
||||
</div>
|
||||
|
||||
{calendarProposals.length === 0 ? (
|
||||
{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">
|
||||
{calendarProposals.map((cp) => (
|
||||
{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>
|
||||
@@ -654,16 +1031,22 @@ export default function EventsDemo() {
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{ASSETS.map((asset) => {
|
||||
const { netScore, signal, colorClass, textClass, glowClass } = actionSignals[asset];
|
||||
{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}
|
||||
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`}
|
||||
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}</span>
|
||||
<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>
|
||||
@@ -741,56 +1124,9 @@ export default function EventsDemo() {
|
||||
</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">
|
||||
|
||||
@@ -813,7 +1149,13 @@ export default function EventsDemo() {
|
||||
<div className="bg-slate-950/40 border border-slate-805 p-3 rounded-xl col-span-2">
|
||||
<span className="block text-[10px] text-slate-500 uppercase font-semibold">Optimal Youden Threshold</span>
|
||||
<span className="text-sm font-bold font-mono text-slate-200">
|
||||
Score ≥ {optimalThreshold}
|
||||
{(() => {
|
||||
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>
|
||||
@@ -823,7 +1165,7 @@ export default function EventsDemo() {
|
||||
{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.
|
||||
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>
|
||||
@@ -833,8 +1175,8 @@ export default function EventsDemo() {
|
||||
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>
|
||||
{assets.map(asset => (
|
||||
<option key={asset.symbol} value={asset.name}>{asset.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -842,11 +1184,13 @@ export default function EventsDemo() {
|
||||
<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>
|
||||
<span className="text-sm font-bold text-slate-200 font-mono">30 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>
|
||||
<span className="text-sm font-bold text-slate-200 font-mono">
|
||||
{lmmResults?.survival?.observationCount ?? 30} Event Runs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -904,8 +1248,8 @@ export default function EventsDemo() {
|
||||
{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>
|
||||
<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%">
|
||||
@@ -917,8 +1261,8 @@ export default function EventsDemo() {
|
||||
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} />
|
||||
<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>
|
||||
@@ -966,6 +1310,13 @@ export default function EventsDemo() {
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -975,6 +1326,7 @@ export default function EventsDemo() {
|
||||
|
||||
</div>
|
||||
|
||||
<EconometricsMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user