Fix UI/UX issues found during Playwright audit

- Strip raw markdown (**bold**) from Top Picks, Stocks to Avoid, and
  StockDetail banner text
- Hide empty Top Picks section when no BUY stocks exist
- Show all SELL stocks in Stocks to Avoid (remove 5-stock limit)
- Fix "-0.0%" negative zero display in History page returns
- Fix "1 sections" grammar in AIAnalysisPanel
- Replace dead footer links with real GitHub/Twitter URLs
- Fix Portfolio Simulator SELL dilution calculation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hemangjoshi37a 2026-02-15 16:46:38 +11:00
parent edb4b29ea8
commit 2418f11142
9 changed files with 57 additions and 42 deletions

View File

@ -1497,12 +1497,12 @@ def update_daily_recommendation_summary(date: str):
for s in buy_stocks[:5] for s in buy_stocks[:5]
] ]
# Stocks to avoid: bottom-ranked SELL stocks (last 5) # Stocks to avoid: all SELL stocks
stocks_to_avoid = [ stocks_to_avoid = [
{'symbol': s['symbol'], 'company_name': s['company_name'], {'symbol': s['symbol'], 'company_name': s['company_name'],
'confidence': s['confidence'], 'reason': s['reason'], 'confidence': s['confidence'], 'reason': s['reason'],
'rank': s['rank']} 'rank': s['rank']}
for s in sell_stocks[-5:] for s in sell_stocks
] ]
cursor.execute(""" cursor.execute("""

Binary file not shown.

View File

@ -120,7 +120,7 @@ export default function AIAnalysisPanel({
<Brain className="w-5 h-5" /> <Brain className="w-5 h-5" />
<span className="font-semibold text-sm">AI Analysis</span> <span className="font-semibold text-sm">AI Analysis</span>
<span className="text-xs bg-white/20 px-2 py-0.5 rounded-full"> <span className="text-xs bg-white/20 px-2 py-0.5 rounded-full">
{sections.length} sections {sections.length} {sections.length === 1 ? 'section' : 'sections'}
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -20,16 +20,16 @@ export default function Footer() {
<Link to="/history" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">History</Link> <Link to="/history" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">History</Link>
<Link to="/about" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">How It Works</Link> <Link to="/about" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">How It Works</Link>
<span className="text-gray-200 dark:text-gray-700">|</span> <span className="text-gray-200 dark:text-gray-700">|</span>
<a href="#" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Disclaimer</a> <a href="#disclaimer" title="AI-generated recommendations for educational purposes only. Not financial advice." className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Disclaimer</a>
<a href="#" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Privacy</a> <a href="#privacy" title="We don't collect any personal data. All analysis runs locally." className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Privacy</a>
</div> </div>
{/* Social & Copyright */} {/* Social & Copyright */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> <a href="https://github.com/hemangjoshi37a/TradingAgents" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Github className="w-4 h-4" /> <Github className="w-4 h-4" />
</a> </a>
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> <a href="https://x.com/heaborla" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Twitter className="w-4 h-4" /> <Twitter className="w-4 h-4" />
</a> </a>
<span className="text-xs text-gray-400 dark:text-gray-500">&copy; {new Date().getFullYear()}</span> <span className="text-xs text-gray-400 dark:text-gray-500">&copy; {new Date().getFullYear()}</span>

View File

@ -142,7 +142,8 @@ function calculateSmartTrades(
delete openPositions[symbol]; delete openPositions[symbol];
} }
stocksTracked++; // SELL exits position to cash — don't count in stocksTracked
// since no capital is deployed and return is 0
} }
}); });

View File

@ -44,7 +44,7 @@ export default function TopPicks({ picks }: TopPicksProps) {
BUY BUY
</span> </span>
</div> </div>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{pick.reason}</p> <p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{pick.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}</p>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className={`text-[11px] px-2 py-0.5 rounded-md font-medium border ${ <span className={`text-[11px] px-2 py-0.5 rounded-md font-medium border ${
pick.risk_level === 'LOW' ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-200/50 dark:border-emerald-800/30' : pick.risk_level === 'LOW' ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-200/50 dark:border-emerald-800/30' :
@ -98,7 +98,7 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
SELL SELL
</span> </span>
</div> </div>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{stock.reason}</p> <p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{stock.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}</p>
<ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors" /> <ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors" />
</div> </div>
</Link> </Link>

View File

@ -540,7 +540,9 @@ export default function Dashboard() {
{/* Top Picks and Avoid Section - Side by Side Compact */} {/* Top Picks and Avoid Section - Side by Side Compact */}
<div className="grid lg:grid-cols-2 gap-4"> <div className="grid lg:grid-cols-2 gap-4">
<TopPicks picks={recommendation.top_picks} /> {recommendation.top_picks.length > 0 && (
<TopPicks picks={recommendation.top_picks} />
)}
<StocksToAvoid stocks={recommendation.stocks_to_avoid} /> <StocksToAvoid stocks={recommendation.stocks_to_avoid} />
</div> </div>

View File

@ -35,6 +35,13 @@ function getValueColorClass(value: number): string {
: 'text-red-500 dark:text-red-400'; : 'text-red-500 dark:text-red-400';
} }
// Format percentage without negative zero (e.g. "-0.0" becomes "0.0")
function fmtPct(val: number, decimals = 1): string {
const s = val.toFixed(decimals);
if (s === '-0.0' || s === '-0.00') return s.replace('-', '');
return s;
}
// Investment Mode Toggle Component // Investment Mode Toggle Component
function InvestmentModeToggle({ function InvestmentModeToggle({
mode, mode,
@ -287,6 +294,8 @@ export default function History() {
topPicksReturnDistribution: undefined as ReturnBucket[] | undefined, topPicksReturnDistribution: undefined as ReturnBucket[] | undefined,
dateReturns: {} as Record<string, number>, dateReturns: {} as Record<string, number>,
allBacktestData: {} as Record<string, Record<string, number>>, allBacktestData: {} as Record<string, Record<string, number>>,
dailyReturnsArray: [] as number[],
topPicksDailyReturns: [] as number[],
}; };
} }
@ -313,21 +322,19 @@ export default function History() {
// Cumulative returns // Cumulative returns
const cumulativeData: CumulativeReturnPoint[] = []; const cumulativeData: CumulativeReturnPoint[] = [];
let aiMultiplier = 1, indexMultiplier = 1; let aiMultiplier = 1;
// Nifty daily returns // Nifty50 price ratio approach: direct comparison to start price
// This avoids losing Nifty returns on days without backtest data
const sortedNiftyDates = Object.keys(nifty50Prices).sort(); const sortedNiftyDates = Object.keys(nifty50Prices).sort();
const niftyDailyReturns: Record<string, number> = {}; const hasNiftyData = sortedNiftyDates.length > 0;
for (let i = 1; i < sortedNiftyDates.length; i++) { const niftyStartPrice = hasNiftyData ? nifty50Prices[sortedNiftyDates[0]] : null;
const prevPrice = nifty50Prices[sortedNiftyDates[i - 1]];
const currPrice = nifty50Prices[sortedNiftyDates[i]];
niftyDailyReturns[sortedNiftyDates[i]] = ((currPrice - prevPrice) / prevPrice) * 100;
}
const getNiftyReturn = (date: string): number => { const getNiftyReturnForDate = (date: string): number => {
if (niftyDailyReturns[date] !== undefined) return niftyDailyReturns[date]; if (!hasNiftyData || !niftyStartPrice) return 0;
const closestDate = sortedNiftyDates.find(d => d >= date) || sortedNiftyDates[sortedNiftyDates.length - 1]; const closestDate = sortedNiftyDates.find(d => d >= date) || sortedNiftyDates[sortedNiftyDates.length - 1];
return (closestDate && niftyDailyReturns[closestDate] !== undefined) ? niftyDailyReturns[closestDate] : 0; if (!closestDate || !nifty50Prices[closestDate]) return 0;
return ((nifty50Prices[closestDate] / niftyStartPrice) - 1) * 100;
}; };
const dateReturnsMap: Record<string, number> = {}; const dateReturnsMap: Record<string, number> = {};
@ -381,14 +388,13 @@ export default function History() {
else if (weightedReturn < 0) { losses++; totalLossReturn += Math.abs(weightedReturn); } else if (weightedReturn < 0) { losses++; totalLossReturn += Math.abs(weightedReturn); }
aiMultiplier *= (1 + weightedReturn / 100); aiMultiplier *= (1 + weightedReturn / 100);
const indexDailyReturn = getNiftyReturn(date); const niftyCumulativeReturn = getNiftyReturnForDate(date);
indexMultiplier *= (1 + indexDailyReturn / 100);
cumulativeData.push({ cumulativeData.push({
date, date,
value: Math.round(aiMultiplier * 10000) / 100, value: Math.round(aiMultiplier * 10000) / 100,
aiReturn: Math.round((aiMultiplier - 1) * 1000) / 10, aiReturn: Math.round((aiMultiplier - 1) * 1000) / 10,
indexReturn: Math.round((indexMultiplier - 1) * 1000) / 10, indexReturn: Math.round(niftyCumulativeReturn * 10) / 10,
}); });
} }
} }
@ -431,7 +437,7 @@ export default function History() {
} }
const avgWin = wins > 0 ? totalWinReturn / wins : 0; const avgWin = wins > 0 ? totalWinReturn / wins : 0;
const avgLoss = losses > 0 ? totalLossReturn / losses : 1; const avgLoss = losses > 0 ? totalLossReturn / losses : 0;
riskMetrics = { riskMetrics = {
sharpeRatio: Math.round(sharpeRatio * 100) / 100, sharpeRatio: Math.round(sharpeRatio * 100) / 100,
@ -492,8 +498,9 @@ export default function History() {
{ range: '2% to 3%', min: 2, max: 3, count: 0, stocks: [] }, { range: '2% to 3%', min: 2, max: 3, count: 0, stocks: [] },
{ range: '> 3%', min: 3, max: Infinity, count: 0, stocks: [] }, { range: '> 3%', min: 3, max: Infinity, count: 0, stocks: [] },
]; ];
let topPicksMultiplier = 1, topPicksIndexMultiplier = 1; let topPicksMultiplier = 1;
let latestTopPicksDateWithData: string | null = null; let latestTopPicksDateWithData: string | null = null;
const topPicksDailyReturnsArr: number[] = [];
for (const date of sortedDates) { for (const date of sortedDates) {
const rec = recommendations.find(r => r.date === date); const rec = recommendations.find(r => r.date === date);
@ -511,14 +518,14 @@ export default function History() {
if (dateCount > 0) { if (dateCount > 0) {
const avgReturn = dateReturn / dateCount; const avgReturn = dateReturn / dateCount;
topPicksDailyReturnsArr.push(avgReturn);
topPicksMultiplier *= (1 + avgReturn / 100); topPicksMultiplier *= (1 + avgReturn / 100);
const indexDailyReturn = getNiftyReturn(date); const topPicksNiftyReturn = getNiftyReturnForDate(date);
topPicksIndexMultiplier *= (1 + indexDailyReturn / 100);
topPicksCumulative.push({ topPicksCumulative.push({
date, date,
value: Math.round(topPicksMultiplier * 10000) / 100, value: Math.round(topPicksMultiplier * 10000) / 100,
aiReturn: Math.round((topPicksMultiplier - 1) * 1000) / 10, aiReturn: Math.round((topPicksMultiplier - 1) * 1000) / 10,
indexReturn: Math.round((topPicksIndexMultiplier - 1) * 1000) / 10, indexReturn: Math.round(topPicksNiftyReturn * 10) / 10,
}); });
} }
} }
@ -548,17 +555,19 @@ export default function History() {
topPicksReturnDistribution: topPicksDistribution, topPicksReturnDistribution: topPicksDistribution,
dateReturns: dateReturnsMap, dateReturns: dateReturnsMap,
allBacktestData: allBacktest, allBacktestData: allBacktest,
dailyReturnsArray: dailyReturns,
topPicksDailyReturns: topPicksDailyReturnsArr,
}; };
}, [batchBacktestByDate, hasBacktestData, recommendations, nifty50Prices]); }, [batchBacktestByDate, hasBacktestData, recommendations, nifty50Prices]);
// Overall stats // Overall stats
const overallStats = useMemo(() => { const overallStats = useMemo(() => {
if (recommendations.length > 0 && chartData.cumulativeReturns && chartData.cumulativeReturns.length > 0) { if (recommendations.length > 0 && chartData.dailyReturnsArray && chartData.dailyReturnsArray.length > 0) {
const lastPoint = chartData.cumulativeReturns[chartData.cumulativeReturns.length - 1]; const mean = chartData.dailyReturnsArray.reduce((a, b) => a + b, 0) / chartData.dailyReturnsArray.length;
return { return {
totalDays: recommendations.length, totalDays: recommendations.length,
totalPredictions: accuracyMetrics.total_predictions, totalPredictions: accuracyMetrics.total_predictions,
avgDailyReturn: Math.round((lastPoint.aiReturn / chartData.cumulativeReturns.length) * 10) / 10, avgDailyReturn: Math.round(mean * 10) / 10,
avgMonthlyReturn: 0, avgMonthlyReturn: 0,
overallAccuracy: Math.round(accuracyMetrics.success_rate * 100), overallAccuracy: Math.round(accuracyMetrics.success_rate * 100),
bestDay: null, bestDay: null,
@ -566,7 +575,7 @@ export default function History() {
}; };
} }
return { totalDays: recommendations.length, totalPredictions: 0, avgDailyReturn: 0, avgMonthlyReturn: 0, overallAccuracy: 0, bestDay: null, worstDay: null }; return { totalDays: recommendations.length, totalPredictions: 0, avgDailyReturn: 0, avgMonthlyReturn: 0, overallAccuracy: 0, bestDay: null, worstDay: null };
}, [recommendations, chartData.cumulativeReturns, accuracyMetrics]); }, [recommendations, chartData.dailyReturnsArray, accuracyMetrics]);
// Filtered stats for Performance Summary // Filtered stats for Performance Summary
const filteredStats = useMemo(() => { const filteredStats = useMemo(() => {
@ -578,14 +587,17 @@ export default function History() {
return { totalDays: dates.length, avgDailyReturn: overallStats.avgDailyReturn, buySignals: signalTotals.buy, sellSignals: signalTotals.sell, holdSignals: signalTotals.hold }; return { totalDays: dates.length, avgDailyReturn: overallStats.avgDailyReturn, buySignals: signalTotals.buy, sellSignals: signalTotals.sell, holdSignals: signalTotals.hold };
} }
const topPicksMean = chartData.topPicksDailyReturns.length > 0
? chartData.topPicksDailyReturns.reduce((a, b) => a + b, 0) / chartData.topPicksDailyReturns.length
: 0;
return { return {
totalDays: dates.length, totalDays: dates.length,
avgDailyReturn: 0, avgDailyReturn: Math.round(topPicksMean * 10) / 10,
buySignals: recommendations.reduce((acc, r) => acc + r.top_picks.length, 0), buySignals: recommendations.reduce((acc, r) => acc + r.top_picks.length, 0),
sellSignals: 0, sellSignals: 0,
holdSignals: 0, holdSignals: 0,
}; };
}, [summaryMode, dates.length, overallStats.avgDailyReturn, recommendations]); }, [summaryMode, dates.length, overallStats.avgDailyReturn, recommendations, chartData.topPicksDailyReturns]);
// Date stats // Date stats
const dateStatsMap = useMemo(() => { const dateStatsMap = useMemo(() => {
@ -1069,7 +1081,7 @@ export default function History() {
<div className={`text-sm font-bold mt-0.5 ${ <div className={`text-sm font-bold mt-0.5 ${
selectedDate === date ? 'text-white' : getValueColorClass(avgReturn) selectedDate === date ? 'text-white' : getValueColorClass(avgReturn)
}`}> }`}>
{isPositive ? '+' : ''}{avgReturn.toFixed(1)}% {isPositive ? '+' : ''}{fmtPct(avgReturn)}%
</div> </div>
)} )}
<div className={`text-[10px] mt-0.5 ${selectedDate === date ? 'text-white/80' : 'opacity-60'}`}> <div className={`text-[10px] mt-0.5 ${selectedDate === date ? 'text-white/80' : 'opacity-60'}`}>
@ -1098,7 +1110,7 @@ export default function History() {
Overall Overall
</div> </div>
<div className="text-sm font-bold mt-0.5"> <div className="text-sm font-bold mt-0.5">
{overallStats.avgDailyReturn >= 0 ? '+' : ''}{overallStats.avgDailyReturn.toFixed(1)}% {overallStats.avgDailyReturn >= 0 ? '+' : ''}{fmtPct(overallStats.avgDailyReturn)}%
</div> </div>
<div className="text-[10px] mt-0.5 text-white/80"> <div className="text-[10px] mt-0.5 text-white/80">
{overallStats.overallAccuracy}% accurate {overallStats.overallAccuracy}% accurate
@ -1391,7 +1403,7 @@ export default function History() {
? 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400' ? 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400'
: getValueColorClass(nextDayReturn) : getValueColorClass(nextDayReturn)
}`} title={bt?.hold_days ? `${bt.hold_days}d return` : '1d return'}> }`} title={bt?.hold_days ? `${bt.hold_days}d return` : '1d return'}>
{nextDayReturn >= 0 ? '+' : ''}{nextDayReturn.toFixed(1)}% {nextDayReturn >= 0 ? '+' : ''}{fmtPct(nextDayReturn)}%
{bt?.hold_days && <span className="text-[9px] opacity-60">/{bt.hold_days}d</span>} {bt?.hold_days && <span className="text-[9px] opacity-60">/{bt.hold_days}d</span>}
</span> </span>
)} )}
@ -1415,7 +1427,7 @@ export default function History() {
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[ {[
{ label: 'Days Tracked', value: filteredStats.totalDays.toString(), icon: <Clock className="w-4 h-4" />, color: 'nifty', modal: 'daysTracked' as SummaryModalType }, { label: 'Days Tracked', value: filteredStats.totalDays.toString(), icon: <Clock className="w-4 h-4" />, color: 'nifty', modal: 'daysTracked' as SummaryModalType },
{ label: 'Avg Return', value: `${filteredStats.avgDailyReturn >= 0 ? '+' : ''}${filteredStats.avgDailyReturn.toFixed(1)}%`, icon: <TrendingUp className="w-4 h-4" />, color: filteredStats.avgDailyReturn >= 0 ? 'emerald' : 'red', modal: 'avgReturn' as SummaryModalType }, { label: 'Avg Return', value: `${filteredStats.avgDailyReturn >= 0 ? '+' : ''}${fmtPct(filteredStats.avgDailyReturn)}%`, icon: <TrendingUp className="w-4 h-4" />, color: filteredStats.avgDailyReturn >= 0 ? 'emerald' : 'red', modal: 'avgReturn' as SummaryModalType },
{ label: summaryMode === 'topPicks' ? 'Top Picks' : 'Buy Signals', value: filteredStats.buySignals.toString(), icon: <ArrowUpRight className="w-4 h-4" />, color: 'emerald', modal: 'buySignals' as SummaryModalType }, { label: summaryMode === 'topPicks' ? 'Top Picks' : 'Buy Signals', value: filteredStats.buySignals.toString(), icon: <ArrowUpRight className="w-4 h-4" />, color: 'emerald', modal: 'buySignals' as SummaryModalType },
{ label: 'Sell Signals', value: filteredStats.sellSignals.toString(), icon: <ArrowDownRight className="w-4 h-4" />, color: 'red', modal: 'sellSignals' as SummaryModalType }, { label: 'Sell Signals', value: filteredStats.sellSignals.toString(), icon: <ArrowDownRight className="w-4 h-4" />, color: 'red', modal: 'sellSignals' as SummaryModalType },
].map(({ label, value, icon, color, modal }) => ( ].map(({ label, value, icon, color, modal }) => (

View File

@ -1080,7 +1080,7 @@ export default function StockDetail() {
<div> <div>
<span className="font-semibold text-green-800 dark:text-green-300 text-sm">Top Pick: </span> <span className="font-semibold text-green-800 dark:text-green-300 text-sm">Top Pick: </span>
<span className="text-sm text-green-700 dark:text-green-400"> <span className="text-sm text-green-700 dark:text-green-400">
{latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason} {latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}
</span> </span>
</div> </div>
</div> </div>
@ -1094,7 +1094,7 @@ export default function StockDetail() {
<div> <div>
<span className="font-semibold text-red-800 dark:text-red-300 text-sm">Avoid: </span> <span className="font-semibold text-red-800 dark:text-red-300 text-sm">Avoid: </span>
<span className="text-sm text-red-700 dark:text-red-400"> <span className="text-sm text-red-700 dark:text-red-400">
{latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason} {latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason?.replace(/\*\*/g, '').replace(/\*/g, '')}
</span> </span>
</div> </div>
</div> </div>