diff --git a/DEV_LOG.md b/DEV_LOG.md index f7cddbc..899bd41 100644 --- a/DEV_LOG.md +++ b/DEV_LOG.md @@ -275,6 +275,19 @@ This document tracks all modifications, npm packages, active compilation states, * **Active Bugs**: None. * **Type Checker Status**: Verified 100% clean type verification (`npx tsc --noEmit` returns exit code 0). +--- + +## [2026-06-14] - Isolated PEAD Fundamental Momentum Screener Integration (#ISSUE-023) + +### Added +* **Earnings Surprise Parsing & Parsing Fallbacks**: Implemented `fetchFmpEarningsSurprise` in [/api/scanner/route.ts](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/app/api/scanner/route.ts) to query quarterly EPS surprises. Implemented `getSimulatedPEAD` to deterministically simulate PEAD variables offline, preserving the `2026-06-14` date-lock. +* **SUE Momentum calculations**: Computes Earnings Surprise percent (using the consensus EPS expectation difference) and tracks days elapsed since announcement. Assigns `"Active Drift"` status if surprise absolute value exceeds 4.5% and time elapsed is under 45 days. +* **PEAD Drift Radar Workstation Grid**: Embedded a sub-tab layout in [ScannerDemo.tsx](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/components/modules/scanner/ScannerDemo.tsx) housing the high-density glassmorphic "🚀 PEAD Drift Radar" table, mapping ticker/name, sector, release date, reported vs consensus EPS, surprise %, and drift momentum status. Bound to an isolated `peadData` react state hook. + +### Active Bugs / Compile Status +* **Active Bugs**: None. +* **Type Checker Status**: Verified 100% clean type verification (`npx tsc --noEmit` returns exit code 0). + diff --git a/app/api/scanner/route.ts b/app/api/scanner/route.ts index 116a405..fec6951 100644 --- a/app/api/scanner/route.ts +++ b/app/api/scanner/route.ts @@ -20,6 +20,13 @@ interface TickerDetails { peg?: number; priceToBook?: number; dividendYield?: number; + peadSector?: string; + announcementDate?: string; + daysElapsed?: number; + epsActual?: number; + epsConsensus?: number; + surprisePercent?: number; + driftStatus?: string; } // 14-day Welles Wilder RSI solver @@ -397,6 +404,150 @@ async function fetchFMPFundamentalData(ticker: string, apiKey: string) { return { ...mock, dividendYield: mock.dividendYield * 100 }; } +function getSimulatedPEAD(ticker: string): { + peadSector: string; + announcementDate: string; + daysElapsed: number; + epsActual: number; + epsConsensus: number; + surprisePercent: number; + driftStatus: 'Active Drift' | 'Consolidating'; +} { + const getSector = (t: string) => { + const tech = ['AAPL', 'MSFT', 'NVDA', 'AMD', 'SMCI', 'ADBE', 'CRM', 'AVGO', 'QCOM', 'TXN', 'INTC', 'MU', 'AMAT', 'LRCX', 'PLTR']; + const consumer = ['TSLA', 'NKE', 'SBUX', 'MCD', 'ABNB', 'BKNG', 'DIS', 'WMT', 'PG', 'COST', 'PEP', 'KO']; + const financial = ['BAC', 'JPM', 'GS', 'MS', 'BLK', 'PYPL', 'SQ', 'V', 'MA', 'AXP', 'WFC']; + const healthcare = ['JNJ', 'MRK', 'UNH', 'LLY', 'ABBV', 'MRNA', 'PFE', 'GILD', 'AMGN']; + const energy = ['CVX', 'XOM', 'SHEL', 'BP']; + + if (tech.includes(t)) return 'Technology'; + if (consumer.includes(t)) return 'Consumer Goods'; + if (financial.includes(t)) return 'Financial Services'; + if (healthcare.includes(t)) return 'Healthcare'; + if (energy.includes(t)) return 'Energy'; + return 'Conglomerate'; + }; + + const sector = getSector(ticker); + + if (ticker.includes('-USD') || ticker.includes('BTC') || ticker.includes('ETH') || ticker.includes('SOL')) { + return { + peadSector: 'Cryptocurrency', + announcementDate: 'N/A', + daysElapsed: 0, + epsActual: 0, + epsConsensus: 0, + surprisePercent: 0, + driftStatus: 'Consolidating' + }; + } + + const charCodeSum = ticker.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + const daysElapsed = (charCodeSum % 85) + 1; // 1 to 85 + + const today = new Date('2026-06-14'); + today.setDate(today.getDate() - daysElapsed); + const announcementDate = today.toISOString().split('T')[0]; + + const consensusSeed = (charCodeSum % 4) + 0.5; + const epsConsensus = Number((consensusSeed + (charCodeSum % 10) / 10).toFixed(2)); + + const surprisePercent = Number((((charCodeSum % 30) - 12) + (charCodeSum % 10) / 10).toFixed(2)); + const epsActual = Number((epsConsensus * (1 + surprisePercent / 100)).toFixed(2)); + + const driftStatus = (Math.abs(surprisePercent) > 4.5 && daysElapsed < 45) ? 'Active Drift' : 'Consolidating'; + + return { + peadSector: sector, + announcementDate, + daysElapsed, + epsActual, + epsConsensus, + surprisePercent, + driftStatus + }; +} + +async function fetchFmpEarningsSurprise(ticker: string, apiKey: string): Promise<{ + peadSector: string; + announcementDate: string; + daysElapsed: number; + epsActual: number; + epsConsensus: number; + surprisePercent: number; + driftStatus: 'Active Drift' | 'Consolidating'; +}> { + const getSector = (t: string) => { + const tech = ['AAPL', 'MSFT', 'NVDA', 'AMD', 'SMCI', 'ADBE', 'CRM', 'AVGO', 'QCOM', 'TXN', 'INTC', 'MU', 'AMAT', 'LRCX', 'PLTR']; + const consumer = ['TSLA', 'NKE', 'SBUX', 'MCD', 'ABNB', 'BKNG', 'DIS', 'WMT', 'PG', 'COST', 'PEP', 'KO']; + const financial = ['BAC', 'JPM', 'GS', 'MS', 'BLK', 'PYPL', 'SQ', 'V', 'MA', 'AXP', 'WFC']; + const healthcare = ['JNJ', 'MRK', 'UNH', 'LLY', 'ABBV', 'MRNA', 'PFE', 'GILD', 'AMGN']; + const energy = ['CVX', 'XOM', 'SHEL', 'BP']; + + if (tech.includes(t)) return 'Technology'; + if (consumer.includes(t)) return 'Consumer Goods'; + if (financial.includes(t)) return 'Financial Services'; + if (healthcare.includes(t)) return 'Healthcare'; + if (energy.includes(t)) return 'Energy'; + return 'Conglomerate'; + }; + + const sector = getSector(ticker); + + if (ticker.includes('-USD') || ticker.includes('BTC') || ticker.includes('ETH') || ticker.includes('SOL')) { + return { + peadSector: 'Cryptocurrency', + announcementDate: 'N/A', + daysElapsed: 0, + epsActual: 0, + epsConsensus: 0, + surprisePercent: 0, + driftStatus: 'Consolidating' + }; + } + + try { + const url = `https://financialmodelingprep.com/api/v3/earnings-surprises/${ticker}?apikey=${apiKey}`; + const res = await fetch(url, { signal: AbortSignal.timeout(2000) }); + if (res.ok) { + const data = await res.json(); + if (Array.isArray(data) && data.length > 0) { + const latest = data[0]; + const dateStr = latest.date; + const epsActual = latest.actualEarningResult !== null ? latest.actualEarningResult : 0; + const epsConsensus = latest.estimatedEarning !== null ? latest.estimatedEarning : 0; + + // Calculate days elapsed since announcement + const annDate = new Date(dateStr); + const today = new Date('2026-06-14'); // System date lock + const diffTime = Math.abs(today.getTime() - annDate.getTime()); + const daysElapsed = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + // Surprise % calculation + const estDenom = epsConsensus === 0 ? 0.01 : Math.abs(epsConsensus); + const surprisePercent = Number((((epsActual - epsConsensus) / estDenom) * 100).toFixed(2)); + + const driftStatus = (Math.abs(surprisePercent) > 4.5 && daysElapsed < 45) ? 'Active Drift' : 'Consolidating'; + + return { + peadSector: sector, + announcementDate: dateStr, + daysElapsed, + epsActual, + epsConsensus, + surprisePercent, + driftStatus + }; + } + } + } catch (err) { + console.warn(`Error fetching earnings surprise for ${ticker}:`, err); + } + + // Fallback to deterministic simulation + return getSimulatedPEAD(ticker); +} + export async function GET(request: Request) { const { searchParams } = new URL(request.url); const mode = searchParams.get('mode') || 'day_crash'; @@ -485,11 +636,16 @@ export async function GET(request: Request) { ? getSimulatedSloan(item.ticker) : await fetchFmpSloanRatio(item.ticker, fmpApiKey); + const pead = useMock + ? getSimulatedPEAD(item.ticker) + : await fetchFmpEarningsSurprise(item.ticker, fmpApiKey); + return { ...item, ...fund, dividendYield: fund.dividendYield ? Number((fund.dividendYield * (useMock ? 100 : 1)).toFixed(2)) : 0, - ...sloan + ...sloan, + ...pead }; }) ); diff --git a/components/modules/scanner/ScannerDemo.tsx b/components/modules/scanner/ScannerDemo.tsx index f7a8553..b65c727 100644 --- a/components/modules/scanner/ScannerDemo.tsx +++ b/components/modules/scanner/ScannerDemo.tsx @@ -11,7 +11,7 @@ import ScannerBlueprintModal from './ScannerBlueprintModal'; import { ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2, ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play, - BookOpen + BookOpen, TrendingUp } from 'lucide-react'; // Predefined mock database for deep-check searches (Removed mock database) @@ -30,6 +30,18 @@ interface SearchResult { sloanRegime?: 'SAFE' | 'ANOMALY'; } +interface PEADData { + ticker: string; + name: string; + peadSector: string; + announcementDate: string; + daysElapsed: number; + epsActual: number; + epsConsensus: number; + surprisePercent: number; + driftStatus: 'Active Drift' | 'Consolidating'; +} + export default function ScannerDemo() { const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore(); @@ -51,6 +63,14 @@ export default function ScannerDemo() { const [isMathModalOpen, setIsMathModalOpen] = useState(false); const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false); const [isShieldActive, setIsShieldActive] = useState(false); + const [leftPanelTab, setLeftPanelTab] = useState<'screener' | 'pead'>('screener'); + const [peadData, setPeadData] = useState([ + { ticker: 'NVDA', name: 'NVIDIA Corporation', peadSector: 'Technology', announcementDate: '2026-05-20', daysElapsed: 25, epsActual: 6.12, epsConsensus: 5.58, surprisePercent: 9.68, driftStatus: 'Active Drift' }, + { ticker: 'AAPL', name: 'Apple Inc.', peadSector: 'Technology', announcementDate: '2026-05-02', daysElapsed: 43, epsActual: 1.53, epsConsensus: 1.50, surprisePercent: 2.00, driftStatus: 'Consolidating' }, + { ticker: 'MSFT', name: 'Microsoft Corporation', peadSector: 'Technology', announcementDate: '2026-04-25', daysElapsed: 50, epsActual: 2.94, epsConsensus: 2.82, surprisePercent: 4.26, driftStatus: 'Consolidating' }, + { ticker: 'TSLA', name: 'Tesla Inc.', peadSector: 'Consumer Goods', announcementDate: '2026-04-23', daysElapsed: 52, epsActual: 0.45, epsConsensus: 0.51, surprisePercent: -11.76, driftStatus: 'Consolidating' }, + { ticker: 'JPM', name: 'JPMorgan Chase & Co.', peadSector: 'Financial Services', announcementDate: '2026-04-12', daysElapsed: 63, epsActual: 4.44, epsConsensus: 4.15, surprisePercent: 6.99, driftStatus: 'Consolidating' } + ]); // Cache for metadata and prices retrieved dynamically const [alertsMetadata, setAlertsMetadata] = useState>({}); @@ -161,6 +181,24 @@ export default function ScannerDemo() { setAlertsPrices(prev => ({ ...prev, ...newPrices })); setActiveAlerts(newAlerts); + const peadList: PEADData[] = []; + results.forEach((r: any) => { + if (r.error || r.peadSector === 'Cryptocurrency') return; + peadList.push({ + ticker: r.ticker, + name: r.name, + peadSector: r.peadSector || 'Conglomerate', + announcementDate: r.announcementDate || 'N/A', + daysElapsed: r.daysElapsed || 0, + epsActual: r.epsActual || 0, + epsConsensus: r.epsConsensus || 0, + surprisePercent: r.surprisePercent || 0, + driftStatus: r.driftStatus || 'Consolidating' + }); + }); + peadList.sort((a, b) => Math.abs(b.surprisePercent) - Math.abs(a.surprisePercent)); + setPeadData(peadList); + // Update global store alerts for Sandbox module use updateScannerAlerts(newAlerts); @@ -717,38 +755,114 @@ export default function ScannerDemo() { {/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
- {/* Left 2 Columns: 3-Tier Capacity Grid (Top 5 per tier) */} + {/* Left 2 Columns: 3-Tier Capacity Grid (Top 5 per tier) or PEAD Drift Radar */}
-

- 3-Tier Screener Kapazitäts-Grid (Top 5) -

+
+ + +
Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}
-
- {/* Category A: Mega Caps */} - {renderCategoryTable( - "Kategorie A: Mega Caps (> 100B USD)", - "Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung", - categorizedResults.mega - )} + {leftPanelTab === 'screener' ? ( +
+ {/* Category A: Mega Caps */} + {renderCategoryTable( + "Kategorie A: Mega Caps (> 100B USD)", + "Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung", + categorizedResults.mega + )} - {/* Category B: Mid Caps */} - {renderCategoryTable( - "Kategorie B: Mid Caps (10B - 100B USD)", - "Wachstumsstarke Standardwerte und etablierte Branchenführer", - categorizedResults.mid - )} + {/* Category B: Mid Caps */} + {renderCategoryTable( + "Kategorie B: Mid Caps (10B - 100B USD)", + "Wachstumsstarke Standardwerte und etablierte Branchenführer", + categorizedResults.mid + )} - {/* Category C: Small Caps */} - {renderCategoryTable( - "Kategorie C: Small Caps (< 10B USD)", - "Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets", - categorizedResults.small - )} -
+ {/* Category C: Small Caps */} + {renderCategoryTable( + "Kategorie C: Small Caps (< 10B USD)", + "Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets", + categorizedResults.small + )} +
+ ) : ( +
+
+ Monitors Post-Earnings Announcement Drift (PEAD) anomaly using standardized unanticipated earnings momentum. +
+ +
+ + + + + + + + + + + + + {peadData.length === 0 ? ( + + + + ) : ( + peadData.map((pead) => { + const isPositive = pead.surprisePercent >= 0; + const isActiveDrift = pead.driftStatus === 'Active Drift'; + + return ( + + + + + + + + + ); + }) + )} + +
AssetSectorAnnouncement DateReported vs ConsensusSurprise %Drift Status
+ No equity assets available in scan history. +
+ {pead.ticker} + {pead.name} + + {pead.peadSector} + + {pead.announcementDate} + ({pead.daysElapsed} days elapsed) + + {pead.epsActual.toFixed(2)} + vs {pead.epsConsensus.toFixed(2)} + + {isPositive ? '+' : ''}{pead.surprisePercent.toFixed(2)}% + + + {pead.driftStatus} + +
+
+
+ )}