'use client'; import React, { useState, useMemo } from 'react'; import { useSandboxStore, PortfolioHolding, Transaction } from '@/lib/store'; import { calculateEWMA, calculateKellyFraction, calculateAssetCovariance } from '@/lib/math/statistics'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import 'katex/dist/katex.min.css'; import { BlockMath, InlineMath } from 'react-katex'; import { TrendingUp, Wallet, ArrowDownRight, ArrowUpRight, Percent, Plus, FolderSync, HelpCircle, Settings, Calendar, DollarSign, Tag, Check, AlertCircle, ChevronDown, ChevronUp, Sparkles } from 'lucide-react'; export default function SandboxDemo() { const { portfolios, activePortfolioId, ewmaLambda, createPortfolio, setActivePortfolio, executeTransaction, setEwmaLambda, scannerAlerts, posteriorProbability } = useSandboxStore(); // Selected portfolio const activePortfolio = useMemo(() => { return portfolios.find(p => p.id === activePortfolioId) || portfolios[0]; }, [portfolios, activePortfolioId]); const [mounted, setMounted] = useState(false); React.useEffect(() => { setMounted(true); }, []); if (!mounted) { return (
Lade Sandbox-Modul...
); } // UI state const [showNewPortfolioModal, setShowNewPortfolioModal] = useState(false); const [newPortfolioName, setNewPortfolioName] = useState(''); const [newStartingBalance, setNewStartingBalance] = useState(50000); const [tradeSymbol, setTradeSymbol] = useState('AAPL'); const [tradeWknOrIsin, setTradeWknOrIsin] = useState('865985'); const [tradeShares, setTradeShares] = useState(10); const [tradePrice, setTradePrice] = useState(182); const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY'); const [simulateFees, setSimulateFees] = useState(true); const [isBackfill, setIsBackfill] = useState(false); const [backfillDate, setBackfillDate] = useState('2026-05-20'); const [hypothesisTag, setHypothesisTag] = useState('Fokus auf KI-Infrastruktur'); const [orderError, setOrderError] = useState(null); const [orderSuccess, setOrderSuccess] = useState(false); const [showMathAccordion, setShowMathAccordion] = useState(false); const [showMsciBenchmark, setShowMsciBenchmark] = useState(true); // Kelly Position Sizing states const [kellySource, setKellySource] = useState<'scanner' | 'crypto' | 'econometric' | 'custom'>('custom'); const [customProb, setCustomProb] = useState(0.60); const [oddsRatio, setOddsRatio] = useState(1.5); // Compute Net Worth const netWorth = useMemo(() => { const assetsVal = activePortfolio.holdings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0); return Math.round((activePortfolio.cash + assetsVal) * 100) / 100; }, [activePortfolio]); // Dynamic winning probability (p) based on selected source const kellyProbability = useMemo(() => { if (kellySource === 'scanner') { const alert = scannerAlerts.find(a => a.ticker.toUpperCase() === tradeSymbol.toUpperCase()); return alert ? alert.overreactionScore / 100 : 0.52; } if (kellySource === 'crypto') { return posteriorProbability; // e.g. 0.72 } if (kellySource === 'econometric') { return 0.65; // ROC target probability } return customProb; }, [kellySource, customProb, tradeSymbol, scannerAlerts, posteriorProbability]); // Check potential cluster risk for the input symbol const potentialClusterRisk = useMemo(() => { if (!tradeSymbol) return false; const holdingsWithWeights = activePortfolio.holdings.map(h => ({ symbol: h.symbol, weight: (h.shares * h.currentPrice) / (netWorth || 1.0) })); const covResult = calculateAssetCovariance(holdingsWithWeights, tradeSymbol); return covResult.clusterRisk; }, [activePortfolio.holdings, tradeSymbol, netWorth]); // Compute Kelly fraction and recommended cash amount const kellyFraction = useMemo(() => { const rawKelly = calculateKellyFraction(kellyProbability, oddsRatio); // Cap at Half-Kelly already done in calculateKellyFraction, but we can scale by 50% if there is cluster risk return potentialClusterRisk ? rawKelly * 0.5 : rawKelly; }, [kellyProbability, oddsRatio, potentialClusterRisk]); const recommendedKellyCash = useMemo(() => { return activePortfolio.cash * kellyFraction; }, [activePortfolio.cash, kellyFraction]); // Compute returns based on active portfolio's historical value series const portfolioReturns = useMemo(() => { const vals = activePortfolio.historicalValues; if (vals.length < 2) return []; const r: number[] = []; for (let i = 1; i < vals.length; i++) { r.push((vals[i].value - vals[i - 1].value) / vals[i - 1].value); } return r; }, [activePortfolio.historicalValues]); // Calculate EWMA Volatility live const ewmaResult = useMemo(() => { return calculateEWMA(portfolioReturns, ewmaLambda); }, [portfolioReturns, ewmaLambda]); // Combine data for charting const chartData = useMemo(() => { const vals = activePortfolio.historicalValues; if (vals.length === 0) return []; // Normalize MSCI World index from the same starting value of the portfolio const baseValue = vals[0].value; let msciVal = baseValue; return vals.map((hv, idx) => { // Deterministic pseudo-random walk for MSCI World if (idx > 0) { const rand = Math.sin(idx * 57.8) * 0.45 + 0.05; // range: -0.4% to +0.5% return msciVal = msciVal * (1 + rand * 0.015); } const vol = ewmaResult.series[idx - 1] || 0; return { date: hv.date, Portfolio: hv.value, 'MSCI World (Benchmark)': Math.round(msciVal), 'EWMA Vol (%)': parseFloat(vol.toFixed(2)), }; }); }, [activePortfolio.historicalValues, ewmaResult]); // Total gain/loss const totalGainLoss = netWorth - activePortfolio.startingBalance; const totalGainLossPct = (totalGainLoss / activePortfolio.startingBalance) * 100; const isPositiveOverall = totalGainLoss >= 0; const handleCreatePortfolio = (e: React.FormEvent) => { e.preventDefault(); if (!newPortfolioName.trim()) return; createPortfolio(newPortfolioName, newStartingBalance); setNewPortfolioName(''); setShowNewPortfolioModal(false); }; const handleTransactionSubmit = (e: React.FormEvent) => { e.preventDefault(); setOrderError(null); setOrderSuccess(false); if (tradeShares <= 0 || tradePrice <= 0) { setOrderError('Bitte geben Sie eine gültige Stückzahl und einen Kurs an.'); return; } const ok = executeTransaction( activePortfolio.id, tradeSymbol, tradeWknOrIsin, tradeType, tradeShares, tradePrice, simulateFees, isBackfill, backfillDate, hypothesisTag ); if (ok) { setOrderSuccess(true); setTimeout(() => setOrderSuccess(false), 3000); } else { setOrderError( tradeType === 'BUY' ? 'Unzureichendes Barguthaben (inklusive allfälliger Transaktionsgebühren).' : 'Unzureichende Anteile im Depot für den Verkauf.' ); } }; return (
{activePortfolio.riskProfile?.status === 'RED' && (
Kritische Klumpenrisiken (Kovarianz RED): {activePortfolio.riskProfile.message}
)} {/* SECTION 1: Portfolio Selector & Stats Bar */}
Strategic Sandbox
{/* Net Worth Card */}
Gesamtwert
${netWorth.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{/* Performance Card */}
GuV (Gesamt)
{isPositiveOverall ? : } {isPositiveOverall ? '+' : ''}{totalGainLossPct.toFixed(2)}%
{/* Live EWMA Vol Card */}
EWMA Volatilität
{ewmaResult.latest.toFixed(2)}%
{/* Covariance Risk Traffic Light Card */}
Kovarianz-Ampel
{activePortfolio.riskProfile?.status || 'GREEN'} RISK
{/* Modal for creating a new Portfolio */} {showNewPortfolioModal && (

Neues Sandbox-Portfolio

setNewPortfolioName(e.target.value)} />
setNewStartingBalance(Number(e.target.value))} />
)} {/* Accordion / Math Button */}
{showMathAccordion && (

1. EWMA Volatilitätsmodell

Die Volatilität wird mittels des Exponentially Weighted Moving Average (EWMA)-Modells ermittelt. Jüngere Renditen erhalten hierbei ein höheres Gewicht als weiter in der Vergangenheit liegende Renditen, gesteuert durch den Zerfallsparameter (Lambda).

Die tägliche Volatilität wird auf ein ganzes Jahr hochgerechnet (Annualisierung) unter der Annahme von 252 Handelstagen:

RiskMetrics empfiehlt für tägliche Finanzdaten einen Lambda-Wert von .

2. Kelly-Kriterium zur Positionsgrößenbestimmung

Die Kelly-Formel bestimmt den optimalen Anteil des Kapitals (), der in ein Geschäft investiert werden soll, um das exponentielle Wachstum des Kapitals zu maximieren:

Um Risiken durch ungenaue Schätzungen zu verringern, wenden wir das konservative Half-Kelly-Sizing an und begrenzen das Ergebnis auf (zusätzlich begrenzt auf ).

3. Covariance & Cluster Risk (Kovarianz-Ampel)

Die Kovarianz zwischen Assets wird durch Multiplikation ihrer paarweisen Korrelation mit ihren jeweiligen Standardabweichungen (Volatilitäten) bestimmt:

Ein Klumpenrisiko (Risk RED) wird ausgelöst, wenn ein Asset eine Korrelation zu bestehenden Positionen aufweist und diese Positionen jeweils mehr als 15% des Portfolios ausmachen.

)}
{/* SECTION 2: Chart / Analytics & Order Form */}
{/* Left 2 Columns: Analytics Performance Plot */}

Portfolio Wertentwicklung & Benchmark

`$${v.toLocaleString()}`} /> {showMsciBenchmark && ( )}
{/* EWMA parameter tuner slider */}

Parameteranpassung EWMA Lambda (λ)

Zerfallsfaktor steuert die Schock-Sensitivität des Volatilitätsmodells.

setEwmaLambda(parseFloat(e.target.value))} className="w-40 accent-emerald-400 cursor-pointer" /> λ = {ewmaLambda.toFixed(2)}
{/* Right 1 Column: Advanced Order Mask */}

Order-Maske (Simuliert)

setTradeSymbol(e.target.value.toUpperCase())} />
setTradeWknOrIsin(e.target.value)} />
setTradeShares(Number(e.target.value))} />
setTradePrice(Number(e.target.value))} />
{/* Order direction buttons */}
{/* Kelly Sizing Risk Recommendation Widget */} {tradeType === 'BUY' && (
Risk-Engine Sizing Kelly recommendation
setOddsRatio(Number(e.target.value))} className="w-full bg-slate-900 border border-slate-800 rounded px-1.5 py-1 text-[10px] text-slate-200 font-mono focus:outline-none" />
{kellySource === 'custom' && (
Erfolgswahrscheinlichkeit: {(kellyProbability * 100).toFixed(0)}%
setCustomProb(Number(e.target.value))} className="w-full accent-emerald-500 h-1 bg-slate-900 rounded" />
)} {kellySource !== 'custom' && (
System-Wahrscheinlichkeit: {(kellyProbability * 100).toFixed(1)}%
)} {potentialClusterRisk && (
Klumpenrisiko! Korrelation > 0.70 zu bestehenden Positionen. Kelly-Empfehlung wurde um 50% halbiert.
)}
Kelly-Anteil: {(kellyFraction * 100).toFixed(1)}% des Cashs
Kaufvolumen: ${Math.round(recommendedKellyCash).toLocaleString()}
)} {/* Hypothesis input */}
setHypothesisTag(e.target.value)} />
{/* Fees Toggle */}
Ordergebühren simulieren setSimulateFees(e.target.checked)} className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer" />
{/* Backfill Date Picker Toggle */}
Historischer Backfill setIsBackfill(e.target.checked)} className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer" />
{isBackfill && ( setBackfillDate(e.target.value)} /> )}
{orderError && (
{orderError}
)} {orderSuccess && (
Transaktion erfolgreich gebucht!
)}
{/* SECTION 3: Holdings Table & Transactions Log */}
{/* Left 2 Columns: Holdings List */}

Depotbestände ({activePortfolio.holdings.length})

Barguthaben: ${activePortfolio.cash.toLocaleString()}
{activePortfolio.holdings.length === 0 ? ( ) : ( activePortfolio.holdings.map((hold) => { const profitLoss = (hold.currentPrice - hold.avgPrice) * hold.shares; const isPositive = profitLoss >= 0; return ( ); }) )}
Asset Stücke Einstand Kurs Hypothese GuV
Keine Bestände in diesem Sandbox-Portfolio. Nutzen Sie die Order-Maske, um Werte hinzuzufügen.
{hold.symbol}
{hold.wknOrIsin &&
WKN: {hold.wknOrIsin}
}
{hold.shares} ${hold.avgPrice.toFixed(2)} ${hold.currentPrice.toFixed(2)} {hold.hypothesisTag || '-'} {isPositive ? : } ${Math.abs(profitLoss).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{/* Right 1 Column: Transactions History */}

Letzte Orderbuch-Einträge

{activePortfolio.transactions.length === 0 ? (

Bislang keine Transaktionen in diesem Portfolio.

) : ( activePortfolio.transactions.map((tx) => { const isBuy = tx.type === 'BUY'; return (
{isBuy ? 'KAUF' : 'VERKAUF'} {tx.symbol}
{tx.timestamp}
{tx.shares} Stk @ ${tx.price.toFixed(2)} Gebühr: ${tx.feeApplied.toFixed(2)}
{tx.hypothesisTag && (
{tx.hypothesisTag}
)}
); }) )}
); }