add-search-and-top100
Build Institutional Trader / build-and-deploy (push) Successful in 3m9s
Details
Build Institutional Trader / build-and-deploy (push) Successful in 3m9s
Details
This commit is contained in:
parent
f177252d6d
commit
0ac5176df6
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue