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 ? (
+
+ ) : loading && processedData.length === 0 ? (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ ) : processedData.length === 0 ? (
+
+ 📁
+ No reversals detected. Live feed imports and scans active.
+
+ ) : (
+
+
+
+ | handleSort('symbol')}>Symbol |
+ handleSort('signal')}>Reversal Signal |
+ Sentiment Shift |
+ handleSort('currentPremium')}>Current Premium |
+ handleSort('previousPremium')}>Previous Premium |
+ handleSort('priceChange')}>Price Reaction |
+ handleSort('timestamp')}>Detected At |
+
+
+
+ {processedData.map((row, idx) => {
+ const isBullish = row.signal === 'REVERSAL_BULLISH';
+ return (
+ onSelectSymbol?.(row.symbol)}
+ >
+ |
+
+ {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
+
+
+ );
+}