717 lines
24 KiB
TypeScript
717 lines
24 KiB
TypeScript
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<string, number[]>; // 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<string, number>; // asset -> score
|
|
isSuggestion?: Record<string, boolean>;
|
|
}[];
|
|
calendarProposals: {
|
|
id: string;
|
|
name: string;
|
|
date: string;
|
|
archetype: string;
|
|
defaultScores: Record<string, number>;
|
|
}[];
|
|
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<WatchlistItem, 'id' | 'addedAt' | 'hoursTracked' | 'reboundPerformance'>) => void;
|
|
removeFromWatchlist: (id: string) => void;
|
|
simulateWatchlistTick: () => void;
|
|
addInsiderTrade: (trade: Omit<InsiderTrade, 'id'>) => void;
|
|
addCongressTrade: (trade: Omit<CongressTrade, 'id'>) => void;
|
|
addWhaleTrade: (trade: Omit<WhaleTrade, 'id'>) => void;
|
|
addModelTrial: (isSuccess: boolean) => void;
|
|
addEventToMatrix: (name: string, date: string, scores: Record<string, number>) => 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<SandboxState>((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]
|
|
};
|
|
}),
|
|
}));
|