import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useActivation } from '../hooks/useActivation'; import { useTrades } from '../hooks/useTrades'; import { useWatchlist } from '../hooks/useWatchlist'; import { usePerformance } from '../hooks/usePerformance'; import { Callout } from '../components/ui/Callout'; import { Section } from '../components/ui/Section'; import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton'; import { formatPrice } from '../lib/format'; import { recommendationActionLabel } from '../lib/recommendation'; import { qualifiesSetup, activationSummary, primaryTargetProbability, expectedValueR } from '../lib/qualification'; import type { TradeSetup } from '../lib/types'; function fmtR(value: number | null): string { if (value === null) return '—'; return `${value > 0 ? '+' : ''}${value.toFixed(2)}R`; } function rColor(value: number | null): string { if (value === null) return 'text-gray-400'; if (value > 0) return 'text-emerald-400'; if (value < 0) return 'text-red-400'; return 'text-gray-300'; } function Metric({ label, value, sub, valueClass = 'text-gray-100' }: { label: string; value: string; sub?: string; valueClass?: string; }) { return (

{label}

{value}

{sub &&

{sub}

}
); } function DirectionTag({ direction }: { direction: string }) { const isLong = direction === 'long'; return ( {direction} ); } export default function DashboardPage() { const trades = useTrades(); const watchlist = useWatchlist(); const activation = useActivation(); const performance = usePerformance(); const qualifiedSetups = useMemo( () => activation.data ? (trades.data ?? []).filter((t) => qualifiesSetup(t, activation.data!)) : [], [trades.data, activation.data], ); // Show qualified setups first; fall back to the full list when none qualify. // Rank by expected value (R) so the best opportunity sits at the top. const showingQualified = qualifiedSetups.length > 0; const topSetups: TradeSetup[] = useMemo(() => { const pool = showingQualified ? qualifiedSetups : trades.data ?? []; return [...pool] .sort((a, b) => (expectedValueR(b) ?? -Infinity) - (expectedValueR(a) ?? -Infinity)) .slice(0, 5); }, [showingQualified, qualifiedSetups, trades.data]); const topWatchlist = useMemo( () => [...(watchlist.data ?? [])] .sort((a, b) => (b.composite_score ?? -1) - (a.composite_score ?? -1)) .slice(0, 6), [watchlist.data], ); const today = new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', }); const stats = performance.data?.overall; return (
{/* Hero */}

{today}

Market overview

{/* Metric strip */} {(trades.isLoading || performance.isLoading) ? (
) : (
0 ? 'text-blue-300' : 'text-gray-100'} />
)}
{/* Top setups */}
{trades.isLoading && } {trades.isError && Failed to load setups} {trades.data && topSetups.length === 0 && ( No active setups. Run the scanner from the Signals page. )} {topSetups.length > 0 && (
{topSetups.map((setup, i) => { const ev = expectedValueR(setup); const isTopPick = i === 0; return ( ); })}
Ticker Dir Entry R:R Target Prob Exp. Value Action
{setup.symbol} {isTopPick && ( Top pick )}
{formatPrice(setup.entry_price)} {setup.rr_ratio.toFixed(1)}:1 {(() => { const p = primaryTargetProbability(setup); return p != null ? `${Math.round(p)}%` : '—'; })()} {fmtR(ev)} {recommendationActionLabel(setup.recommended_action)}
Exp. Value = probability-weighted payoff per unit of risk All setups →
)}
{/* Watchlist pulse */}
{watchlist.isLoading && } {watchlist.isError && Failed to load watchlist} {watchlist.data && topWatchlist.length === 0 && ( Watchlist is empty — add tickers on the Market page. )} {topWatchlist.length > 0 && (
    {topWatchlist.map((entry) => (
  • {entry.symbol} {entry.rr_ratio != null && ( {entry.rr_ratio.toFixed(1)}:1 )} {entry.composite_score != null ? entry.composite_score.toFixed(0) : '—'}
  • ))}
Full watchlist →
)}
); }