feat: implement stock evaluation filters, fundamentals styling, and AI trading profiles
Build Institutional Trader / build-and-deploy (push) Successful in 2m1s Details

This commit is contained in:
Deep Koluguri 2026-06-22 23:19:38 -04:00
parent c981faf3a6
commit f91534e976
6 changed files with 217 additions and 135 deletions

View File

@ -49,6 +49,8 @@ router.get('/universe', async (req, res) => {
change_pct: quote?.change_pct ?? null,
market_cap: quote?.market_cap ?? null,
volume: quote?.volume ?? null,
day_high: quote?.high ?? null,
day_low: quote?.low ?? null,
high_52w: null, // populated by quoteSummary if needed
low_52w: null,
market_state: quote?.market_state ?? null,

View File

@ -303,20 +303,26 @@ ${yahooData ? `STOCK DATA (Yahoo Finance):
- Volume: ${yahooData.volume?.toLocaleString() || 'N/A'}
` : ''}
Provide a concise analysis (2-3 sentences max) with:
1. Overall assessment (Bullish/Bearish/Neutral)
2. Key risk factors or opportunities
3. Clear action recommendation (Enter/Wait/Avoid)
Provide a detailed analysis categorized into three distinct trader profiles based on the data.
Format as JSON:
{
"assessment": "BULLISH|BEARISH|NEUTRAL",
"summary": "Brief 1-2 sentence summary",
"keyFactors": ["factor1", "factor2", "factor3"],
"recommendation": "ENTER|WAIT|AVOID",
"reasoning": "Brief explanation",
"riskLevel": "LOW|MEDIUM|HIGH",
"timeHorizon": "INTRADAY|SWING|POSITION"
"longTerm": {
"assessment": "BULLISH|BEARISH|NEUTRAL",
"moatAndFundamentals": "Brief analysis of economic moat, EPS growth, and fundamental ratios (P/E, P/B)",
"recommendation": "ENTER|WAIT|AVOID"
},
"swing": {
"assessment": "BULLISH|BEARISH|NEUTRAL",
"technicalSetup": "Analysis of technical setups, consolidation, moving averages, and RSI momentum",
"recommendation": "ENTER|WAIT|AVOID"
},
"dayTrade": {
"assessment": "BULLISH|BEARISH|NEUTRAL",
"volatilityAndLiquidity": "Analysis of immediate catalysts, intraday liquidity/volume, and volatility risks",
"recommendation": "ENTER|WAIT|AVOID"
},
"summary": "Overall 1-2 sentence conclusion covering the most viable approach"
}`;
// Use model from env or default to stable version
@ -346,13 +352,22 @@ Format as JSON:
// Fallback: create structured response from text
console.warn('Failed to parse AI response as JSON, using fallback');
analysis = {
assessment: direction,
summary: responseText.substring(0, 200),
keyFactors: [],
recommendation: score >= 2.0 ? 'ENTER' : 'WAIT',
reasoning: responseText,
riskLevel: score < 2.0 ? 'HIGH' : score < 5.0 ? 'MEDIUM' : 'LOW',
timeHorizon: 'INTRADAY'
longTerm: {
assessment: direction,
moatAndFundamentals: 'Unable to perform deep fundamental analysis. Please review ratios manually.',
recommendation: score >= 5.0 ? 'ENTER' : 'WAIT'
},
swing: {
assessment: direction,
technicalSetup: 'Technical analysis currently unavailable.',
recommendation: score >= 3.0 ? 'ENTER' : 'WAIT'
},
dayTrade: {
assessment: direction,
volatilityAndLiquidity: 'Analysis of intraday volume and catalysts unavailable.',
recommendation: score >= 2.0 ? 'ENTER' : 'WAIT'
},
summary: responseText.substring(0, 200) || 'Analysis unavailable'
};
}
@ -369,13 +384,10 @@ Format as JSON:
success: false,
error: error.message,
analysis: {
assessment: 'NEUTRAL',
summary: 'Analysis unavailable',
keyFactors: [],
recommendation: 'WAIT',
reasoning: 'Unable to generate analysis',
riskLevel: 'MEDIUM',
timeHorizon: 'INTRADAY'
longTerm: { assessment: 'NEUTRAL', moatAndFundamentals: 'Analysis unavailable', recommendation: 'WAIT' },
swing: { assessment: 'NEUTRAL', technicalSetup: 'Analysis unavailable', recommendation: 'WAIT' },
dayTrade: { assessment: 'NEUTRAL', volatilityAndLiquidity: 'Analysis unavailable', recommendation: 'WAIT' },
summary: 'Analysis unavailable'
}
};
}

View File

@ -57,6 +57,7 @@ function CapBadge({ tier }) {
export default function MarketScreenerPanel({ onSelectSymbol }) {
const [activeTab, setActiveTab] = useState('all');
const [setupFilter, setSetupFilter] = useState('all');
const [sortBy, setSortBy] = useState('market_cap');
const [sortDir, setSortDir] = useState('desc');
const [search, setSearch] = useState('');
@ -98,11 +99,30 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
}, [fetchData]);
const processed = [...data]
.map(r => {
// Calculate intraday volatility if we have high/low
const volPct = (r.day_high && r.day_low && r.day_low > 0)
? ((r.day_high - r.day_low) / r.day_low) * 100
: null;
return { ...r, volatility_pct: volPct };
})
.filter(r => {
if (!search) return true;
const q = search.toUpperCase();
return r.symbol.includes(q) || r.name.toUpperCase().includes(q) || r.sector?.toUpperCase().includes(q);
})
.filter(r => {
if (setupFilter === 'all') return true;
if (setupFilter === 'day') {
// Day Trade: high volume (>1M), high volatility (>3%)
return (r.volume >= 1000000) && (r.volatility_pct > 3 || Math.abs(r.change_pct || 0) > 3);
}
if (setupFilter === 'swing') {
// Swing Trade: potential reversal (massive drop > 5% or massive gain > 5% on high volume)
return (r.volume >= 500000) && (Math.abs(r.change_pct || 0) > 5);
}
return true;
})
.sort((a, b) => {
const av = a[sortBy] ?? -Infinity;
const bv = b[sortBy] ?? -Infinity;
@ -161,20 +181,38 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
</div>
{/* Cap tier tabs */}
<div className="flex border-b border-slate-700/50 bg-slate-950/30">
{CAP_TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2.5 text-sm font-medium transition-all ${
activeTab === tab.id
? `border-b-2 border-blue-500 ${tab.color} bg-blue-500/5`
: 'text-slate-500 hover:text-slate-300'
}`}
>
{tab.label}
</button>
))}
<div className="flex flex-col sm:flex-row justify-between border-b border-slate-700/50 bg-slate-950/30">
<div className="flex">
{CAP_TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2.5 text-sm font-medium transition-all ${
activeTab === tab.id
? `border-b-2 border-blue-500 ${tab.color} bg-blue-500/5`
: 'text-slate-500 hover:text-slate-300'
}`}
>
{tab.label}
</button>
))}
</div>
<div className="flex items-center px-4 space-x-2 border-t sm:border-t-0 border-slate-700/50 py-2 sm:py-0">
<span className="text-xs text-slate-500 uppercase font-semibold mr-2">Setups:</span>
{['all', 'day', 'swing'].map(f => (
<button
key={f}
onClick={() => setSetupFilter(f)}
className={`px-3 py-1 text-xs rounded transition-all ${
setupFilter === f
? 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
: 'bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700'
}`}
>
{f === 'all' ? 'None' : f === 'day' ? '⚡ Day Trade' : '🌊 Swing Trade'}
</button>
))}
</div>
</div>
{/* Table */}
@ -207,6 +245,10 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
className="text-right px-4 py-3 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => toggleSort('volume')}
>Volume <SortIcon col="volume" /></th>
<th
className="text-right px-4 py-3 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => toggleSort('volatility_pct')}
>Volatility <SortIcon col="volatility_pct" /></th>
<th className="text-center px-4 py-3 font-medium">Cap Tier</th>
</tr>
</thead>
@ -214,7 +256,7 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
{loading && data.length === 0 ? (
Array.from({ length: 10 }).map((_, i) => (
<tr key={i} className="animate-pulse">
{Array.from({ length: 8 }).map((_, j) => (
{Array.from({ length: 9 }).map((_, j) => (
<td key={j} className="px-4 py-3">
<div className="h-4 bg-slate-800 rounded w-full" />
</td>
@ -222,7 +264,7 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
</tr>
))
) : processed.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-12 text-center text-slate-500">No results found</td></tr>
<tr><td colSpan={9} className="px-4 py-12 text-center text-slate-500">No results found</td></tr>
) : (
processed.map(row => (
<tr
@ -255,6 +297,13 @@ export default function MarketScreenerPanel({ onSelectSymbol }) {
<td className="px-4 py-3 text-right text-slate-400 tabular-nums">
{fmtVolume(row.volume)}
</td>
<td className="px-4 py-3 text-right tabular-nums">
{row.volatility_pct != null ? (
<span className={`text-sm ${row.volatility_pct > 3 ? 'text-emerald-400 font-medium' : 'text-slate-400'}`}>
{row.volatility_pct.toFixed(2)}%
</span>
) : '—'}
</td>
<td className="px-4 py-3 text-center">
<CapBadge tier={row.capTier} />
</td>

View File

@ -241,9 +241,9 @@ function FundamentalsTab({ f }) {
{
title: '💰 Valuation',
metrics: [
{ label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1) },
{ label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1) },
{ label: 'Price / Book', value: fmt(f.price_to_book, 2) },
{ label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1), h: (f.pe_ttm >= 15 && f.pe_ttm <= 25) ? 'positive' : (f.pe_ttm < 0 || f.pe_ttm > 50) ? 'negative' : undefined },
{ label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1), h: (f.pe_forward >= 15 && f.pe_forward <= 25) ? 'positive' : (f.pe_forward < 0 || f.pe_forward > 50) ? 'negative' : undefined },
{ label: 'Price / Book', value: fmt(f.price_to_book, 2), h: (f.price_to_book >= 1 && f.price_to_book <= 3) ? 'positive' : (f.price_to_book > 10) ? 'negative' : undefined },
{ label: 'Price / Sales', value: fmt(f.price_to_sales, 2) },
{ label: 'EV / EBITDA', value: fmt(f.ev_ebitda, 1) },
{ label: 'Market Cap', value: fmtLarge(f.market_cap) },
@ -267,7 +267,7 @@ function FundamentalsTab({ f }) {
{ label: 'EBITDA Margin', value: fmtPct(f.ebitda_margins) },
{ label: 'Operating Margin', value: fmtPct(f.operating_margins) },
{ label: 'Profit Margin', value: fmtPct(f.profit_margins) },
{ label: 'Return on Equity', value: fmtPct(f.return_on_equity), h: f.return_on_equity > 0.15 ? 'positive' : f.return_on_equity < 0 ? 'negative' : undefined },
{ label: 'Return on Equity', value: fmtPct(f.return_on_equity), h: (f.return_on_equity >= 0.1 && f.return_on_equity <= 0.2) ? 'positive' : f.return_on_equity < 0 ? 'negative' : undefined },
{ label: 'Return on Assets', value: fmtPct(f.return_on_assets) },
]
},
@ -303,7 +303,7 @@ function FundamentalsTab({ f }) {
{ label: 'Inst. Ownership', value: fmtPct(f.held_pct_institutions) },
{ label: 'Insider Ownership', value: fmtPct(f.held_pct_insiders) },
{ label: 'Dividend Yield', value: fmtPct(f.dividend_yield), h: f.dividend_yield > 0 ? 'positive' : undefined },
{ label: 'Payout Ratio', value: fmtPct(f.payout_ratio) },
{ label: 'Payout Ratio', value: fmtPct(f.payout_ratio), h: f.payout_ratio < 0.6 ? 'positive' : f.payout_ratio > 0.7 ? 'negative' : undefined },
]
},
];

View File

@ -495,49 +495,60 @@ export function TradeAnalysisModal({ row, isOpen, onClose }) {
)}
{aiAnalysis && aiAnalysis.analysis && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Badge
variant={
aiAnalysis.analysis.assessment === 'BULLISH' ? 'success' :
aiAnalysis.analysis.assessment === 'BEARISH' ? 'destructive' : 'secondary'
}
className="text-xs"
>
{aiAnalysis.analysis.assessment}
</Badge>
<Badge
variant={
aiAnalysis.analysis.recommendation === 'ENTER' ? 'success' :
aiAnalysis.analysis.recommendation === 'WAIT' ? 'warning' : 'destructive'
}
className="text-xs"
>
{aiAnalysis.analysis.recommendation}
</Badge>
<Badge variant="outline" className="text-xs">
{aiAnalysis.analysis.riskLevel} Risk
</Badge>
<div className="space-y-4">
<div className="text-sm text-slate-200 bg-slate-700/30 p-3 rounded border border-slate-600/50">
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Long Term */}
{aiAnalysis.analysis.longTerm && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Long-Term Investor</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.longTerm.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.longTerm.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.longTerm.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.longTerm.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
</div>
)}
{/* Swing Trade */}
{aiAnalysis.analysis.swing && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Swing Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.swing.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.swing.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.swing.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.swing.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.swing.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.swing.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.swing.technicalSetup}</p>
</div>
)}
{/* Day Trade */}
{aiAnalysis.analysis.dayTrade && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Day Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.dayTrade.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.dayTrade.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.dayTrade.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.dayTrade.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}</p>
</div>
)}
</div>
<p className="text-sm text-slate-200">{aiAnalysis.analysis.summary}</p>
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
<div>
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
<li key={idx}>{factor}</li>
))}
</ul>
</div>
)}
{aiAnalysis.analysis.reasoning && (
<div>
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
</div>
)}
</div>
)}
</div>

View File

@ -60,56 +60,64 @@ export function AnalysisSection({ row, hasPhase1Data, onPhase1Click }) {
)}
{aiAnalysis && aiAnalysis.analysis && (
<div className="space-y-3">
<div>
<div className="flex items-center gap-2 mb-2">
<Badge
variant={
aiAnalysis.analysis.assessment === 'BULLISH' ? 'success' :
aiAnalysis.analysis.assessment === 'BEARISH' ? 'destructive' : 'secondary'
}
className="text-xs"
>
{aiAnalysis.analysis.assessment}
</Badge>
<Badge
variant={
aiAnalysis.analysis.recommendation === 'ENTER' ? 'success' :
aiAnalysis.analysis.recommendation === 'WAIT' ? 'warning' : 'destructive'
}
className="text-xs"
>
{aiAnalysis.analysis.recommendation}
</Badge>
<Badge variant="outline" className="text-xs">
{aiAnalysis.analysis.riskLevel} Risk
</Badge>
</div>
<p className="text-sm text-slate-200">{aiAnalysis.analysis.summary}</p>
<div className="space-y-4">
<div className="text-sm text-slate-200 bg-slate-700/30 p-2 rounded border border-slate-600/50">
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{/* Long Term */}
{aiAnalysis.analysis.longTerm && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Long-Term Investor</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.longTerm.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.longTerm.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.longTerm.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.longTerm.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.longTerm.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
</div>
)}
{/* Swing Trade */}
{aiAnalysis.analysis.swing && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Swing Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.swing.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.swing.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.swing.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.swing.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.swing.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.swing.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.swing.technicalSetup}</p>
</div>
)}
{/* Day Trade */}
{aiAnalysis.analysis.dayTrade && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Day Trader</div>
<div className="flex gap-2 mb-2">
<Badge variant={aiAnalysis.analysis.dayTrade.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.dayTrade.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.assessment}
</Badge>
<Badge variant={aiAnalysis.analysis.dayTrade.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.dayTrade.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
{aiAnalysis.analysis.dayTrade.recommendation}
</Badge>
</div>
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}</p>
</div>
)}
</div>
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
<div>
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
<ul className="text-xs text-slate-300 space-y-1">
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
<li key={idx}> {factor}</li>
))}
</ul>
</div>
)}
{aiAnalysis.analysis.reasoning && (
<div>
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
</div>
)}
<div className="flex items-center gap-4 text-xs text-slate-400 pt-2 border-t border-slate-700">
<span>Time Horizon: {aiAnalysis.analysis.timeHorizon}</span>
<div className="flex items-center gap-4 text-xs text-slate-500 pt-2 border-t border-slate-700">
{aiAnalysis.timestamp && (
<span> {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
<span>Generated: {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
)}
</div>
</div>