Add Reversal Screener tab to dashboard
Build Institutional Trader / build-and-deploy (push) Successful in 7m35s Details

This commit is contained in:
Deep Koluguri 2026-06-29 19:04:27 -04:00 committed by Antigravity
parent 2b3355cddb
commit e784bc46eb
2 changed files with 240 additions and 0 deletions

View File

@ -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() {
<TabsTrigger value="flow" className="data-[state=active]:bg-slate-800">
🎯 Options Flow
</TabsTrigger>
<TabsTrigger value="reversals" className="data-[state=active]:bg-slate-800">
🔄 Reversal Screener
</TabsTrigger>
<TabsTrigger value="scanner" className="data-[state=active]:bg-slate-800">
🔍 Multi-Signal Scanner
</TabsTrigger>
@ -77,6 +81,10 @@ export default function App() {
<OptionsFlowPanel />
</TabsContent>
<TabsContent value="reversals" className="mt-6">
<ReversalScreenerPanel onSelectSymbol={setSelectedSymbol} />
</TabsContent>
<TabsContent value="scanner" className="mt-6">
<ScannerPanel />
</TabsContent>

View File

@ -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 (
<div className="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-xl">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-4 px-6 py-5 border-b border-slate-800/80 bg-slate-900/50">
<div>
<h2 className="text-white font-bold text-lg flex items-center gap-2">
🔄 Institutional Reversal Screener
<span className="text-[10px] uppercase tracking-wider font-bold bg-yellow-500/10 text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
Live Flow Flips
</span>
</h2>
<p className="text-slate-400 text-xs mt-1">
Detects when institutional options volume sentiment flips (Bullish 🟢 Bearish 🔴) within a 15-minute window.
</p>
</div>
{/* Controls */}
<div className="flex items-center gap-3">
<input
type="text"
placeholder="Filter by symbol..."
value={searchQuery}
onChange={e => 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"
/>
<select
value={filterSignal}
onChange={e => setFilterSignal(e.target.value)}
className="bg-slate-950 border border-slate-800 text-slate-300 text-xs rounded-xl px-3 py-1.5 outline-none focus:border-blue-500/50 cursor-pointer transition-colors"
>
<option value="all">All Sentiment</option>
<option value="bullish">🟢 Bullish Reversals</option>
<option value="bearish">🔴 Bearish Reversals</option>
</select>
<button
onClick={() => { setLoading(true); fetchReversals(); }}
disabled={loading}
className="bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 p-2 rounded-xl text-xs transition-colors flex items-center justify-center"
title="Refresh reversals"
>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin text-blue-400' : ''}`} />
</button>
</div>
</div>
{/* Main content table */}
<div className="overflow-x-auto min-h-[300px]">
{error ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500 text-sm gap-2">
<AlertTriangle className="w-8 h-8 text-red-500/80" />
<span className="text-red-400 font-semibold">{error}</span>
</div>
) : loading && processedData.length === 0 ? (
<div className="divide-y divide-slate-800/60">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="animate-pulse flex items-center justify-between p-6">
<div className="h-4 bg-slate-800 rounded w-16" />
<div className="h-4 bg-slate-800 rounded w-32" />
<div className="h-4 bg-slate-800 rounded w-24" />
<div className="h-4 bg-slate-800 rounded w-20" />
</div>
))}
</div>
) : processedData.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-slate-500 text-sm">
<span className="text-xl mb-2">📁</span>
<span>No reversals detected. Live feed imports and scans active.</span>
</div>
) : (
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-800 text-slate-400 text-xs font-semibold bg-slate-950/20 uppercase tracking-wider">
<th className="px-6 py-4 cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('symbol')}>Symbol</th>
<th className="px-6 py-4 cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('signal')}>Reversal Signal</th>
<th className="px-6 py-4">Sentiment Shift</th>
<th className="px-6 py-4 text-right cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('currentPremium')}>Current Premium</th>
<th className="px-6 py-4 text-right cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('previousPremium')}>Previous Premium</th>
<th className="px-6 py-4 text-right cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('priceChange')}>Price Reaction</th>
<th className="px-6 py-4 text-right cursor-pointer hover:text-white transition-colors" onClick={() => handleSort('timestamp')}>Detected At</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/40 text-slate-300 text-sm">
{processedData.map((row, idx) => {
const isBullish = row.signal === 'REVERSAL_BULLISH';
return (
<tr
key={`${row.symbol}-${row.timestamp}-${idx}`}
className="hover:bg-slate-800/30 transition-colors group cursor-pointer"
onClick={() => onSelectSymbol?.(row.symbol)}
>
<td className="px-6 py-4.5">
<span className="font-bold text-white font-mono group-hover:text-blue-400 transition-colors text-base">
{row.symbol}
</span>
</td>
<td className="px-6 py-4.5">
<div className="flex items-center gap-1.5">
{isBullish ? (
<>
<TrendingUp className="w-4 h-4 text-emerald-400" />
<Badge variant="success" className="text-[10px] py-0.5">BULLISH FLIP</Badge>
</>
) : (
<>
<TrendingDown className="w-4 h-4 text-red-400" />
<Badge variant="destructive" className="text-[10px] py-0.5">BEARISH FLIP</Badge>
</>
)}
</div>
</td>
<td className="px-6 py-4.5 font-mono text-xs text-slate-400">
{row.from} {row.to}
</td>
<td className="px-6 py-4.5 text-right font-mono text-white font-semibold">
{formatPremium(row.currentPremium)}
</td>
<td className="px-6 py-4.5 text-right font-mono text-slate-400">
{formatPremium(row.previousPremium)}
</td>
<td className="px-6 py-4.5 text-right">
<span className={`font-mono font-bold ${
row.priceChange.startsWith('-') ? 'text-red-400' : 'text-emerald-400'
}`}>
{row.priceChange}
</span>
</td>
<td className="px-6 py-4.5 text-right text-xs text-slate-500 font-mono">
{new Date(row.timestamp).toLocaleTimeString()}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
{/* Footer info */}
<div className="px-6 py-4 border-t border-slate-800/80 bg-slate-900/30 flex justify-between items-center text-xs text-slate-500">
<span>Showing {processedData.length} of {reversals.length} active flow reversals</span>
<span>Polling database intervals: 30s</span>
</div>
</div>
);
}