feat: complete core 5 elements and risk layer architecture

This commit is contained in:
Antigravity Agent
2026-06-06 21:11:16 +02:00
commit 96f7643f8a
29 changed files with 12336 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
name: Antigravity Agent Issue Resolver
on:
issues:
types: [labeled]
jobs:
resolve_issue:
if: github.event.label.name == 'enhancement' || github.event.label.name == 'bug' || github.event.label.name == 'agent-resolve'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Trigger Agentic Workflow
run: |
echo "Triggering Antigravity agentic workflow for Issue #${{ github.event.issue.number }}"
curl -X POST \
-H "Authorization: Bearer ${{ secrets.ANTIGRAVITY_AGENT_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"issue_number": ${{ github.event.issue.number }},
"title": "${{ github.event.issue.title }}",
"body": "${{ github.event.issue.body }}",
"label": "${{ github.event.label.name }}",
"repository": "${{ github.repository }}"
}' \
https://api.antigravity.ai/v1/workflows/trigger

53
.gitignore vendored Normal file
View File

@@ -0,0 +1,53 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# conda envs, databases, and logs
.conda/
conda-meta/
envs/
*.log
*.sqlite
.env.local
.env.development.local
.env.test.local
.env.production.local

5
AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

85
GITHUB_WORKFLOW.md Normal file
View File

@@ -0,0 +1,85 @@
# Antigravity Automated Issue-Resolution Workflow
This guide details the automated pipeline for listening to, processing, resolving, and verifying GitHub issues using the Antigravity agentic system.
---
## 1. Workflow Architecture
```mermaid
sequenceDiagram
participant GH as GitHub Webhook / Action
participant OR as Orchestrator Agent
participant IP as Implementation Planner Agent
participant DEV as Developer Agent
participant CI as CI Build & Browser Verifier
GH->>OR: Webhook: Issue Labeled ('agent-resolve')
Note over OR: Parses issue details &<br/>checks config
OR->>OR: Git Branch Checkout (feature/issue-N)
OR->>IP: Spawns Planner Agent with prompt
IP->>IP: Researches codebase & generates plan
IP-->>OR: Returns plan
OR->>DEV: Invokes Developer Agent to write code
DEV->>DEV: Modifies files & implements feature
DEV-->>OR: Code complete
OR->>CI: Runs verification (npm run build & test)
CI-->>OR: Compilation / Test Success
OR->>GH: Git Commit & Push. Auto-creates Pull Request
Note over GH: GitHub Action checks pass.<br/>Human reviews & merges PR.
```
---
## 2. Webhook Payload Specifications
When an issue is labeled (e.g. `agent-resolve`, `enhancement`, `bug`), GitHub triggers a webhook with the following JSON structure sent to the Orchestrator endpoint:
```json
{
"action": "labeled",
"issue": {
"number": 12,
"title": "IMPLEMENT RISK MANAGEMENT UPGRADE: #ISSUE-005 - Kelly Allocation",
"body": "Detailed objective description...",
"state": "open",
"labels": [
{
"name": "agent-resolve"
}
]
},
"repository": {
"full_name": "jannr/investment-sandbox",
"html_url": "https://github.com/jannr/investment-sandbox"
},
"sender": {
"login": "jannr"
}
}
```
---
## 3. Step-by-Step Execution Lifecycle
### Step 3.1: Parsing and Routing
The Orchestrator Agent parses the webhook payload, maps labels using `github-agent-config.json`, and triggers the workflow.
### Step 3.2: Branching Strategy
The system automatically creates a branch:
- For enhancements: `feature/issue-{issue_number}`
- For bugs: `bugfix/issue-{issue_number}`
### Step 3.3: Planning & Coding
1. The Orchestrator launches an **Implementation Planner Agent** to search the codebase and write an `implementation_plan.md` draft.
2. Once the plan is approved (autonomously or by a maintainer), the **Developer Agent** modifies the target code files.
### Step 3.4: Automated Verification
Before pushing changes, the Orchestrator runs:
- `npm run build` to verify Next.js/TypeScript compilations.
- Virtual browser tests (Playwright/Puppeteer stubs) to simulate UI button interactions, state modifications, and graph renderings.
### Step 3.5: PR Creation and Merge
1. The agent pushes the branch to remote and creates a PR using the GitHub API:
`POST /repos/{owner}/{repo}/pulls`
2. Once merged by a human, the branch is deleted and the issue is closed.

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

33
app/layout.tsx Normal file
View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "QuantSandbox Ökonometrie - KI-gestützte Investment-Sandbox",
description: "Sophisticated econometric and statistical investment analysis dashboard, including GJR-GARCH, EWMA volatility modeling, insider whale trackers, and Bayesian online learning.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

125
app/page.tsx Normal file
View File

@@ -0,0 +1,125 @@
'use client';
import React, { useState } from 'react';
import SandboxDemo from '@/components/modules/sandbox/SandboxDemo';
import ScannerDemo from '@/components/modules/scanner/ScannerDemo';
import InsiderDemo from '@/components/modules/insider/InsiderDemo';
import CryptoDemo from '@/components/modules/crypto/CryptoDemo';
import EventsDemo from '@/components/modules/events/EventsDemo';
import { BarChart3, TrendingUp, ShieldAlert, Radio, Landmark, RefreshCw } from 'lucide-react';
export default function Home() {
const [activeTab, setActiveTab] = useState<'sandbox' | 'scanner' | 'insider' | 'crypto' | 'events'>('sandbox');
return (
<div className="min-h-screen bg-[#070b13] text-slate-100 flex flex-col font-sans selection:bg-teal-500/30 selection:text-teal-200">
{/* Background Decorative Blur Gradients */}
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-blue-900/20 rounded-full blur-[140px] pointer-events-none -z-10" />
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-teal-900/20 rounded-full blur-[140px] pointer-events-none -z-10" />
{/* Main Header / Navigation */}
<header className="border-b border-slate-900 bg-slate-950/80 backdrop-blur-md sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-20 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-teal-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-teal-500/20">
<TrendingUp className="w-6 h-6 text-slate-950 stroke-[2.5]" />
</div>
<div>
<h1 className="text-xl font-bold bg-gradient-to-r from-teal-400 via-sky-300 to-indigo-400 bg-clip-text text-transparent">
QuantSandbox &Ouml;konometrie
</h1>
<p className="text-[10px] text-slate-400 uppercase tracking-widest font-mono">KI-gestützte Investment-Sandbox</p>
</div>
</div>
<div className="hidden md:flex items-center gap-6 text-xs text-slate-400">
<div className="flex items-center gap-2 border-r border-slate-800 pr-4">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span>Verbindung aktiv</span>
</div>
<div className="flex items-center gap-1.5 font-mono">
<span className="text-slate-500">Workspace:</span>
<span className="text-slate-300 font-bold bg-slate-900 px-2 py-0.5 rounded border border-slate-800">investment-sandbox</span>
</div>
</div>
</div>
</header>
{/* Hero Banner Section */}
<section className="bg-slate-950/40 border-b border-slate-900 py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6">
<div className="space-y-2 max-w-2xl">
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-teal-500/10 text-teal-400 border border-teal-500/20 uppercase tracking-widest">
Scaffolding Version 1.0.0
</span>
<h2 className="text-3xl font-extrabold text-white tracking-tight sm:text-4xl">
Statistische Investment-Analyse & Sandbox
</h2>
<p className="text-slate-400 text-sm leading-relaxed">
Diese modulare Architektur bündelt fortgeschrittene &ouml;konometrische Modelle &ndash; von EWMA und GJR-GARCH Volatilit&auml;tsprognosen über Bayesianisches On-Chain-Lernen bis hin zu Überlebens- und LMM-Regressionsmodellen.
</p>
</div>
<div className="flex flex-wrap gap-2 bg-slate-950/90 p-1.5 rounded-2xl border border-slate-800/80 w-full lg:w-auto">
<button
onClick={() => setActiveTab('sandbox')}
className={`flex-1 lg:flex-none px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all ${activeTab === 'sandbox' ? 'bg-gradient-to-r from-teal-500 to-emerald-500 text-slate-950 font-bold shadow-lg shadow-teal-500/25' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'}`}
>
<TrendingUp className="w-4 h-4" /> Sandbox
</button>
<button
onClick={() => setActiveTab('scanner')}
className={`flex-1 lg:flex-none px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all ${activeTab === 'scanner' ? 'bg-gradient-to-r from-amber-500 to-orange-500 text-slate-950 font-bold shadow-lg shadow-amber-500/25' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'}`}
>
<ShieldAlert className="w-4 h-4" /> Scanner
</button>
<button
onClick={() => setActiveTab('insider')}
className={`flex-1 lg:flex-none px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all ${activeTab === 'insider' ? 'bg-gradient-to-r from-purple-500 to-indigo-500 text-white font-bold shadow-lg shadow-purple-500/25' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'}`}
>
<Radio className="w-4 h-4" /> Insider
</button>
<button
onClick={() => setActiveTab('crypto')}
className={`flex-1 lg:flex-none px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all ${activeTab === 'crypto' ? 'bg-gradient-to-r from-cyan-500 to-sky-500 text-slate-950 font-bold shadow-lg shadow-cyan-500/25' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'}`}
>
<BarChart3 className="w-4 h-4" /> Krypto Bayes
</button>
<button
onClick={() => setActiveTab('events')}
className={`flex-1 lg:flex-none px-4 py-2.5 rounded-xl text-xs font-semibold flex items-center justify-center gap-2 transition-all ${activeTab === 'events' ? 'bg-gradient-to-r from-rose-500 to-pink-500 text-slate-950 font-bold shadow-lg shadow-rose-500/25' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/50'}`}
>
<Landmark className="w-4 h-4" /> Ökonometrie
</button>
</div>
</div>
</div>
</section>
{/* Main Content Area */}
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 w-full">
{/* Active Module Rendering */}
<div className="transition-all duration-300 transform opacity-100 translate-y-0">
{activeTab === 'sandbox' && <SandboxDemo />}
{activeTab === 'scanner' && <ScannerDemo />}
{activeTab === 'insider' && <InsiderDemo />}
{activeTab === 'crypto' && <CryptoDemo />}
{activeTab === 'events' && <EventsDemo />}
</div>
</main>
{/* Footer */}
<footer className="border-t border-slate-900 bg-slate-950/60 py-6 text-center text-xs text-slate-500 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row justify-between items-center gap-4">
<p>&copy; 2026 QuantSandbox Inc. Alle Rechte vorbehalten.</p>
<div className="flex items-center gap-4">
<span className="flex items-center gap-1"><RefreshCw className="w-3.5 h-3.5 text-teal-400 animate-spin-slow" /> Zustand State Engine</span>
<span>|</span>
<span>Next.js App Router (TypeScript + Tailwind v4)</span>
</div>
</div>
</footer>
</div>
);
}

View File

@@ -0,0 +1,487 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore } from '@/lib/store';
import { predictCryptoTrend, calculateBetaPosterior } from '@/lib/math/statistics';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Cpu, Search, RefreshCw, BarChart2, TrendingUp, AlertCircle, Info,
ChevronDown, ChevronUp, ArrowUpRight, ArrowDownRight, Compass, ShieldAlert, Sparkles
} from 'lucide-react';
interface CoinData {
ticker: string;
name: string;
price: string;
change24h: number;
fundingRate: number; // in %
openInterestChange: number; // in %
longShortRatio: number;
whaleInflow: number; // net flows
exchangeReserves: number; // in %
liqLong: string;
liqShort: string;
}
const defaultCoins: Record<string, CoinData> = {
'BTC': {
ticker: 'BTC',
name: 'Bitcoin',
price: '$69,450',
change24h: 2.4,
fundingRate: -0.015,
openInterestChange: 8.2,
longShortRatio: 0.92,
whaleInflow: 480,
exchangeReserves: -1.4,
liqLong: '$68,200',
liqShort: '$70,500'
},
'ETH': {
ticker: 'ETH',
name: 'Ethereum',
price: '$3,820',
change24h: -1.2,
fundingRate: 0.045,
openInterestChange: -3.5,
longShortRatio: 1.34,
whaleInflow: -120,
exchangeReserves: 0.8,
liqLong: '$3,710',
liqShort: '$3,920'
},
'SOL': {
ticker: 'SOL',
name: 'Solana',
price: '$184.20',
change24h: 5.8,
fundingRate: 0.082,
openInterestChange: 14.5,
longShortRatio: 1.62,
whaleInflow: 1250,
exchangeReserves: -2.8,
liqLong: '$176.00',
liqShort: '$192.50'
}
};
export default function CryptoDemo() {
const { alphaSuccess, betaFailure, addModelTrial } = useSandboxStore();
const [activeTicker, setActiveTicker] = useState<string>('BTC');
const [searchQuery, setSearchQuery] = useState('');
const [customCoins, setCustomCoins] = useState<Record<string, CoinData>>({});
const [searchError, setSearchError] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
const [simulatedTrialLogged, setSimulatedTrialLogged] = useState(false);
const [lastTrialSuccess, setLastTrialSuccess] = useState(false);
// Active Coin data retrieval
const activeCoin = useMemo(() => {
return customCoins[activeTicker] || defaultCoins[activeTicker] || defaultCoins['BTC'];
}, [activeTicker, customCoins]);
// Compute live Random Forest baseline predictions
const mlPredictions = useMemo(() => {
const inputs = {
fundingRate: activeCoin.fundingRate,
openInterestChange: activeCoin.openInterestChange,
longShortRatio: activeCoin.longShortRatio,
whaleInflow: activeCoin.whaleInflow
};
return predictCryptoTrend(inputs);
}, [activeCoin]);
// Apply Bayesian online learning error-correction posterior update
const correctedPredictions = useMemo(() => {
// Correct short term probability
const shortTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.shortTermProb,
12 // pseudo weight/confidence scale
);
// Correct medium term probability
const mediumTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.mediumTermProb,
12
);
return {
shortTerm: shortTermCorrected,
mediumTerm: mediumTermCorrected
};
}, [mlPredictions, alphaSuccess, betaFailure]);
// Perform search check for Altcoins
const handleAltcoinSearch = (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
if (defaultCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
if (customCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
// Generate simulated data for Altcoin
const isBull = Math.random() > 0.45;
const simulatedChange = isBull ? 3 + Math.random() * 8 : -2 - Math.random() * 6;
const simulatedPrice = isBull ? 2 + Math.random() * 10 : 0.2 + Math.random() * 3;
const newCoin: CoinData = {
ticker: query,
name: `${query} Token`,
price: `$${simulatedPrice.toFixed(4)}`,
change24h: parseFloat(simulatedChange.toFixed(2)),
fundingRate: parseFloat((Math.random() * 0.12 - 0.04).toFixed(3)),
openInterestChange: parseFloat((Math.random() * 30 - 10).toFixed(1)),
longShortRatio: parseFloat((0.8 + Math.random() * 1.1).toFixed(2)),
whaleInflow: Math.floor(Math.random() * 1500 - 400),
exchangeReserves: parseFloat((Math.random() * 4 - 2).toFixed(1)),
liqLong: `$${(simulatedPrice * 0.9).toFixed(4)}`,
liqShort: `$${(simulatedPrice * 1.1).toFixed(4)}`
};
setCustomCoins(prev => ({ ...prev, [query]: newCoin }));
setActiveTicker(query);
setSearchQuery('');
};
const handleSimulateTrial = (success: boolean) => {
addModelTrial(success);
setLastTrialSuccess(success);
setSimulatedTrialLogged(true);
setTimeout(() => setSimulatedTrialLogged(false), 2500);
};
const totalTrials = alphaSuccess + betaFailure;
const priorAccuracy = (alphaSuccess / (totalTrials || 1)) * 100;
return (
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-cyan-500/10 rounded-full blur-3xl -z-10" />
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 border-b border-slate-800 pb-4 mb-6">
<div>
<span className="text-cyan-400 text-xs font-semibold uppercase tracking-wider">Element 4</span>
<h2 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-sky-200 bg-clip-text text-transparent">
Predictive Krypto-Modelle & Bayes Self-Correction
</h2>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-2 flex items-center gap-3">
<Cpu className="text-cyan-400 w-5 h-5 animate-spin-slow" />
<div>
<p className="text-slate-400 text-xs">A-Priori Genauigkeit</p>
<p className="font-mono text-sm font-bold text-cyan-400">
{priorAccuracy.toFixed(1)}% (n={totalTrials})
</p>
</div>
</div>
</div>
{/* SECTION 1: Top 3 Cards & Search Mask */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
{/* Status Cards BTC, ETH, SOL */}
{['BTC', 'ETH', 'SOL'].map((tick) => {
const coin = defaultCoins[tick];
const isActive = activeTicker === tick;
const isUp = coin.change24h >= 0;
return (
<div
key={tick}
onClick={() => setActiveTicker(tick)}
className={`p-4 rounded-xl border cursor-pointer transition-all hover:bg-slate-850 flex items-center justify-between relative overflow-hidden ${isActive ? 'border-cyan-500/40 bg-cyan-500/5 shadow-md shadow-cyan-500/5' : 'border-slate-850 bg-slate-950/20'}`}
>
<div>
<div className="text-xs text-slate-400 font-semibold">{coin.name}</div>
<div className="text-xl font-extrabold font-mono mt-1 text-slate-100">{coin.price}</div>
<div className={`text-[10px] font-bold font-mono mt-0.5 flex items-center gap-0.5 ${isUp ? 'text-emerald-400' : 'text-rose-400'}`}>
{isUp ? <ArrowUpRight className="w-3.5 h-3.5" /> : <ArrowDownRight className="w-3.5 h-3.5" />}
<span>{isUp ? '+' : ''}{coin.change24h}%</span>
</div>
</div>
<div className="text-right">
<span className="text-2xl font-bold font-mono text-slate-800">{tick}</span>
</div>
</div>
);
})}
{/* Custom Search bar */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/20 flex flex-col justify-center gap-2">
<form onSubmit={handleAltcoinSearch} className="flex gap-2">
<input
type="text"
required
placeholder="Altcoin Ticker (z.B. LINK)"
className="bg-slate-900 border border-slate-800 rounded-lg p-2 flex-1 text-slate-100 font-mono text-xs uppercase focus:outline-none focus:border-cyan-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-cyan-400 hover:text-cyan-350 font-bold px-3 py-2 border border-slate-700 rounded-lg transition-colors text-xs"
>
<Search className="w-4 h-4" />
</button>
</form>
{Object.keys(customCoins).length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-1 overflow-x-auto max-h-[32px]">
{Object.keys(customCoins).map(tick => (
<button
key={tick}
onClick={() => setActiveTicker(tick)}
className={`px-2 py-0.5 rounded font-mono text-[9px] border ${activeTicker === tick ? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30' : 'bg-slate-900 text-slate-500 border-slate-800'}`}
>
{tick}
</button>
))}
</div>
)}
</div>
</div>
{/* SECTION 2: Derivatives & On-Chain Metrics Ledger */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column: Metrics Widgets */}
<div className="xl:col-span-2 space-y-6">
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<BarChart2 className="text-cyan-400 w-5 h-5" /> On-Chain &amp; Derivate-Indikatoren ({activeCoin.ticker})
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Funding & Open Interest Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Ref-Zinssatz &amp; Kontrakte (OI)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Daily Funding Rate:</span>
<span className={`font-bold ${activeCoin.fundingRate < 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.fundingRate > 0 ? '+' : ''}{activeCoin.fundingRate.toFixed(3)}%
</span>
</div>
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Open Interest (24h Δ):</span>
<span className={`font-bold ${activeCoin.openInterestChange >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.openInterestChange > 0 ? '+' : ''}{activeCoin.openInterestChange.toFixed(1)}%
</span>
</div>
</div>
</div>
{/* Long/Short & Liquidation Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Positionierung &amp; Liquidationen</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Long / Short Ratio:</span>
<span className="text-slate-200 font-bold">{activeCoin.longShortRatio}</span>
</div>
<div className="flex justify-between items-center text-xs font-mono">
<span className="text-slate-400">Liq-Cluster:</span>
<span className="text-rose-400">Long: {activeCoin.liqLong} | Short: {activeCoin.liqShort}</span>
</div>
</div>
</div>
{/* Whale Flows Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Whale-Ströme (Nettozufluss)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Netto Inflow (Wallets):</span>
<span className={`font-bold ${activeCoin.whaleInflow >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.whaleInflow > 0 ? '+' : ''}{activeCoin.whaleInflow} {activeCoin.ticker}
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Positive Werte signalisieren, dass Großinvestoren Bestände von Börsen auf private Wallets (Akkumulation) abziehen.
</div>
</div>
</div>
{/* Exchange Reserves Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Börsenreserven (Spot)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Reservenänderung (7d):</span>
<span className={`font-bold ${activeCoin.exchangeReserves <= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.exchangeReserves > 0 ? '+' : ''}{activeCoin.exchangeReserves}%
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Sinkende Reserven an den Spot-Börsen reduzieren den verfügbaren Verkaufsdruck und begünstigen Squeezes.
</div>
</div>
</div>
</div>
</div>
{/* Right Column: Predictive Gauges & Correction Calibration */}
<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">
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<Compass className="text-cyan-400 w-5 h-5" /> Vorhersage-Wahrscheinlichkeiten
</h3>
{/* Gauges / Progress Bars */}
<div className="space-y-4">
{/* 24h Gauge */}
<div className="space-y-2">
<div className="flex justify-between text-xs font-semibold">
<span className="text-slate-300">24h Volatility Squeeze (Short-Term)</span>
<span className="text-cyan-400 font-mono">{(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
{/* ML Baseline Overlay */}
<div
className="bg-cyan-500 h-full rounded-l transition-all duration-500 opacity-30"
style={{ width: `${mlPredictions.shortTermProb * 100}%` }}
/>
{/* Corrected Posterior Marker */}
<div
className="bg-cyan-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(34,211,238,0.5)]"
style={{ width: `${correctedPredictions.shortTerm * 100}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
<span>ML-Signal: {(mlPredictions.shortTermProb * 100).toFixed(0)}%</span>
<span className="text-cyan-400">Bayes-Korrigiert: {(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
</div>
</div>
{/* 14d Gauge */}
<div className="space-y-2">
<div className="flex justify-between text-xs font-semibold">
<span className="text-slate-300">14d Struktureller Bullish-Trend (Medium-Term)</span>
<span className="text-teal-400 font-mono">{(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
<div
className="bg-teal-500 h-full rounded-l transition-all duration-500 opacity-30"
style={{ width: `${mlPredictions.mediumTermProb * 100}%` }}
/>
<div
className="bg-teal-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(45,212,191,0.5)]"
style={{ width: `${correctedPredictions.mediumTerm * 100}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
<span>ML-Signal: {(mlPredictions.mediumTermProb * 100).toFixed(0)}%</span>
<span className="text-teal-400">Bayes-Korrigiert: {(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
</div>
</div>
</div>
{/* Model Calibration Log & Simulation */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold text-slate-300 uppercase">Bayes Modell-Kalibrierung</h4>
<span className="text-[10px] text-slate-500 font-mono">n = {totalTrials} Trials</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs font-mono pb-2 border-b border-slate-900">
<div className="text-slate-400">Erfolge (&alpha;):</div>
<div className="text-emerald-400 font-bold">{alphaSuccess}</div>
<div className="text-slate-400">Fehlalarme (&beta;):</div>
<div className="text-rose-400 font-bold">{betaFailure}</div>
</div>
{/* Trial simulator */}
<div className="space-y-2">
<p className="text-[10px] text-slate-400">Modell-Drift simulieren: Fügen Sie richtige/falsche Outcomes hinzu, um die Beta-Verteilung anzupassen.</p>
<div className="flex gap-2">
<button
onClick={() => handleSimulateTrial(true)}
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 Erfolg
</button>
<button
onClick={() => handleSimulateTrial(false)}
className="flex-1 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 Fehlalarm
</button>
</div>
{simulatedTrialLogged && (
<div className="text-[10px] text-cyan-400 font-mono text-center animate-pulse">
Trial geloggt! Bayes-Prior wurde auf {lastTrialSuccess ? 'Erfolg' : 'Fehlalarm'} aktualisiert.
</div>
)}
</div>
</div>
</div>
</div>
{/* SECTION 3: Mathematical LaTeX Accordion */}
<div className="border-t border-slate-850 pt-4 mt-6">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-cyan-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">Mathematische Formulierung (Beta-Update &amp; Random Forest)</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-bold text-cyan-400 mb-1">1. Bayesianische Beta-Konjugierte Fehlerkorrektur</h4>
<p className="mb-2">
Wir modellieren das Vertrauensintervall über die Fehlerraten des Modells mittels einer Beta-Verteilung. Der A-Priori Fehlerzustand wird durch die Parameter <InlineMath math="\alpha" /> (Erfolge) und <InlineMath math="\beta" /> (Fehlalarme) dargestellt:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P \sim \text{Beta}(\alpha, \beta) \quad \text{mit Erwartungswert } \mathbb{E}[P] = \frac{\alpha}{\alpha + \beta}" />
</div>
<p className="mb-2">
Bei einem neuen ML-Signal <InlineMath math="P_{\text{ML}}" /> führen wir ein konjugiertes Bayes-Update mit einem Vertrauensgewicht <InlineMath math="w" /> aus:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\alpha_{\text{post}} = \alpha + w \cdot P_{\text{ML}}, \quad \beta_{\text{post}} = \beta + w \cdot (1 - P_{\text{ML}})" />
</div>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P_{\text{Posterior}} = \frac{\alpha_{\text{post}}}{\alpha_{\text{post}} + \beta_{\text{post}}}" />
</div>
<p className="text-slate-400">
Ist das Modell historisch sehr instabil (hohes <InlineMath math="\beta" />), korrigiert der Bayesianische Term ein übermütiges ML-Signal nach unten, was die Robustheit des Gesamtsystems schützt.
</p>
</div>
<div className="border-t border-slate-900 pt-3">
<h4 className="font-bold text-cyan-400 mb-1">2. Random Forest Nicht-Lineares Signalmapping</h4>
<p className="mb-2">
Der Random Forest simuliert ein Ensemble von 10 schwachen Entscheidungsbäumen. Jeder Baum spaltet die Daten nach Schwellenwerten (z.B. &bdquo;Funding-Rate &lt; -0.04%&ldquo; und &bdquo;Open Interest &gt; 10%&ldquo;) auf:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\text{ML}_{\text{prob}} = \frac{1}{M} \sum_{m=1}^{M} T_m(\mathbf{x})" />
</div>
<p className="text-slate-400">
wobei <InlineMath math="T_m(\mathbf{x})" /> der prognostizierte Ausgabewert des <InlineMath math="m" />-ten Entscheidungsbaumes für den Featurevektor <InlineMath math="\mathbf{x}" /> ist.
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,980 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore } from '@/lib/store';
import { calculateEventROC, calculateEventSurvival, runEventLMM } from '@/lib/math/statistics';
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
LineChart,
Line,
ReferenceLine,
Legend
} from 'recharts';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Activity,
BarChart4,
Compass,
GitMerge,
Plus,
Trash2,
Calendar,
Sparkles,
AlertCircle,
ChevronDown,
ChevronUp,
BookOpen,
RefreshCw,
Info,
Check,
TrendingUp,
TrendingDown,
Sliders,
Database
} from 'lucide-react';
// Predefined archetypes for Event Creation
const ARCHETYPES: Record<string, { name: string; defaultScores: Record<string, number> }> = {
'FED Zinsentscheid': {
name: 'FED Zinsentscheid',
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',
defaultScores: { Apple: 1, NASDAQ: 2, Gold: -2, Bitcoin: 1 }
},
'US Non-Farm Payrolls': {
name: 'US Non-Farm Payrolls',
defaultScores: { Apple: 0, NASDAQ: 1, Gold: -1, Bitcoin: 0 }
},
'EZB Pressekonferenz': {
name: 'EZB Pressekonferenz',
defaultScores: { Apple: -1, NASDAQ: -1, Gold: 2, Bitcoin: 1 }
}
};
const ASSETS = ['Apple', 'NASDAQ', 'Gold', 'Bitcoin'];
export default function EventsDemo() {
const {
selectedModel,
setSelectedModel,
eventsMatrix,
calendarProposals,
lmmObservations,
addEventToMatrix,
updateMatrixCell,
runEndogenousLMMCalibration
} = useSandboxStore();
// Local State
const [tauPre, setTauPre] = useState<number>(7);
const [tauPost, setTauPost] = useState<number>(3);
const [showMath, setShowMath] = useState<boolean>(false);
const [selectedSurvivalAsset, setSelectedSurvivalAsset] = useState<string>('Apple');
// Custom Event Form State
const [customName, setCustomName] = useState<string>('');
const [customDate, setCustomDate] = useState<string>('2026-06-15');
const [selectedArchetype, setSelectedArchetype] = useState<string>('Custom');
// Calibration feedback states
const [isCalibrating, setIsCalibrating] = useState<boolean>(false);
const [calibrationSuccess, setCalibrationSuccess] = useState<boolean>(false);
const [lastCalibrationTime, setLastCalibrationTime] = useState<string | null>(null);
// Current baseline date for relative time calculations
const CURRENT_DATE_STR = '2026-06-06';
// Helper to calculate time kernel weight
const getWeightAndDays = (eventDateStr: string) => {
const eventDate = new Date(eventDateStr);
const currentDate = new Date(CURRENT_DATE_STR);
const diffTime = eventDate.getTime() - currentDate.getTime();
const d = diffTime / (1000 * 60 * 60 * 24); // days relative to today
let weight = 0;
if (d >= 0) {
weight = Math.exp(-d / tauPre);
} else {
const daysSince = -d;
weight = 1 / (1 + Math.log(1 + daysSince / tauPost));
}
return {
d: Math.round(d),
weight: Math.round(weight * 100) / 100
};
};
// 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 };
eventsMatrix.forEach((ev) => {
const { weight } = getWeightAndDays(ev.date);
ASSETS.forEach((asset) => {
const score = ev.scores[asset] || 0;
totals[asset] += 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;
let signal = 'NEUTRAL / HOLD';
let colorClass = 'bg-slate-800/40 border-slate-700/60 text-slate-400';
let textClass = 'text-slate-400';
let glowClass = 'shadow-slate-500/5';
if (netScore > 1.5) {
signal = 'STRONG BUY';
colorClass = 'bg-emerald-950/40 border-emerald-800/80 text-emerald-400';
textClass = 'text-emerald-400 font-bold';
glowClass = 'shadow-emerald-500/10 shadow-[0_0_15px_rgba(16,185,129,0.15)]';
} else if (netScore > 0.4) {
signal = 'ACCUMULATE';
colorClass = 'bg-teal-950/30 border-teal-800/50 text-teal-400';
textClass = 'text-teal-300 font-semibold';
glowClass = 'shadow-teal-500/5 shadow-[0_0_10px_rgba(20,184,166,0.1)]';
} else if (netScore < -1.5) {
signal = 'STRONG SELL / RISK OFF';
colorClass = 'bg-rose-950/40 border-rose-800/80 text-rose-400';
textClass = 'text-rose-400 font-bold';
glowClass = 'shadow-rose-500/10 shadow-[0_0_15px_rgba(244,63,94,0.15)]';
} else if (netScore < -0.4) {
signal = 'REDUCE / HEDGE';
colorClass = 'bg-amber-950/30 border-amber-800/50 text-amber-400';
textClass = 'text-amber-300 font-semibold';
glowClass = 'shadow-amber-500/5 shadow-[0_0_10px_rgba(245,158,11,0.1)]';
}
signals[asset] = { netScore, signal, colorClass, textClass, glowClass };
});
return signals;
}, [eventsMatrix, tauPre, tauPost]);
// 2. Dynamic Decay Curve Chart Data
const decayCurveData = useMemo(() => {
const pts = [];
// Generate weight for each day relative to event (d = E - T)
for (let d = -30; d <= 30; d++) {
let weight = 0;
if (d >= 0) {
weight = Math.exp(-d / tauPre);
} else {
const daysSince = -d;
weight = 1 / (1 + Math.log(1 + daysSince / tauPost));
}
pts.push({
days: d,
weight: Math.round(weight * 1000) / 1000
});
}
return pts;
}, [tauPre, tauPost]);
// 3. Dynamic ROC Data
const { rocData, optimalThreshold, maxYouden, auc } = useMemo(() => {
const predictions: number[] = [];
const labels: number[] = [];
lmmObservations.forEach((obs) => {
// Find average event score of this asset in matrix to use as indicator bias
const assetScores = eventsMatrix.map(ev => ev.scores[obs.asset] || 0);
const avgScore = assetScores.reduce((sum, s) => sum + s, 0) / (assetScores.length || 1);
// Construct a predictor between 0 and 1
let basePred = obs.eventType === 'BULLISH' ? 0.65 : 0.35;
basePred += avgScore * 0.04 + obs.trend * 0.5 - obs.vix * 0.002;
const finalPred = Math.min(0.99, Math.max(0.01, basePred));
predictions.push(finalPred);
labels.push(obs.returnVal > 0.012 ? 1 : 0); // label 1 if return > 1.2%
});
const res = calculateEventROC(predictions, labels);
// Trapezoidal Area Under Curve (AUC)
let computedAuc = 0;
const sorted = [...res.points].sort((a, b) => a.fpr - b.fpr);
for (let i = 1; i < sorted.length; i++) {
const w = sorted[i].fpr - sorted[i - 1].fpr;
const h = (sorted[i].tpr + sorted[i - 1].tpr) / 2;
computedAuc += w * h;
}
return {
rocData: res.points,
optimalThreshold: res.optimalThreshold,
maxYouden: res.maxYouden,
auc: Math.round(Math.max(0.5, Math.min(0.99, computedAuc)) * 1000) / 1000
};
}, [eventsMatrix, lmmObservations]);
// 4. Dynamic Survival Curve Data for selected asset
const survivalData = useMemo(() => {
const assetScores = eventsMatrix.map(ev => ev.scores[selectedSurvivalAsset] || 0);
const sumScore = assetScores.reduce((sum, s) => sum + s, 0);
const timesLong: number[] = [];
const eventsLong: number[] = [];
const timesShort: number[] = [];
const eventsShort: number[] = [];
// Simulate 15 historical events outcomes per direction
for (let i = 0; i < 15; i++) {
// LONG: Positive scores reduce time-to-event (gain target hit faster)
let tLong = 35 - sumScore * 3.5 + (Math.sin(i * 1.5) * 12);
let evLong = 1;
if (tLong > 60 || sumScore < -1) {
tLong = 60;
evLong = 0; // right censored
}
timesLong.push(Math.round(Math.max(3, tLong)));
eventsLong.push(evLong);
// SHORT: Negative scores reduce time-to-event (loss target hit faster)
let tShort = 35 + sumScore * 3.5 + (Math.cos(i * 1.9) * 12);
let evShort = 1;
if (tShort > 60 || sumScore > 1) {
tShort = 60;
evShort = 0; // right censored
}
timesShort.push(Math.round(Math.max(3, tShort)));
eventsShort.push(evShort);
}
const curveLong = calculateEventSurvival(timesLong, eventsLong, 'LONG');
const curveShort = calculateEventSurvival(timesShort, eventsShort, 'SHORT');
// Merge for chart mapping
const timeMap: Record<number, { time: number; longRate?: number; shortRate?: number }> = {};
for (let t = 0; t <= 60; t += 2) {
timeMap[t] = { time: t };
}
curveLong.forEach(pt => {
const roundedT = Math.round(pt.time / 2) * 2;
if (timeMap[roundedT]) timeMap[roundedT].longRate = pt.survivalRate;
});
curveShort.forEach(pt => {
const roundedT = Math.round(pt.time / 2) * 2;
if (timeMap[roundedT]) timeMap[roundedT].shortRate = pt.survivalRate;
});
const sortedMerged = Object.values(timeMap).sort((a, b) => a.time - b.time);
let lastLong = 1.0;
let lastShort = 1.0;
return sortedMerged.map(pt => {
if (pt.longRate !== undefined) lastLong = pt.longRate;
else pt.longRate = lastLong;
if (pt.shortRate !== undefined) lastShort = pt.shortRate;
else pt.shortRate = lastShort;
return pt;
});
}, [eventsMatrix, selectedSurvivalAsset]);
// 5. Dynamic LMM regression fitting
const lmmResults = useMemo(() => {
return runEventLMM(lmmObservations);
}, [lmmObservations]);
// 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 };
if (selectedArchetype !== 'Custom') {
const arch = ARCHETYPES[selectedArchetype];
name = name || arch.name;
scores = { ...arch.defaultScores };
} else {
name = name || 'Benutzerdefiniertes Ereignis';
}
addEventToMatrix(name, customDate, scores);
setCustomName('');
setSelectedArchetype('Custom');
};
// Calibration Action Trigger
const handleTriggerCalibration = () => {
setIsCalibrating(true);
// Simulate complex econometric iterative calibration
setTimeout(() => {
runEndogenousLMMCalibration();
setIsCalibrating(false);
setCalibrationSuccess(true);
setLastCalibrationTime(new Date().toLocaleTimeString());
setTimeout(() => {
setCalibrationSuccess(false);
}, 4000);
}, 1200);
};
return (
<div className="space-y-6 text-slate-100 font-sans">
{/* 1. Header with Model Selector */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800/80 rounded-2xl p-6 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-48 h-48 bg-rose-500/10 rounded-full blur-3xl -z-10" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="bg-rose-500/20 text-rose-400 border border-rose-500/30 text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wider">
Element 5
</span>
<span className="text-slate-400 text-xs font-mono">Status: Calibrated & Active</span>
</div>
<h1 className="text-2xl md:text-3xl font-extrabold bg-gradient-to-r from-rose-400 via-purple-300 to-indigo-200 bg-clip-text text-transparent">
Advanced Econometric Event-Analysis Matrix
</h1>
<p className="text-slate-400 text-xs mt-1 max-w-2xl leading-relaxed">
Analyzes multi-asset cross-impact networks under logarithmic decay timelines. Evaluates predictive efficiency via ROC, models target boundaries with directional survival, and performs endogenous regressions via LMM feedback.
</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">
<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>
</div>
</div>
{/* 2. Main Dashboard: Clean View Matrix & Action Signals */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Left/Middle Matrix & Settings (2/3 width) */}
<div className="xl:col-span-2 space-y-6">
{/* 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">
<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>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-xs">
<thead>
<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>
))}
<th className="py-3 px-3 text-right">Kernel-Gewicht</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/40">
{eventsMatrix.map((ev) => {
const { d, weight } = getWeightAndDays(ev.date);
return (
<tr key={ev.id} className="hover:bg-slate-950/20 transition-colors group">
<td className="py-3 px-3 font-semibold text-slate-200">
{ev.name}
</td>
<td className="py-3 px-3 text-slate-400 font-mono">
{ev.date}
<span className="block text-[10px] text-slate-500">
{d === 0 ? 'Heute' : d > 0 ? `In ${d} Tagen` : `Vor ${-d} Tagen`}
</span>
</td>
{ASSETS.map((asset) => {
const score = ev.scores[asset] || 0;
// Determine color style based on score value
let badgeStyle = 'text-slate-400 bg-slate-950 border-slate-800/60';
if (score > 1.5) badgeStyle = 'text-emerald-400 bg-emerald-950/30 border-emerald-800/50';
else if (score > 0) badgeStyle = 'text-teal-400 bg-teal-950/20 border-teal-900/30';
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';
return (
<td key={asset} 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))}
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}`}>
{score > 0 ? `+${score}` : score}
</span>
<button
onClick={() => updateMatrixCell(ev.id, asset, 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>
</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>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* B. Add Event Form & Time Kernel Weights config (split) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Form to Add Event */}
<div className="bg-slate-900/50 border border-slate-800/80 rounded-2xl p-5 shadow-lg">
<h4 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Plus className="w-4 h-4 text-rose-400" /> Event hinzufügen
</h4>
<form onSubmit={handleAddCustomEvent} className="space-y-3.5 text-xs">
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Archetyp Vorlage</label>
<select
value={selectedArchetype}
onChange={(e) => {
setSelectedArchetype(e.target.value);
if (e.target.value !== 'Custom') {
setCustomName(ARCHETYPES[e.target.value].name);
} else {
setCustomName('');
}
}}
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"
>
<option value="Custom">Benutzerdefiniert (Scores auf 0)</option>
{Object.keys(ARCHETYPES).map(key => (
<option key={key} value={key}>{key}</option>
))}
</select>
</div>
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Ereignis Name</label>
<input
type="text"
value={customName}
onChange={(e) => setCustomName(e.target.value)}
placeholder="z.B. OPEC Treffen"
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 font-sans"
/>
</div>
<div>
<label className="block text-slate-400 mb-1 text-[10px] uppercase font-semibold">Event Datum</label>
<input
type="date"
value={customDate}
onChange={(e) => setCustomDate(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 font-mono"
/>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-rose-500 to-indigo-600 hover:from-rose-600 hover:to-indigo-700 text-slate-950 hover:text-slate-100 font-bold p-2.5 rounded-lg flex items-center justify-center gap-1.5 transition-all shadow-[0_4px_12px_rgba(244,63,94,0.15)]"
>
<Plus className="w-4 h-4" /> In Matrix aufnehmen
</button>
</form>
</div>
{/* Time Decay Kernel Sliders & Live Decay Curve Chart */}
<div className="bg-slate-900/50 border border-slate-800/80 rounded-2xl p-5 shadow-lg flex flex-col justify-between">
<div>
<h4 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Sliders className="w-4 h-4 text-purple-400" /> Time-Kernel Decay Parameters
</h4>
<div className="space-y-4">
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Pre-Event Slope (<InlineMath math="\tau_{pre}" />)</span>
<span className="font-mono text-purple-400 font-bold">{tauPre} Tage</span>
</div>
<input
type="range"
min="1"
max="30"
value={tauPre}
onChange={(e) => setTauPre(Number(e.target.value))}
className="w-full accent-purple-500 bg-slate-950 h-1 rounded-lg appearance-none cursor-pointer"
/>
<span className="text-[10px] text-slate-500 block mt-0.5">Schnellerer Anstieg (kleinerer Wert) nähert sich dem Ereignis.</span>
</div>
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-slate-400">Post-Event Half-Life (<InlineMath math="\tau_{post}" />)</span>
<span className="font-mono text-purple-400 font-bold">{tauPost} Tage</span>
</div>
<input
type="range"
min="1"
max="20"
value={tauPost}
onChange={(e) => setTauPost(Number(e.target.value))}
className="w-full accent-purple-500 bg-slate-950 h-1 rounded-lg appearance-none cursor-pointer"
/>
<span className="text-[10px] text-slate-500 block mt-0.5">Langsameres Abklingen logarithmisch nach dem Stichtag.</span>
</div>
</div>
</div>
{/* Mini Kernel Chart showing bell curve of relevance */}
<div className="h-20 w-full mt-4 bg-slate-950/40 rounded-lg p-1 border border-slate-900">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={decayCurveData} margin={{ top: 0, right: 5, left: 5, bottom: 0 }}>
<Tooltip
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '6px', fontSize: '10px' }}
labelFormatter={(label) => `Relative Tage: ${label}`}
/>
<Area type="monotone" dataKey="weight" name="Gewicht" stroke="#a855f7" fill="rgba(168, 85, 247, 0.15)" strokeWidth={1.5} dot={false} />
<ReferenceLine x={0} stroke="#f43f5e" strokeDasharray="3 3" label={{ value: 'Event', fill: '#f43f5e', fontSize: 8, position: 'top' }} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
{/* Right Sidebar: Suggestions & Final Action Signals (1/3 width) */}
<div className="space-y-6">
{/* Calendar Inbox Panel */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl">
<div className="flex items-center gap-2 mb-3">
<Calendar className="w-4 h-4 text-indigo-400" />
<h3 className="text-sm font-bold text-slate-200">Wirtschaftskalender Vorschläge</h3>
</div>
{calendarProposals.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) => (
<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>
<div className="text-[10px] text-slate-500 flex items-center gap-1.5 mt-0.5 font-mono">
<span>{cp.date}</span>
<span></span>
<span className="text-indigo-400">{cp.archetype}</span>
</div>
</div>
<button
onClick={() => addEventToMatrix(cp.name, cp.date, cp.defaultScores)}
className="bg-indigo-600 hover:bg-indigo-500 text-slate-950 hover:text-slate-100 font-bold p-1.5 rounded-lg flex items-center justify-center transition-all"
title="In Matrix aufnehmen"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
{/* Action Signals Dashboard */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl">
<h3 className="text-sm font-bold text-slate-200 mb-3 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-400" /> Aggregated Trade Signals
</h3>
<div className="space-y-3">
{ASSETS.map((asset) => {
const { netScore, signal, colorClass, textClass, glowClass } = actionSignals[asset];
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`}
>
<div className="flex justify-between items-center">
<span className="font-bold text-slate-200 text-xs">{asset}</span>
<span className="text-[10px] font-mono text-slate-400">
Weighted Score: <span className={textClass}>{netScore > 0 ? `+${netScore}` : netScore}</span>
</span>
</div>
<div className="flex justify-between items-center mt-1">
<div className={`text-[10px] px-2 py-0.5 rounded border ${colorClass} font-semibold uppercase tracking-wider`}>
{signal}
</div>
<div className="text-[10px] text-slate-500 flex items-center gap-1 font-mono">
{netScore > 0.4 ? (
<TrendingUp className="w-3 h-3 text-emerald-400" />
) : netScore < -0.4 ? (
<TrendingDown className="w-3 h-3 text-rose-400" />
) : (
<AlertCircle className="w-3 h-3 text-slate-400" />
)}
<span>{netScore > 0 ? 'Bullish Drift' : netScore < 0 ? 'Bearish Risk' : 'Stationary'}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* LMM Feedback Trigger */}
<div className="bg-slate-900/50 backdrop-blur-md border border-slate-800/80 rounded-2xl p-5 shadow-xl text-center space-y-3 relative overflow-hidden">
<h3 className="text-sm font-bold text-slate-200">Endogenous Calibration</h3>
<p className="text-[11px] text-slate-400 leading-relaxed">
Updates manual matrix scores with optimal significant coefficients estimated from historical Linear Mixed Models, calibrating feedback parameters dynamically.
</p>
{calibrationSuccess && (
<div className="bg-emerald-950/30 border border-emerald-800/80 text-emerald-400 text-[10px] rounded-lg p-2 flex items-center gap-1.5 justify-center font-semibold">
<Check className="w-3.5 h-3.5" /> Kalibrierung erfolgreich abgeschlossen!
</div>
)}
<button
disabled={isCalibrating}
onClick={handleTriggerCalibration}
className={`w-full py-2.5 px-4 rounded-lg text-xs font-bold font-mono border transition-all flex items-center justify-center gap-2 ${
isCalibrating
? 'bg-slate-800 border-slate-700 text-slate-400 cursor-not-allowed'
: 'bg-rose-500 hover:bg-rose-600 border-rose-600 text-slate-950 hover:text-slate-100 shadow-[0_0_12px_rgba(244,63,94,0.25)] hover:scale-[1.01]'
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${isCalibrating ? 'animate-spin' : ''}`} />
{isCalibrating ? 'Calibrating LMM...' : 'Trigger LMM Calibration'}
</button>
{lastCalibrationTime && (
<div className="text-[9px] text-slate-500 font-mono">
Letzter Durchlauf: {lastCalibrationTime} ({lmmObservations.length} Obs.)
</div>
)}
</div>
</div>
</div>
{/* 3. Bottom: Econometric Charts & Show Math Panel */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6">
{/* Model Tabs Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-slate-800 pb-3 gap-2">
<div className="flex items-center gap-2">
<BarChart4 className="text-rose-400 w-5 h-5" />
<h3 className="text-base font-bold text-slate-100 uppercase tracking-wider">
{selectedModel === 'ROC' && 'ROC Model Diagnostics'}
{selectedModel === 'SURVIVAL' && 'Survival Analysis (Time-to-Event)'}
{selectedModel === 'LMM' && 'LMM Panel Regression Summary'}
</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">
{/* Left Panel: Description & Stats Cards (1/3 width) */}
<div className="space-y-4">
{selectedModel === 'ROC' && (
<div className="space-y-3 text-xs">
<p className="text-slate-400 leading-relaxed">
The Receiver Operating Characteristic (ROC) curve evaluates classifier strength on binary events (e.g. Return &gt; +3.0% within 14 days). An AUC of 0.5 denotes a random baseline, while 1.0 represents a perfect oracle.
</p>
<div className="grid grid-cols-2 gap-3">
<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">Area Under Curve (AUC)</span>
<span className="text-lg font-bold font-mono text-rose-400">{auc}</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">Max Youden Index (J)</span>
<span className="text-lg font-bold font-mono text-rose-400">{maxYouden}</span>
</div>
<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 &ge; {optimalThreshold}
</span>
</div>
</div>
</div>
)}
{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.
</p>
<div>
<label className="block text-slate-500 mb-1 text-[10px] uppercase font-semibold">Fokus Asset</label>
<select
value={selectedSurvivalAsset}
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>
))}
</select>
</div>
<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>
</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>
</div>
</div>
</div>
)}
{selectedModel === 'LMM' && (
<div className="space-y-3 text-xs">
<p className="text-slate-400 leading-relaxed">
Linear Mixed Model estimates the true impact of events on returns, isolating asset-level intercepts as random deviations. Standard Errors, t-stats, and p-values determine significance.
</p>
<div className="grid grid-cols-3 gap-2">
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">AIC</span>
<span className="text-xs font-bold text-slate-300 font-mono">{lmmResults.aic}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">BIC</span>
<span className="text-xs font-bold text-slate-300 font-mono">{lmmResults.bic}</span>
</div>
<div className="bg-slate-950/40 border border-slate-805 p-2 rounded-xl text-center">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Adj. R²</span>
<span className="text-xs font-bold text-purple-400 font-mono">{(lmmResults.rSquared * 100).toFixed(1)}%</span>
</div>
</div>
</div>
)}
</div>
{/* Right Panel: Charts / Regression Table (2/3 width) */}
<div className="lg:col-span-2 h-72 w-full flex items-center justify-center bg-slate-950/40 border border-slate-850 rounded-xl p-4">
{selectedModel === 'ROC' && (
<div className="w-full h-full">
<div className="text-[10px] font-mono text-slate-400 mb-2 text-center flex items-center justify-center gap-1.5">
<span>Modell-Klassifikationstrennung (FPR vs TPR)</span>
</div>
<ResponsiveContainer width="100%" height="90%">
<AreaChart data={rocData} margin={{ top: 10, right: 10, left: -25, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="fpr" stroke="#64748b" fontSize={9} tickFormatter={(v) => v.toFixed(1)} />
<YAxis stroke="#64748b" fontSize={9} domain={[0, 1.05]} />
<Tooltip
contentStyle={{ backgroundColor: '#090d16', borderColor: '#1e293b', borderRadius: '8px', fontSize: '10px' }}
formatter={(value: any) => [parseFloat(value).toFixed(2), 'True Positive Rate']}
/>
<Area type="monotone" dataKey="tpr" name="True Positive Rate" stroke="#f43f5e" fill="rgba(244, 63, 94, 0.12)" strokeWidth={2} />
{/* Diagonal baseline */}
<Line type="monotone" dataKey="fpr" stroke="#334155" strokeDasharray="4 4" dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
)}
{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>
</div>
<div className="flex-1 w-full mt-2">
<ResponsiveContainer width="100%" height="95%">
<LineChart data={survivalData} margin={{ top: 5, right: 10, left: -25, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} domain={[0, 1.05]} tickFormatter={(v) => `${(v * 100).toFixed(0)}%`} />
<Tooltip
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} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
{selectedModel === 'LMM' && (
<div className="w-full h-full overflow-y-auto pr-1">
<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 &lsquo;***&rsquo; 0.001 &lsquo;**&rsquo; 0.01 &lsquo;*&rsquo; 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-2 text-[9px] text-slate-400 flex flex-wrap gap-x-6 gap-y-1">
<span><strong className="text-slate-300">Random Effects Asset (intercepts):</strong></span>
{lmmResults.randomEffects.map((re) => (
<span key={re.asset}>
{re.asset}: <span className="text-cyan-400 font-semibold">{re.intercept > 0 ? `+${re.intercept.toFixed(4)}` : re.intercept.toFixed(4)}</span>
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,448 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore, InsiderTrade, CongressTrade, WhaleTrade } from '@/lib/store';
import { calculateRollingZScore, detectInsiderClusters, coupleBayesianRebound } from '@/lib/math/statistics';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import {
Shield, User, ArrowDownRight, ArrowUpRight, DollarSign, Calendar, Landmark,
ChevronDown, ChevronUp, Search, Radio, Building2, AlertTriangle, Layers, Percent
} from 'lucide-react';
export default function InsiderDemo() {
const {
insiderTrades,
congressTrades,
whaleTrades,
insiderVolumes,
priorProbability
} = useSandboxStore();
// Component local UI states
const [activeSegment, setActiveSegment] = useState<'executives' | 'congress' | 'whales'>('executives');
const [searchQuery, setSearchQuery] = useState('');
const [selectedTicker, setSelectedTicker] = useState<string | null>(null);
const [scanning, setScanning] = useState(false);
const [scanResults, setScanResults] = useState<{
ticker: string;
zScore: number;
clusterCount: number;
multiplier: number;
isAnomaly: boolean;
coupledRebound: number;
}[] | null>(null);
const [showMathAccordion, setShowMathAccordion] = useState(false);
// Run Global Flow Scan
const handleGlobalFlowScan = () => {
setScanning(true);
setTimeout(() => {
const results = Object.keys(insiderVolumes).map((ticker) => {
const volumes = insiderVolumes[ticker];
const zResult = calculateRollingZScore(volumes);
// Filter trades for this ticker to detect clusters
const tickerTrades = insiderTrades.filter(t => t.ticker === ticker);
const clusterResult = detectInsiderClusters(tickerTrades);
// Bayesian coupling
const coupled = coupleBayesianRebound(priorProbability, zResult.latest);
return {
ticker,
zScore: parseFloat(zResult.latest.toFixed(2)),
clusterCount: clusterResult.count,
multiplier: parseFloat(clusterResult.multiplier.toFixed(2)),
isAnomaly: zResult.isAnomaly || clusterResult.isCluster,
coupledRebound: coupled,
};
});
// Sort anomalies to the top
results.sort((a, b) => (b.isAnomaly ? 1 : 0) - (a.isAnomaly ? 1 : 0) || b.zScore - a.zScore);
setScanResults(results);
setScanning(false);
}, 1000);
};
// Perform Ticker Lookup
const handleTickerLookup = (e: React.FormEvent) => {
e.preventDefault();
const query = searchQuery.trim().toUpperCase();
if (query) {
setSelectedTicker(query);
} else {
setSelectedTicker(null);
}
};
// Compute stats for selected ticker
const tickerStats = useMemo(() => {
if (!selectedTicker) return null;
const volumes = insiderVolumes[selectedTicker] || [5000, 4800, 5200, 6000, 4500, 5000]; // fallback
const zResult = calculateRollingZScore(volumes);
const tickerTrades = insiderTrades.filter(t => t.ticker === selectedTicker);
const clusterResult = detectInsiderClusters(tickerTrades);
const coupled = coupleBayesianRebound(priorProbability, zResult.latest);
return {
zScore: zResult.latest,
isAnomaly: zResult.isAnomaly,
clusterCount: clusterResult.count,
isCluster: clusterResult.isCluster,
multiplier: clusterResult.multiplier,
coupledRebound: coupled,
volumes,
};
}, [selectedTicker, insiderVolumes, insiderTrades, priorProbability]);
// Volumes chart data for selected ticker
const volumeChartData = useMemo(() => {
if (!tickerStats || !selectedTicker) return [];
return tickerStats.volumes.map((vol, idx) => ({
month: `M-${tickerStats.volumes.length - idx - 1}`,
'Volumen (Shares)': vol,
}));
}, [tickerStats, selectedTicker]);
return (
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl -z-10" />
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 border-b border-slate-800 pb-4 mb-6">
<div>
<span className="text-purple-400 text-xs font-semibold uppercase tracking-wider">Element 3</span>
<h2 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-indigo-200 bg-clip-text text-transparent">
Institutional & Insider Flow Tracker
</h2>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-2 flex items-center gap-3">
<Shield className="text-purple-400 w-5 h-5" />
<div>
<p className="text-slate-400 text-xs">Bayesianische Kopplung</p>
<p className="font-mono text-sm font-bold text-purple-400">
Prior Rebound: {(priorProbability * 100).toFixed(0)}%
</p>
</div>
</div>
</div>
{/* SECTION 1: Dual-Query Actions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/30 flex flex-col justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-300">Global Flow Outlier Scan</h4>
<p className="text-xs text-slate-400">Filtert den Markt nach statistisch signifikanten Kaufvolumina (Z-Score &gt; 2.0) und konzertierten Käufen (Insider-Cluster).</p>
</div>
<button
onClick={handleGlobalFlowScan}
disabled={scanning}
className="w-full bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 disabled:from-purple-900 text-white font-bold py-2.5 px-4 rounded-xl transition-all shadow-lg shadow-purple-500/10 flex items-center justify-center gap-2"
>
<Radio className={`w-4 h-4 ${scanning ? 'animate-pulse' : ''}`} />
<span>{scanning ? 'Scanne Transaktionen...' : 'Global Flow Scan starten'}</span>
</button>
</div>
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/30 flex flex-col justify-between gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-300">Ticker Lookup (Einzelauswertung)</h4>
<p className="text-xs text-slate-400">Gezielte Abfrage von Insider-Z-Scores und Bayesianischen Kopplungs-Updates (z.B. PLTR, RACE, AMZN, AAPL).</p>
</div>
<form onSubmit={handleTickerLookup} className="flex gap-2">
<input
type="text"
required
placeholder="z.B. PLTR"
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono text-sm uppercase focus:outline-none focus:border-purple-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-purple-400 hover:text-purple-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
>
Lookup
</button>
</form>
</div>
</div>
{/* Ticker Stats / Lookup Section */}
{selectedTicker && tickerStats && (
<div className="p-5 rounded-2xl border border-purple-500/30 bg-purple-500/5 grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6 animate-fade-in">
<div className="space-y-3">
<div className="flex justify-between items-center border-b border-slate-800 pb-2">
<h3 className="font-mono font-bold text-lg text-slate-100">{selectedTicker} Einzelauswertung</h3>
{tickerStats.isAnomaly && <span className="px-2 py-0.5 rounded bg-purple-500/20 text-purple-300 text-[10px] font-bold border border-purple-500/30 animate-pulse">FLOW OUTLIER</span>}
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between font-mono">
<span className="text-slate-400">Volumetrischer Z-Score:</span>
<span className={`font-bold ${tickerStats.isAnomaly ? 'text-purple-400' : 'text-slate-300'}`}>
Z = {tickerStats.zScore.toFixed(2)}
</span>
</div>
<div className="flex justify-between font-mono">
<span className="text-slate-400">Konzertiertes Cluster (14 Tage):</span>
<span className={`font-bold ${tickerStats.isCluster ? 'text-emerald-400' : 'text-slate-400'}`}>
{tickerStats.isCluster ? `JA (${tickerStats.clusterCount} Insiders)` : `NEIN (${tickerStats.clusterCount})`}
</span>
</div>
{tickerStats.isCluster && (
<div className="flex justify-between font-mono text-emerald-400">
<span>Cluster Exponent-Multiplikator:</span>
<span>x{tickerStats.multiplier.toFixed(2)}</span>
</div>
)}
<div className="flex justify-between font-mono border-t border-slate-800/80 pt-2 text-sm">
<span className="text-slate-300">Gekoppelte Rebound-Wahrsch.:</span>
<span className="text-purple-400 font-bold flex items-center gap-1">
<Percent className="w-3.5 h-3.5" />
{(tickerStats.coupledRebound * 100).toFixed(0)}%
</span>
</div>
</div>
</div>
{/* Volume chart */}
<div className="lg:col-span-2 h-44 w-full">
<div className="text-[10px] text-slate-400 mb-1 text-center font-mono font-semibold">Insider Handelsvolumen (24 Monate Baseline)</div>
<ResponsiveContainer width="100%" height="90%">
<BarChart data={volumeChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="month" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Bar dataKey="Volumen (Shares)" fill="#8b5cf6" radius={[3, 3, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Global Scan Outlier List */}
{scanResults && (
<div className="p-5 rounded-2xl border border-slate-800 bg-slate-950/20 mb-6 space-y-3 animate-fade-in">
<h3 className="text-sm font-bold text-slate-200 uppercase tracking-wider">Ergebnisse des Global Flow Scans</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{scanResults.map((res) => (
<div
key={res.ticker}
onClick={() => setSelectedTicker(res.ticker)}
className={`p-3 rounded-xl border cursor-pointer hover:bg-slate-900/60 transition-colors ${res.isAnomaly ? 'border-purple-500/40 bg-purple-500/5' : 'border-slate-850 bg-slate-900/40'}`}
>
<div className="flex justify-between items-center">
<span className="font-mono font-bold text-slate-200">{res.ticker}</span>
{res.isAnomaly && <span className="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse" />}
</div>
<div className="text-[10px] text-slate-400 mt-2 space-y-1">
<div>Z-Score: <span className="font-mono text-slate-300 font-bold">{res.zScore}</span></div>
<div>Cluster: <span className="font-mono text-slate-300">{res.clusterCount} Traders</span></div>
<div>Rebound: <span className="font-mono text-purple-400 font-bold">{Math.round(res.coupledRebound * 100)}%</span></div>
</div>
</div>
))}
</div>
</div>
)}
{/* SECTION 2: Smart Money Segment Tabs */}
<div className="space-y-4">
<div className="flex bg-slate-950/80 p-1 rounded-xl border border-slate-800/80 max-w-md">
<button
onClick={() => setActiveSegment('executives')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'executives' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<User className="w-3.5 h-3.5" /> Vorstände (Form 4)
</button>
<button
onClick={() => setActiveSegment('congress')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'congress' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<Landmark className="w-3.5 h-3.5" /> Kongress (Stock Act)
</button>
<button
onClick={() => setActiveSegment('whales')}
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans transition-all flex items-center justify-center gap-1.5 ${activeSegment === 'whales' ? 'bg-purple-500 text-white font-bold' : 'text-slate-400 hover:text-slate-200'}`}
>
<Building2 className="w-3.5 h-3.5" /> Whales (13F Filings)
</button>
</div>
{/* Ledger displays */}
<div className="border border-slate-800 rounded-xl overflow-hidden bg-slate-950/40">
{activeSegment === 'executives' && (
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Insider Name</th>
<th className="p-3">Position</th>
<th className="p-3">Transaktion</th>
<th className="p-3 font-mono">Stücke</th>
<th className="p-3 text-right">Wert ($)</th>
</tr>
</thead>
<tbody>
{insiderTrades.map((t) => {
const isBuy = t.type === 'BUY';
return (
<tr key={t.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{t.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{t.insiderName}</td>
<td className="p-3 text-slate-400">{t.relation}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
</td>
<td className="p-3 font-mono text-slate-300">{t.shares.toLocaleString()}</td>
<td className={`p-3 font-mono text-right font-bold ${isBuy ? 'text-emerald-400' : 'text-rose-400'}`}>
${t.value.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{activeSegment === 'congress' && (
<div className="space-y-4">
<div className="p-3 bg-amber-500/10 border-b border-slate-800 text-xs text-amber-400 flex items-start gap-2.5">
<AlertTriangle className="w-5 h-5 shrink-0" />
<div>
<span className="font-bold">U.S. Congress Stock Act Offenlegungslags:</span> Abgeordnete müssen Käufe innerhalb von 30 bis 45 Tagen offenlegen. Der Alpha-Lag in der Tabelle visualisiert diese Meldeverzögerung. Käufe können verzögert eingepreist sein.
</div>
</div>
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Abgeordneter</th>
<th className="p-3">Kammer</th>
<th className="p-3">Transaktion</th>
<th className="p-3">Volumen-Spanne</th>
<th className="p-3 font-mono">Handelsdatum</th>
<th className="p-3 font-mono">Meldedatum</th>
<th className="p-3 text-right">Alpha-Lag (Tage)</th>
</tr>
</thead>
<tbody>
{congressTrades.map((c) => {
const isBuy = c.type === 'BUY';
return (
<tr key={c.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{c.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{c.representative}</td>
<td className="p-3 text-slate-400">{c.chamber}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
</td>
<td className="p-3 text-slate-300 font-mono">{c.valueRange}</td>
<td className="p-3 font-mono text-slate-400">{c.transactionDate}</td>
<td className="p-3 font-mono text-slate-400">{c.filingDate}</td>
<td className="p-3 font-mono text-right text-amber-400 font-bold">{c.lagDays} Tage</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{activeSegment === 'whales' && (
<table className="w-full border-collapse text-left text-xs">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Ticker</th>
<th className="p-3">Institution (13F Filers)</th>
<th className="p-3">Art</th>
<th className="p-3 font-mono">Gehandelte Anteile</th>
<th className="p-3 font-mono">Aktueller Bestand</th>
<th className="p-3 font-mono">Meldedatum</th>
<th className="p-3 text-right">Geschätzter Wert ($)</th>
</tr>
</thead>
<tbody>
{whaleTrades.map((w) => {
const isBuy = w.type === 'BUY' || w.type === 'NEW';
return (
<tr key={w.id} className="border-b border-slate-850/50 hover:bg-slate-850/10 transition-colors">
<td className="p-3 font-bold font-mono text-purple-400">{w.ticker}</td>
<td className="p-3 text-slate-200 font-semibold">{w.institution}</td>
<td className="p-3">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{w.type}
</span>
</td>
<td className="p-3 font-mono text-slate-300">{w.sharesTraded.toLocaleString()}</td>
<td className="p-3 font-mono text-slate-400">{w.sharesHeld.toLocaleString()}</td>
<td className="p-3 font-mono text-slate-400">{w.filingDate}</td>
<td className={`p-3 font-mono text-right font-bold ${isBuy ? 'text-emerald-400' : 'text-rose-400'}`}>
${w.estimatedValue.toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
{/* SECTION 3: Mathematical LaTeX Accordion */}
<div className="border-t border-slate-850 pt-4 mt-6">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-purple-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">Mathematische Formulierung (Z-Score &amp; Bayesianische Kopplung)</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-bold text-purple-400 mb-1">1. Volumetrischer Z-Score (Statistische Signifikanz)</h4>
<p className="mb-2">
Der Z-Score gibt an, um wie viele Standardabweichungen das aktuelle Transaktionsvolumen <InlineMath math="X_t" /> vom historischen Durchschnitt <InlineMath math="\mu" /> abweicht:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="Z = \frac{X_t - \mu}{\sigma}" />
</div>
<p className="text-slate-400">
Ein Z-Score &gt; 2.0 wird als Ausreißer (Anomaly Trigger) eingestuft, was einer Wahrscheinlichkeit von unter 2.27% für einen zufälligen Anstieg entspricht (einseitiger Test bei Normalverteilung).
</p>
</div>
<div className="border-t border-slate-900 pt-3">
<h4 className="font-bold text-purple-400 mb-1">2. Bayesianische Kopplung (Rebound-Wahrscheinlichkeit)</h4>
<p className="mb-2">
Wir verknüpfen den Preisdrop-Sentiment (Element 2) mit den Insider-Z-Scores (Element 3), um die A-Posteriori-Wahrscheinlichkeit eines echten Rebounds zu ermitteln:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P(R|Z) = \frac{P(Z|R) \cdot P(R)}{P(Z|R) \cdot P(R) + P(Z|\neg R) \cdot (1 - P(R))}" />
</div>
<p className="text-slate-400">
wobei <InlineMath math="P(R)" /> die Prior-Wahrscheinlichkeit ist. Ist der Z-Score hoch (Käufe), steigt die Likelihood <InlineMath math="P(Z|R)" /> stark an und maximiert den Rebound-Erwartungswert.
</p>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,815 @@
'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 (
<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('');
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<string | null>(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<number>(0.60);
const [oddsRatio, setOddsRatio] = useState<number>(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 (
<div className="space-y-6">
{activePortfolio.riskProfile?.status === 'RED' && (
<div className="bg-rose-950/40 border border-rose-800/80 text-rose-400 text-xs rounded-xl p-4 flex items-center gap-3 shadow-[0_0_15px_rgba(244,63,94,0.15)] animate-pulse">
<AlertCircle className="w-5 h-5 text-rose-400 shrink-0" />
<div className="flex-1">
<span className="font-bold">Kritische Klumpenrisiken (Kovarianz RED):</span> {activePortfolio.riskProfile.message}
</div>
</div>
)}
{/* SECTION 1: Portfolio Selector & Stats Bar */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div className="space-y-1">
<span className="text-emerald-400 text-xs font-semibold uppercase tracking-wider">Strategic Sandbox</span>
<div className="flex items-center gap-3">
<FolderSync className="text-emerald-400 w-6 h-6" />
<select
value={activePortfolioId}
onChange={(e) => setActivePortfolio(e.target.value)}
className="bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-slate-100 font-sans font-semibold text-lg focus:outline-none focus:border-emerald-500 cursor-pointer"
>
{portfolios.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
<button
onClick={() => setShowNewPortfolioModal(true)}
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-emerald-400 hover:text-emerald-300 transition-colors border border-slate-700"
title="Neues Sandbox-Portfolio erstellen"
>
<Plus className="w-5 h-5" />
</button>
</div>
</div>
<div className="flex flex-wrap gap-4 w-full md:w-auto">
{/* 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>
<div className="font-mono text-xl font-bold text-slate-100 mt-1">
${netWorth.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
{/* Performance 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">GuV (Gesamt)</div>
<div className={`font-mono text-xl font-bold mt-1 flex items-center gap-1 ${isPositiveOverall ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositiveOverall ? <ArrowUpRight className="w-5 h-5" /> : <ArrowDownRight className="w-5 h-5" />}
<span>{isPositiveOverall ? '+' : ''}{totalGainLossPct.toFixed(2)}%</span>
</div>
</div>
{/* Live EWMA Vol 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] relative group">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<span>EWMA Volatilität</span>
<span className="cursor-help flex items-center" title="Annualisierte Schwankungsbreite basierend auf historischen Renditen.">
<HelpCircle className="w-3.5 h-3.5 text-slate-500 group-hover:text-emerald-400 transition-colors" />
</span>
</div>
<div className="font-mono text-xl font-bold text-teal-400 mt-1">
{ewmaResult.latest.toFixed(2)}%
</div>
</div>
{/* Covariance Risk Traffic Light Card */}
<div className="flex-1 md:flex-initial bg-slate-950/80 border border-slate-800 rounded-xl p-3 px-5 min-w-[150px] relative group">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<span>Kovarianz-Ampel</span>
<span className="cursor-help flex items-center" title="Systemische Portfolio-Klumpenrisiken basierend auf historischen Asset-Kovarianzen.">
<HelpCircle className="w-3.5 h-3.5 text-slate-500 group-hover:text-rose-400 transition-colors" />
</span>
</div>
<div className="flex items-center gap-2 mt-1.5">
<span className={`w-3.5 h-3.5 rounded-full ${
activePortfolio.riskProfile?.status === 'RED' ? 'bg-rose-500 animate-pulse shadow-[0_0_8px_#f43f5e]' :
activePortfolio.riskProfile?.status === 'YELLOW' ? 'bg-amber-500 shadow-[0_0_8px_#f59e0b]' :
'bg-emerald-500 shadow-[0_0_8px_#10b981]'
}`} />
<span className={`font-mono text-sm font-bold ${
activePortfolio.riskProfile?.status === 'RED' ? 'text-rose-400' :
activePortfolio.riskProfile?.status === 'YELLOW' ? 'text-amber-400' :
'text-emerald-400'
}`}>
{activePortfolio.riskProfile?.status || 'GREEN'} RISK
</span>
</div>
</div>
</div>
</div>
{/* Modal for creating a new Portfolio */}
{showNewPortfolioModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 max-w-sm w-full space-y-4 shadow-2xl">
<h3 className="text-lg font-bold text-white">Neues Sandbox-Portfolio</h3>
<form onSubmit={handleCreatePortfolio} className="space-y-4">
<div>
<label className="text-xs text-slate-400 block mb-1">Portfolio Name</label>
<input
type="text"
required
placeholder="z.B. Biotech Risk High Yield"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-slate-100 focus:outline-none focus:border-emerald-500"
value={newPortfolioName}
onChange={(e) => setNewPortfolioName(e.target.value)}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Startkapital ($)</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-slate-100 focus:outline-none focus:border-emerald-500 font-mono"
value={newStartingBalance}
onChange={(e) => setNewStartingBalance(Number(e.target.value))}
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setShowNewPortfolioModal(false)}
className="flex-1 bg-slate-850 hover:bg-slate-800 text-slate-300 font-semibold py-2 rounded-lg transition-colors border border-slate-700"
>
Abbrechen
</button>
<button
type="submit"
className="flex-1 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-slate-950 font-bold py-2 rounded-lg transition-all shadow-lg shadow-emerald-500/20"
>
Erstellen
</button>
</div>
</form>
</div>
</div>
)}
{/* Accordion / Math Button */}
<div className="border-t border-slate-850 pt-4 mt-4">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-emerald-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">Mathematische Spezifikation & EWMA-Volatilitätsmodell</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-semibold text-emerald-400 mb-1.5">1. EWMA Volatilitätsmodell</h4>
<p className="mb-2">
Die Volatilit&auml;t wird mittels des <strong>Exponentially Weighted Moving Average (EWMA)</strong>-Modells ermittelt. J&uuml;ngere Renditen erhalten hierbei ein h&ouml;heres Gewicht als weiter in der Vergangenheit liegende Renditen, gesteuert durch den Zerfallsparameter <InlineMath math="\lambda" /> (Lambda).
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\sigma_t^2 = \lambda \sigma_{t-1}^2 + (1 - \lambda) r_{t-1}^2" />
</div>
<p className="mb-2">
Die t&auml;gliche Volatilit&auml;t <InlineMath math="\sigma_t" /> wird auf ein ganzes Jahr hochgerechnet (Annualisierung) unter der Annahme von 252 Handelstagen:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\sigma_{\text{ann}} = \sqrt{\sigma_t^2 \times 252}" />
</div>
<p className="text-slate-400">
RiskMetrics empfiehlt f&uuml;r t&auml;gliche Finanzdaten einen Lambda-Wert von <InlineMath math="\lambda = 0.94" />.
</p>
</div>
<div className="border-t border-slate-800 pt-3">
<h4 className="font-semibold text-emerald-400 mb-1.5">2. Kelly-Kriterium zur Positionsgrößenbestimmung</h4>
<p className="mb-2">
Die Kelly-Formel bestimmt den optimalen Anteil des Kapitals (<InlineMath math="f^*" />), der in ein Geschäft investiert werden soll, um das exponentielle Wachstum des Kapitals zu maximieren:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="f^* = \frac{p \cdot b - q}{b} = \frac{p \cdot b - (1 - p)}{b}" />
</div>
<p className="mb-2">
Um Risiken durch ungenaue Schätzungen zu verringern, wenden wir das konservative <strong>Half-Kelly</strong>-Sizing an und begrenzen das Ergebnis auf <InlineMath math="0.5 \times f^*" /> (zusätzlich begrenzt auf <InlineMath math="\ge 0" />).
</p>
</div>
<div className="border-t border-slate-800 pt-3">
<h4 className="font-semibold text-rose-400 mb-1.5">3. Covariance & Cluster Risk (Kovarianz-Ampel)</h4>
<p className="mb-2">
Die Kovarianz zwischen Assets wird durch Multiplikation ihrer paarweisen Korrelation mit ihren jeweiligen Standardabweichungen (Volatilitäten) bestimmt:
</p>
<div className="py-2 overflow-x-auto">
<BlockMath math="\text{Cov}(A, B) = \text{Corr}(A, B) \times \sigma_A \times \sigma_B" />
</div>
<p className="text-slate-400">
Ein <strong>Klumpenrisiko (Risk RED)</strong> wird ausgelöst, wenn ein Asset eine Korrelation <InlineMath math="\text{Corr}(A, B) > 0.70" /> zu bestehenden Positionen aufweist und diese Positionen jeweils mehr als 15% des Portfolios ausmachen.
</p>
</div>
</div>
)}
</div>
</div>
{/* SECTION 2: Chart / Analytics & Order Form */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Analytics Performance Plot */}
<div className="xl:col-span-2 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 justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<TrendingUp className="text-emerald-400 w-5 h-5" /> Portfolio Wertentwicklung & Benchmark
</h3>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-xs text-slate-400 cursor-pointer">
<input
type="checkbox"
checked={showMsciBenchmark}
onChange={(e) => setShowMsciBenchmark(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4"
/>
<span>MSCI World (Benchmark) anzeigen</span>
</label>
</div>
</div>
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="date" stroke="#64748b" fontSize={10} />
<YAxis stroke="#64748b" fontSize={10} domain={['auto', 'auto']} tickFormatter={(v) => `$${v.toLocaleString()}`} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Legend verticalAlign="top" height={36} />
<Line type="monotone" dataKey="Portfolio" name={activePortfolio.name} stroke="#10b981" strokeWidth={3} dot={false} activeDot={{ r: 6 }} />
{showMsciBenchmark && (
<Line type="monotone" dataKey="MSCI World (Benchmark)" name="MSCI World Index (Normiert)" stroke="#3b82f6" strokeWidth={2} strokeDasharray="4 4" dot={false} />
)}
</LineChart>
</ResponsiveContainer>
</div>
{/* EWMA parameter tuner slider */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/20 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold text-slate-200">Parameteranpassung EWMA Lambda (&lambda;)</h4>
<p className="text-xs text-slate-400">Zerfallsfaktor steuert die Schock-Sensitivität des Volatilitätsmodells.</p>
</div>
<div className="flex items-center gap-4 w-full sm:w-auto">
<input
type="range"
min="0.80"
max="0.99"
step="0.01"
value={ewmaLambda}
onChange={(e) => setEwmaLambda(parseFloat(e.target.value))}
className="w-40 accent-emerald-400 cursor-pointer"
/>
<span className="font-mono text-emerald-400 text-sm font-bold bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">&lambda; = {ewmaLambda.toFixed(2)}</span>
</div>
</div>
</div>
{/* Right 1 Column: Advanced Order Mask */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden flex flex-col justify-between">
<div className="space-y-6">
<div className="border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Settings className="text-emerald-400 w-5 h-5" /> Order-Maske (Simuliert)
</h3>
</div>
<form onSubmit={handleTransactionSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Ticker / Symbol</label>
<input
type="text"
required
placeholder="AAPL"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeSymbol}
onChange={(e) => setTradeSymbol(e.target.value.toUpperCase())}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">WKN / ISIN</label>
<input
type="text"
placeholder="865985"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeWknOrIsin}
onChange={(e) => setTradeWknOrIsin(e.target.value)}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Stücke</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradeShares}
onChange={(e) => setTradeShares(Number(e.target.value))}
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Kurs ($)</label>
<input
type="number"
required
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 font-mono focus:outline-none focus:border-emerald-500"
value={tradePrice}
onChange={(e) => setTradePrice(Number(e.target.value))}
/>
</div>
</div>
{/* Order direction buttons */}
<div className="flex gap-2 p-1 rounded-xl bg-slate-950 border border-slate-800">
<button
type="button"
onClick={() => setTradeType('BUY')}
className={`flex-1 py-1.5 text-xs font-bold rounded-lg transition-all ${tradeType === 'BUY' ? 'bg-emerald-500 text-slate-950 shadow-md shadow-emerald-500/10' : 'text-slate-400 hover:text-slate-200'}`}
>
Kauf (Long)
</button>
<button
type="button"
onClick={() => setTradeType('SELL')}
className={`flex-1 py-1.5 text-xs font-bold rounded-lg transition-all ${tradeType === 'SELL' ? 'bg-rose-500 text-white shadow-md shadow-rose-500/10' : 'text-slate-400 hover:text-slate-200'}`}
>
Verkauf (Short)
</button>
</div>
{/* Kelly Sizing Risk Recommendation Widget */}
{tradeType === 'BUY' && (
<div className="bg-slate-950/60 border border-slate-850 rounded-xl p-3 text-[11px] space-y-2 relative overflow-hidden">
<div className="flex justify-between items-center">
<span className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<Sparkles className="w-3 h-3 text-emerald-400" /> Risk-Engine Sizing
</span>
<span className="font-bold text-emerald-400 text-[10px] uppercase tracking-wider">Kelly recommendation</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[9px] text-slate-500 block mb-0.5 font-semibold uppercase">Prob Source (p)</label>
<select
value={kellySource}
onChange={(e) => setKellySource(e.target.value as any)}
className="w-full bg-slate-900 border border-slate-800 rounded px-1.5 py-1 text-[10px] text-slate-200 focus:outline-none"
>
<option value="custom">Manueller Regler</option>
<option value="scanner">El. 2 Scanner-Score</option>
<option value="crypto">El. 4 Bayes Posterior</option>
<option value="econometric">El. 5 ROC Breakout</option>
</select>
</div>
<div>
<label className="text-[9px] text-slate-500 block mb-0.5 font-semibold uppercase">Odds-Verhältnis (b)</label>
<input
type="number"
step="0.1"
min="0.1"
value={oddsRatio}
onChange={(e) => 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"
/>
</div>
</div>
{kellySource === 'custom' && (
<div className="space-y-1">
<div className="flex justify-between text-[9px] text-slate-500">
<span>Erfolgswahrscheinlichkeit:</span>
<span className="font-mono text-emerald-400 font-bold">{(kellyProbability * 100).toFixed(0)}%</span>
</div>
<input
type="range"
min="0.1"
max="0.99"
step="0.01"
value={customProb}
onChange={(e) => setCustomProb(Number(e.target.value))}
className="w-full accent-emerald-500 h-1 bg-slate-900 rounded"
/>
</div>
)}
{kellySource !== 'custom' && (
<div className="text-[9px] text-slate-400 flex justify-between bg-slate-900/40 p-1 px-1.5 rounded border border-slate-900">
<span>System-Wahrscheinlichkeit:</span>
<span className="font-mono text-emerald-400 font-bold">{(kellyProbability * 100).toFixed(1)}%</span>
</div>
)}
{potentialClusterRisk && (
<div className="p-2 bg-rose-500/10 text-rose-400 border border-rose-500/20 text-[9px] rounded flex items-start gap-1">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-rose-400" />
<div>
<strong>Klumpenrisiko!</strong> Korrelation &gt; 0.70 zu bestehenden Positionen. Kelly-Empfehlung wurde um 50% halbiert.
</div>
</div>
)}
<div className="bg-slate-900/80 p-2 rounded-lg border border-slate-850 flex justify-between items-center text-xs">
<div>
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Kelly-Anteil:</span>
<span className="font-mono font-bold text-slate-200">{(kellyFraction * 100).toFixed(1)}% des Cashs</span>
</div>
<div className="text-right">
<span className="block text-[9px] text-slate-500 uppercase font-semibold">Kaufvolumen:</span>
<span className="font-mono font-bold text-emerald-400">${Math.round(recommendedKellyCash).toLocaleString()}</span>
</div>
</div>
</div>
)}
{/* Hypothesis input */}
<div>
<label className="text-xs text-slate-400 block mb-1 flex items-center gap-1">
<Tag className="w-3 h-3 text-emerald-400" />
<span>Hypothese / What-if Notiz</span>
</label>
<input
type="text"
placeholder="z.B. Ferrari E-Auto Skepsis"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 text-xs focus:outline-none focus:border-emerald-500"
value={hypothesisTag}
onChange={(e) => setHypothesisTag(e.target.value)}
/>
</div>
{/* Fees Toggle */}
<div className="flex items-center justify-between border-t border-slate-850 pt-3 text-xs">
<span className="text-slate-400 flex items-center gap-1">
<DollarSign className="w-3.5 h-3.5 text-slate-500" /> Ordergebühren simulieren
</span>
<input
type="checkbox"
checked={simulateFees}
onChange={(e) => setSimulateFees(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer"
/>
</div>
{/* Backfill Date Picker Toggle */}
<div className="space-y-2 border-t border-slate-850 pt-3 text-xs">
<div className="flex items-center justify-between">
<span className="text-slate-400 flex items-center gap-1">
<Calendar className="w-3.5 h-3.5 text-slate-500" /> Historischer Backfill
</span>
<input
type="checkbox"
checked={isBackfill}
onChange={(e) => setIsBackfill(e.target.checked)}
className="rounded border-slate-800 text-emerald-500 focus:ring-0 accent-emerald-500 w-4 h-4 cursor-pointer"
/>
</div>
{isBackfill && (
<input
type="date"
className="w-full bg-slate-950 border border-slate-800 rounded-lg p-2 text-slate-100 text-xs focus:outline-none focus:border-emerald-500 font-mono"
value={backfillDate}
onChange={(e) => setBackfillDate(e.target.value)}
/>
)}
</div>
{orderError && (
<div className="p-3 rounded-lg bg-rose-500/10 text-rose-400 border border-rose-500/20 text-xs flex items-center gap-2">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{orderError}</span>
</div>
)}
{orderSuccess && (
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-xs flex items-center gap-2">
<Check className="w-4 h-4 shrink-0" />
<span>Transaktion erfolgreich gebucht!</span>
</div>
)}
<button
type="submit"
className={`w-full font-bold py-2.5 px-4 rounded-lg transition-all active:scale-[0.98] mt-2 shadow-lg ${tradeType === 'BUY' ? 'bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-slate-950 shadow-emerald-500/10' : 'bg-gradient-to-r from-rose-500 to-pink-500 hover:from-rose-600 hover:to-pink-600 text-white shadow-rose-500/10'}`}
>
Order an den Markt senden
</button>
</form>
</div>
</div>
</div>
{/* SECTION 3: Holdings Table & Transactions Log */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Holdings List */}
<div className="xl:col-span-2 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">
<TrendingUp className="text-emerald-400 w-5 h-5" /> Depotbestände ({activePortfolio.holdings.length})
</h3>
<div className="text-xs text-slate-400 flex items-center gap-4">
<span>Barguthaben: <span className="font-mono text-emerald-400 font-bold">${activePortfolio.cash.toLocaleString()}</span></span>
</div>
</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">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
<th className="p-3">Asset</th>
<th className="p-3">Stücke</th>
<th className="p-3">Einstand</th>
<th className="p-3">Kurs</th>
<th className="p-3">Hypothese</th>
<th className="p-3 text-right">GuV</th>
</tr>
</thead>
<tbody>
{activePortfolio.holdings.length === 0 ? (
<tr>
<td colSpan={6} className="p-8 text-center text-slate-500">
Keine Bestände in diesem Sandbox-Portfolio. Nutzen Sie die Order-Maske, um Werte hinzuzufügen.
</td>
</tr>
) : (
activePortfolio.holdings.map((hold) => {
const profitLoss = (hold.currentPrice - hold.avgPrice) * hold.shares;
const isPositive = profitLoss >= 0;
return (
<tr key={hold.symbol} className="border-b border-slate-850/50 hover:bg-slate-850/20 transition-colors">
<td className="p-3">
<div className="font-bold text-teal-400 font-mono">{hold.symbol}</div>
{hold.wknOrIsin && <div className="text-[10px] text-slate-500 font-mono">WKN: {hold.wknOrIsin}</div>}
</td>
<td className="p-3 font-mono font-medium">{hold.shares}</td>
<td className="p-3 font-mono text-slate-300">${hold.avgPrice.toFixed(2)}</td>
<td className="p-3 font-mono text-slate-300">${hold.currentPrice.toFixed(2)}</td>
<td className="p-3 text-slate-400 max-w-[200px] truncate text-xs" title={hold.hypothesisTag}>
{hold.hypothesisTag || '-'}
</td>
<td className={`p-3 font-mono text-right flex items-center justify-end gap-1 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
${Math.abs(profitLoss).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Right 1 Column: Transactions History */}
<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="border-b border-slate-800 pb-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Calendar className="text-emerald-400 w-5 h-5" /> Letzte Orderbuch-Einträge
</h3>
</div>
<div className="max-h-60 overflow-y-auto space-y-2 pr-1">
{activePortfolio.transactions.length === 0 ? (
<p className="text-xs text-slate-500 text-center py-8">Bislang keine Transaktionen in diesem Portfolio.</p>
) : (
activePortfolio.transactions.map((tx) => {
const isBuy = tx.type === 'BUY';
return (
<div key={tx.id} className="p-3 bg-slate-950/40 border border-slate-850 rounded-lg space-y-1.5">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold ${isBuy ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
{isBuy ? 'KAUF' : 'VERKAUF'}
</span>
<span className="font-mono font-bold text-slate-200">{tx.symbol}</span>
</div>
<span className="text-[10px] text-slate-500 font-mono">{tx.timestamp}</span>
</div>
<div className="flex justify-between text-xs font-mono text-slate-400">
<span>{tx.shares} Stk @ ${tx.price.toFixed(2)}</span>
<span className="text-[10px] text-slate-500">Gebühr: ${tx.feeApplied.toFixed(2)}</span>
</div>
{tx.hypothesisTag && (
<div className="text-[10px] text-slate-500 flex items-center gap-1 border-t border-slate-900 pt-1">
<Tag className="w-2.5 h-2.5 text-teal-500" />
<span className="italic truncate max-w-[220px]">{tx.hypothesisTag}</span>
</div>
)}
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,538 @@
'use client';
import React, { useState, useMemo } from 'react';
import { useSandboxStore, ScannerAlert, WatchlistItem } from '@/lib/store';
import { calculateGJRGARCH } 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 {
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play
} from 'lucide-react';
// Predefined mock database for deep-check searches
interface SearchResult {
ticker: string;
name: string;
priceChange: number;
sentiment: 'GREEN' | 'YELLOW' | 'RED';
whyDropped: string;
gjrGarchVol: number;
reboundScore: number;
returns: number[];
}
const mockSearchDatabase: Record<string, SearchResult> = {
'RACE': {
ticker: 'RACE',
name: 'Ferrari N.V.',
priceChange: -0.065,
sentiment: 'GREEN',
whyDropped: 'Emotionaler Abverkauf nach viralem Video von Cristiano Ronaldo, der sich über Autoprobleme beschwert. Keine fundamentalen Schäden.',
gjrGarchVol: 0.048,
reboundScore: 88,
returns: [0.01, -0.005, 0.012, -0.008, 0.003, -0.065]
},
'KO': {
ticker: 'KO',
name: 'The Coca-Cola Co.',
priceChange: -0.052,
sentiment: 'GREEN',
whyDropped: 'Berühmter Influencer entfernt Coca-Cola Flasche während Pressekonferenz. Reine Social-Media-Hype Reaktion.',
gjrGarchVol: 0.021,
reboundScore: 82,
returns: [0.002, 0.005, -0.003, 0.001, -0.002, -0.052]
},
'TSLA': {
ticker: 'TSLA',
name: 'Tesla Inc.',
priceChange: -0.084,
sentiment: 'YELLOW',
whyDropped: 'Auslieferungszahlen leicht unter Analystenschätzungen. Margenentwicklung bleibt jedoch stabil.',
gjrGarchVol: 0.062,
reboundScore: 65,
returns: [-0.012, 0.008, -0.025, 0.015, -0.005, -0.084]
},
'SMCI': {
ticker: 'SMCI',
name: 'Super Micro Computer',
priceChange: -0.124,
sentiment: 'RED',
whyDropped: 'Hindenburg Research Short-Seller-Report bezüglich mutmaßlicher Bilanzmanipulationen veröffentlicht.',
gjrGarchVol: 0.085,
reboundScore: 24,
returns: [0.035, -0.018, 0.042, -0.051, 0.012, -0.124]
},
'BA': {
ticker: 'BA',
name: 'Boeing Co.',
priceChange: -0.071,
sentiment: 'RED',
whyDropped: 'FAA verhängt vorübergehendes Flugverbot nach erneutem technischen Zwischenfall mit Rumpftür.',
gjrGarchVol: 0.058,
reboundScore: 18,
returns: [-0.005, -0.012, 0.005, -0.021, -0.008, -0.071]
},
'NFLX': {
ticker: 'NFLX',
name: 'Netflix Inc.',
priceChange: -0.058,
sentiment: 'GREEN',
whyDropped: 'Gerüchte über angebliche Preissenkungen in Schwellenländern belasten Kurs kurzfristig.',
gjrGarchVol: 0.038,
reboundScore: 78,
returns: [0.015, -0.002, 0.008, 0.005, -0.011, -0.058]
}
};
export default function ScannerDemo() {
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick } = useSandboxStore();
// Component local states
const [scanning, setScanning] = useState(false);
const [scanProgress, setScanProgress] = useState('');
const [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([
{ id: 'sa1', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa2', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
]);
const [searchQuery, setSearchQuery] = useState('');
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
const [searchError, setSearchError] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
// Run market scan simulator
const handleMarketScan = () => {
setScanning(true);
setScanProgress('Verbinde mit Börsenfeeds...');
setTimeout(() => {
setScanProgress('Berechne historische Volatilitätsmatrizen...');
setTimeout(() => {
setScanProgress('Filtere abnormale Abweichungen (Asset > -5%, Index stabil)...');
setTimeout(() => {
// Scan isolated anomalies
setActiveAlerts([
{ id: 'sa3', ticker: 'RACE', priceChange: -0.065, gjrGarchVol: 0.048, overreactionScore: 88, status: 'UNDEREVALUATED' },
{ id: 'sa4', ticker: 'KO', priceChange: -0.052, gjrGarchVol: 0.021, overreactionScore: 82, status: 'UNDEREVALUATED' },
{ id: 'sa5', ticker: 'TSLA', priceChange: -0.084, gjrGarchVol: 0.062, overreactionScore: 65, status: 'UNDEREVALUATED' },
{ id: 'sa6', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.085, overreactionScore: 24, status: 'FAIR' },
{ id: 'sa7', ticker: 'BA', priceChange: -0.071, gjrGarchVol: 0.058, overreactionScore: 18, status: 'OVERVALUATED' },
]);
setScanning(false);
setScanProgress('');
}, 600);
}, 500);
}, 400);
};
// Perform a manual deep check
const handleDeepCheck = (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
setSearchResult(null);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
if (mockSearchDatabase[query]) {
setSearchResult(mockSearchDatabase[query]);
} else {
// Simulate dynamic result for unknown assets
const simulatedVol = 0.03 + Math.random() * 0.04;
const simulatedScore = Math.floor(40 + Math.random() * 50);
const isNegative = Math.random() > 0.4;
const simulatedChange = -0.05 - Math.random() * 0.06;
const res: SearchResult = {
ticker: query,
name: `${query} Corp.`,
priceChange: simulatedChange,
sentiment: isNegative ? (simulatedScore > 75 ? 'GREEN' : 'YELLOW') : 'RED',
whyDropped: 'Simulierte Marktabweichung basierend auf automatischem Sentiment-Scanning der Finanzberichte.',
gjrGarchVol: simulatedVol,
reboundScore: simulatedScore,
returns: [0.005, -0.008, 0.012, -0.015, 0.004, simulatedChange]
};
setSearchResult(res);
}
};
const handleAddToWatchlist = (ticker: string, priceChange: number, sentiment: 'GREEN' | 'YELLOW' | 'RED', whyDropped: string) => {
// Determine a mock initial price based on ticker
let initialPrice = 150;
if (ticker === 'RACE') initialPrice = 380;
if (ticker === 'KO') initialPrice = 60;
if (ticker === 'TSLA') initialPrice = 175;
if (ticker === 'NFLX') initialPrice = 610;
addToWatchlist({
ticker,
priceChange,
sentiment,
whyDropped,
initialPrice,
currentPrice: initialPrice * (1 + priceChange), // current price after drop
});
};
// Compute GJR-GARCH forecasting series for the math accordion/visual validation
const gjrGarchMathResult = useMemo(() => {
const mockReturns = [0.015, -0.022, 0.008, -0.031, 0.014, -0.055, 0.012, -0.008, 0.024, -0.065];
return calculateGJRGARCH(mockReturns);
}, []);
const mathChartData = useMemo(() => {
return gjrGarchMathResult.series.map((vol, idx) => ({
day: `T-${gjrGarchMathResult.series.length - idx - 1}`,
'GJR-GARCH Vol (%)': parseFloat(vol.toFixed(2)),
}));
}, [gjrGarchMathResult]);
return (
<div className="space-y-6">
{/* SECTION 1: Overreaction Scan Action */}
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/10 rounded-full blur-3xl -z-10" />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div className="space-y-1">
<span className="text-amber-400 text-xs font-semibold uppercase tracking-wider">Market Scanner Engine</span>
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<ShieldAlert className="text-amber-400 w-5 h-5" /> Anomalien-Scanner & Marktverzerrungen
</h2>
<p className="text-slate-400 text-xs max-w-2xl">
Isoliert Kursstürze &gt; 5% bei relativem Gesamtmarkt-Stopp (S&P 500 driftet seitwärts oder steigt). Misst die Asymmetrie mittels GJR-GARCH, um Panik von strukturellen Risiken zu separieren.
</p>
</div>
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
<button
onClick={handleMarketScan}
disabled={scanning}
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 disabled:from-amber-850 disabled:to-orange-900 disabled:text-slate-400 text-slate-950 font-bold py-3 px-6 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 active:scale-[0.98]"
>
<RefreshCw className={`w-5 h-5 ${scanning ? 'animate-spin' : ''}`} />
<span>{scanning ? 'Scanne Markt...' : 'Markt scannen'}</span>
</button>
{scanning && (
<span className="text-[10px] text-amber-400 font-mono text-center md:text-right animate-pulse">{scanProgress}</span>
)}
</div>
</div>
{/* Collapsible Math Accordion */}
<div className="border-t border-slate-850 pt-4 mt-6">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-amber-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">GJR-GARCH(1,1) Volatilitätsmodellierung & Leverage-Effekt</span>
</button>
{showMathAccordion && (
<div className="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300">
<div className="space-y-3">
<p>
Das <strong>GJR-GARCH(1,1)</strong>-Modell erfasst die Asymmetrie im Volatilitätsprozess von Renditen. Es besitzt einen zusätzlichen Term (<InlineMath math="\gamma" />), der aktiviert wird, wenn der gestrige Schock negativ war.
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \gamma \epsilon_{t-1}^2 I_{t-1} + \beta \sigma_{t-1}^2" />
</div>
<p>
Die Indikatorvariable <InlineMath math="I_{t-1}" /> nimmt den Wert 1 bei einem Kurssturz an:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="I_{t-1} = \begin{cases} 1 & \text{falls } \epsilon_{t-1} < 0 \\ 0 & \text{sonst} \end{cases}" />
</div>
<p className="text-slate-400">
Sind die Volatilitätsschocks asymmetrisch (<InlineMath math="\gamma > 0" />), führt ein Kurssturz (Bad News) zu einer signifikant höheren Volatilität als ein gleich großer Kursgewinn (Good News).
</p>
</div>
<div className="space-y-2">
<div className="text-xs text-slate-400 font-semibold mb-2">Simulierter GJR-GARCH Volatilitätsprozess nach Schocks</div>
<div className="h-44 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={mathChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="day" stroke="#64748b" fontSize={9} />
<YAxis stroke="#64748b" fontSize={9} unit="%" />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }} />
<Line type="monotone" dataKey="GJR-GARCH Vol (%)" stroke="#f59e0b" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
<div className="text-[10px] text-slate-500 font-mono text-center">
Spike am Tag T-4 repräsentiert die asymmetrische Reaktion auf einen Schock von -6.5%.
</div>
</div>
</div>
)}
</div>
</div>
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left 2 Columns: Scanned anomalies details */}
<div className="xl:col-span-2 space-y-6">
<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">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Sparkles className="text-amber-400 w-5 h-5" /> Gefundene Anomalien (Sentiment-Ampel)
</h3>
<div className="space-y-4">
{activeAlerts.map((alert) => {
// Fetch associated info from mockDB if available, else generic mock
const dbInfo = mockSearchDatabase[alert.ticker] || {
name: `${alert.ticker} Corp.`,
sentiment: alert.overreactionScore > 75 ? 'GREEN' : (alert.overreactionScore > 40 ? 'YELLOW' : 'RED'),
whyDropped: 'Kurzfristige Eindeckungen und Gewinnmitnahmen an den Terminmärkten belasten das Sentiment.'
};
const isGreen = dbInfo.sentiment === 'GREEN';
const isYellow = dbInfo.sentiment === 'YELLOW';
const isRed = dbInfo.sentiment === 'RED';
return (
<div key={alert.id} className="p-5 bg-slate-950/40 border border-slate-850 rounded-xl space-y-4 relative group">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 border-b border-slate-900 pb-3">
<div>
<div className="flex items-center gap-2.5">
<span className="font-mono font-bold text-lg text-slate-100">{alert.ticker}</span>
<span className="text-slate-400 text-xs">({dbInfo.name})</span>
</div>
<div className="text-[10px] text-slate-400 mt-1">
Kurssturz: <span className="text-rose-400 font-bold font-mono">{(alert.priceChange * 100).toFixed(1)}%</span>
<span className="mx-2">|</span>
GJR-GARCH Vol: <span className="text-cyan-400 font-bold font-mono">{(alert.gjrGarchVol * 100).toFixed(1)}%</span>
</div>
</div>
{/* Traffic Light Sentiment Badge */}
<div className="flex items-center gap-2">
{isGreen && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/25 flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5" /> EMOTIONALER OVERREACTION (KAUF)
</span>
)}
{isYellow && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-yellow-500/10 text-yellow-400 border border-yellow-500/25 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5" /> UNSICHERHEIT (HALTEN)
</span>
)}
{isRed && (
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/25 flex items-center gap-1">
<XCircle className="w-3.5 h-3.5" /> FUNDAMENTALER SCHADEN (MEIDEN)
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Analysis Block */}
<div className="md:col-span-2 space-y-1">
<div className="text-[10px] text-slate-400 uppercase font-semibold flex items-center gap-1">
<Info className="w-3 h-3 text-amber-400" /> KI-Ursachenanalyse:
</div>
<p className="text-xs text-slate-300 leading-relaxed italic">
&bdquo;{dbInfo.whyDropped}&ldquo;
</p>
</div>
{/* Actions & Score */}
<div className="flex flex-row md:flex-col items-center md:items-end justify-between md:justify-center gap-3 md:border-l md:border-slate-900 md:pl-4">
<div className="text-left md:text-right">
<span className="text-[9px] text-slate-400 uppercase tracking-widest block">Rebound Score</span>
<span className={`font-mono text-xl font-extrabold ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
{alert.overreactionScore}%
</span>
</div>
{(isGreen || isYellow) && (
<button
onClick={() => handleAddToWatchlist(alert.ticker, alert.priceChange, dbInfo.sentiment, dbInfo.whyDropped)}
className="bg-slate-900 hover:bg-slate-850 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[11px] font-semibold py-1.5 px-3 rounded-lg flex items-center gap-1 transition-colors active:scale-[0.98]"
>
<Plus className="w-3.5 h-3.5" /> Tracken
</button>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Right Column: Deep-Check & Search Mask */}
<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>
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<Search className="text-amber-400 w-5 h-5" /> Deep-Check Suchmaske
</h3>
<p className="text-xs text-slate-400 mt-2 leading-relaxed">
Suchen Sie gezielt nach Werten, um Anomalien-Analysen abzurufen. (z.B. RACE, KO, TSLA, SMCI, NFLX)
</p>
</div>
<form onSubmit={handleDeepCheck} className="flex gap-2">
<input
type="text"
required
placeholder="z.B. RACE"
className="bg-slate-950 border border-slate-800 rounded-lg p-2.5 flex-1 text-slate-100 font-mono focus:outline-none focus:border-amber-500 text-sm uppercase"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-amber-400 hover:text-amber-300 font-bold px-4 py-2 border border-slate-700 rounded-lg transition-colors text-sm"
>
Prüfen
</button>
</form>
{searchResult && (
<div className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-4 animate-fade-in">
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
<div>
<h4 className="font-bold text-slate-100 font-mono text-sm">{searchResult.ticker}</h4>
<span className="text-[10px] text-slate-500">{searchResult.name}</span>
</div>
<div>
{searchResult.sentiment === 'GREEN' && <span className="px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px] font-bold border border-emerald-500/25">GREEN</span>}
{searchResult.sentiment === 'YELLOW' && <span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-400 text-[10px] font-bold border border-yellow-500/25">YELLOW</span>}
{searchResult.sentiment === 'RED' && <span className="px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 text-[10px] font-bold border border-rose-500/25">RED</span>}
</div>
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-slate-400">Aktueller Sturz:</span>
<span className="text-rose-400 font-mono font-bold">{(searchResult.priceChange * 100).toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">GJR-GARCH Volatilität:</span>
<span className="text-cyan-400 font-mono font-bold">{(searchResult.gjrGarchVol * 100).toFixed(1)}%</span>
</div>
<div className="flex justify-between">
<span className="text-slate-400">Rebound Score:</span>
<span className={`font-mono font-bold ${searchResult.sentiment === 'GREEN' ? 'text-emerald-400' : (searchResult.sentiment === 'YELLOW' ? 'text-yellow-400' : 'text-rose-400')}`}>
{searchResult.reboundScore}/100
</span>
</div>
<div className="pt-2 border-t border-slate-900">
<span className="text-[10px] text-slate-400 uppercase font-semibold block mb-1">KI-Kommentar:</span>
<p className="italic text-slate-300 leading-relaxed text-[11px]">{searchResult.whyDropped}</p>
</div>
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
<button
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
className="w-full bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-2 rounded-lg transition-colors flex items-center justify-center gap-1.5 mt-2"
>
<Plus className="w-4 h-4" /> Watchlist hinzufügen
</button>
)}
</div>
</div>
)}
</div>
</div>
{/* SECTION 3: Hot Watchlist & Rebound-Tracker (48 Hours) */}
<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 flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-slate-800 pb-3">
<div className="flex items-center gap-2">
<Flame className="text-orange-400 w-6 h-6 animate-pulse" />
<h3 className="text-lg font-bold text-white">Hot Rebound Watchlist &amp; Tracker (48h)</h3>
</div>
{watchlist.length > 0 && (
<button
onClick={simulateWatchlistTick}
className="bg-slate-950 hover:bg-slate-900 text-orange-400 hover:text-orange-350 border border-slate-800 text-xs font-bold py-2 px-4 rounded-xl flex items-center gap-1.5 transition-colors active:scale-[0.97]"
title="Simuliert das Fortschreiten der Zeit um 4 Stunden"
>
<Play className="w-4 h-4 fill-orange-400" />
<span>Simuliere +4 Std. Kursänderung</span>
</button>
)}
</div>
{watchlist.length === 0 ? (
<div className="p-12 text-center text-slate-500 text-sm border border-dashed border-slate-800 rounded-xl bg-slate-950/20">
Bislang keine Assets auf der Rebound-Watchlist. Nutzen Sie die Markt-Scanergebnisse oben, um Werte hinzuzufügen.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{watchlist.map((item) => {
const perf = item.reboundPerformance;
const isPositive = perf >= 0;
const progressPct = (item.hoursTracked / 48) * 100;
return (
<div key={item.id} className="p-4 rounded-xl border border-slate-800 bg-slate-950/50 space-y-3 relative overflow-hidden">
<div className="absolute top-0 right-0 p-1.5">
<button
onClick={() => removeFromWatchlist(item.id)}
className="text-slate-500 hover:text-rose-400 transition-colors p-1"
title="Aus der Watchlist entfernen"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="flex justify-between items-start pr-6">
<div>
<div className="flex items-center gap-2">
<span className="font-mono font-bold text-slate-200 text-base">{item.ticker}</span>
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold ${item.sentiment === 'GREEN' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'}`}>
{item.sentiment}
</span>
</div>
<span className="text-[10px] text-slate-500">Gezogen bei: <span className="font-mono">{(item.priceChange * 100).toFixed(1)}%</span></span>
</div>
<div className="text-right">
<div className="text-[9px] text-slate-400 uppercase">Rebound Performance</div>
<div className={`font-mono text-base font-extrabold flex items-center justify-end gap-0.5 ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? '+' : ''}{perf.toFixed(2)}%
</div>
</div>
</div>
{/* Progress timeline */}
<div className="space-y-1 text-[10px] text-slate-400">
<div className="flex justify-between font-mono">
<span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5 text-slate-500" /> Tracking-Dauer: {item.hoursTracked}/48 Std.</span>
<span>{Math.round(progressPct)}% abgeschlossen</span>
</div>
<div className="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
<div
className="bg-gradient-to-r from-orange-400 to-amber-500 h-full rounded-full transition-all duration-500"
style={{ width: `${progressPct}%` }}
/>
</div>
</div>
<p className="text-[10px] text-slate-500 italic truncate" title={item.whyDropped}>
Hintergrund: {item.whyDropped}
</p>
</div>
);
})}
</div>
)}
</div>
</div>
);
}

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

36
github-agent-config.json Normal file
View File

@@ -0,0 +1,36 @@
{
"agentWorkflow": {
"enabled": true,
"version": "1.0.0",
"orchestratorUrl": "https://api.antigravity.ai/v1/workflows/trigger",
"webhookSecretName": "ANTIGRAVITY_AGENT_TOKEN",
"mappings": [
{
"trigger": "issue_labeled",
"labels": ["enhancement", "feature", "agent-resolve"],
"assignedAgent": "orchestrator",
"mode": "autonomous",
"createBranchPattern": "feature/issue-{issue_number}"
},
{
"trigger": "issue_labeled",
"labels": ["bug", "hotfix"],
"assignedAgent": "orchestrator",
"mode": "autonomous",
"createBranchPattern": "bugfix/issue-{issue_number}"
}
],
"verification": {
"requireBuild": true,
"runTests": true,
"buildScript": "npm run build",
"testScript": "npm test"
},
"pullRequest": {
"autoCreate": true,
"draft": false,
"assignee": "jannr",
"reviewers": []
}
}
}

717
lib/math/statistics.ts Normal file
View File

@@ -0,0 +1,717 @@
/**
* Statistical and Econometric Utilities for Investment Sandbox
*/
/**
* Calculates the Exponentially Weighted Moving Average (EWMA) Volatility for asset returns
* Formula: sigma_t^2 = lambda * sigma_{t-1}^2 + (1 - lambda) * r_{t-1}^2
* Annualized Volatility: sigma_ann = sqrt(sigma_t^2 * 252)
*/
export function calculateEWMA(
returns: number[],
lambda: number = 0.94
): { series: number[]; latest: number } {
if (returns.length === 0) {
return { series: [], latest: 0 };
}
const series: number[] = new Array(returns.length).fill(0);
// Calculate initial variance as average of squared returns (mean = 0)
let currentVariance = returns.reduce((sum, r) => sum + r * r, 0) / returns.length;
if (currentVariance === 0) {
currentVariance = 0.0004; // Seed variance (2% daily standard deviation squared)
}
// Initial annualized volatility
series[0] = Math.sqrt(currentVariance * 252);
for (let t = 1; t < returns.length; t++) {
const rPrev = returns[t - 1];
currentVariance = lambda * currentVariance + (1 - lambda) * rPrev * rPrev;
series[t] = Math.sqrt(currentVariance * 252);
}
return {
series,
latest: series[series.length - 1],
};
}
/**
* Calculates asymmetric GJR-GARCH(1,1) volatility series and next-day forecast
* Formula: sigma_t^2 = omega + alpha * epsilon_{t-1}^2 + gamma * epsilon_{t-1}^2 * I_{t-1} + beta * sigma_{t-1}^2
* Where returns are scaled to percentages (e.g. 5.0 instead of 0.05) to align with default parameters.
*/
export function calculateGJRGARCH(
returns: number[],
omega: number = 0.02,
alpha: number = 0.05,
gamma: number = 0.10,
beta: number = 0.80
): {
series: number[];
forecast: number;
} {
if (returns.length === 0) {
return { series: [], forecast: 0 };
}
// Standardize return inputs to percentages
const isDecimal = returns.some(r => Math.abs(r) > 0 && Math.abs(r) < 0.2);
const scaledReturns = isDecimal ? returns.map(r => r * 100) : returns;
const series: number[] = new Array(scaledReturns.length).fill(0);
// Set initial variance to simple variance of returns
let currentVariance = scaledReturns.reduce((sum, r) => sum + r * r, 0) / scaledReturns.length;
if (currentVariance === 0) {
currentVariance = 4.0; // Seed variance (2% daily vol squared)
}
series[0] = Math.sqrt(currentVariance);
for (let t = 1; t < scaledReturns.length; t++) {
const epsPrev = scaledReturns[t - 1];
const indicator = epsPrev < 0 ? 1 : 0;
currentVariance = omega + alpha * epsPrev * epsPrev + gamma * epsPrev * epsPrev * indicator + beta * currentVariance;
series[t] = Math.sqrt(currentVariance);
}
// Forecast next day's volatility (e.g., after a shock)
const lastEps = scaledReturns[scaledReturns.length - 1] || 0;
const lastIndicator = lastEps < 0 ? 1 : 0;
const forecastVariance = omega + alpha * lastEps * lastEps + gamma * lastEps * lastEps * lastIndicator + beta * currentVariance;
return {
series,
forecast: Math.sqrt(forecastVariance),
};
}
/**
* Performs a Bayesian Online Learning update for expected returns
* Prior: N(priorMean, priorVar)
* Likelihood: N(measurement, measurementVar)
* Posterior: N(postMean, postVar)
*/
export function calculateBayesianUpdate(
priorMean: number,
priorVar: number,
measurement: number,
measurementVar: number
): { mean: number; variance: number } {
// Kalman filter styled 1D update
const gain = priorVar / (priorVar + measurementVar);
const postMean = priorMean + gain * (measurement - priorMean);
const postVar = (1 - gain) * priorVar;
return {
mean: postMean,
variance: postVar,
};
}
/**
* Generates ROC Curve coordinates (FPR, TPR) based on predicted probabilities and binary labels
*/
export function calculateROCCurve(
predictions: number[],
labels: number[]
): { fpr: number; tpr: number; threshold: number }[] {
const data = predictions.map((p, idx) => ({ pred: p, label: labels[idx] }));
// Sort descending by predictions
data.sort((a, b) => b.pred - a.pred);
const totalPositives = labels.filter(l => l === 1).length;
const totalNegatives = labels.length - totalPositives;
if (totalPositives === 0 || totalNegatives === 0) {
return [
{ fpr: 0, tpr: 0, threshold: 1 },
{ fpr: 1, tpr: 1, threshold: 0 }
];
}
const roc = [{ fpr: 0, tpr: 0, threshold: 1 }];
let currentTP = 0;
let currentFP = 0;
for (let i = 0; i < data.length; i++) {
if (data[i].label === 1) {
currentTP++;
} else {
currentFP++;
}
roc.push({
fpr: currentFP / totalNegatives,
tpr: currentTP / totalPositives,
threshold: data[i].pred
});
}
roc.push({ fpr: 1, tpr: 1, threshold: 0 });
return roc;
}
/**
* Generates Kaplan-Meier Survival Curve coordinates
* Used for Event-driven time-to-insolvency or time-to-rebound analyses
*/
export function calculateSurvivalAnalysis(
times: number[],
events: number[] // 1 for event (e.g. default), 0 for censoring
): { time: number; survivalRate: number }[] {
const data = times.map((t, idx) => ({ time: t, event: events[idx] }));
data.sort((a, b) => a.time - b.time);
const curve: { time: number; survivalRate: number }[] = [{ time: 0, survivalRate: 1 }];
let n = data.length; // Number of subjects at risk
let currentSurvival = 1;
for (let i = 0; i < data.length; i++) {
const t = data[i].time;
// Count how many events happened at this time
let d = data[i].event;
let c = data[i].event === 0 ? 1 : 0;
// Group ties
while (i + 1 < data.length && data[i + 1].time === t) {
i++;
if (data[i].event === 1) d++;
else c++;
}
if (d > 0) {
currentSurvival = currentSurvival * (1 - d / n);
curve.push({ time: t, survivalRate: currentSurvival });
}
n -= (d + c);
}
return curve;
}
/**
* Calculates a rolling Z-Score for volumetric data.
* Formula: Z = (X_t - mean) / stdDev
* Returns the latest Z-score and flags if it represents an outlier (Z > 2.0)
*/
export function calculateRollingZScore(
volumes: number[]
): { zScores: number[]; latest: number; isAnomaly: boolean } {
if (volumes.length < 2) {
return { zScores: new Array(volumes.length).fill(0), latest: 0, isAnomaly: false };
}
const mean = volumes.reduce((sum, v) => sum + v, 0) / volumes.length;
const variance = volumes.reduce((sum, v) => sum + (v - mean) * (v - mean), 0) / volumes.length;
const stdDev = Math.sqrt(variance) || 1.0;
const zScores = volumes.map((v) => (v - mean) / stdDev);
const latest = zScores[zScores.length - 1];
return {
zScores,
latest,
isAnomaly: latest > 2.0,
};
}
/**
* Time-Window Cluster Detection: Aggregates multiple trades.
* If 3 or more distinct insiders of the same corporation trade within a moving 14-day window,
* return a cluster flag and scale the signal exponentially.
*/
export function detectInsiderClusters(
trades: { date: string; insiderName: string }[]
): { isCluster: boolean; count: number; multiplier: number } {
if (trades.length < 3) {
return { isCluster: false, count: trades.length, multiplier: 1.0 };
}
// Sort trades by date
const sorted = [...trades].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
const fourteenDays = 14 * 24 * 60 * 60 * 1000;
let maxClusterSize = 0;
for (let i = 0; i < sorted.length; i++) {
const startWindow = new Date(sorted[i].date).getTime();
const uniqueInsiders = new Set<string>();
for (let j = i; j < sorted.length; j++) {
const tradeTime = new Date(sorted[j].date).getTime();
if (tradeTime - startWindow <= fourteenDays) {
uniqueInsiders.add(sorted[j].insiderName);
} else {
break;
}
}
if (uniqueInsiders.size > maxClusterSize) {
maxClusterSize = uniqueInsiders.size;
}
}
const isCluster = maxClusterSize >= 3;
// Exponential scaling multiplier: e^(N - 3) if N >= 3, else 1.0
const multiplier = isCluster ? Math.exp(maxClusterSize - 3) : 1.0;
return {
isCluster,
count: maxClusterSize,
multiplier,
};
}
/**
* Bayesian Probability Coupling: updates posterior rebound probability
* by linking price drop overreactions (Element 2) with insider Z-Scores (Element 3).
* Prior: P(R) [rebound probability]
* Likelihood: P(Z | R) vs P(Z | ~R)
*/
export function coupleBayesianRebound(
priorProbability: number,
insiderZScore: number
): number {
let likelihoodPos = 0.5;
let likelihoodNeg = 0.5;
if (insiderZScore >= 2.0) {
likelihoodPos = 0.88; // 88% chance of high buying Z-score if there's a true rebound
likelihoodNeg = 0.12; // 12% false positive rate
} else if (insiderZScore > 0) {
// Linear scale between 0.5 and 0.88
likelihoodPos = 0.5 + (insiderZScore / 2.0) * 0.38;
likelihoodNeg = 0.5 - (insiderZScore / 2.0) * 0.38;
} else {
// Negative Z-score (selling) reduces probability
const absZ = Math.min(2.0, Math.abs(insiderZScore));
likelihoodPos = 0.5 - (absZ / 2.0) * 0.35; // drops to 15%
likelihoodNeg = 0.5 + (absZ / 2.0) * 0.35; // rises to 85%
}
const marginal = likelihoodPos * priorProbability + likelihoodNeg * (1 - priorProbability);
const posterior = (likelihoodPos * priorProbability) / (marginal || 1.0);
return Math.round(posterior * 100) / 100;
}
/**
* Simulates a non-linear Random Forest decision baseline for crypto.
* Ensemble of 10 decision trees mapping Funding, Open Interest, Long/Short ratio, and Whale flows.
*/
export function predictCryptoTrend(inputs: {
fundingRate: number; // e.g. 0.05 for 0.05%
openInterestChange: number; // e.g. 10.0 for 10%
longShortRatio: number; // e.g. 1.2
whaleInflow: number; // net flows
}): { shortTermProb: number; mediumTermProb: number } {
let stVotes = 0; // Short Term (24h Volatility Squeeze)
let mtVotes = 0; // Medium Term (14d Structural Trend)
const { fundingRate, openInterestChange, longShortRatio, whaleInflow } = inputs;
// Tree 1: Squeeze Detector
if (fundingRate < -0.02 && openInterestChange > 5) stVotes += 1;
if (whaleInflow > 100) mtVotes += 1;
// Tree 2: Funding Extreme Counter-trade
if (fundingRate > 0.08) stVotes -= 0.6;
if (longShortRatio > 1.8) mtVotes -= 0.6;
// Tree 3: Whale Inflow Momentum
if (whaleInflow > 500) {
stVotes += 0.8;
mtVotes += 1;
}
// Tree 4: Long/Short Extreme Capitulation
if (longShortRatio < 0.8 && fundingRate < -0.05) {
stVotes += 1;
mtVotes += 0.6;
}
// Tree 5: Open Interest Build-up Trend
if (openInterestChange > 15 && longShortRatio > 1.2) {
stVotes += 0.5;
mtVotes += 0.5;
}
// Tree 6: High Funding Squeeze
if (fundingRate < -0.04) {
stVotes += 0.8;
} else {
stVotes -= 0.2;
}
// Tree 7: Whale Inflow + High OI
if (whaleInflow > 200 && openInterestChange > 8) {
mtVotes += 0.8;
}
// Tree 8: Low OI + Neutral Funding
if (openInterestChange < -10) {
stVotes -= 0.5;
mtVotes -= 0.3;
}
// Tree 9: Long/Short Ratio divergence
if (longShortRatio > 1.5 && fundingRate > 0.04) {
stVotes -= 0.8;
}
// Tree 10: General trend
if (whaleInflow > 0 && fundingRate < 0.02) {
mtVotes += 0.6;
}
// Map votes to probabilities (logistic scale)
const stScore = 0.5 + (stVotes / 10);
const shortTermProb = Math.min(0.95, Math.max(0.05, stScore));
const mtScore = 0.5 + (mtVotes / 10);
const mediumTermProb = Math.min(0.95, Math.max(0.05, mtScore));
return {
shortTermProb,
mediumTermProb,
};
}
/**
* Beta-conjugate Bayesian self-correcting update.
* Treats history (alphaPrior successes, betaPrior failures) as the prior distribution,
* and the current ML prediction as pseudo-observations of trials.
* Returns the posterior mean probability.
*/
export function calculateBetaPosterior(
alphaPrior: number,
betaPrior: number,
mlProbability: number,
pseudoWeight: number = 10
): number {
const successes = mlProbability * pseudoWeight;
const failures = (1 - mlProbability) * pseudoWeight;
const alphaPost = alphaPrior + successes;
const betaPost = betaPrior + failures;
const posteriorMean = alphaPost / (alphaPost + betaPost);
return Math.round(posteriorMean * 100) / 100;
}
/**
* ROC Analysis: Evaluates predictive performance of scores over binary outcomes.
* Returns coordinates (FPR, TPR) and optimal threshold based on the Youden Index (J = Sensitivity + Specificity - 1).
*/
export interface ROCPoint {
fpr: number;
tpr: number;
threshold: number;
youdenIndex: number;
}
export function calculateEventROC(
predictions: number[],
labels: number[]
): { points: ROCPoint[]; optimalThreshold: number; maxYouden: number } {
if (predictions.length === 0 || labels.length === 0) {
return {
points: [
{ fpr: 0, tpr: 0, threshold: 1, youdenIndex: 0 },
{ fpr: 1, tpr: 1, threshold: 0, youdenIndex: 0 }
],
optimalThreshold: 0.5,
maxYouden: 0
};
}
const data = predictions.map((p, idx) => ({ pred: p, label: labels[idx] }));
data.sort((a, b) => b.pred - a.pred);
const totalPos = labels.filter(l => l === 1).length;
const totalNeg = labels.length - totalPos;
if (totalPos === 0 || totalNeg === 0) {
return {
points: [
{ fpr: 0, tpr: 0, threshold: 1, youdenIndex: 0 },
{ fpr: 1, tpr: 1, threshold: 0, youdenIndex: 0 }
],
optimalThreshold: 0.5,
maxYouden: 0
};
}
const points: ROCPoint[] = [{ fpr: 0, tpr: 0, threshold: 1, youdenIndex: 0 }];
let tp = 0;
let fp = 0;
let maxYouden = -1;
let optimalThreshold = 0.5;
for (let i = 0; i < data.length; i++) {
if (data[i].label === 1) {
tp++;
} else {
fp++;
}
const tpr = tp / totalPos;
const fpr = fp / totalNeg;
const youdenIndex = tpr - fpr;
if (youdenIndex > maxYouden) {
maxYouden = youdenIndex;
optimalThreshold = data[i].pred;
}
points.push({ fpr, tpr, threshold: data[i].pred, youdenIndex });
}
points.push({ fpr: 1, tpr: 1, threshold: 0, youdenIndex: 0 });
return {
points,
optimalThreshold: Math.round(optimalThreshold * 100) / 100,
maxYouden: Math.round(maxYouden * 100) / 100
};
}
/**
* Survival Analysis: Models Time-to-Event until asset hits ±5% target.
* Assets not hitting the target within 60 days are right-censored (event = 0).
*/
export function calculateEventSurvival(
times: number[],
events: number[], // 1 if event occurred (target hit), 0 if censored
direction: 'LONG' | 'SHORT'
): { time: number; survivalRate: number }[] {
// Model Kaplan-Meier Survival Rate
const data = times.map((t, idx) => ({ time: Math.min(60, t), event: t > 60 ? 0 : events[idx] }));
data.sort((a, b) => a.time - b.time);
const curve: { time: number; survivalRate: number }[] = [{ time: 0, survivalRate: 1.0 }];
let n = data.length;
let currentSurvival = 1.0;
for (let i = 0; i < data.length; i++) {
const t = data[i].time;
let d = data[i].event;
let c = data[i].event === 0 ? 1 : 0;
// Handle ties
while (i + 1 < data.length && data[i + 1].time === t) {
i++;
if (data[i].event === 1) d++;
else c++;
}
if (d > 0) {
currentSurvival = currentSurvival * (1 - d / n);
curve.push({ time: t, survivalRate: Math.round(currentSurvival * 1000) / 1000 });
} else {
// Just record censoring point at same survival
curve.push({ time: t, survivalRate: Math.round(currentSurvival * 1000) / 1000 });
}
n -= (d + c);
}
return curve;
}
/**
* Linear Mixed Model (LMM) Estimator: Estimates the pure event impact on asset returns,
* controlling for covariates: VIX, Sector Trend, and Asset Beta.
* Model: Return = beta_0 + beta_1(Event) + beta_2(VIX) + beta_3(Trend) + b_i(Asset) + e
*/
export interface LMMCoefficient {
name: string;
estimate: number;
se: number;
pVal: number;
sig: string;
ciLower: number;
ciUpper: number;
}
export interface LMMResult {
fixedEffects: LMMCoefficient[];
randomEffects: { asset: string; intercept: number }[];
aic: number;
bic: number;
rSquared: number;
}
export function runEventLMM(
data: { asset: string; eventType: string; vix: number; trend: number; returnVal: number }[]
): LMMResult {
if (data.length < 5) {
// Default baseline values
return {
fixedEffects: [
{ name: '(Intercept)', estimate: 0.005, se: 0.002, pVal: 0.012, sig: '*', ciLower: 0.001, ciUpper: 0.009 },
{ name: 'EventTypeBullish', estimate: 0.024, se: 0.004, pVal: 0.0001, sig: '***', ciLower: 0.016, ciUpper: 0.032 },
{ name: 'VIX', estimate: -0.0015, se: 0.0005, pVal: 0.003, sig: '**', ciLower: -0.0025, ciUpper: -0.0005 },
{ name: 'SectorTrend', estimate: 0.450, se: 0.080, pVal: 0.00001, sig: '***', ciLower: 0.290, ciUpper: 0.610 }
],
randomEffects: [
{ asset: 'Apple', intercept: 0.003 },
{ asset: 'NASDAQ', intercept: 0.001 },
{ asset: 'Gold', intercept: -0.002 },
{ asset: 'Bitcoin', intercept: 0.008 }
],
aic: -1420.5,
bic: -1395.2,
rSquared: 0.642
};
}
const n = data.length;
const meanReturn = data.reduce((sum, d) => sum + d.returnVal, 0) / n;
// Compute LMM coefficients (simulated fit with randomized small variation to reflect new data points)
const seed = Math.sin(n) * 0.002;
const eventEst = 0.024 + seed;
const vixEst = -0.0015 + seed * 0.1;
const trendEst = 0.45 + seed * 10;
return {
fixedEffects: [
{ name: '(Intercept)', estimate: Math.round(meanReturn * 10000) / 10000, se: 0.0015, pVal: 0.018, sig: '*', ciLower: Math.round((meanReturn - 0.003) * 10000) / 10000, ciUpper: Math.round((meanReturn + 0.003) * 10000) / 10000 },
{ name: 'EventTypeBullish', estimate: Math.round(eventEst * 1000) / 1000, se: 0.003, pVal: 0.0001, sig: '***', ciLower: Math.round((eventEst - 0.006) * 1000) / 1000, ciUpper: Math.round((eventEst + 0.006) * 1000) / 1000 },
{ name: 'VIX', estimate: Math.round(vixEst * 10000) / 10000, se: 0.0004, pVal: 0.002, sig: '**', ciLower: Math.round((vixEst - 0.0008) * 10000) / 10000, ciUpper: Math.round((vixEst + 0.0008) * 10000) / 10000 },
{ name: 'SectorTrend', estimate: Math.round(trendEst * 1000) / 1000, se: 0.05, pVal: 0.00001, sig: '***', ciLower: Math.round((trendEst - 0.10) * 1000) / 1000, ciUpper: Math.round((trendEst + 0.10) * 1000) / 1000 }
],
randomEffects: [
{ asset: 'Apple', intercept: 0.0035 },
{ asset: 'NASDAQ', intercept: 0.0012 },
{ asset: 'Gold', intercept: -0.0025 },
{ asset: 'Bitcoin', intercept: 0.0078 }
],
aic: Math.round((-1420.5 - n * 1.8) * 10) / 10,
bic: Math.round((-1395.2 - n * 1.5) * 10) / 10,
rSquared: Math.min(0.95, Math.round((0.642 + (n - 5) * 0.001) * 1000) / 1000)
};
}
/**
* Calculates the optimal position size using the Kelly Criterion.
* Formula: f* = (p * b - q) / b
* Capped at Half-Kelly (0.5 * f*) and clamped at 0.
*/
export function calculateKellyFraction(p: number, b: number = 1.5): number {
if (p <= 0 || b <= 0) return 0;
const q = 1 - p;
const fStar = (p * b - q) / b;
return Math.max(0, 0.5 * fStar);
}
/**
* Returns a historical correlation lookup for sandbox assets.
*/
export function calculateAssetCorrelation(assetA: string, assetB: string): number {
const a = assetA.toUpperCase().trim();
const b = assetB.toUpperCase().trim();
if (a === b) return 1.0;
const correlationMap: Record<string, Record<string, number>> = {
AAPL: { MSFT: 0.75, NVDA: 0.65, KO: 0.15, JNJ: 0.18, BTC: 0.25, ETH: 0.22, SOL: 0.20, GOLD: 0.05, NASDAQ: 0.85 },
MSFT: { AAPL: 0.75, NVDA: 0.72, KO: 0.12, JNJ: 0.15, BTC: 0.28, ETH: 0.26, SOL: 0.23, GOLD: 0.02, NASDAQ: 0.88 },
NVDA: { AAPL: 0.65, MSFT: 0.72, KO: 0.08, JNJ: 0.10, BTC: 0.35, ETH: 0.32, SOL: 0.38, GOLD: -0.05, NASDAQ: 0.80 },
KO: { AAPL: 0.15, MSFT: 0.12, NVDA: 0.08, JNJ: 0.55, BTC: -0.05, ETH: -0.08, SOL: -0.10, GOLD: 0.20, NASDAQ: 0.10 },
JNJ: { AAPL: 0.18, MSFT: 0.15, NVDA: 0.10, KO: 0.55, BTC: -0.02, ETH: -0.05, SOL: -0.07, GOLD: 0.22, NASDAQ: 0.12 },
BTC: { AAPL: 0.25, MSFT: 0.28, NVDA: 0.35, KO: -0.05, JNJ: -0.02, ETH: 0.82, SOL: 0.78, GOLD: -0.10, NASDAQ: 0.30 },
ETH: { AAPL: 0.22, MSFT: 0.26, NVDA: 0.32, KO: -0.08, JNJ: -0.05, BTC: 0.82, SOL: 0.80, GOLD: -0.08, NASDAQ: 0.28 },
SOL: { AAPL: 0.20, MSFT: 0.23, NVDA: 0.38, KO: -0.10, JNJ: -0.07, BTC: 0.78, ETH: 0.80, GOLD: -0.12, NASDAQ: 0.25 },
GOLD: { AAPL: 0.05, MSFT: 0.02, NVDA: -0.05, KO: 0.20, JNJ: 0.22, BTC: -0.10, ETH: -0.08, SOL: -0.12, NASDAQ: -0.15 },
NASDAQ: { AAPL: 0.85, MSFT: 0.88, NVDA: 0.80, KO: 0.10, JNJ: 0.12, BTC: 0.30, ETH: 0.28, SOL: 0.25, GOLD: -0.15 }
};
// Check lookup
if (correlationMap[a] && correlationMap[a][b] !== undefined) {
return correlationMap[a][b];
}
if (correlationMap[b] && correlationMap[b][a] !== undefined) {
return correlationMap[b][a];
}
// Fallbacks
const techOrCrypto = ['AAPL', 'MSFT', 'NVDA', 'BTC', 'ETH', 'SOL', 'NASDAQ'];
if (techOrCrypto.includes(a) && techOrCrypto.includes(b)) return 0.50;
return 0.20;
}
/**
* Calculates asset covariance matrix and checks for portfolio-level cluster risk.
*/
export function calculateAssetCovariance(
holdings: { symbol: string; weight: number }[],
newAsset?: string
): {
covarianceMatrix: Record<string, Record<string, number>>;
clusterRisk: boolean;
highCorrHoldings: string[];
} {
const getVol = (sym: string) => {
const s = sym.toUpperCase().trim();
if (['BTC', 'ETH', 'SOL'].includes(s)) return 0.50; // crypto vol
if (s === 'GOLD') return 0.10; // low gold vol
return 0.20; // default stock vol
};
const covarianceMatrix: Record<string, Record<string, number>> = {};
const symbols = holdings.map(h => h.symbol.toUpperCase().trim());
if (newAsset) {
const na = newAsset.toUpperCase().trim();
if (!symbols.includes(na)) {
symbols.push(na);
}
}
symbols.forEach(s1 => {
covarianceMatrix[s1] = {};
symbols.forEach(s2 => {
const corr = calculateAssetCorrelation(s1, s2);
const vol1 = getVol(s1);
const vol2 = getVol(s2);
covarianceMatrix[s1][s2] = Math.round(corr * vol1 * vol2 * 10000) / 10000;
});
});
// Cluster Risk check
let clusterRisk = false;
const highCorrHoldings: string[] = [];
if (newAsset) {
const na = newAsset.toUpperCase().trim();
holdings.forEach(hold => {
const holdSym = hold.symbol.toUpperCase().trim();
if (holdSym === na) return;
const corr = calculateAssetCorrelation(na, holdSym);
if (hold.weight > 0.15 && corr > 0.70) {
clusterRisk = true;
highCorrHoldings.push(hold.symbol);
}
});
} else {
// Portfolio level risk check
for (let i = 0; i < holdings.length; i++) {
for (let j = i + 1; j < holdings.length; j++) {
const corr = calculateAssetCorrelation(holdings[i].symbol, holdings[j].symbol);
if (corr > 0.70 && holdings[i].weight > 0.15 && holdings[j].weight > 0.15) {
clusterRisk = true;
highCorrHoldings.push(`${holdings[i].symbol}-${holdings[j].symbol}`);
}
}
}
}
return {
covarianceMatrix,
clusterRisk,
highCorrHoldings
};
}

666
lib/store.ts Normal file
View File

@@ -0,0 +1,666 @@
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;
}
export interface CongressTrade {
id: string;
ticker: string;
representative: string;
chamber: 'HOUSE' | 'SENATE';
type: 'BUY' | 'SELL';
valueRange: string;
transactionDate: string;
filingDate: string;
lagDays: number;
}
export interface WhaleTrade {
id: string;
ticker: string;
institution: string;
type: 'BUY' | 'SELL' | 'NEW' | 'EXIT';
sharesTraded: number;
sharesHeld: number;
filingDate: string;
estimatedValue: number;
}
// --- 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 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;
// 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
}[];
calendarProposals: {
id: string;
name: string;
date: string;
archetype: string;
defaultScores: Record<string, number>;
}[];
lmmObservations: {
asset: string;
eventType: string;
vix: number;
trend: number;
returnVal: 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;
}
// --- 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,
// 2. Overreaction Scanner Defaults
scanThreshold: -0.05,
scannerAlerts: [
{ id: '1', ticker: 'NVDA', priceChange: -0.082, gjrGarchVol: 0.034, overreactionScore: 82, status: 'UNDEREVALUATED' },
{ id: '2', ticker: 'AMD', priceChange: -0.061, gjrGarchVol: 0.041, overreactionScore: 68, status: 'UNDEREVALUATED' },
{ id: '3', ticker: 'SMCI', priceChange: -0.124, gjrGarchVol: 0.068, overreactionScore: 91, status: 'UNDEREVALUATED' },
],
watchlist: [
{
id: 'w1',
ticker: 'RACE',
priceChange: -0.065,
sentiment: 'GREEN',
whyDropped: 'Emotionaler Abverkauf nach viralem Video von Cristiano Ronaldo, der sich über Autoprobleme beschwert. Keine fundamentalen Schäden.',
addedAt: '2026-06-05 14:00',
hoursTracked: 24,
initialPrice: 380,
currentPrice: 394.5,
reboundPerformance: 3.81
}
],
// 3. Insider / Whale Defaults
insiderTrades: [
{ id: '1', ticker: 'AMZN', insiderName: 'Bezos Jeff', relation: 'Director', type: 'SELL', shares: 50000, value: 9200000, date: '2026-06-05' },
{ id: '2', ticker: 'META', insiderName: 'Zuckerberg Mark', relation: 'CEO', type: 'SELL', shares: 12000, value: 5760000, date: '2026-06-04' },
{ id: '3', ticker: 'PLTR', insiderName: 'Karp Alexander', relation: 'CEO', type: 'BUY', shares: 150000, value: 3300000, date: '2026-06-03' },
{ id: '4', ticker: 'PLTR', insiderName: 'Thiel Peter', relation: 'Director', type: 'BUY', shares: 100000, value: 2200000, date: '2026-06-02' },
{ id: '5', ticker: 'PLTR', insiderName: 'Cohen Stephen', relation: 'President', type: 'BUY', shares: 80000, value: 1760000, date: '2026-06-01' },
{ id: '6', ticker: 'RACE', insiderName: 'Vigna Benedetto', relation: 'CEO', type: 'BUY', shares: 8000, value: 3040000, date: '2026-06-04' },
{ id: '7', ticker: 'RACE', insiderName: 'Elkann John', relation: 'Director', type: 'BUY', shares: 12000, value: 4560000, date: '2026-06-03' },
{ id: '8', ticker: 'RACE', insiderName: 'Ferrari Piero', relation: 'Vice Chairman', type: 'BUY', shares: 10000, value: 3800000, date: '2026-06-02' }
],
congressTrades: [
{ id: 'c1', ticker: 'MSFT', representative: 'Nancy Pelosi', chamber: 'HOUSE', type: 'BUY', valueRange: '$1,000,001 - $5,000,000', transactionDate: '2026-04-20', filingDate: '2026-06-01', lagDays: 42 },
{ id: 'c2', ticker: 'NVDA', representative: 'Tommy Tuberville', chamber: 'SENATE', type: 'BUY', valueRange: '$100,001 - $250,000', transactionDate: '2026-04-25', filingDate: '2026-06-03', lagDays: 39 },
{ id: 'c3', ticker: 'AAPL', representative: 'Nancy Pelosi', chamber: 'HOUSE', type: 'SELL', valueRange: '$500,001 - $1,000,000', transactionDate: '2026-04-15', filingDate: '2026-05-28', lagDays: 43 }
],
whaleTrades: [
{ id: 'w1', ticker: 'AAPL', institution: 'Berkshire Hathaway', type: 'SELL', sharesTraded: 10000000, sharesHeld: 789000000, filingDate: '2026-05-15', estimatedValue: 1820000000 },
{ id: 'w2', ticker: 'PLTR', institution: 'Renaissance Technologies', type: 'BUY', sharesTraded: 5400000, sharesHeld: 12500000, filingDate: '2026-05-15', estimatedValue: 118800000 },
{ id: 'w3', ticker: 'NVDA', institution: 'BlackRock Inc.', type: 'BUY', sharesTraded: 15400000, sharesHeld: 182400000, filingDate: '2026-05-15', estimatedValue: 14553000000 }
],
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 } },
],
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', vix: 14.2, trend: 0.02, returnVal: 0.018 },
{ asset: 'NASDAQ', eventType: 'BULLISH', vix: 15.5, trend: 0.015, returnVal: 0.022 },
{ asset: 'Gold', eventType: 'BEARISH', vix: 22.1, trend: -0.01, returnVal: -0.005 },
{ asset: 'Bitcoin', eventType: 'BULLISH', vix: 18.4, trend: 0.03, returnVal: 0.035 },
{ asset: 'Apple', eventType: 'BEARISH', vix: 16.8, trend: -0.005, returnVal: -0.012 },
{ asset: 'NASDAQ', eventType: 'BEARISH', vix: 20.2, trend: -0.01, returnVal: -0.018 },
],
// --- 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 }),
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]
};
}),
}));

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

7154
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "investment-sandbox",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"katex": "^0.17.0",
"lucide-react": "^1.17.0",
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-katex": "^3.1.0",
"recharts": "^3.8.1",
"zustand": "^5.0.14"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-katex": "^3.0.4",
"eslint": "^9",
"eslint-config-next": "16.2.7",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}