add-search-and-top100
Build Institutional Trader / build-and-deploy (push) Successful in 3m9s Details

This commit is contained in:
Antigravity 2026-06-30 00:30:53 +00:00
parent f177252d6d
commit 0ac5176df6
3 changed files with 117 additions and 30 deletions

View File

@ -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,

View File

@ -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
});
}
}

View File

@ -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 }) {
</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"
/>
<div className="flex flex-wrap items-center gap-3">
<select
value={listType}
onChange={e => {
const val = e.target.value;
if (val !== 'search') {
setSearchQuery('');
}
setListType(val);
setLoading(true);
fetchReversals(val, '');
}}
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="active">🔥 Top 100 Active</option>
<option value="popular"> Top 100 Popular</option>
{listType === 'search' && <option value="search">🔍 Search Results</option>}
</select>
<form onSubmit={handleSearchSubmit} className="flex items-center gap-1">
<input
type="text"
placeholder="Search ticker..."
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-36 transition-colors"
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-slate-300 p-1.5 rounded-lg transition-colors"
>
<Search className="w-3.5 h-3.5" />
</button>
</form>
<select
value={filterSignal}
@ -168,7 +217,10 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
</thead>
<tbody className="divide-y divide-slate-800/40 text-slate-300 text-sm">
{processedData.map((row, idx) => {
const isBullish = row.signal === 'REVERSAL_BULLISH';
const isBullish = row.signal === 'REVERSAL_BULLISH' || row.signal === 'BULLISH';
const isNeutral = row.signal === 'NEUTRAL';
const isReversal = row.isReversal !== false;
return (
<tr
key={`${row.symbol}-${row.timestamp}-${idx}`}
@ -182,21 +234,40 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
</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>
</>
{isReversal ? (
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>
</>
)
) : (
<>
<TrendingDown className="w-4 h-4 text-red-400" />
<Badge variant="destructive" className="text-[10px] py-0.5">BEARISH FLIP</Badge>
</>
isNeutral ? (
<>
<Minus className="w-4 h-4 text-slate-400" />
<Badge variant="outline" className="text-[10px] py-0.5 text-slate-400 border-slate-700">NEUTRAL</Badge>
</>
) : isBullish ? (
<>
<TrendingUp className="w-4 h-4 text-emerald-400/50" />
<Badge variant="outline" className="text-[10px] py-0.5 text-emerald-400 border-emerald-900/50">BULLISH (NO FLIP)</Badge>
</>
) : (
<>
<TrendingDown className="w-4 h-4 text-red-400/50" />
<Badge variant="outline" className="text-[10px] py-0.5 text-red-400 border-red-900/50">BEARISH (NO FLIP)</Badge>
</>
)
)}
</div>
</td>
<td className="px-6 py-4.5 font-mono text-xs text-slate-400">
{row.from} {row.to}
{isReversal ? `${row.from}${row.to}` : row.to}
</td>
<td className="px-6 py-4.5 text-right font-mono text-white font-semibold">
{formatPremium(row.currentPremium)}