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;
|
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. */
|
/** Short human summary of the active gate, e.g. for tooltips/labels. */
|
||||||
export function activationSummary(config: ActivationConfig): string {
|
export function activationSummary(config: ActivationConfig): string {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { useTickerDetail } from '../hooks/useTickerDetail';
|
import { useTickerDetail } from '../hooks/useTickerDetail';
|
||||||
import { useFetchSymbolData } from '../hooks/useFetchSymbolData';
|
import { useFetchSymbolData } from '../hooks/useFetchSymbolData';
|
||||||
import { useWatchlist, useAddToWatchlist, useRemoveFromWatchlist } from '../hooks/useWatchlist';
|
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 type { FetchSelector } from '../api/ingestion';
|
||||||
import { CandlestickChart } from '../components/charts/CandlestickChart';
|
import { CandlestickChart } from '../components/charts/CandlestickChart';
|
||||||
import { ScoreCard } from '../components/ui/ScoreCard';
|
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 {
|
function timeAgo(iso: string): string {
|
||||||
const diff = Date.now() - new Date(iso).getTime();
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
const mins = Math.floor(diff / 60_000);
|
const mins = Math.floor(diff / 60_000);
|
||||||
@@ -107,6 +126,21 @@ export default function TickerDetailPage() {
|
|||||||
[watchlist.data, symbol],
|
[watchlist.data, symbol],
|
||||||
);
|
);
|
||||||
const watchlistBusy = addToWatchlist.isPending || removeFromWatchlist.isPending;
|
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 [activeTab, setActiveTab] = useState<DetailTab>('Analysis');
|
||||||
const [refreshingLabel, setRefreshingLabel] = useState<string | null>(null);
|
const [refreshingLabel, setRefreshingLabel] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -226,6 +260,20 @@ export default function TickerDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user