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:
115
components/modules/sandbox/PortfolioMathModal.tsx
Normal file
115
components/modules/sandbox/PortfolioMathModal.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { BlockMath, InlineMath } from 'react-katex';
|
||||
|
||||
interface PortfolioMathModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PortfolioMathModal({ isOpen, onClose }: PortfolioMathModalProps) {
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/85 backdrop-blur-md p-4 sm:p-6 md:p-8">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden shadow-2xl relative text-slate-300">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="flex justify-between items-center px-6 py-4 bg-slate-950/40 border-b border-slate-800/60">
|
||||
<div>
|
||||
<h2 className="text-base font-bold bg-gradient-to-r from-teal-400 to-emerald-400 bg-clip-text text-transparent flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5 text-teal-400" /> Portfolio Sandbox - Math & Logic Specification
|
||||
</h2>
|
||||
<p className="text-[10px] text-slate-500 font-mono">Institutional Specification Manual</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-200 bg-slate-950/50 border border-slate-800 hover:border-slate-700 px-3 py-1.5 rounded-lg text-xs font-semibold font-mono transition-all cursor-pointer"
|
||||
>
|
||||
Schließen (ESC)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="flex-1 overflow-y-auto p-6 sm:p-8 space-y-6 text-slate-300 scrollbar-thin">
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-800/80 pb-3">
|
||||
<h3 className="text-base font-bold text-slate-200">5. Portfolio Sandbox & Rebalancing Engine</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">Estimates aggregate portfolio drawdowns and controls covariance drift boundaries.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-teal-400 uppercase tracking-wider font-mono">A. Synthetic Portfolio Model & Asset Weightings</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
Constructs a continuous synthetic asset representing your active weight allocations and its daily return track:
|
||||
</p>
|
||||
<div className="bg-slate-950/40 p-4 rounded-xl border border-slate-800/60 my-2 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-400 mb-1">Active Percentage Weighting (<InlineMath math="w_i" />) Calculation:</p>
|
||||
<BlockMath math="w_i = \\frac{\\text{Shares}_i \\times P_{\\text{current}, i}}{\\sum_{j} \\text{Shares}_j \\times P_{\\text{current}, j}}" />
|
||||
</div>
|
||||
<div className="border-t border-slate-850 pt-3">
|
||||
<p className="text-xs text-slate-400 mb-1">Synthetic Portfolio Log Return (<InlineMath math="R_{pt}" />):</p>
|
||||
<BlockMath math="R_{pt} = \\sum_{i} w_i \\times \\ln\\left(\\frac{P_{t, i}}{P_{t-1, i}}\\right)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-teal-400 uppercase tracking-wider font-mono">B. Linear Mixed Effects Panel Regression (LMM)</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
Solves the system-wide macro response model across all historical event instances <InlineMath math="j" /> using a Swamy-Arora GLS estimator:
|
||||
</p>
|
||||
<div className="bg-slate-950/40 p-4 rounded-xl border border-slate-800/60 my-2 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-400 mb-1">Panel Model Specification with VIX Controls:</p>
|
||||
<BlockMath math="R_{ptj} = \\beta_0 + \\beta_{\\text{drift}} \\text{Pre}_t + \\beta_{\\text{impact}} \\text{Post}_t + \\beta_{\\text{VIX}} VIX_{tj} + u_j + e_{ptj}" />
|
||||
<p className="text-[10px] text-slate-500 mt-2 font-mono leading-relaxed">
|
||||
where:
|
||||
<br />
|
||||
- <InlineMath math="t \\in [-30, +30]" /> is the relative day offset from event date <InlineMath math="T_j" />.
|
||||
<br />
|
||||
- <InlineMath math="\\text{Pre}_t = \\mathbb{I}(t < 0)" /> and <InlineMath math="\\text{Post}_t = \\mathbb{I}(t > 0)" /> are relative phase indicators.
|
||||
<br />
|
||||
- <InlineMath math="VIX_{tj}" /> is the background market-wide volatility covariate.
|
||||
<br />
|
||||
- <InlineMath math="u_j \\sim N(0, \\sigma_u^2)" /> is the random group intercept (event instance shock).
|
||||
<br />
|
||||
- <InlineMath math="e_{ptj} \\sim N(0, \\sigma_e^2)" /> is the residual error.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t border-slate-850 pt-3">
|
||||
<p className="text-xs text-slate-400 mb-1">Optimal Kelly Criterion Position Sizing:</p>
|
||||
<BlockMath math="f^* = \\frac{p \\times b - (1 - p)}{b}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-teal-400 uppercase tracking-wider font-mono">C. Reinvestment & Optimization Generation</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
Integrates signals across three engines: Scanner (underpriced value), Econometrics (macro event post-event betas), and Insiders (corporate buying).
|
||||
Ranks candidates and suggests target reallocations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
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 { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, ReferenceLine, AreaChart, Area } from 'recharts';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { BlockMath, InlineMath } from 'react-katex';
|
||||
import PortfolioMathModal from './PortfolioMathModal';
|
||||
import {
|
||||
TrendingUp, Wallet, ArrowDownRight, ArrowUpRight, Percent, Plus, FolderSync,
|
||||
HelpCircle, Settings, Calendar, DollarSign, Tag, Check, AlertCircle, ChevronDown, ChevronUp, Sparkles
|
||||
HelpCircle, Settings, Calendar, DollarSign, Tag, Check, AlertCircle, ChevronDown, ChevronUp, Sparkles,
|
||||
BookOpen, Trash2
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function SandboxDemo() {
|
||||
@@ -21,7 +23,11 @@ export default function SandboxDemo() {
|
||||
executeTransaction,
|
||||
setEwmaLambda,
|
||||
scannerAlerts,
|
||||
posteriorProbability
|
||||
posteriorProbability,
|
||||
portfolio,
|
||||
watchlist,
|
||||
updatePortfolioAsset,
|
||||
removePortfolioAsset
|
||||
} = useSandboxStore();
|
||||
|
||||
// Selected portfolio
|
||||
@@ -34,14 +40,6 @@ export default function SandboxDemo() {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl min-h-[400px] flex items-center justify-center">
|
||||
<div className="text-slate-400 text-sm font-mono animate-pulse">Lade Sandbox-Modul...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// UI state
|
||||
const [showNewPortfolioModal, setShowNewPortfolioModal] = useState(false);
|
||||
const [newPortfolioName, setNewPortfolioName] = useState('');
|
||||
@@ -60,6 +58,7 @@ export default function SandboxDemo() {
|
||||
const [orderSuccess, setOrderSuccess] = useState(false);
|
||||
|
||||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||||
const [showMsciBenchmark, setShowMsciBenchmark] = useState(true);
|
||||
|
||||
// Kelly Position Sizing states
|
||||
@@ -67,6 +66,158 @@ export default function SandboxDemo() {
|
||||
const [customProb, setCustomProb] = useState<number>(0.60);
|
||||
const [oddsRatio, setOddsRatio] = useState<number>(1.5);
|
||||
|
||||
// Systemic Macro Stress-Test States
|
||||
const [activeStressTab, setActiveStressTab] = useState<'FOMC Rates' | 'CPI Inflation' | 'Labor Market'>('FOMC Rates');
|
||||
const [stressLoading, setStressLoading] = useState(false);
|
||||
const [stressData, setStressData] = useState<any>(null);
|
||||
const [stressError, setStressError] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchStressTest = async () => {
|
||||
setStressLoading(true);
|
||||
setStressError(null);
|
||||
try {
|
||||
const response = await fetch('/api/sandbox/lmm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
portfolio: portfolio,
|
||||
eventType: activeStressTab
|
||||
})
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStressData(data);
|
||||
} else {
|
||||
setStressError("Fehler beim Laden der Stresstest-Daten.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Stress test fetch error:", err);
|
||||
setStressError("Netzwerkfehler beim Laden des Stresstests.");
|
||||
} finally {
|
||||
setStressLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStressTest();
|
||||
}, [portfolio, activeStressTab]);
|
||||
|
||||
// Ingested Portfolio Ingestion Cockpit States
|
||||
const [newAssetTicker, setNewAssetTicker] = useState('');
|
||||
const [newAssetShares, setNewAssetShares] = useState<number | ''>('');
|
||||
const [newAssetPrice, setNewAssetPrice] = useState<number | ''>('');
|
||||
const [portfolioPrices, setPortfolioPrices] = useState<Record<string, { currentPrice: number; name: string }>>({});
|
||||
|
||||
const MOCK_PRICES: Record<string, number> = {
|
||||
'AAPL': 185.20, 'MSFT': 415.50, 'NVDA': 945.00, 'TSLA': 178.50, 'AMD': 160.20,
|
||||
'SMCI': 820.00, 'NFLX': 610.00, 'AMZN': 182.40, 'GOOGL': 175.50, 'META': 475.00,
|
||||
'WMT': 60.50, 'JNJ': 158.30, 'PG': 162.10, 'MRK': 128.40, 'PLTR': 21.50,
|
||||
'BABA': 78.40, 'CVX': 155.20, 'XOM': 118.60, 'BAC': 38.20, 'JPM': 195.40,
|
||||
'ASML': 920.00, 'SAP': 178.50, 'MC.PA': 810.00, 'OR.PA': 440.00, 'NESN': 92.40,
|
||||
'NOVOB': 125.60, 'SHEL': 32.40, 'BP': 38.50, 'HSBC': 42.10, 'ALV.DE': 248.50,
|
||||
'VOW3.DE': 118.40, 'BMW.DE': 98.60, 'SIE.DE': 172.40, 'DTE.DE': 22.10,
|
||||
'MBG.DE': 68.45, 'BAS.DE': 48.20, 'SAN.MC': 4.50, 'BBVA.MC': 9.80,
|
||||
'BTC-USD': 65420.00, 'ETH-USD': 3480.00, 'SOL-USD': 148.50, 'ADA-USD': 0.46,
|
||||
'XRP-USD': 0.49, 'DOGE-USD': 0.14, 'DOT-USD': 6.20, 'LINK-USD': 15.40,
|
||||
'LTC-USD': 78.50, 'AVAX-USD': 32.40, 'BNB-USD': 580.00, 'TRX-USD': 0.12,
|
||||
'NEAR-USD': 5.80
|
||||
};
|
||||
|
||||
const portfolioTickers = useMemo(() => {
|
||||
return portfolio.map(p => p.ticker);
|
||||
}, [portfolio]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchPrices = async () => {
|
||||
if (portfolioTickers.length === 0) return;
|
||||
try {
|
||||
const response = await fetch(`/api/finance?tickers=${portfolioTickers.join(',')}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const pricesMap: Record<string, { currentPrice: number; name: string }> = {};
|
||||
data.results.forEach((r: any) => {
|
||||
if (!r.error) {
|
||||
pricesMap[r.ticker] = { currentPrice: r.currentPrice, name: r.name };
|
||||
}
|
||||
});
|
||||
setPortfolioPrices(prev => ({ ...prev, ...pricesMap }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching sandbox portfolio prices:", err);
|
||||
}
|
||||
};
|
||||
fetchPrices();
|
||||
}, [portfolioTickers]);
|
||||
|
||||
const getTickerPrice = (ticker: string): number => {
|
||||
if (portfolioPrices[ticker]) return portfolioPrices[ticker].currentPrice;
|
||||
const w = watchlist.find(item => item.ticker === ticker);
|
||||
if (w) return w.currentPrice;
|
||||
const h = activePortfolio.holdings.find(item => item.symbol === ticker);
|
||||
if (h) return h.currentPrice;
|
||||
return MOCK_PRICES[ticker] || 100.0;
|
||||
};
|
||||
|
||||
const getTickerName = (ticker: string): string => {
|
||||
if (portfolioPrices[ticker]) return portfolioPrices[ticker].name;
|
||||
const w = watchlist.find(item => item.ticker === ticker);
|
||||
if (w) return w.ticker + ' Corp.';
|
||||
const h = activePortfolio.holdings.find(item => item.symbol === ticker);
|
||||
if (h) return h.symbol + ' Corp.';
|
||||
return ticker + ' Corp.';
|
||||
};
|
||||
|
||||
const handleAddNewAsset = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const ticker = newAssetTicker.trim().toUpperCase();
|
||||
if (!ticker) return;
|
||||
const shares = Number(newAssetShares);
|
||||
const price = Number(newAssetPrice);
|
||||
if (isNaN(shares) || shares <= 0 || isNaN(price) || price <= 0) {
|
||||
alert("Bitte geben Sie eine gültige Stückzahl und einen Einstandskurs an.");
|
||||
return;
|
||||
}
|
||||
updatePortfolioAsset(ticker, shares, price);
|
||||
setNewAssetTicker('');
|
||||
setNewAssetShares('');
|
||||
setNewAssetPrice('');
|
||||
};
|
||||
|
||||
const portfolioCalculated = useMemo(() => {
|
||||
let totalValue = 0;
|
||||
const items = portfolio.map((asset) => {
|
||||
const currentPrice = getTickerPrice(asset.ticker);
|
||||
const name = getTickerName(asset.ticker);
|
||||
const positionValue = asset.shares * currentPrice;
|
||||
totalValue += positionValue;
|
||||
|
||||
const pnlUsd = asset.shares * (currentPrice - asset.entryPrice);
|
||||
const pnlPct = ((currentPrice - asset.entryPrice) / asset.entryPrice) * 100;
|
||||
|
||||
return {
|
||||
...asset,
|
||||
name,
|
||||
currentPrice,
|
||||
positionValue,
|
||||
pnlUsd,
|
||||
pnlPct
|
||||
};
|
||||
});
|
||||
|
||||
const itemsWithWeights = items.map((item) => {
|
||||
const weight = totalValue > 0 ? item.positionValue / totalValue : 0;
|
||||
return {
|
||||
...item,
|
||||
weight
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalValue,
|
||||
items: itemsWithWeights
|
||||
};
|
||||
}, [portfolio, portfolioPrices, watchlist, activePortfolio.holdings]);
|
||||
|
||||
// Compute Net Worth
|
||||
const netWorth = useMemo(() => {
|
||||
const assetsVal = activePortfolio.holdings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0);
|
||||
@@ -153,6 +304,14 @@ export default function SandboxDemo() {
|
||||
});
|
||||
}, [activePortfolio.historicalValues, ewmaResult]);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl min-h-[400px] flex items-center justify-center">
|
||||
<div className="text-slate-400 text-sm font-mono animate-pulse">Lade Sandbox-Modul...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Total gain/loss
|
||||
const totalGainLoss = netWorth - activePortfolio.startingBalance;
|
||||
const totalGainLossPct = (totalGainLoss / activePortfolio.startingBalance) * 100;
|
||||
@@ -243,7 +402,15 @@ export default function SandboxDemo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 w-full md:w-auto">
|
||||
<div className="flex flex-wrap gap-4 w-full md:w-auto items-center">
|
||||
<button
|
||||
onClick={() => setIsMathModalOpen(true)}
|
||||
className="flex items-center gap-1.5 px-4 py-3 rounded-xl 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-emerald-400 justify-center h-[58px] shrink-0"
|
||||
>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>📖 Modulerklärung</span>
|
||||
</button>
|
||||
|
||||
{/* Net Worth Card */}
|
||||
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[140px]">
|
||||
<div className="text-[10px] text-slate-400 uppercase font-semibold">Gesamtwert</div>
|
||||
@@ -408,6 +575,384 @@ export default function SandboxDemo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SECTION: Mein Portfolio Ingestion Cockpit */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-4">
|
||||
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Wallet className="text-emerald-400 w-5 h-5" /> Mein Portfolio Cockpit
|
||||
</h3>
|
||||
<span className="text-xs text-slate-400 font-mono">
|
||||
Gesamt-Inventarwert: <span className="text-emerald-400 font-bold font-mono">${portfolioCalculated.totalValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto border border-slate-850 rounded-xl bg-slate-950/40">
|
||||
<table className="w-full border-collapse text-left text-sm min-w-[800px]">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-3">Asset / Ticker</th>
|
||||
<th className="p-3 text-center">Stücke (Shares)</th>
|
||||
<th className="p-3 text-center">Einstandspreis</th>
|
||||
<th className="p-3 text-center">Aktueller Kurs</th>
|
||||
<th className="p-3 text-right">Positionswert</th>
|
||||
<th className="p-3 text-right">Performance (PnL)</th>
|
||||
<th className="p-3">Gewichtung (w_i)</th>
|
||||
<th className="p-3 text-center">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{portfolioCalculated.items.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="p-8 text-center text-slate-500 italic">
|
||||
Bislang keine Assets im Ingestion-Cockpit. Fügen Sie unten ein Asset hinzu.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
portfolioCalculated.items.map((item) => {
|
||||
const isPositive = item.pnlUsd >= 0;
|
||||
const weightPct = item.weight * 100;
|
||||
|
||||
return (
|
||||
<tr key={item.ticker} className="border-b border-slate-850/50 hover:bg-slate-850/20 transition-colors">
|
||||
{/* Symbol & Name */}
|
||||
<td className="p-3">
|
||||
<div className="font-bold text-slate-100 font-mono">{item.ticker}</div>
|
||||
<div className="text-[10px] text-slate-500">{item.name}</div>
|
||||
</td>
|
||||
{/* Shares (Inline input) */}
|
||||
<td className="p-3 text-center">
|
||||
<input
|
||||
type="number"
|
||||
key={`${item.ticker}-shares-${item.shares}`}
|
||||
defaultValue={item.shares}
|
||||
onBlur={(e) => {
|
||||
const val = Number(e.target.value);
|
||||
if (val > 0) {
|
||||
updatePortfolioAsset(item.ticker, val, item.entryPrice);
|
||||
} else {
|
||||
e.target.value = String(item.shares);
|
||||
}
|
||||
}}
|
||||
className="w-20 bg-slate-950 border border-slate-800 rounded px-2 py-1 text-slate-100 font-mono text-center focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
</td>
|
||||
{/* Entry Price (Inline input) */}
|
||||
<td className="p-3 text-center">
|
||||
<input
|
||||
type="number"
|
||||
key={`${item.ticker}-entry-${item.entryPrice}`}
|
||||
defaultValue={item.entryPrice}
|
||||
onBlur={(e) => {
|
||||
const val = Number(e.target.value);
|
||||
if (val > 0) {
|
||||
updatePortfolioAsset(item.ticker, item.shares, val);
|
||||
} else {
|
||||
e.target.value = String(item.entryPrice);
|
||||
}
|
||||
}}
|
||||
className="w-24 bg-slate-950 border border-slate-800 rounded px-2 py-1 text-slate-100 font-mono text-center focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
</td>
|
||||
{/* Current Price */}
|
||||
<td className="p-3 text-center font-mono text-slate-350">
|
||||
${item.currentPrice.toFixed(2)}
|
||||
</td>
|
||||
{/* Position Value */}
|
||||
<td className="p-3 text-right font-mono font-semibold text-slate-200">
|
||||
${item.positionValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
{/* PnL */}
|
||||
<td className={`p-3 text-right font-mono ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
<div className="flex items-center justify-end gap-1 font-semibold">
|
||||
{isPositive ? '+' : ''}${item.pnlUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
<div className="text-[10px]">
|
||||
{isPositive ? '+' : ''}{item.pnlPct.toFixed(2)}%
|
||||
</div>
|
||||
</td>
|
||||
{/* Weighting Progress Bar */}
|
||||
<td className="p-3 max-w-[150px]">
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<span className="font-mono text-xs text-slate-300 font-bold">{weightPct.toFixed(1)}%</span>
|
||||
<div className="w-20 bg-slate-950 rounded-full h-1.5 overflow-hidden border border-slate-800">
|
||||
<div
|
||||
className="bg-gradient-to-r from-emerald-500 to-teal-500 h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${weightPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* Actions */}
|
||||
<td className="p-3 text-center">
|
||||
<button
|
||||
onClick={() => removePortfolioAsset(item.ticker)}
|
||||
className="p-1.5 rounded-lg bg-slate-950 hover:bg-rose-950/40 text-slate-500 hover:text-rose-400 transition-colors border border-slate-850 hover:border-rose-900/30 cursor-pointer"
|
||||
title="Asset löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Adding Asset Inline Row */}
|
||||
<tr className="bg-slate-950/20 border-t border-slate-800">
|
||||
<td className="p-3">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Ticker (z.B. AAPL)"
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono text-xs uppercase focus:border-emerald-500 focus:outline-none"
|
||||
value={newAssetTicker}
|
||||
onChange={(e) => setNewAssetTicker(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
placeholder="Stücke"
|
||||
className="w-24 bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono text-xs text-center focus:border-emerald-500 focus:outline-none"
|
||||
value={newAssetShares === '' ? '' : newAssetShares}
|
||||
onChange={(e) => setNewAssetShares(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-3 text-center">
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
placeholder="Einstand ($)"
|
||||
className="w-28 bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono text-xs text-center focus:border-emerald-500 focus:outline-none"
|
||||
value={newAssetPrice === '' ? '' : newAssetPrice}
|
||||
onChange={(e) => setNewAssetPrice(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-3 text-center text-slate-500 font-mono text-xs">-</td>
|
||||
<td className="p-3 text-right text-slate-500 font-mono text-xs">-</td>
|
||||
<td className="p-3 text-right text-slate-500 font-mono text-xs">-</td>
|
||||
<td className="p-3 text-slate-500 font-mono text-xs">-</td>
|
||||
<td className="p-3 text-center">
|
||||
<button
|
||||
onClick={handleAddNewAsset}
|
||||
className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-1.5 px-3 rounded-lg text-xs shadow-md shadow-emerald-500/10 flex items-center justify-center gap-1 mx-auto transition-all active:scale-[0.96] cursor-pointer animate-pulse hover:animate-none"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> Hinzufügen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SECTION: Systemischer Makro-Stresstest (Portfolio-LMM) */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-800 pb-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<TrendingUp className="text-purple-400 w-5 h-5" /> Systemischer Makro-Stresstest (Portfolio-LMM)
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
Analysiert die historische Sensitivität des Portfolios gegenüber Kern-Makro-Ereignissen über die letzten 36 Monate.
|
||||
</p>
|
||||
</div>
|
||||
{/* Event type tabs */}
|
||||
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-850 w-full sm:w-auto shrink-0">
|
||||
{(['FOMC Rates', 'CPI Inflation', 'Labor Market'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveStressTab(tab)}
|
||||
className={`flex-1 sm:flex-none px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeStressTab === tab
|
||||
? 'bg-purple-600 text-white font-bold shadow-md shadow-purple-500/20'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
{tab === 'FOMC Rates' ? '🏦 FOMC Rates' : tab === 'CPI Inflation' ? '🎯 CPI Inflation' : '💼 Labor Market'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stressLoading ? (
|
||||
<div className="h-80 flex flex-col items-center justify-center space-y-3">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-purple-500 border-t-transparent animate-spin" />
|
||||
<span className="text-xs text-slate-400 font-mono animate-pulse">Kalkuliere Swamy-Arora GLS-Schätzer...</span>
|
||||
</div>
|
||||
) : stressError || !stressData ? (
|
||||
<div className="h-80 flex items-center justify-center text-slate-500 italic">
|
||||
{stressError || 'Keine Daten geladen.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* LMM Summary Statistics */}
|
||||
<div className="bg-slate-950/40 rounded-xl p-4 border border-slate-850 flex flex-col justify-between space-y-4">
|
||||
<div>
|
||||
<span className="text-[10px] uppercase tracking-wider text-purple-400 font-bold block mb-2">Regressions-Koeffizienten (GLS)</span>
|
||||
|
||||
{/* Fixed Effects list */}
|
||||
<div className="space-y-3">
|
||||
{stressData.regressionResults?.fixedEffects.map((fe: any) => {
|
||||
const isPositive = fe.estimate >= 0;
|
||||
const isImpact = fe.name === 'Post-Event Impact';
|
||||
return (
|
||||
<div key={fe.name} className={`flex justify-between items-center p-2 rounded-lg border ${
|
||||
isImpact ? 'bg-purple-950/20 border-purple-900/40' : 'bg-slate-950/60 border-slate-900'
|
||||
}`}>
|
||||
<div>
|
||||
<div className={`text-xs font-semibold ${isImpact ? 'text-purple-300 font-bold' : 'text-slate-350'}`}>
|
||||
{fe.name === 'Intercept' ? 'Basisschnittstelle (Intercept)' :
|
||||
fe.name === 'Pre-Event Drift' ? 'Pre-Event Trend (Drift)' :
|
||||
fe.name === 'Post-Event Impact' ? 'Systemisches Portfolio Beta' :
|
||||
'VIX-Volatilitäts-Sensitivität'}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-500">
|
||||
SE: {fe.se.toFixed(4)} | p-Wert: {fe.pVal.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`font-mono text-sm font-bold ${
|
||||
isImpact ? 'text-purple-400 text-base' :
|
||||
isPositive ? 'text-emerald-400' : 'text-rose-400'
|
||||
}`}>
|
||||
{isPositive ? '+' : ''}{fe.estimate.toFixed(4)}
|
||||
</span>
|
||||
<span className="text-purple-400 text-xs font-bold font-mono ml-1">{fe.sig}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Fit metrics */}
|
||||
<div className="border-t border-slate-850 pt-3 space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-400">R-Quadrat (Bestimmtheitsmaß):</span>
|
||||
<span className="font-mono font-bold text-slate-200">{(stressData.regressionResults?.rSquared * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-[11px] text-slate-500 font-mono">
|
||||
<span>AIC: {stressData.regressionResults?.aic}</span>
|
||||
<span>BIC: {stressData.regressionResults?.bic}</span>
|
||||
<span>Events: {stressData.eventCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recharts Area/Line Chart (2/3 width) */}
|
||||
<div className="lg:col-span-2 bg-slate-950/30 rounded-xl p-4 border border-slate-850 space-y-3">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-slate-400 font-mono">Durchschnittlicher kumulierter Ertrag im Zeitfenster [-30, +30] Tage</span>
|
||||
<span className="text-[9px] text-slate-500 font-mono">Akkumulierte Log-Renditen</span>
|
||||
</div>
|
||||
|
||||
<div className="h-64 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={stressData.chartData} margin={{ top: 10, right: 10, left: -20, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorPortfolio" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#c084fc" stopOpacity={0.2}/>
|
||||
<stop offset="95%" stopColor="#c084fc" stopOpacity={0.0}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorBenchmark" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#60a5fa" stopOpacity={0.1}/>
|
||||
<stop offset="95%" stopColor="#60a5fa" stopOpacity={0.0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
||||
<XAxis
|
||||
dataKey="relativeDay"
|
||||
stroke="#64748b"
|
||||
fontSize={10}
|
||||
tickFormatter={(v) => `T${v >= 0 ? '+' : ''}${v}`}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#64748b"
|
||||
fontSize={10}
|
||||
tickFormatter={(v) => `${v.toFixed(1)}%`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '8px', fontSize: '11px' }}
|
||||
labelFormatter={(label) => `Relativer Tag: T${label >= 0 ? '+' : ''}${label}`}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36} iconType="circle" />
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="Mein Portfolio (%)"
|
||||
stroke="#c084fc"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPortfolio)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="NASDAQ Benchmark (%)"
|
||||
stroke="#60a5fa"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorBenchmark)"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="4 4"
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="LMM Trend (%)"
|
||||
name="Purged LMM Trend (%)"
|
||||
stroke="#f43f5e"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<ReferenceLine
|
||||
x={0}
|
||||
stroke="#ef4444"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="3 3"
|
||||
label={{ value: 'Stichtag (T0)', fill: '#ef4444', fontSize: 9, position: 'top' }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quantitative Commentary Card */}
|
||||
{stressData && !stressLoading && (
|
||||
<div className="bg-purple-950/20 border border-purple-900/40 rounded-xl p-4 flex gap-3 items-start text-xs text-purple-300">
|
||||
<Sparkles className="w-5 h-5 text-purple-400 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="font-bold uppercase tracking-wider block mb-1">Quantitative Analyse-Auswertung</span>
|
||||
<p className="leading-relaxed">
|
||||
{(() => {
|
||||
const impactBeta = stressData.regressionResults?.fixedEffects.find((f: any) => f.name === 'Post-Event Impact')?.estimate || 0;
|
||||
const isNegative = impactBeta < 0;
|
||||
const absBeta = Math.abs(impactBeta).toFixed(2);
|
||||
const pVal = stressData.regressionResults?.fixedEffects.find((f: any) => f.name === 'Post-Event Impact')?.pVal || 0;
|
||||
const isSignificant = pVal < 0.05;
|
||||
|
||||
let eventNameText = activeStressTab === 'FOMC Rates' ? 'FOMC-Zinsentscheiden' :
|
||||
activeStressTab === 'CPI Inflation' ? 'CPI-Inflationsdaten' : 'Arbeitsmarktdaten';
|
||||
|
||||
let significanceText = isSignificant
|
||||
? `Dieser Effekt ist mit einem p-Wert von ${pVal.toFixed(4)} statistisch signifikant.`
|
||||
: `Dieser Effekt ist mit einem p-Wert von ${pVal.toFixed(4)} statistisch nicht hochgradig signifikant (Rauscheinfluss möglich).`;
|
||||
|
||||
if (isNegative) {
|
||||
return `Historische Reaktivität: Bei ${eventNameText} zeigt dein Depot ein negatives Beta von -${absBeta}. ${significanceText} Eine Absicherung über defensive Sektoren (z.B. Erhöhung der Bargeldquote oder defensive Consumer-Titel) senkt das Volatilitätsrisiko in dieser Post-Event-Phase um ca. ${Math.round(Math.abs(impactBeta) * 35)}%.`;
|
||||
} else {
|
||||
return `Historische Reaktivität: Bei ${eventNameText} zeigt dein Depot ein positives Beta von +${absBeta}. ${significanceText} Dein Portfolio profitiert tendenziell von der anschließenden Marktdynamik. Sie können erwägen, die Hebelwirkung durch Zukäufe in Momentum-Aktien zu erhöhen.`;
|
||||
}
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SECTION 2: Chart / Analytics & Order Form */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
|
||||
@@ -810,6 +1355,7 @@ export default function SandboxDemo() {
|
||||
|
||||
</div>
|
||||
|
||||
<PortfolioMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user