diff --git a/backend/src/routes/reversals.js b/backend/src/routes/reversals.js index 38c469e..6e127d5 100644 --- a/backend/src/routes/reversals.js +++ b/backend/src/routes/reversals.js @@ -31,7 +31,8 @@ router.get('/detect', async (req, res) => { }); } - const reversals = await detectReversals(symbolList); + const returnAll = !!symbols; + const reversals = await detectReversals(symbolList, returnAll); res.json({ success: true, diff --git a/backend/src/services/reversalDetector.js b/backend/src/services/reversalDetector.js index 83a32b0..ceb8b1d 100644 --- a/backend/src/services/reversalDetector.js +++ b/backend/src/services/reversalDetector.js @@ -6,7 +6,7 @@ import { calculateBadges } from './badgeCalculator.js'; * Compares current badges vs 15min ago vs 30min ago */ -export async function detectReversals(symbolList) { +export async function detectReversals(symbolList, returnAll = false) { const reversals = []; for (const symbol of symbolList) { @@ -63,10 +63,25 @@ export async function detectReversals(symbolList) { currentPremium: parseFloat(current.premium_num || 0), previousPremium: parseFloat(prev15min.premium_num || 0), priceChange, - signal: currentBadge === '🟢' ? 'REVERSAL_BULLISH' : 'REVERSAL_BEARISH' + signal: currentBadge === '🟢' ? 'REVERSAL_BULLISH' : 'REVERSAL_BEARISH', + isReversal: true }; reversals.push(reversal); + } else if (returnAll && current) { + // Return current status even if no reversal + const currentBadge = current.badges?.round || '⚪'; + reversals.push({ + symbol: symbol.toUpperCase(), + from: currentBadge, + to: currentBadge, + timestamp: new Date(current.timestamp).toISOString(), + currentPremium: parseFloat(current.premium_num || 0), + previousPremium: 0, + priceChange: '0%', + signal: currentBadge === '🟢' ? 'BULLISH' : currentBadge === '🔴' ? 'BEARISH' : 'NEUTRAL', + isReversal: false + }); } } diff --git a/frontend/src/components/dashboard/ReversalScreenerPanel.jsx b/frontend/src/components/dashboard/ReversalScreenerPanel.jsx index 4ef0894..d61a39f 100644 --- a/frontend/src/components/dashboard/ReversalScreenerPanel.jsx +++ b/frontend/src/components/dashboard/ReversalScreenerPanel.jsx @@ -1,7 +1,9 @@ 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'; +import { AlertTriangle, TrendingUp, TrendingDown, RefreshCw, Search, Minus } from 'lucide-react'; + +const POPULAR_STOCKS = ['AAPL', 'MSFT', 'NVDA', 'TSLA', 'AMZN', 'META', 'GOOGL', 'AMD', 'NFLX', 'SPY', 'QQQ', 'IWM', 'BA', 'DIS', 'JPM', 'V', 'WMT', 'JNJ', 'UNH', 'XOM', 'CVX', 'PEP', 'KO', 'COST', 'AVGO', 'CSCO', 'MCD', 'NKE', 'CRM', 'ADBE', 'TXN', 'QCOM', 'INTC', 'CAT', 'IBM', 'GE', 'GS', 'MS', 'BLK', 'C', 'WFC', 'BAC', 'T', 'VZ', 'CMCSA', 'PYPL', 'SQ', 'SHOP', 'UBER', 'ABNB', 'SNOW', 'PLTR', 'RIVN', 'COIN', 'HOOD', 'ROKU', 'ZM', 'DKNG', 'AMC', 'GME', 'TSM', 'ASML', 'LRCX', 'AMAT', 'MU', 'ARM', 'SMCI', 'DELL', 'PANW', 'CRWD', 'FTNT', 'ZS', 'NET', 'DDOG', 'NOW']; export default function ReversalScreenerPanel({ onSelectSymbol }) { const [reversals, setReversals] = useState([]); @@ -9,12 +11,20 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) { const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [filterSignal, setFilterSignal] = useState('all'); + const [listType, setListType] = useState('active'); // 'active', 'popular', 'search' const [sortBy, setSortBy] = useState('timestamp'); const [sortDir, setSortDir] = useState('desc'); - const fetchReversals = useCallback(async () => { + const fetchReversals = useCallback(async (customListType = listType, customQuery = searchQuery) => { try { - const response = await fetch(getApiUrl('/api/reversals/detect')); + let url = '/api/reversals/detect'; + if (customListType === 'search' && customQuery) { + url += `?symbols=${customQuery}`; + } else if (customListType === 'popular') { + url += `?symbols=${POPULAR_STOCKS.join(',')}`; + } + + const response = await fetch(getApiUrl(url)); const data = await response.json(); if (data.success) { setReversals(data.reversals || []); @@ -33,10 +43,23 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) { useEffect(() => { setLoading(true); fetchReversals(); - const interval = setInterval(fetchReversals, 30000); // Auto-refresh every 30s + const interval = setInterval(() => fetchReversals(), 30000); // Auto-refresh every 30s return () => clearInterval(interval); }, [fetchReversals]); + const handleSearchSubmit = (e) => { + if (e) e.preventDefault(); + if (!searchQuery.trim()) { + setListType('active'); + setLoading(true); + fetchReversals('active', ''); + return; + } + setListType('search'); + setLoading(true); + fetchReversals('search', searchQuery); + }; + // Handle manual sorting const handleSort = (field) => { if (sortBy === field) { @@ -50,12 +73,12 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) { // Filter and Sort data const processedData = [...reversals] .filter(row => { - // Search filter - const matchesSearch = row.symbol.toUpperCase().includes(searchQuery.toUpperCase()); + // Local Search filter (only if not doing a backend search) + const matchesSearch = listType === 'search' ? true : 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'); + (filterSignal === 'bullish' && (row.signal === 'REVERSAL_BULLISH' || row.signal === 'BULLISH')) || + (filterSignal === 'bearish' && (row.signal === 'REVERSAL_BEARISH' || row.signal === 'BEARISH')); return matchesSearch && matchesSignal; }) .sort((a, b) => { @@ -100,14 +123,40 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) { {/* 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" - /> +
+ + +
+ 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-36 transition-colors" + /> + +