feat: top-pick and open-trade status labels on the ticker page
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -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 (
|
||||
<span
|
||||
title={title}
|
||||
className={`inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium ${tones[tone]}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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<DetailTab>('Analysis');
|
||||
const [refreshingLabel, setRefreshingLabel] = useState<string | null>(null);
|
||||
|
||||
@@ -226,6 +260,20 @@ export default function TickerDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isTopPick && (
|
||||
<StatusPill
|
||||
tone="blue"
|
||||
label="★ Top Pick"
|
||||
title="Current top pick — highest-momentum qualified setup right now"
|
||||
/>
|
||||
)}
|
||||
{hasOpenTrade && (
|
||||
<StatusPill
|
||||
tone="emerald"
|
||||
label="● Open Trade"
|
||||
title="You have an open paper trade on this ticker"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
|
||||
Reference in New Issue
Block a user