Closes #023 - Isolated PEAD fundamental momentum screener integration
This commit is contained in:
13
DEV_LOG.md
13
DEV_LOG.md
@@ -275,6 +275,19 @@ This document tracks all modifications, npm packages, active compilation states,
|
|||||||
* **Active Bugs**: None.
|
* **Active Bugs**: None.
|
||||||
* **Type Checker Status**: Verified 100% clean type verification (`npx tsc --noEmit` returns exit code 0).
|
* **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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ interface TickerDetails {
|
|||||||
peg?: number;
|
peg?: number;
|
||||||
priceToBook?: number;
|
priceToBook?: number;
|
||||||
dividendYield?: number;
|
dividendYield?: number;
|
||||||
|
peadSector?: string;
|
||||||
|
announcementDate?: string;
|
||||||
|
daysElapsed?: number;
|
||||||
|
epsActual?: number;
|
||||||
|
epsConsensus?: number;
|
||||||
|
surprisePercent?: number;
|
||||||
|
driftStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14-day Welles Wilder RSI solver
|
// 14-day Welles Wilder RSI solver
|
||||||
@@ -397,6 +404,150 @@ async function fetchFMPFundamentalData(ticker: string, apiKey: string) {
|
|||||||
return { ...mock, dividendYield: mock.dividendYield * 100 };
|
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) {
|
export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const mode = searchParams.get('mode') || 'day_crash';
|
const mode = searchParams.get('mode') || 'day_crash';
|
||||||
@@ -485,11 +636,16 @@ export async function GET(request: Request) {
|
|||||||
? getSimulatedSloan(item.ticker)
|
? getSimulatedSloan(item.ticker)
|
||||||
: await fetchFmpSloanRatio(item.ticker, fmpApiKey);
|
: await fetchFmpSloanRatio(item.ticker, fmpApiKey);
|
||||||
|
|
||||||
|
const pead = useMock
|
||||||
|
? getSimulatedPEAD(item.ticker)
|
||||||
|
: await fetchFmpEarningsSurprise(item.ticker, fmpApiKey);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
...fund,
|
...fund,
|
||||||
dividendYield: fund.dividendYield ? Number((fund.dividendYield * (useMock ? 100 : 1)).toFixed(2)) : 0,
|
dividendYield: fund.dividendYield ? Number((fund.dividendYield * (useMock ? 100 : 1)).toFixed(2)) : 0,
|
||||||
...sloan
|
...sloan,
|
||||||
|
...pead
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ScannerBlueprintModal from './ScannerBlueprintModal';
|
|||||||
import {
|
import {
|
||||||
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
ShieldAlert, Sparkles, RefreshCw, Flame, Search, Plus, Trash2,
|
||||||
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
|
ChevronDown, ChevronUp, AlertTriangle, CheckCircle2, XCircle, Info, Clock, Play,
|
||||||
BookOpen
|
BookOpen, TrendingUp
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Predefined mock database for deep-check searches (Removed mock database)
|
// Predefined mock database for deep-check searches (Removed mock database)
|
||||||
@@ -30,6 +30,18 @@ interface SearchResult {
|
|||||||
sloanRegime?: 'SAFE' | 'ANOMALY';
|
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() {
|
export default function ScannerDemo() {
|
||||||
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore();
|
const { watchlist, addToWatchlist, removeFromWatchlist, simulateWatchlistTick, updateScannerAlerts } = useSandboxStore();
|
||||||
|
|
||||||
@@ -51,6 +63,14 @@ export default function ScannerDemo() {
|
|||||||
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
|
||||||
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false);
|
const [isBlueprintModalOpen, setIsBlueprintModalOpen] = useState(false);
|
||||||
const [isShieldActive, setIsShieldActive] = useState(false);
|
const [isShieldActive, setIsShieldActive] = useState(false);
|
||||||
|
const [leftPanelTab, setLeftPanelTab] = useState<'screener' | 'pead'>('screener');
|
||||||
|
const [peadData, setPeadData] = useState<PEADData[]>([
|
||||||
|
{ 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
|
// Cache for metadata and prices retrieved dynamically
|
||||||
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
|
const [alertsMetadata, setAlertsMetadata] = useState<Record<string, { name: string; whyDropped: string; sentiment: 'GREEN' | 'YELLOW' | 'RED' }>>({});
|
||||||
@@ -161,6 +181,24 @@ export default function ScannerDemo() {
|
|||||||
setAlertsPrices(prev => ({ ...prev, ...newPrices }));
|
setAlertsPrices(prev => ({ ...prev, ...newPrices }));
|
||||||
setActiveAlerts(newAlerts);
|
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
|
// Update global store alerts for Sandbox module use
|
||||||
updateScannerAlerts(newAlerts);
|
updateScannerAlerts(newAlerts);
|
||||||
|
|
||||||
@@ -717,38 +755,114 @@ export default function ScannerDemo() {
|
|||||||
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
|
{/* SECTION 2: Scanned Anomalies & Sentiment Traffic Light */}
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||||
|
|
||||||
{/* 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 */}
|
||||||
<div className="xl:col-span-2 space-y-6">
|
<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-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-6">
|
||||||
<div className="flex justify-between items-center border-b border-slate-850 pb-3">
|
<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">
|
<div className="flex items-center gap-4">
|
||||||
<Sparkles className="text-amber-400 w-5 h-5 animate-pulse" /> 3-Tier Screener Kapazitäts-Grid (Top 5)
|
<button
|
||||||
</h3>
|
onClick={() => setLeftPanelTab('screener')}
|
||||||
|
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'screener' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||||
|
>
|
||||||
|
<Sparkles className="text-amber-400 w-4 h-4 animate-pulse" /> 3-Tier Screener Capacity Grid
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setLeftPanelTab('pead')}
|
||||||
|
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all cursor-pointer ${leftPanelTab === 'pead' ? 'text-white border-b-2 border-amber-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||||
|
>
|
||||||
|
<TrendingUp className="text-amber-400 w-4 h-4" /> 🚀 PEAD Drift Radar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
|
<span className="text-[10px] text-slate-400 font-mono">Modus: {scanMode.toUpperCase()} | Region: {marketRegion.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
{leftPanelTab === 'screener' ? (
|
||||||
{/* Category A: Mega Caps */}
|
<div className="space-y-6">
|
||||||
{renderCategoryTable(
|
{/* Category A: Mega Caps */}
|
||||||
"Kategorie A: Mega Caps (> 100B USD)",
|
{renderCategoryTable(
|
||||||
"Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung",
|
"Kategorie A: Mega Caps (> 100B USD)",
|
||||||
categorizedResults.mega
|
"Großkonzerne mit hoher institutioneller Liquidität und marktbeherrschender Stellung",
|
||||||
)}
|
categorizedResults.mega
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Category B: Mid Caps */}
|
{/* Category B: Mid Caps */}
|
||||||
{renderCategoryTable(
|
{renderCategoryTable(
|
||||||
"Kategorie B: Mid Caps (10B - 100B USD)",
|
"Kategorie B: Mid Caps (10B - 100B USD)",
|
||||||
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
|
"Wachstumsstarke Standardwerte und etablierte Branchenführer",
|
||||||
categorizedResults.mid
|
categorizedResults.mid
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Category C: Small Caps */}
|
{/* Category C: Small Caps */}
|
||||||
{renderCategoryTable(
|
{renderCategoryTable(
|
||||||
"Kategorie C: Small Caps (< 10B USD)",
|
"Kategorie C: Small Caps (< 10B USD)",
|
||||||
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
|
"Hochvolatile Nebenwerte, spekulative Nischenplayer und Krypto-Assets",
|
||||||
categorizedResults.small
|
categorizedResults.small
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-xs text-slate-400 leading-relaxed font-sans">
|
||||||
|
Monitors Post-Earnings Announcement Drift (PEAD) anomaly using standardized unanticipated earnings momentum.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||||||
|
<table className="w-full text-left text-xs border-collapse min-w-[700px] font-mono">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-900 text-slate-500 font-mono text-[10px] uppercase tracking-wider bg-slate-900/40">
|
||||||
|
<th className="py-2.5 px-3">Asset</th>
|
||||||
|
<th className="py-2.5 px-3">Sector</th>
|
||||||
|
<th className="py-2.5 px-3">Announcement Date</th>
|
||||||
|
<th className="py-2.5 px-3 text-right">Reported vs Consensus</th>
|
||||||
|
<th className="py-2.5 px-3 text-right">Surprise %</th>
|
||||||
|
<th className="py-2.5 px-3 text-center">Drift Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-900/60">
|
||||||
|
{peadData.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="py-6 text-center text-slate-500 italic text-xs">
|
||||||
|
No equity assets available in scan history.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
peadData.map((pead) => {
|
||||||
|
const isPositive = pead.surprisePercent >= 0;
|
||||||
|
const isActiveDrift = pead.driftStatus === 'Active Drift';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={pead.ticker} className="hover:bg-slate-850/10 transition-colors">
|
||||||
|
<td className="py-2.5 px-3 font-bold text-slate-200">
|
||||||
|
<span className="text-amber-400 block font-bold">{pead.ticker}</span>
|
||||||
|
<span className="text-[10px] text-slate-500 block font-normal">{pead.name}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-slate-400 font-sans text-xs">
|
||||||
|
{pead.peadSector}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-slate-300 font-mono">
|
||||||
|
<span>{pead.announcementDate}</span>
|
||||||
|
<span className="text-[10px] text-slate-500 block">({pead.daysElapsed} days elapsed)</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-right text-slate-300">
|
||||||
|
<span>{pead.epsActual.toFixed(2)}</span>
|
||||||
|
<span className="text-[10px] text-slate-500 block font-normal">vs {pead.epsConsensus.toFixed(2)}</span>
|
||||||
|
</td>
|
||||||
|
<td className={`py-2.5 px-3 text-right font-bold ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
{isPositive ? '+' : ''}{pead.surprisePercent.toFixed(2)}%
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 px-3 text-center">
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[9px] font-bold border ${isActiveDrift ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20 shadow-[0_0_8px_rgba(16,185,129,0.1)]' : 'bg-slate-900 text-slate-400 border-slate-800'}`}>
|
||||||
|
{pead.driftStatus}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user