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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { calculateBadges } from './badgeCalculator.js';
|
||||||
* Compares current badges vs 15min ago vs 30min ago
|
* Compares current badges vs 15min ago vs 30min ago
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function detectReversals(symbolList) {
|
export async function detectReversals(symbolList, returnAll = false) {
|
||||||
const reversals = [];
|
const reversals = [];
|
||||||
|
|
||||||
for (const symbol of symbolList) {
|
for (const symbol of symbolList) {
|
||||||
|
|
@ -63,10 +63,25 @@ export async function detectReversals(symbolList) {
|
||||||
currentPremium: parseFloat(current.premium_num || 0),
|
currentPremium: parseFloat(current.premium_num || 0),
|
||||||
previousPremium: parseFloat(prev15min.premium_num || 0),
|
previousPremium: parseFloat(prev15min.premium_num || 0),
|
||||||
priceChange,
|
priceChange,
|
||||||
signal: currentBadge === '🟢' ? 'REVERSAL_BULLISH' : 'REVERSAL_BEARISH'
|
signal: currentBadge === '🟢' ? 'REVERSAL_BULLISH' : 'REVERSAL_BEARISH',
|
||||||
|
isReversal: true
|
||||||
};
|
};
|
||||||
|
|
||||||
reversals.push(reversal);
|
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 { useEffect, useState, useCallback } from 'react';
|
||||||
import { getApiUrl } from '@/config/api';
|
import { getApiUrl } from '@/config/api';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
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 }) {
|
export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
const [reversals, setReversals] = useState([]);
|
const [reversals, setReversals] = useState([]);
|
||||||
|
|
@ -9,12 +11,20 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [filterSignal, setFilterSignal] = useState('all');
|
const [filterSignal, setFilterSignal] = useState('all');
|
||||||
|
const [listType, setListType] = useState('active'); // 'active', 'popular', 'search'
|
||||||
const [sortBy, setSortBy] = useState('timestamp');
|
const [sortBy, setSortBy] = useState('timestamp');
|
||||||
const [sortDir, setSortDir] = useState('desc');
|
const [sortDir, setSortDir] = useState('desc');
|
||||||
|
|
||||||
const fetchReversals = useCallback(async () => {
|
const fetchReversals = useCallback(async (customListType = listType, customQuery = searchQuery) => {
|
||||||
try {
|
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();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setReversals(data.reversals || []);
|
setReversals(data.reversals || []);
|
||||||
|
|
@ -33,10 +43,23 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetchReversals();
|
fetchReversals();
|
||||||
const interval = setInterval(fetchReversals, 30000); // Auto-refresh every 30s
|
const interval = setInterval(() => fetchReversals(), 30000); // Auto-refresh every 30s
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchReversals]);
|
}, [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
|
// Handle manual sorting
|
||||||
const handleSort = (field) => {
|
const handleSort = (field) => {
|
||||||
if (sortBy === field) {
|
if (sortBy === field) {
|
||||||
|
|
@ -50,12 +73,12 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
// Filter and Sort data
|
// Filter and Sort data
|
||||||
const processedData = [...reversals]
|
const processedData = [...reversals]
|
||||||
.filter(row => {
|
.filter(row => {
|
||||||
// Search filter
|
// Local Search filter (only if not doing a backend search)
|
||||||
const matchesSearch = row.symbol.toUpperCase().includes(searchQuery.toUpperCase());
|
const matchesSearch = listType === 'search' ? true : row.symbol.toUpperCase().includes(searchQuery.toUpperCase());
|
||||||
// Signal type filter
|
// Signal type filter
|
||||||
const matchesSignal = filterSignal === 'all' ||
|
const matchesSignal = filterSignal === 'all' ||
|
||||||
(filterSignal === 'bullish' && row.signal === 'REVERSAL_BULLISH') ||
|
(filterSignal === 'bullish' && (row.signal === 'REVERSAL_BULLISH' || row.signal === 'BULLISH')) ||
|
||||||
(filterSignal === 'bearish' && row.signal === 'REVERSAL_BEARISH');
|
(filterSignal === 'bearish' && (row.signal === 'REVERSAL_BEARISH' || row.signal === 'BEARISH'));
|
||||||
return matchesSearch && matchesSignal;
|
return matchesSearch && matchesSignal;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|
@ -100,14 +123,40 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<input
|
<select
|
||||||
type="text"
|
value={listType}
|
||||||
placeholder="Filter by symbol..."
|
onChange={e => {
|
||||||
value={searchQuery}
|
const val = e.target.value;
|
||||||
onChange={e => setSearchQuery(e.target.value.toUpperCase())}
|
if (val !== 'search') {
|
||||||
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('');
|
||||||
/>
|
}
|
||||||
|
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
|
<select
|
||||||
value={filterSignal}
|
value={filterSignal}
|
||||||
|
|
@ -168,7 +217,10 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/40 text-slate-300 text-sm">
|
<tbody className="divide-y divide-slate-800/40 text-slate-300 text-sm">
|
||||||
{processedData.map((row, idx) => {
|
{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 (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={`${row.symbol}-${row.timestamp}-${idx}`}
|
key={`${row.symbol}-${row.timestamp}-${idx}`}
|
||||||
|
|
@ -182,21 +234,40 @@ export default function ReversalScreenerPanel({ onSelectSymbol }) {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4.5">
|
<td className="px-6 py-4.5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{isBullish ? (
|
{isReversal ? (
|
||||||
<>
|
isBullish ? (
|
||||||
<TrendingUp className="w-4 h-4 text-emerald-400" />
|
<>
|
||||||
<Badge variant="success" className="text-[10px] py-0.5">BULLISH FLIP</Badge>
|
<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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
isNeutral ? (
|
||||||
<TrendingDown className="w-4 h-4 text-red-400" />
|
<>
|
||||||
<Badge variant="destructive" className="text-[10px] py-0.5">BEARISH FLIP</Badge>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4.5 font-mono text-xs text-slate-400">
|
<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>
|
||||||
<td className="px-6 py-4.5 text-right font-mono text-white font-semibold">
|
<td className="px-6 py-4.5 text-right font-mono text-white font-semibold">
|
||||||
{formatPremium(row.currentPremium)}
|
{formatPremium(row.currentPremium)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue