feat(sandbox): deploy Phase 1 and Phase 2 of Portfolio Sandbox including Swamy-Arora GLS solver and stress-test visualization
This commit is contained in:
@@ -6,12 +6,14 @@ 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 ScannerMathModal from './ScannerMathModal';
|
||||
import {
|
||||
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
||||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play
|
||||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
|
||||
BookOpen
|
||||
} from 'lucide-react';
|
||||
|
||||
// Predefined mock database for deep-check searches
|
||||
// Predefined mock database for deep-check searches (Removed mock database)
|
||||
interface SearchResult {
|
||||
ticker: string;
|
||||
name: string;
|
||||
@@ -21,159 +23,208 @@ interface SearchResult {
|
||||
gjrGarchVol: number;
|
||||
reboundScore: number;
|
||||
returns: number[];
|
||||
currentPrice?: number;
|
||||
peakPrice?: 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();
|
||||
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = 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 [activeAlerts, setActiveAlerts] = useState<ScannerAlert[]>([]);
|
||||
|
||||
const [scanMode, setScanMode] = useState<'day_crash' | 'ma_drop' | '52w_dist' | 'rsi_oversold'>('day_crash');
|
||||
const [marketRegion, setMarketRegion] = useState<'us' | 'eu' | 'crypto'>('us');
|
||||
const [scanResults, setScanResults] = useState<any[]>([]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResult, setSearchResult] = useState<SearchResult | null>(null);
|
||||
const [searchError, setSearchError] = useState(false);
|
||||
const [checkingDeep, setCheckingDeep] = useState(false);
|
||||
|
||||
const [showMathAccordion, setShowMathAccordion] = useState(false);
|
||||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||||
|
||||
// Cache for metadata and prices retrieved dynamically
|
||||
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
|
||||
const [alertsPrices, setAlertsPrices] = useState<Record<string, { peakPrice: number; currentPrice: number }>>({});
|
||||
|
||||
// Run market scan simulator
|
||||
const handleMarketScan = () => {
|
||||
// Run market scan simulator querying real live API
|
||||
const handleMarketScan = async () => {
|
||||
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);
|
||||
try {
|
||||
setScanProgress(`Rufe die ${marketRegion.toUpperCase()} Marktdaten ab...`);
|
||||
const response = await fetch(`/api/finance?mode=${scanMode}®ion=${marketRegion}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch scanner tickers');
|
||||
}
|
||||
const data = await response.json();
|
||||
const results = data.results || [];
|
||||
|
||||
setScanProgress('Berechne GJR-GARCH Volatilitäten...');
|
||||
|
||||
setScanResults(results);
|
||||
|
||||
const newAlerts: ScannerAlert[] = [];
|
||||
const newMetadata: Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }> = {};
|
||||
const newPrices: Record<string, { peakPrice: number; currentPrice: number }> = {};
|
||||
|
||||
results.forEach((result: any) => {
|
||||
if (result.error) return;
|
||||
|
||||
// Calculate dynamic volatility from return series
|
||||
const gjrResult = calculateGJRGARCH(result.returns);
|
||||
const gjrGarchVol = gjrResult.forecast / 100;
|
||||
|
||||
// Calculate overreaction ratio
|
||||
let dropAbs = Math.abs(result.priceChange);
|
||||
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
|
||||
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
|
||||
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
|
||||
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
|
||||
|
||||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||||
|
||||
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
|
||||
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
|
||||
|
||||
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
|
||||
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
|
||||
|
||||
const whyDropped = `Wert liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH(1,1) Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||||
|
||||
newAlerts.push({
|
||||
id: `sa_${result.ticker}_${Date.now()}`,
|
||||
ticker: result.ticker,
|
||||
priceChange: result.priceChange,
|
||||
gjrGarchVol,
|
||||
overreactionScore,
|
||||
status
|
||||
});
|
||||
|
||||
newMetadata[result.ticker] = {
|
||||
name: result.name,
|
||||
whyDropped,
|
||||
sentiment
|
||||
};
|
||||
|
||||
newPrices[result.ticker] = {
|
||||
peakPrice: result.peakPrice,
|
||||
currentPrice: result.currentPrice
|
||||
};
|
||||
});
|
||||
|
||||
setAlertsMetadata(prev => ({ ...prev, ...newMetadata }));
|
||||
setAlertsPrices(prev => ({ ...prev, ...newPrices }));
|
||||
setActiveAlerts(newAlerts);
|
||||
|
||||
// Update global store alerts for Sandbox module use
|
||||
updateScannerAlerts(newAlerts);
|
||||
|
||||
setScanProgress('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setScanProgress('Fehler beim Scannen der Live-Daten.');
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Perform a manual deep check
|
||||
const handleDeepCheck = (e: React.FormEvent) => {
|
||||
// Trigger scan automatically when scan mode or region toggles change
|
||||
React.useEffect(() => {
|
||||
handleMarketScan();
|
||||
}, [scanMode, marketRegion]);
|
||||
|
||||
// Perform a manual deep check using real live API
|
||||
const handleDeepCheck = async (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;
|
||||
setCheckingDeep(true);
|
||||
try {
|
||||
const response = await fetch(`/api/finance?ticker=${query}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch');
|
||||
}
|
||||
const data = await response.json();
|
||||
const result = data.results?.[0];
|
||||
if (!result || result.error) {
|
||||
throw new Error(result?.error || 'Invalid result');
|
||||
}
|
||||
|
||||
const gjrResult = calculateGJRGARCH(result.returns);
|
||||
const gjrGarchVol = gjrResult.forecast / 100;
|
||||
|
||||
const dropAbs = Math.abs(result.priceChange);
|
||||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||||
if (result.priceChange > -0.03) {
|
||||
overreactionScore = Math.round(overreactionScore * 0.5);
|
||||
}
|
||||
|
||||
const sentiment = overreactionScore > 70 ? 'GREEN' : (overreactionScore >= 40 ? 'YELLOW' : 'RED');
|
||||
const whyDropped = `Mathematischer Ausbruchspunkt: Der Kurs verzeichnet einen Rückgang von ${Math.round(Math.abs(result.priceChange) * 100)}% ausgehend vom 90-Tage-Hoch von $${result.peakPrice.toFixed(2)}. GJR-GARCH-Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||||
|
||||
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]
|
||||
name: result.name,
|
||||
priceChange: result.priceChange,
|
||||
sentiment,
|
||||
whyDropped,
|
||||
gjrGarchVol,
|
||||
reboundScore: overreactionScore,
|
||||
returns: result.returns,
|
||||
currentPrice: result.currentPrice,
|
||||
peakPrice: result.peakPrice
|
||||
};
|
||||
|
||||
setAlertsMetadata(prev => ({
|
||||
...prev,
|
||||
[query]: { name: result.name, whyDropped, sentiment }
|
||||
}));
|
||||
|
||||
setAlertsPrices(prev => ({
|
||||
...prev,
|
||||
[query]: { peakPrice: result.peakPrice, currentPrice: result.currentPrice }
|
||||
}));
|
||||
|
||||
setSearchResult(res);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSearchError(true);
|
||||
} finally {
|
||||
setCheckingDeep(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
const handleAddToWatchlist = (
|
||||
ticker: string,
|
||||
priceChange: number,
|
||||
sentiment: 'GREEN' | 'YELLOW' | 'RED',
|
||||
whyDropped: string,
|
||||
peakPrice?: number,
|
||||
currentPrice?: number
|
||||
) => {
|
||||
const realInitial = peakPrice || 100;
|
||||
const realCurrent = currentPrice || (realInitial * (1 + priceChange));
|
||||
|
||||
addToWatchlist({
|
||||
ticker,
|
||||
priceChange,
|
||||
sentiment,
|
||||
whyDropped,
|
||||
initialPrice,
|
||||
currentPrice: initialPrice * (1 + priceChange), // current price after drop
|
||||
initialPrice: realInitial,
|
||||
currentPrice: realCurrent,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,6 +241,192 @@ export default function ScannerDemo() {
|
||||
}));
|
||||
}, [gjrGarchMathResult]);
|
||||
|
||||
const categorizedResults = useMemo(() => {
|
||||
const mega: any[] = [];
|
||||
const mid: any[] = [];
|
||||
const small: any[] = [];
|
||||
|
||||
scanResults.forEach((result: any) => {
|
||||
// Calculate dynamic volatility from return series
|
||||
const gjrResult = calculateGJRGARCH(result.returns || []);
|
||||
const gjrGarchVol = gjrResult.forecast / 100;
|
||||
|
||||
// Calculate overreaction ratio based on selected mode
|
||||
let dropAbs = Math.abs(result.priceChange);
|
||||
if (scanMode === 'day_crash') dropAbs = Math.abs(result.dayChange);
|
||||
else if (scanMode === 'ma_drop') dropAbs = Math.abs(result.maDeviation);
|
||||
else if (scanMode === '52w_dist') dropAbs = Math.abs(result.dist52w);
|
||||
else if (scanMode === 'rsi_oversold') dropAbs = Math.max(0, (50 - result.rsi14) / 100);
|
||||
|
||||
const ratio = dropAbs / (gjrGarchVol || 0.01);
|
||||
let overreactionScore = Math.round(ratio * 30 + 30);
|
||||
overreactionScore = Math.max(10, Math.min(95, overreactionScore));
|
||||
|
||||
const status: 'UNDEREVALUATED' | 'FAIR' | 'OVERVALUATED' =
|
||||
overreactionScore > 70 ? 'UNDEREVALUATED' : (overreactionScore < 40 ? 'OVERVALUATED' : 'FAIR');
|
||||
|
||||
const sentiment: 'GREEN' | 'YELLOW' | 'RED' =
|
||||
status === 'UNDEREVALUATED' ? 'GREEN' : (status === 'FAIR' ? 'YELLOW' : 'RED');
|
||||
|
||||
const whyDropped = `Kurs liegt bei $${result.currentPrice.toFixed(2)}. Modus: ${scanMode.toUpperCase()}. GJR-GARCH Volatilitätsschätzung liegt bei ${(gjrGarchVol * 100).toFixed(1)}%.`;
|
||||
|
||||
const enriched = {
|
||||
...result,
|
||||
gjrGarchVol,
|
||||
overreactionScore,
|
||||
status,
|
||||
sentiment,
|
||||
whyDropped
|
||||
};
|
||||
|
||||
const mcap = result.marketCap || 0;
|
||||
if (mcap >= 100e9) {
|
||||
mega.push(enriched);
|
||||
} else if (mcap >= 10e9) {
|
||||
mid.push(enriched);
|
||||
} else {
|
||||
small.push(enriched);
|
||||
}
|
||||
});
|
||||
|
||||
const sortByMode = (list: any[]) => {
|
||||
return list.sort((a, b) => {
|
||||
if (scanMode === 'ma_drop') return a.maDeviation - b.maDeviation;
|
||||
if (scanMode === '52w_dist') return a.dist52w - b.dist52w;
|
||||
if (scanMode === 'rsi_oversold') return a.rsi14 - b.rsi14;
|
||||
return a.dayChange - b.dayChange; // day_crash
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
mega: sortByMode(mega).slice(0, 5),
|
||||
mid: sortByMode(mid).slice(0, 5),
|
||||
small: sortByMode(small).slice(0, 5)
|
||||
};
|
||||
}, [scanResults, scanMode]);
|
||||
|
||||
const renderCategoryTable = (title: string, description: string, assets: any[]) => {
|
||||
return (
|
||||
<div className="bg-slate-950/40 border border-slate-850 rounded-2xl p-5 space-y-3">
|
||||
<div className="flex justify-between items-center border-b border-slate-900 pb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-white text-sm">{title}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-mono">{description}</p>
|
||||
</div>
|
||||
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-slate-900 border border-slate-800 text-slate-300">
|
||||
{assets.length} Assets
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{assets.length === 0 ? (
|
||||
<div className="py-6 text-center text-slate-500 text-xs italic">
|
||||
Keine Assets in dieser Kategorie unter den Scanner-Ergebnissen.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto scrollbar-thin">
|
||||
<table className="w-full text-left text-xs border-collapse min-w-[700px]">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider">
|
||||
<th className="py-2.5 px-3">Asset</th>
|
||||
<th className="py-2.5 px-3">Preis</th>
|
||||
<th className="py-2.5 px-3 text-right">Abweichung</th>
|
||||
<th className="py-2.5 px-3 text-right">KGV (T)</th>
|
||||
<th className="py-2.5 px-3 text-right">KGV (F)</th>
|
||||
<th className="py-2.5 px-3 text-right">PEG</th>
|
||||
<th className="py-2.5 px-3 text-right">KBV</th>
|
||||
<th className="py-2.5 px-3 text-right">Rendite</th>
|
||||
<th className="py-2.5 px-3 text-center">Score</th>
|
||||
<th className="py-2.5 px-3 text-center">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-900/60">
|
||||
{assets.map((asset) => {
|
||||
const isGreen = asset.sentiment === 'GREEN';
|
||||
const isYellow = asset.sentiment === 'YELLOW';
|
||||
|
||||
// Format deviation based on mode
|
||||
let devText = '';
|
||||
let devColor = 'text-slate-300';
|
||||
if (scanMode === 'day_crash') {
|
||||
devText = `${(asset.dayChange * 100).toFixed(2)}%`;
|
||||
devColor = asset.dayChange < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||||
} else if (scanMode === 'ma_drop') {
|
||||
devText = `${(asset.maDeviation * 100).toFixed(2)}%`;
|
||||
devColor = asset.maDeviation < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||||
} else if (scanMode === '52w_dist') {
|
||||
devText = `${(asset.dist52w * 100).toFixed(2)}%`;
|
||||
devColor = asset.dist52w < 0 ? 'text-rose-400 font-bold' : 'text-emerald-400';
|
||||
} else if (scanMode === 'rsi_oversold') {
|
||||
devText = asset.rsi14.toFixed(1);
|
||||
devColor = asset.rsi14 < 30 ? 'text-amber-400 font-bold font-mono' : 'text-slate-400';
|
||||
}
|
||||
|
||||
// Highlight valuation multipliers
|
||||
const peColor = asset.trailingPE && asset.trailingPE > 0 && asset.trailingPE < 15 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||||
const fpeColor = asset.forwardPE && asset.forwardPE > 0 && asset.forwardPE < 12 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||||
const pegColor = asset.peg && asset.peg > 0 && asset.peg < 1.0 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||||
const pbColor = asset.priceToBook && asset.priceToBook > 0 && asset.priceToBook < 1.5 ? 'text-emerald-400 font-semibold' : 'text-slate-300';
|
||||
const divColor = asset.dividendYield && asset.dividendYield > 3.0 ? 'text-emerald-400 font-semibold' : 'text-slate-450';
|
||||
|
||||
return (
|
||||
<tr key={asset.ticker} className="hover:bg-slate-900/30 transition-colors group">
|
||||
<td className="py-3 px-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono font-bold text-slate-100 text-sm">{asset.ticker}</span>
|
||||
<span className="text-[10px] text-slate-500 max-w-[120px] truncate" title={asset.name}>{asset.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-3 font-mono font-semibold text-slate-200">
|
||||
${asset.currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${devColor}`}>
|
||||
{devText}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${peColor}`}>
|
||||
{asset.trailingPE && asset.trailingPE > 0 ? asset.trailingPE.toFixed(1) : 'N/A'}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${fpeColor}`}>
|
||||
{asset.forwardPE && asset.forwardPE > 0 ? asset.forwardPE.toFixed(1) : 'N/A'}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${pegColor}`}>
|
||||
{asset.peg && asset.peg > 0 ? asset.peg.toFixed(2) : 'N/A'}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${pbColor}`}>
|
||||
{asset.priceToBook && asset.priceToBook > 0 ? asset.priceToBook.toFixed(2) : 'N/A'}
|
||||
</td>
|
||||
<td className={`py-3 px-3 text-right font-mono ${divColor}`}>
|
||||
{asset.dividendYield && asset.dividendYield > 0 ? `${asset.dividendYield.toFixed(2)}%` : '0.00%'}
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
<span className={`font-mono font-bold text-xs ${isGreen ? 'text-emerald-400' : (isYellow ? 'text-yellow-400' : 'text-rose-400')}`}>
|
||||
{asset.overreactionScore}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-3 text-center">
|
||||
{(isGreen || isYellow) ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
handleAddToWatchlist(asset.ticker, asset.priceChange, asset.sentiment, asset.whyDropped, asset.peakPrice, asset.currentPrice);
|
||||
}}
|
||||
className="bg-slate-900 hover:bg-slate-850 hover:border-emerald-500/50 text-emerald-400 hover:text-emerald-300 border border-slate-800 text-[10px] font-bold py-1 px-2.5 rounded-md transition-all active:scale-[0.96] cursor-pointer"
|
||||
>
|
||||
Track
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-600 font-mono">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -208,7 +445,15 @@ export default function ScannerDemo() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-auto flex flex-col items-stretch md:items-end gap-2 shrink-0">
|
||||
<div className="w-full md:w-auto flex flex-col sm:flex-row md:flex-row items-stretch sm:items-center gap-3 shrink-0">
|
||||
<button
|
||||
onClick={() => setIsMathModalOpen(true)}
|
||||
className="flex items-center gap-1.5 px-4 py-3 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-amber-400 justify-center"
|
||||
>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>📖 Modulerklärung</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleMarketScan}
|
||||
disabled={scanning}
|
||||
@@ -223,6 +468,58 @@ export default function ScannerDemo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core Scan Modes & Region Toggles */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 border-t border-slate-850 pt-5 mt-5">
|
||||
{/* Mode Toggles */}
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Screener-Modus</span>
|
||||
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit">
|
||||
{[
|
||||
{ id: 'day_crash', label: 'Day-Crashs' },
|
||||
{ id: 'ma_drop', label: 'MA-Drop (SMA50)' },
|
||||
{ id: '52w_dist', label: '52W-Distance' },
|
||||
{ id: 'rsi_oversold', label: 'RSI-Oversold' }
|
||||
].map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => setScanMode(m.id as any)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
scanMode === m.id
|
||||
? 'bg-amber-500 text-slate-950 shadow-md shadow-amber-500/10'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
|
||||
}`}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Region Toggles */}
|
||||
<div className="space-y-2 md:text-right">
|
||||
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block md:pr-1">Markt-Region</span>
|
||||
<div className="flex flex-wrap gap-1.5 bg-slate-950/60 p-1 rounded-xl border border-slate-800/80 w-fit md:ml-auto">
|
||||
{[
|
||||
{ id: 'us', label: 'US Markets' },
|
||||
{ id: 'eu', label: 'EU Markets' },
|
||||
{ id: 'crypto', label: 'Crypto Assets' }
|
||||
].map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => setMarketRegion(r.id as any)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
marketRegion === r.id
|
||||
? 'bg-orange-500 text-slate-950 shadow-md shadow-orange-500/10'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/60'
|
||||
}`}
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collapsible Math Accordion */}
|
||||
<div className="border-t border-slate-850 pt-4 mt-6">
|
||||
<button
|
||||
@@ -278,94 +575,37 @@ export default function ScannerDemo() {
|
||||
{/* 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 */}
|
||||
{/* Left 2 Columns: 3-Tier Capacity Grid (Top 5 per tier) */}
|
||||
<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="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-850 pb-3">
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Sparkles className="text-amber-400 w-5 h-5 animate-pulse" /> 3-Tier Screener Kapazitäts-Grid (Top 5)
|
||||
</h3>
|
||||
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
|
||||
</div>
|
||||
|
||||
<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.'
|
||||
};
|
||||
<div className="space-y-6">
|
||||
{/* Category A: Mega Caps */}
|
||||
{renderCategoryTable(
|
||||
"Kategorie A: Mega Caps (> 100B USD)",
|
||||
"Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung",
|
||||
categorizedResults.mega
|
||||
)}
|
||||
|
||||
const isGreen = dbInfo.sentiment === 'GREEN';
|
||||
const isYellow = dbInfo.sentiment === 'YELLOW';
|
||||
const isRed = dbInfo.sentiment === 'RED';
|
||||
{/* Category B: Mid Caps */}
|
||||
{renderCategoryTable(
|
||||
"Kategorie B: Mid Caps (10B - 100B USD)",
|
||||
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
|
||||
categorizedResults.mid
|
||||
)}
|
||||
|
||||
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">
|
||||
„{dbInfo.whyDropped}“
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
{/* Category C: Small Caps */}
|
||||
{renderCategoryTable(
|
||||
"Kategorie C: Small Caps (< 10B USD)",
|
||||
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
|
||||
categorizedResults.small
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,9 +632,10 @@ export default function ScannerDemo() {
|
||||
/>
|
||||
<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"
|
||||
disabled={checkingDeep}
|
||||
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 disabled:opacity-50"
|
||||
>
|
||||
Prüfen
|
||||
{checkingDeep ? 'Prüft...' : 'Prüfen'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -435,7 +676,14 @@ export default function ScannerDemo() {
|
||||
|
||||
{(searchResult.sentiment === 'GREEN' || searchResult.sentiment === 'YELLOW') && (
|
||||
<button
|
||||
onClick={() => handleAddToWatchlist(searchResult.ticker, searchResult.priceChange, searchResult.sentiment, searchResult.whyDropped)}
|
||||
onClick={() => handleAddToWatchlist(
|
||||
searchResult.ticker,
|
||||
searchResult.priceChange,
|
||||
searchResult.sentiment,
|
||||
searchResult.whyDropped,
|
||||
searchResult.peakPrice,
|
||||
searchResult.currentPrice
|
||||
)}
|
||||
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
|
||||
@@ -533,6 +781,7 @@ export default function ScannerDemo() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScannerMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
131
components/modules/scanner/ScannerMathModal.tsx
Normal file
131
components/modules/scanner/ScannerMathModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { BlockMath, InlineMath } from 'react-katex';
|
||||
|
||||
interface ScannerMathModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ScannerMathModal({ isOpen, onClose }: ScannerMathModalProps) {
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/85 backdrop-blur-md p-4 sm:p-6 md:p-8">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden shadow-2xl relative text-slate-300">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="flex justify-between items-center px-6 py-4 bg-slate-950/40 border-b border-slate-800/60">
|
||||
<div>
|
||||
<h2 className="text-base font-bold bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5 text-amber-400" /> Overreaction Scanner - Math & Logic Specification
|
||||
</h2>
|
||||
<p className="text-[10px] text-slate-500 font-mono">Institutional Specification Manual</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-200 bg-slate-950/50 border border-slate-800 hover:border-slate-700 px-3 py-1.5 rounded-lg text-xs font-semibold font-mono transition-all cursor-pointer"
|
||||
>
|
||||
Schließen (ESC)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="flex-1 overflow-y-auto p-6 sm:p-8 space-y-6 text-slate-300 scrollbar-thin">
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-800/80 pb-3">
|
||||
<h3 className="text-base font-bold text-slate-200">2. Deep-Value & Overreaction Scanner Engine</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">Filters stocks experiencing extreme technical selling deviations backed by cheap fundamental valuations.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-amber-400 uppercase tracking-wider font-mono">A. Ingestion & Pipeline Tiers</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
Scans the entire corporate equity universe daily, segmenting equities into three distinct market capitalization classes to identify localized overreactions:
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3 text-[11px] text-slate-400 font-mono text-center">
|
||||
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-800/50">
|
||||
<span className="block font-bold text-slate-300">Mega Caps</span>
|
||||
<span>> $100B</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-800/50">
|
||||
<span className="block font-bold text-slate-300">Mid Caps</span>
|
||||
<span>$10B - $100B</span>
|
||||
</div>
|
||||
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-800/50">
|
||||
<span className="block font-bold text-slate-300">Small Caps</span>
|
||||
<span>< $10B</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-amber-400 uppercase tracking-wider font-mono">B. Technical Distancing Formulas</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
Calculates price distance ratios relative to support levels:
|
||||
</p>
|
||||
<div className="bg-slate-950/40 p-4 rounded-xl border border-slate-800/60 my-2 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-400 mb-1">1. 52-Week High Deviation:</p>
|
||||
<BlockMath math="\Delta_{52w} = \frac{P_{\text{current}} - P_{52w\_high}}{P_{52w\_high}}" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400 mb-1">2. 50-Day Moving Average Drop:</p>
|
||||
<BlockMath math="\Delta_{MA} = \frac{P_{\text{current}} - \text{MA}_{50}}{\text{MA}_{50}}" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400 mb-1">3. Relative Strength Index (RSI-14) with smoothing:</p>
|
||||
<BlockMath math="\text{RSI} = 100 - \frac{100}{1 + \text{RS}}" />
|
||||
<BlockMath math="\text{RS} = \frac{\text{Smoothed Gain}_t}{\text{Smoothed Loss}_t}" />
|
||||
<p className="text-[11px] text-slate-505 mt-2 font-mono">
|
||||
where smoothed elements use Welles Wilder alpha = 1/14. Deep oversold signals trigger at RSI < 30.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-amber-400 uppercase tracking-wider font-mono">C. Fundamental Cheapness Overlay & Forward Valuations</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
To avoid value traps, technical drop factors are coupled with valuation metrics fetched in real-time from FMP:
|
||||
</p>
|
||||
<div className="bg-slate-950/40 p-4 rounded-xl border border-slate-800/60 my-2 space-y-4">
|
||||
<ul className="list-disc pl-5 text-xs text-slate-400 space-y-2 font-mono">
|
||||
<li><strong>Trailing P/E (KGV)</strong>: Measures the price relative to trailing 12-month earnings.</li>
|
||||
<li><strong>Price-to-Book (KBV)</strong>: Measures the asset backing relative to equity capital.</li>
|
||||
<li><strong>Dividend Yield (%)</strong>: Tangible dividend payouts to measure cash backflow support.</li>
|
||||
<li><strong>PEG Ratio</strong>: Relates PE multiple to annual earnings growth:
|
||||
<BlockMath math="\text{PEG} = \frac{\text{PE Ratio}}{\text{Earnings Growth Rate} \times 100}" />
|
||||
</li>
|
||||
</ul>
|
||||
<div className="border-t border-slate-850 pt-3">
|
||||
<p className="text-xs text-slate-400 mb-1">Implicit Forward P/E calculation from PEG relationship:</p>
|
||||
<BlockMath math="\text{Forward PE} = \frac{\text{Trailing PE}}{1 + g_{\text{implicit}}}" />
|
||||
<BlockMath math="g_{\text{implicit}} = \frac{\text{Trailing PE}}{\text{PEG} \times 100}" />
|
||||
<p className="text-[11px] text-slate-500 mt-2 font-mono">
|
||||
If PEG is unavailable, a default growth rate of 10% is assumed as a conservative fallback baseline.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user