From d15acb8741eabdc64c5a0c825ad2c92f51160ba8 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 27 Jun 2026 16:04:55 +0200 Subject: [PATCH] feat: top-pick and open-trade status labels on the ticker page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two read-only pills in the ticker header, beside the watchlist toggle: - "Top Pick" when the ticker is the current #1 — the same ranking the dashboard highlights, via a shared topPickSymbol() helper so the two stay in sync. - "Open Trade" when an open paper trade exists on the ticker. Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/qualification.ts | 20 +++++++++++ frontend/src/pages/TickerDetailPage.tsx | 48 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/frontend/src/lib/qualification.ts b/frontend/src/lib/qualification.ts index e6334f9..7d74da6 100644 --- a/frontend/src/lib/qualification.ts +++ b/frontend/src/lib/qualification.ts @@ -49,6 +49,26 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo return true; } +/** + * Symbol of the current single 'top pick' — the #1 row the dashboard highlights: + * the highest 12-1 momentum percentile among qualified setups (or among all + * setups when none qualify). Returns null when there are no setups. Keep in step + * with the Top Setups ranking in DashboardPage. + */ +export function topPickSymbol( + trades: TradeSetup[] | undefined, + activation: ActivationConfig | undefined, +): string | null { + const all = trades ?? []; + if (all.length === 0) return null; + const qualified = activation ? all.filter((t) => qualifiesSetup(t, activation)) : []; + const pool = qualified.length > 0 ? qualified : all; + const top = [...pool].sort( + (a, b) => (b.momentum_percentile ?? -Infinity) - (a.momentum_percentile ?? -Infinity), + )[0]; + return top?.symbol ?? null; +} + /** Short human summary of the active gate, e.g. for tooltips/labels. */ export function activationSummary(config: ActivationConfig): string { const parts = []; diff --git a/frontend/src/pages/TickerDetailPage.tsx b/frontend/src/pages/TickerDetailPage.tsx index 51c55f7..4461fc5 100644 --- a/frontend/src/pages/TickerDetailPage.tsx +++ b/frontend/src/pages/TickerDetailPage.tsx @@ -3,6 +3,10 @@ import { useParams } from 'react-router-dom'; import { useTickerDetail } from '../hooks/useTickerDetail'; import { useFetchSymbolData } from '../hooks/useFetchSymbolData'; import { useWatchlist, useAddToWatchlist, useRemoveFromWatchlist } from '../hooks/useWatchlist'; +import { useTrades } from '../hooks/useTrades'; +import { usePaperTrades } from '../hooks/usePaperTrades'; +import { useActivation } from '../hooks/useActivation'; +import { topPickSymbol } from '../lib/qualification'; import type { FetchSelector } from '../api/ingestion'; import { CandlestickChart } from '../components/charts/CandlestickChart'; import { ScoreCard } from '../components/ui/ScoreCard'; @@ -29,6 +33,21 @@ function SectionError({ message, onRetry }: { message: string; onRetry?: () => v ); } +function StatusPill({ tone, label, title }: { tone: 'blue' | 'emerald'; label: string; title?: string }) { + const tones = { + blue: 'bg-blue-500/15 text-blue-300 border-blue-500/30', + emerald: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30', + } as const; + return ( + + {label} + + ); +} + function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); @@ -107,6 +126,21 @@ export default function TickerDetailPage() { [watchlist.data, symbol], ); const watchlistBusy = addToWatchlist.isPending || removeFromWatchlist.isPending; + + // Status labels: is there an open paper trade on this ticker, and is it the + // current top pick (same ranking the dashboard highlights)? + const openTrades = usePaperTrades('open'); + const allTrades = useTrades(); + const activation = useActivation(); + const hasOpenTrade = useMemo( + () => (openTrades.data ?? []).some((t) => t.symbol.toUpperCase() === symbol.toUpperCase()), + [openTrades.data, symbol], + ); + const isTopPick = useMemo( + () => topPickSymbol(allTrades.data, activation.data)?.toUpperCase() === symbol.toUpperCase(), + [allTrades.data, activation.data, symbol], + ); + const [activeTab, setActiveTab] = useState('Analysis'); const [refreshingLabel, setRefreshingLabel] = useState(null); @@ -226,6 +260,20 @@ export default function TickerDetailPage() { )}
+ {isTopPick && ( + + )} + {hasOpenTrade && ( + + )}