import { create } from 'zustand'; import { calculateAssetCorrelation, calculateAssetCovariance } from './math/statistics'; // --- Interfaces for Sandbox Portfolio --- export interface PortfolioHolding { symbol: string; wknOrIsin?: string; shares: number; avgPrice: number; currentPrice: number; hypothesisTag?: string; } export interface Transaction { id: string; type: 'BUY' | 'SELL'; symbol: string; wknOrIsin?: string; shares: number; price: number; timestamp: string; // date/time string hypothesisTag?: string; feeApplied: number; } export interface HistoricalValue { date: string; value: number; // portfolio value (cash + assets) } export interface RiskProfile { status: 'GREEN' | 'YELLOW' | 'RED'; clusterRisk: boolean; highCorrAssets: string[]; message: string; } export interface Portfolio { id: string; name: string; startingBalance: number; cash: number; holdings: PortfolioHolding[]; transactions: Transaction[]; historicalValues: HistoricalValue[]; riskProfile: RiskProfile; } export function computePortfolioRiskProfile( cash: number, holdings: PortfolioHolding[] ): RiskProfile { if (holdings.length === 0) { return { status: 'GREEN', clusterRisk: false, highCorrAssets: [], message: 'Portfolio ist leer. Keine Risiken vorhanden.' }; } const assetsVal = holdings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0); const totalVal = cash + assetsVal; if (totalVal <= 0) { return { status: 'GREEN', clusterRisk: false, highCorrAssets: [], message: 'Gesamtwert ist Null.' }; } const holdingsWithWeights = holdings.map(h => ({ symbol: h.symbol, weight: (h.shares * h.currentPrice) / totalVal })); const covResult = calculateAssetCovariance(holdingsWithWeights); let status: 'GREEN' | 'YELLOW' | 'RED' = 'GREEN'; let message = 'Gut diversifiziert. Geringe Gesamtkovarianz.'; if (covResult.clusterRisk) { status = 'RED'; message = 'Achtung: Hohe Kovarianz festgestellt. Reduziere Positionsgröße um 50%.'; } else { let yellowFlag = false; const yellowAssets: string[] = []; for (let i = 0; i < holdingsWithWeights.length; i++) { for (let j = i + 1; j < holdingsWithWeights.length; j++) { const h1 = holdingsWithWeights[i]; const h2 = holdingsWithWeights[j]; const corr = calculateAssetCorrelation(h1.symbol, h2.symbol); if (corr > 0.50 && h1.weight > 0.10 && h2.weight > 0.10) { yellowFlag = true; yellowAssets.push(`${h1.symbol}-${h2.symbol}`); } } } if (yellowFlag) { status = 'YELLOW'; message = `Moderate Überschneidungen festgestellt zwischen: ${yellowAssets.join(', ')}.`; } } return { status, clusterRisk: covResult.clusterRisk, highCorrAssets: covResult.highCorrHoldings, message }; } // --- Interfaces for Insider & Whale Trades --- export interface InsiderTrade { id: string; ticker: string; insiderName: string; relation: string; type: 'BUY' | 'SELL'; shares: number; value: number; date: string; insight?: string; } export interface CongressTrade { id: string; ticker: string; representative: string; chamber: 'HOUSE' | 'SENATE'; type: 'BUY' | 'SELL'; valueRange: string; transactionDate: string; filingDate: string; lagDays: number; insight?: string; } export interface WhaleTrade { id: string; ticker: string; institution: string; type: 'BUY' | 'SELL' | 'NEW' | 'EXIT'; sharesTraded: number; sharesHeld: number; filingDate: string; estimatedValue: number; insight?: string; } // --- Interfaces for Overreaction Scanner --- export interface ScannerAlert { id: string; ticker: string; priceChange: number; // e.g. -0.12 for -12% gjrGarchVol: number; overreactionScore: number; status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED'; } export interface PortfolioAsset { ticker: string; shares: number; entryPrice: number; } export interface WatchlistItem { id: string; ticker: string; priceChange: number; sentiment: 'GREEN' | 'YELLOW' | 'RED'; whyDropped: string; addedAt: string; hoursTracked: number; initialPrice: number; currentPrice: number; reboundPerformance: number; } // --- Zustand Store Interface --- interface SandboxState { // 1. Sandbox Portfolios portfolios: Portfolio[]; activePortfolioId: string; ewmaLambda: number; portfolio: PortfolioAsset[]; // 2. Overreaction Scanner State scanThreshold: number; scannerAlerts: ScannerAlert[]; watchlist: WatchlistItem[]; // 3. Insider / Whale Tracker State insiderTrades: InsiderTrade[]; congressTrades: CongressTrade[]; whaleTrades: WhaleTrade[]; insiderVolumes: Record; // Ticker -> 24 months volumes // 4. Crypto Bayesian State priorProbability: number; likelihoodPositive: number; posteriorProbability: number; alphaSuccess: number; betaFailure: number; // 5. Econometric Events State selectedModel: 'ROC' | 'SURVIVAL' | 'LMM'; eventsMatrix: { id: string; name: string; date: string; scores: Record; // asset -> score isSuggestion?: Record; }[]; calendarProposals: { id: string; name: string; date: string; archetype: string; defaultScores: Record; }[]; lmmObservations: { asset: string; eventType: string; eventName?: string; score?: number; vix: number; trend: number; returnVal: number; }[]; assetsList: { name: string; symbol: string; }[]; lmmResults?: { fixedEffects: { name: string; estimate: number; se: number; pVal: number; sig: string; ciLower: number; ciUpper: number; }[]; randomEffects: { asset: string; intercept: number; }[]; randomEffectsVariance: { interceptVar: number; vixSlopeVar: number; eventMemoryVar: number; residualVar: number; }; aic: number; bic: number; rSquared: number; roc?: { points: { fpr: number; tpr: number; threshold: number }[]; auc: number; maxYouden: number; optimalThreshold: number; }; survival?: { points: { time: number; highConvRate: number; lowConvRate: number }[]; observationCount: number; }; }; // Actions createPortfolio: (name: string, startingBalance: number) => void; setActivePortfolio: (id: string) => void; executeTransaction: ( portfolioId: string, symbol: string, wknOrIsin: string, type: 'BUY' | 'SELL', shares: number, price: number, simulateFees: boolean, isBackfill: boolean, backfillDate: string, hypothesisTag: string ) => boolean; // returns success setEwmaLambda: (lambda: number) => void; updateScannerAlerts: (alerts: ScannerAlert[]) => void; addToWatchlist: (item: Omit) => void; removeFromWatchlist: (id: string) => void; simulateWatchlistTick: () => void; addInsiderTrade: (trade: Omit) => void; addCongressTrade: (trade: Omit) => void; addWhaleTrade: (trade: Omit) => void; addModelTrial: (isSuccess: boolean) => void; addEventToMatrix: (name: string, date: string, scores: Record) => void; updateMatrixCell: (eventId: string, asset: string, score: number) => void; runEndogenousLMMCalibration: () => void; updateBayesPrior: (prior: number) => void; updateBayesLikelihood: (likelihood: number) => void; setSelectedModel: (model: 'ROC' | 'SURVIVAL' | 'LMM') => void; updatePortfolioAsset: (ticker: string, shares: number, entryPrice: number) => void; removePortfolioAsset: (ticker: string) => void; } // --- Helper: Generate Initial Historical Data --- const generateHistoricalData = (startVal: number, days: number, growthRate: number): HistoricalValue[] => { const data: HistoricalValue[] = []; const date = new Date('2026-05-15'); let currentVal = startVal; for (let i = 0; i < days; i++) { const dStr = date.toISOString().slice(0, 10); data.push({ date: dStr, value: Math.round(currentVal) }); date.setDate(date.getDate() + 1); currentVal = currentVal * (1 + (Math.random() - 0.45) * growthRate); } return data; }; // --- Zustand Store Implementation --- export const useSandboxStore = create((set, get) => ({ // 1. Portfolio State portfolios: [ { id: 'p1', name: 'Tech Breakout Sandbox', startingBalance: 100000, cash: 21374, holdings: [ { symbol: 'AAPL', wknOrIsin: '865985', shares: 150, avgPrice: 172.5, currentPrice: 182.2, hypothesisTag: 'Premium Product Lock-in' }, { symbol: 'MSFT', wknOrIsin: '870747', shares: 80, avgPrice: 388.0, currentPrice: 415.5, hypothesisTag: 'Enterprise AI Lead' }, { symbol: 'NVDA', wknOrIsin: '918422', shares: 45, avgPrice: 910.0, currentPrice: 945.0, hypothesisTag: 'GPU Demand Dominance' }, ], transactions: [ { id: 't1', type: 'BUY', symbol: 'AAPL', wknOrIsin: '865985', shares: 150, price: 172.5, timestamp: '2026-05-18 10:15', hypothesisTag: 'Premium Product Lock-in', feeApplied: 64.69 }, { id: 't2', type: 'BUY', symbol: 'MSFT', wknOrIsin: '870747', shares: 80, price: 388.0, timestamp: '2026-05-20 14:30', hypothesisTag: 'Enterprise AI Lead', feeApplied: 77.6 }, { id: 't3', type: 'BUY', symbol: 'NVDA', wknOrIsin: '918422', shares: 45, price: 910.0, timestamp: '2026-05-25 15:45', hypothesisTag: 'GPU Demand Dominance', feeApplied: 102.38 }, ], historicalValues: generateHistoricalData(100000, 22, 0.018), riskProfile: { status: 'RED', clusterRisk: true, highCorrAssets: ['AAPL', 'MSFT', 'NVDA'], message: 'Achtung: Hohe Kovarianz festgestellt. Reduziere Positionsgröße um 50%.' } }, { id: 'p2', name: 'Dividenden Defensive Sandbox', startingBalance: 50000, cash: 14750, holdings: [ { symbol: 'KO', wknOrIsin: '850663', shares: 350, avgPrice: 58.5, currentPrice: 62.4, hypothesisTag: 'Inflation-resistant Consumer Goods' }, { symbol: 'JNJ', wknOrIsin: '853260', shares: 80, avgPrice: 152.0, currentPrice: 158.3, hypothesisTag: 'Stable Healthcare Cashflows' }, ], transactions: [ { id: 't4', type: 'BUY', symbol: 'KO', wknOrIsin: '850663', shares: 350, price: 58.5, timestamp: '2026-05-16 09:30', hypothesisTag: 'Inflation-resistant Consumer Goods', feeApplied: 51.19 }, { id: 't5', type: 'BUY', symbol: 'JNJ', wknOrIsin: '853260', shares: 80, price: 152.0, timestamp: '2026-05-22 11:20', hypothesisTag: 'Stable Healthcare Cashflows', feeApplied: 30.4 }, ], historicalValues: generateHistoricalData(50000, 22, 0.007), riskProfile: { status: 'YELLOW', clusterRisk: false, highCorrAssets: [], message: 'Moderate Überschneidungen festgestellt zwischen: KO-JNJ.' } } ], activePortfolioId: 'p1', ewmaLambda: 0.94, portfolio: [ { ticker: 'AAPL', shares: 150, entryPrice: 172.5 }, { ticker: 'MSFT', shares: 80, entryPrice: 388.0 }, { ticker: 'BTC-USD', shares: 1.5, entryPrice: 62000.0 } ], // 2. Overreaction Scanner Defaults scanThreshold: -0.05, scannerAlerts: [], watchlist: [], // 3. Insider / Whale Defaults insiderTrades: [], congressTrades: [], whaleTrades: [], insiderVolumes: { 'PLTR': [30000, 25000, 45000, 18000, 22000, 31000, 27000, 36000, 29000, 40000, 33000, 150000], // 12-month rolling (scaled down representation for monthly) 'RACE': [8000, 6000, 7500, 9000, 5200, 7100, 6800, 9500, 8100, 10200, 9300, 30000], 'AMZN': [45000, 52000, 48000, 61000, 49000, 53000, 50000, 55000, 42000, 59000, 48000, 50000], 'AAPL': [12000, 15000, 11000, 13000, 14000, 16000, 12000, 13000, 15000, 11000, 13000, 14000], 'MSFT': [10000, 8000, 12000, 9000, 11000, 13000, 10000, 14000, 11000, 10000, 12000, 15000] }, // 4. Crypto Bayes Defaults priorProbability: 0.45, likelihoodPositive: 0.72, posteriorProbability: 0.72, alphaSuccess: 394, betaFailure: 118, // 5. Econometric Events Defaults selectedModel: 'ROC', eventsMatrix: [ { id: 'ev1', name: 'FED Zinsentscheid', date: '2026-05-14', scores: { Apple: 1, NASDAQ: 2, Gold: -1, Bitcoin: 2 } }, { id: 'ev2', name: 'US Wahlen (Präsidentschaft)', date: '2026-11-03', scores: { Apple: 2, NASDAQ: 1, Gold: 3, Bitcoin: 2 } }, { id: 'ev3', name: 'SpaceX IPO (Gerüchte)', date: '2026-06-25', scores: { Apple: 0, NASDAQ: 2, Gold: -1, Bitcoin: 1 } }, ], assetsList: [ { name: 'Apple', symbol: 'AAPL' }, { name: 'NASDAQ', symbol: '^IXIC' }, { name: 'Gold', symbol: 'GLD' }, { name: 'Bitcoin', symbol: 'BTC-USD' } ], calendarProposals: [ { id: 'cp1', name: 'CPI Inflationsdaten', date: '2026-06-12', archetype: 'Macro Announcement', defaultScores: { Apple: 1, NASDAQ: 2, Gold: -2, Bitcoin: 1 } }, { id: 'cp2', name: 'US Non-Farm Payrolls', date: '2026-06-15', archetype: 'Employment Report', defaultScores: { Apple: 0, NASDAQ: 1, Gold: -1, Bitcoin: 0 } }, { id: 'cp3', name: 'EZB Pressekonferenz', date: '2026-06-18', archetype: 'Central Bank Policy', defaultScores: { Apple: -1, NASDAQ: -1, Gold: 2, Bitcoin: 1 } }, ], lmmObservations: [ { asset: 'Apple', eventType: 'BULLISH', eventName: 'Fed-Zinsentscheid (FOMC)', score: 1, vix: 14.2, trend: 0.02, returnVal: 0.018 }, { asset: 'NASDAQ', eventType: 'BULLISH', eventName: 'Fed-Zinsentscheid (FOMC)', score: 2, vix: 15.5, trend: 0.015, returnVal: 0.022 }, { asset: 'Gold', eventType: 'BEARISH', eventName: 'Fed-Zinsentscheid (FOMC)', score: -1, vix: 22.1, trend: -0.01, returnVal: -0.005 }, { asset: 'Bitcoin', eventType: 'BULLISH', eventName: 'Fed-Zinsentscheid (FOMC)', score: 2, vix: 18.4, trend: 0.03, returnVal: 0.035 }, { asset: 'Apple', eventType: 'BEARISH', eventName: 'US-Inflationsdaten (CPI)', score: -1, vix: 16.8, trend: -0.005, returnVal: -0.012 }, { asset: 'NASDAQ', eventType: 'BEARISH', eventName: 'US-Inflationsdaten (CPI)', score: -2, vix: 20.2, trend: -0.01, returnVal: -0.018 }, ], lmmResults: undefined, // --- Actions --- createPortfolio: (name, startingBalance) => set((state) => { const newPort: Portfolio = { id: 'p_' + Math.random().toString(36).substring(7), name, startingBalance, cash: startingBalance, holdings: [], transactions: [], historicalValues: generateHistoricalData(startingBalance, 22, 0.005), riskProfile: { status: 'GREEN', clusterRisk: false, highCorrAssets: [], message: 'Portfolio ist leer. Keine Risiken vorhanden.' } }; return { portfolios: [...state.portfolios, newPort], activePortfolioId: newPort.id, }; }), setActivePortfolio: (id) => set({ activePortfolioId: id }), executeTransaction: ( portfolioId, symbol, wknOrIsin, type, shares, price, simulateFees, isBackfill, backfillDate, hypothesisTag ) => { let success = false; set((state) => { const portfoliosCopy = state.portfolios.map((p) => { if (p.id !== portfolioId) return p; const totalCost = shares * price; // Fee calculation: fixed $4.90 or 0.25% of volume, whichever is larger const fee = simulateFees ? Math.max(4.90, totalCost * 0.0025) : 0; const netCost = totalCost + fee; const netRevenue = totalCost - fee; if (type === 'BUY' && p.cash < netCost) { return p; // insufficient cash } let newCash = p.cash; let newHoldings = [...p.holdings]; if (type === 'BUY') { success = true; newCash -= netCost; const index = newHoldings.findIndex(h => h.symbol === symbol || (wknOrIsin && h.wknOrIsin === wknOrIsin)); if (index >= 0) { const h = newHoldings[index]; const totalShares = h.shares + shares; const avgPrice = (h.shares * h.avgPrice + totalCost) / totalShares; newHoldings[index] = { ...h, shares: totalShares, avgPrice, currentPrice: price, hypothesisTag }; } else { newHoldings.push({ symbol, wknOrIsin, shares, avgPrice: price, currentPrice: price, hypothesisTag }); } } else { // Sell const index = newHoldings.findIndex(h => h.symbol === symbol || (wknOrIsin && h.wknOrIsin === wknOrIsin)); if (index < 0 || newHoldings[index].shares < shares) { return p; // insufficient shares } success = true; newCash += netRevenue; const h = newHoldings[index]; const remainingShares = h.shares - shares; if (remainingShares === 0) { newHoldings = newHoldings.filter((_, i) => i !== index); } else { newHoldings[index] = { ...h, shares: remainingShares, currentPrice: price }; } } const dateStr = isBackfill && backfillDate ? backfillDate : new Date().toISOString().slice(0, 16).replace('T', ' '); const newTx: Transaction = { id: 't_' + Math.random().toString(36).substring(7), type, symbol, wknOrIsin, shares, price, timestamp: dateStr, hypothesisTag, feeApplied: fee, }; // Recalculate historicalValues to reflect current cash + asset valuations over time // Just scale historical values relative to current net worth const currentNetWorth = newCash + newHoldings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0); const oldNetWorth = p.cash + p.holdings.reduce((sum, h) => sum + h.shares * h.currentPrice, 0); let newHistory = p.historicalValues; if (oldNetWorth > 0) { const ratio = currentNetWorth / oldNetWorth; newHistory = p.historicalValues.map(hv => ({ ...hv, value: Math.round(hv.value * ratio) })); } const updatedRisk = computePortfolioRiskProfile(newCash, newHoldings); return { ...p, cash: Math.round(newCash * 100) / 100, holdings: newHoldings, transactions: [newTx, ...p.transactions], historicalValues: newHistory, riskProfile: updatedRisk, }; }); return { portfolios: portfoliosCopy }; }); return success; }, setEwmaLambda: (ewmaLambda) => set({ ewmaLambda }), updateScannerAlerts: (scannerAlerts) => set({ scannerAlerts }), updatePortfolioAsset: (ticker, shares, entryPrice) => set((state) => { const existingIndex = state.portfolio.findIndex(p => p.ticker === ticker); let newPortfolio = [...state.portfolio]; if (existingIndex !== -1) { if (shares <= 0) { newPortfolio.splice(existingIndex, 1); } else { newPortfolio[existingIndex] = { ticker, shares, entryPrice }; } } else if (shares > 0) { newPortfolio.push({ ticker, shares, entryPrice }); } return { portfolio: newPortfolio }; }), removePortfolioAsset: (ticker) => set((state) => ({ portfolio: state.portfolio.filter(p => p.ticker !== ticker) })), addToWatchlist: (item) => set((state) => { const newItem: WatchlistItem = { ...item, id: 'w_' + Math.random().toString(36).substring(7), addedAt: new Date().toISOString().slice(0, 16).replace('T', ' '), hoursTracked: 0, reboundPerformance: 0, }; if (state.watchlist.some(w => w.ticker === item.ticker)) { return {}; } return { watchlist: [...state.watchlist, newItem] }; }), removeFromWatchlist: (id) => set((state) => ({ watchlist: state.watchlist.filter(w => w.id !== id) })), simulateWatchlistTick: () => set((state) => { const updated = state.watchlist.map((item) => { if (item.hoursTracked >= 48) return item; const newHours = Math.min(48, item.hoursTracked + 4); let hourlyChange = 0; if (item.sentiment === 'GREEN') { hourlyChange = (Math.random() * 0.8 + 0.1) / 100; } else if (item.sentiment === 'YELLOW') { hourlyChange = (Math.random() * 0.6 - 0.25) / 100; } else { hourlyChange = (Math.random() * 0.4 - 0.5) / 100; } const newPrice = item.currentPrice * (1 + hourlyChange); const perf = ((newPrice - item.initialPrice) / item.initialPrice) * 100; return { ...item, hoursTracked: newHours, currentPrice: Math.round(newPrice * 100) / 100, reboundPerformance: Math.round(perf * 100) / 100, }; }); return { watchlist: updated }; }), addInsiderTrade: (trade) => set((state) => ({ insiderTrades: [ { ...trade, id: Math.random().toString(36).substring(7) }, ...state.insiderTrades ] })), addCongressTrade: (trade) => set((state) => ({ congressTrades: [ { ...trade, id: 'c_' + Math.random().toString(36).substring(7) }, ...state.congressTrades ] })), addWhaleTrade: (trade) => set((state) => ({ whaleTrades: [ { ...trade, id: 'w_' + Math.random().toString(36).substring(7) }, ...state.whaleTrades ] })), addModelTrial: (isSuccess) => set((state) => { const newAlpha = isSuccess ? state.alphaSuccess + 1 : state.alphaSuccess; const newBeta = !isSuccess ? state.betaFailure + 1 : state.betaFailure; return { alphaSuccess: newAlpha, betaFailure: newBeta }; }), updateBayesPrior: (priorProbability) => { const { likelihoodPositive } = get(); const falsePositiveRate = 0.3; const marginalLikelihood = likelihoodPositive * priorProbability + falsePositiveRate * (1 - priorProbability); const posterior = (likelihoodPositive * priorProbability) / (marginalLikelihood || 1); set({ priorProbability, posteriorProbability: posterior, }); }, updateBayesLikelihood: (likelihoodPositive) => { const { priorProbability } = get(); const falsePositiveRate = 0.3; const marginalLikelihood = likelihoodPositive * priorProbability + falsePositiveRate * (1 - priorProbability); const posterior = (likelihoodPositive * priorProbability) / (marginalLikelihood || 1); set({ likelihoodPositive, posteriorProbability: posterior, }); }, setSelectedModel: (selectedModel) => set({ selectedModel }), addEventToMatrix: (name, date, scores) => set((state) => ({ eventsMatrix: [ ...state.eventsMatrix, { id: 'ev_' + Math.random().toString(36).substring(7), name, date, scores } ], calendarProposals: state.calendarProposals.filter(cp => cp.name !== name) })), updateMatrixCell: (eventId, asset, score) => set((state) => ({ eventsMatrix: state.eventsMatrix.map(ev => ev.id === eventId ? { ...ev, scores: { ...ev.scores, [asset]: score } } : ev ) })), runEndogenousLMMCalibration: () => set((state) => { const calibratedMatrix = state.eventsMatrix.map((ev) => { const updatedScores = { ...ev.scores }; Object.keys(updatedScores).forEach((asset) => { const currentScore = updatedScores[asset]; const delta = Math.sin(ev.name.charCodeAt(0) + asset.charCodeAt(0)) * 0.6; const newScore = Math.min(3, Math.max(-3, Math.round(currentScore + delta))); updatedScores[asset] = newScore; }); return { ...ev, scores: updatedScores }; }); const newObs = { asset: 'Apple', eventType: 'BULLISH', vix: 15.0 + Math.random() * 5, trend: 0.01 + Math.random() * 0.02, returnVal: 0.02 + Math.random() * 0.01 }; return { eventsMatrix: calibratedMatrix, lmmObservations: [...state.lmmObservations, newObs] }; }), }));