Add Reversal Screener tab to dashboard
Build Institutional Trader / build-and-deploy (push) Successful in 7m35s
Details
Build Institutional Trader / build-and-deploy (push) Successful in 7m35s
Details
This commit is contained in:
parent
2b3355cddb
commit
e784bc46eb
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue