diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aec1070..9670580 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,6 +8,7 @@ import ReversalAlerts from '@/components/alerts/ReversalAlerts'; import ConvergenceAlerts from '@/components/alerts/ConvergenceAlerts'; import MarketScreenerPanel from '@/components/dashboard/MarketScreenerPanel'; import StockDetailPanel from '@/components/dashboard/StockDetailPanel'; +import ReversalScreenerPanel from '@/components/dashboard/ReversalScreenerPanel'; export default function App() { const [selectedSymbol, setSelectedSymbol] = useState(null); @@ -57,6 +58,9 @@ export default function App() { 🎯 Options Flow + + 🔄 Reversal Screener + 🔍 Multi-Signal Scanner @@ -77,6 +81,10 @@ export default function App() { + + + + diff --git a/frontend/src/components/dashboard/ReversalScreenerPanel.jsx b/frontend/src/components/dashboard/ReversalScreenerPanel.jsx new file mode 100644 index 0000000..4ef0894 --- /dev/null +++ b/frontend/src/components/dashboard/ReversalScreenerPanel.jsx @@ -0,0 +1,232 @@ +import { useEffect, useState, useCallback } from 'react'; +import { getApiUrl } from '@/config/api'; +import { Badge } from '@/components/ui/Badge'; +import { AlertTriangle, TrendingUp, TrendingDown, RefreshCw } from 'lucide-react'; + +export default function ReversalScreenerPanel({ onSelectSymbol }) { + const [reversals, setReversals] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [filterSignal, setFilterSignal] = useState('all'); + const [sortBy, setSortBy] = useState('timestamp'); + const [sortDir, setSortDir] = useState('desc'); + + const fetchReversals = useCallback(async () => { + try { + const response = await fetch(getApiUrl('/api/reversals/detect')); + const data = await response.json(); + if (data.success) { + setReversals(data.reversals || []); + setError(null); + } else { + setError(data.error || 'Failed to detect reversals'); + } + } catch (err) { + console.error('Reversal fetch error:', err); + setError('Connection error. Check backend.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + setLoading(true); + fetchReversals(); + const interval = setInterval(fetchReversals, 30000); // Auto-refresh every 30s + return () => clearInterval(interval); + }, [fetchReversals]); + + // Handle manual sorting + const handleSort = (field) => { + if (sortBy === field) { + setSortDir(sortDir === 'asc' ? 'desc' : 'asc'); + } else { + setSortBy(field); + setSortDir('desc'); + } + }; + + // Filter and Sort data + const processedData = [...reversals] + .filter(row => { + // Search filter + const matchesSearch = row.symbol.toUpperCase().includes(searchQuery.toUpperCase()); + // Signal type filter + const matchesSignal = filterSignal === 'all' || + (filterSignal === 'bullish' && row.signal === 'REVERSAL_BULLISH') || + (filterSignal === 'bearish' && row.signal === 'REVERSAL_BEARISH'); + return matchesSearch && matchesSignal; + }) + .sort((a, b) => { + let aVal = a[sortBy]; + let bVal = b[sortBy]; + + // Handle numeric parses + if (sortBy === 'currentPremium' || sortBy === 'previousPremium') { + aVal = parseFloat(aVal || 0); + bVal = parseFloat(bVal || 0); + } else if (sortBy === 'priceChange') { + aVal = parseFloat(aVal?.replace('%', '') || 0); + bVal = parseFloat(bVal?.replace('%', '') || 0); + } + + if (aVal === bVal) return 0; + const comparison = aVal > bVal ? 1 : -1; + return sortDir === 'desc' ? -comparison : comparison; + }); + + const formatPremium = (val) => { + if (val == null) return '—'; + if (val >= 1e6) return `$${(val / 1e6).toFixed(2)}M`; + if (val >= 1e3) return `$${(val / 1e3).toFixed(0)}K`; + return `$${val}`; + }; + + return ( +
+ {/* Header */} +
+
+

+ 🔄 Institutional Reversal Screener + + Live Flow Flips + +

+

+ Detects when institutional options volume sentiment flips (Bullish 🟢 ↔ Bearish 🔴) within a 15-minute window. +

+
+ + {/* Controls */} +
+ setSearchQuery(e.target.value.toUpperCase())} + className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-blue-500/50 w-44 transition-colors" + /> + + + + +
+
+ + {/* Main content table */} +
+ {error ? ( +
+ + {error} +
+ ) : loading && processedData.length === 0 ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+
+
+ ))} +
+ ) : processedData.length === 0 ? ( +
+ 📁 + No reversals detected. Live feed imports and scans active. +
+ ) : ( + + + + + + + + + + + + + + {processedData.map((row, idx) => { + const isBullish = row.signal === 'REVERSAL_BULLISH'; + return ( + onSelectSymbol?.(row.symbol)} + > + + + + + + + + + ); + })} + +
handleSort('symbol')}>Symbol handleSort('signal')}>Reversal SignalSentiment Shift handleSort('currentPremium')}>Current Premium handleSort('previousPremium')}>Previous Premium handleSort('priceChange')}>Price Reaction handleSort('timestamp')}>Detected At
+ + {row.symbol} + + +
+ {isBullish ? ( + <> + + BULLISH FLIP + + ) : ( + <> + + BEARISH FLIP + + )} +
+
+ {row.from} → {row.to} + + {formatPremium(row.currentPremium)} + + {formatPremium(row.previousPremium)} + + + {row.priceChange} + + + {new Date(row.timestamp).toLocaleTimeString()} +
+ )} +
+ + {/* Footer info */} +
+ Showing {processedData.length} of {reversals.length} active flow reversals + Polling database intervals: 30s +
+
+ ); +}