import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type React from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { motion } from "framer-motion"; import { Wallet, BarChart3, AlertCircle, RefreshCcw, PlugZap } from "lucide-react"; import BrokerConnectDialog from "./BrokerConnectDialog"; import LoginRequiredDialog from "./LoginRequiredDialog"; import { getQueryFn, apiRequest } from "@/lib/queryClient"; import { getStrategyStatus, startStrategy, stopStrategy } from "@/api/strategy"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import StrategyTimeline from "@/components/StrategyTimeline"; import { toast } from "@/hooks/use-toast"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis, } from "recharts"; import type { User } from "@shared/schema"; type BrokerStatusResponse = { connected: boolean; broker?: string; connected_at?: string; userName?: string; brokerUserId?: string; }; type SystemArmResponse = { armed_runs: Array<{ run_id: string; status: string; next_run?: string; already_running?: boolean }>; failed_runs: Array<{ run_id: string; status: string; reason: string }>; next_execution?: string | null; broker_state?: { connected?: boolean; auth_state?: string | null; broker?: string | null; user_name?: string | null; }; }; type SystemStatusRun = { run_id: string; status: string; strategy?: string | null; mode?: string | null; broker?: string | null; next_run?: string | null; lifecycle?: string | null; }; type SystemStatusResponse = { runs: SystemStatusRun[]; broker_state?: { connected?: boolean; auth_state?: string | null; broker?: string | null; user_name?: string | null; }; }; type HoldingsResponse = { holdings: any[]; }; type MarketStatusResponse = { status?: "OPEN" | "CLOSED"; checked_at?: string; }; type FundsResponse = { funds?: { net?: number; cash?: number; withdrawable?: number; utilized?: number; balance?: number; }; }; type EquityCurveResponse = { startDate: string; endDate: string; accountOpenDate?: string; points: { date: string; value: number }[]; }; type EngineStatus = { state?: string; run_id?: string | null; last_heartbeat_ts?: string | null; last_execution_ts?: string | null; next_eligible_ts?: string | null; }; type SessionUser = Pick; const MotionButton = motion(Button); function formatCurrency(amount: number, options?: { decimals?: number }) { const decimals = options?.decimals ?? 2; return new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(amount || 0); } function formatDateInput(date: Date) { return date.toISOString().slice(0, 10); } function formatMinuteTimestamp(date: Date) { return date.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); } function formatRelativeSeconds(seconds: number) { if (!Number.isFinite(seconds)) return ""; if (seconds <= 0) return "now"; const total = Math.round(seconds); const days = Math.floor(total / 86400); const hours = Math.floor((total % 86400) / 3600); const minutes = Math.floor((total % 3600) / 60); const parts = []; if (days) parts.push(`${days}d`); if (hours) parts.push(`${hours}h`); if (!days && minutes) parts.push(`${minutes}m`); if (parts.length === 0) parts.push("less than 1m"); return `in ${parts.join(" ")}`; } function usePrefersReducedMotion() { const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const update = () => setPrefersReducedMotion(mediaQuery.matches); update(); if (mediaQuery.addEventListener) { mediaQuery.addEventListener("change", update); return () => mediaQuery.removeEventListener("change", update); } mediaQuery.addListener(update); return () => mediaQuery.removeListener(update); }, []); return prefersReducedMotion; } export default function PortfolioSection() { const sectionRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const [scrollY, setScrollY] = useState(0); const prefersReducedMotion = usePrefersReducedMotion(); const [loginPromptOpen, setLoginPromptOpen] = useState(false); const [connectPromptOpen, setConnectPromptOpen] = useState(false); const [brokerDialogOpen, setBrokerDialogOpen] = useState(false); const [sessionExpired, setSessionExpired] = useState(false); const [reconnectAttempted, setReconnectAttempted] = useState(false); const [cachedHoldings, setCachedHoldings] = useState([]); const [cachedFunds, setCachedFunds] = useState(null); const [cachedEquityCurve, setCachedEquityCurve] = useState(null); const [armSummary, setArmSummary] = useState(null); const { data: brokerStatus, isFetching: brokerStatusLoading, refetch: refetchBrokerStatus, } = useQuery({ queryKey: ["/broker/status"], queryFn: getQueryFn({ on401: "returnNull" }), staleTime: 0, refetchOnMount: "always", }); const { data: sessionUser } = useQuery({ queryKey: ["/me"], queryFn: getQueryFn({ on401: "returnNull" }), }); const systemStatusQuery = useQuery({ queryKey: ["/system/status"], queryFn: getQueryFn({ on401: "throw" }), refetchInterval: 15000, }); const armMutation = useMutation({ mutationFn: async () => { const res = await fetch("/system/arm", { method: "POST", credentials: "include", }); if (res.status === 401) { let payload: any = {}; try { payload = await res.json(); } catch {} const redirect = payload?.detail?.redirect_url || payload?.redirect_url || "/broker/login"; window.location.assign(redirect); return null; } if (!res.ok) { const text = await res.text(); throw new Error(text || res.statusText); } return (await res.json()) as SystemArmResponse; }, onSuccess: (data) => { if (!data) return; setArmSummary(data); toast({ title: "System armed", description: data.next_execution ? `Next execution at ${new Date(data.next_execution).toLocaleString()}` : "All strategies are armed.", }); systemStatusQuery.refetch(); refetchBrokerStatus(); }, onError: (err: any) => toast({ title: "Arm failed", description: err?.message || "Unable to arm system.", }), }); const holdingsQuery = useQuery({ queryKey: ["/zerodha/holdings"], queryFn: async () => { const res = await apiRequest("GET", "/zerodha/holdings"); return res.json(); }, enabled: !!brokerStatus?.connected, retry: 1, retryDelay: 600, onSuccess: (data) => { setCachedHoldings(data?.holdings || []); setSessionExpired(false); setReconnectAttempted(false); }, onError: () => { if (brokerStatus?.connected) { setSessionExpired(true); } }, }); const fundsQuery = useQuery({ queryKey: ["/zerodha/funds"], queryFn: async () => { const res = await apiRequest("GET", "/zerodha/funds"); return res.json(); }, enabled: !!brokerStatus?.connected, retry: 1, retryDelay: 600, onSuccess: (data) => { setCachedFunds(data?.funds ?? null); setSessionExpired(false); setReconnectAttempted(false); }, onError: () => { if (brokerStatus?.connected) { setSessionExpired(true); } }, }); const [startDate, setStartDate] = useState(() => formatDateInput(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)), ); const [sipAmount, setSipAmount] = useState(5000); const [frequencyDays, setFrequencyDays] = useState(30); const [strategyStatus, setStrategyStatus] = useState("STOPPED"); const [isStarting, setIsStarting] = useState(false); const [isStopping, setIsStopping] = useState(false); const [engineStatus, setEngineStatus] = useState(null); const [marketStatus, setMarketStatus] = useState(null); const linkedAtDate = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null; const linkedAtInput = linkedAtDate && !isNaN(linkedAtDate.getTime()) ? formatDateInput(linkedAtDate) : undefined; useEffect(() => { if (linkedAtInput) { setStartDate((prev) => (prev === linkedAtInput ? prev : linkedAtInput)); } }, [linkedAtInput]); const refreshStatus = useCallback(async () => { try { const status = await getStrategyStatus(); setStrategyStatus(status?.status ?? "STOPPED"); } catch (error) { setStrategyStatus("STOPPED"); } }, []); useEffect(() => { refreshStatus(); const interval = window.setInterval(refreshStatus, 15000); return () => window.clearInterval(interval); }, [refreshStatus]); useEffect(() => { const fetchStatus = async () => { try { const res = await fetch("/engine/status"); const data = await res.json(); setEngineStatus(data); } catch { setEngineStatus(null); } }; fetchStatus(); const id = window.setInterval(fetchStatus, 5000); return () => window.clearInterval(id); }, []); useEffect(() => { const fetchMarketStatus = async () => { try { const res = await fetch("/market/status"); const data = await res.json(); setMarketStatus(data); } catch { setMarketStatus(null); } }; fetchMarketStatus(); const id = window.setInterval(fetchMarketStatus, 5000); return () => window.clearInterval(id); }, []); useEffect(() => { if (prefersReducedMotion) { setIsVisible(true); return; } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); } }, { threshold: 0.2 }, ); if (sectionRef.current) { observer.observe(sectionRef.current); } return () => observer.disconnect(); }, [prefersReducedMotion]); useEffect(() => { if (prefersReducedMotion) { setScrollY(0); return; } let rafId: number | null = null; const handleScroll = () => { if (rafId !== null) return; rafId = window.requestAnimationFrame(() => { setScrollY(window.scrollY); rafId = null; }); }; window.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { window.removeEventListener("scroll", handleScroll); if (rafId !== null) { window.cancelAnimationFrame(rafId); } }; }, [prefersReducedMotion]); const equityCurveQuery = useQuery({ queryKey: ["/zerodha/equity-curve", startDate], queryFn: async () => { const res = await apiRequest( "GET", `/zerodha/equity-curve${startDate ? `?from=${startDate}` : ""}`, ); return res.json(); }, enabled: !!brokerStatus?.connected, retry: 1, retryDelay: 600, onSuccess: (data) => { setCachedEquityCurve(data ?? null); setSessionExpired(false); setReconnectAttempted(false); }, onError: () => { if (brokerStatus?.connected) { setSessionExpired(true); } }, }); const isConnected = !!brokerStatus?.connected; const isAuthed = brokerStatus !== null; const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings; const fundsSnapshot = fundsQuery.data?.funds ?? cachedFunds; const noHoldings = holdings.length === 0; const systemRuns = systemStatusQuery.data?.runs ?? []; const armedCount = systemRuns.filter( (run) => (run.status || "").toUpperCase() === "RUNNING", ).length; const nextExecution = useMemo(() => { const nextDates = systemRuns .map((run) => (run.next_run ? new Date(run.next_run) : null)) .filter((value): value is Date => !!value && !Number.isNaN(value.getTime())); if (!nextDates.length) return null; nextDates.sort((a, b) => a.getTime() - b.getTime()); return nextDates[0]; }, [systemRuns]); useEffect(() => { if (!isConnected) { setSessionExpired(false); setReconnectAttempted(false); } }, [isConnected]); useEffect(() => { if (!sessionExpired || reconnectAttempted || !isConnected) { return; } setReconnectAttempted(true); (async () => { try { await refetchBrokerStatus(); await Promise.all([ holdingsQuery.refetch(), fundsQuery.refetch(), equityCurveQuery.refetch(), ]); } catch { return; } })(); }, [ sessionExpired, reconnectAttempted, isConnected, refetchBrokerStatus, holdingsQuery, fundsQuery, equityCurveQuery, ]); const availableFunds = fundsSnapshot?.balance ?? fundsSnapshot?.net ?? fundsSnapshot?.withdrawable ?? fundsSnapshot?.cash ?? fundsSnapshot?.raw?.net ?? fundsSnapshot?.raw?.available?.live_balance ?? fundsSnapshot?.raw?.available?.opening_balance ?? 0; const { totalValue, totalPnl } = useMemo(() => { return holdings.reduce( (acc, item) => { const qty = Number(item.quantity ?? item.qty ?? 0); const last = Number(item.last_price ?? item.average_price ?? 0); const pnl = Number(item.pnl ?? 0); return { totalValue: acc.totalValue + qty * last, totalPnl: acc.totalPnl + pnl, }; }, { totalValue: 0, totalPnl: 0 }, ); }, [holdings]); const equityCurve = equityCurveQuery.data ?? cachedEquityCurve; const equityCurvePoints = equityCurve?.points ?? []; const showSessionExpired = sessionExpired && isConnected; const normalizedStrategyStatus = strategyStatus === "RUNNING" ? "RUNNING" : "STOPPED"; const isStrategyRunning = normalizedStrategyStatus === "RUNNING"; const heartbeatAgeSec = engineStatus?.last_heartbeat_ts ? (Date.now() - new Date(engineStatus.last_heartbeat_ts).getTime()) / 1000 : Infinity; let liveness: "ACTIVE" | "STALLED" | "DEAD" | "STOPPED"; if (!engineStatus) { liveness = "DEAD"; } else if (engineStatus.state !== "RUNNING") { liveness = "STOPPED"; } else if (heartbeatAgeSec < 10) { liveness = "ACTIVE"; } else if (heartbeatAgeSec < 30) { liveness = "STALLED"; } else { liveness = "DEAD"; } const livenessBadgeClass = liveness === "ACTIVE" ? "border-emerald-500/50 bg-emerald-500/15 text-emerald-300" : liveness === "STALLED" ? "border-amber-400/50 bg-amber-400/15 text-amber-200" : liveness === "DEAD" ? "border-red-500/50 bg-red-500/15 text-red-300" : "border-slate-400/40 bg-slate-400/15 text-slate-200"; const marketState = marketStatus?.status ?? "UNKNOWN"; const executionAllowed = marketState === "OPEN" && (liveness === "ACTIVE" || liveness === "STOPPED"); const nextEligibleTs = engineStatus?.next_eligible_ts ? new Date(engineStatus.next_eligible_ts) : null; const nextEligibleValid = nextEligibleTs && !Number.isNaN(nextEligibleTs.getTime()); const eligibleSeconds = nextEligibleValid ? (nextEligibleTs.getTime() - Date.now()) / 1000 : Infinity; const isEligible = eligibleSeconds <= 0; const relativeEligible = formatRelativeSeconds(eligibleSeconds); let nextEligibleLine = "—"; let eligibilityStatus: string | null = "First execution pending"; let eligibilityClass = "text-muted-foreground"; if (nextEligibleValid) { eligibilityStatus = null; if (isEligible && marketState === "OPEN") { nextEligibleLine = "Now"; eligibilityStatus = "Eligible — execution imminent"; eligibilityClass = "text-emerald-400"; } else if (isEligible && marketState === "CLOSED") { nextEligibleLine = `${formatMinuteTimestamp(nextEligibleTs)} (eligible)`; eligibilityStatus = "Eligible — waiting for market open"; eligibilityClass = "text-amber-300"; } else { nextEligibleLine = `${formatMinuteTimestamp(nextEligibleTs)} (${relativeEligible})`; eligibilityStatus = "Not eligible yet"; eligibilityClass = "text-muted-foreground"; } } const requireLogin = useCallback(() => { if (!sessionUser) { setLoginPromptOpen(true); return false; } return true; }, [sessionUser]); const handleReconnectClick = useCallback(() => { if (!requireLogin()) { return; } setBrokerDialogOpen(true); }, [requireLogin]); const handleStart = async () => { if (!requireLogin()) { return; } if (!isConnected) { setConnectPromptOpen(true); return; } setIsStarting(true); try { const result = await startStrategy({ strategy_name: "Golden Nifty", initial_cash: availableFunds, sip_amount: sipAmount, sip_frequency: { value: frequencyDays, unit: "days", }, mode: "PAPER", }); if (result?.status === "already_running") { toast({ title: "Strategy already running", description: "The engine is already active.", }); } } finally { setIsStarting(false); await refreshStatus(); } }; const handleStop = async () => { setIsStopping(true); try { await stopStrategy(); } finally { setIsStopping(false); await refreshStatus(); } }; const summaryCards = [ { icon: , label: "Available funds", value: formatCurrency(isConnected ? availableFunds : 0, { decimals: 2 }), muted: !isConnected, subText: isConnected && fundsQuery.data?.funds?.utilized !== undefined ? `Utilized: ${formatCurrency(fundsQuery.data?.funds?.utilized || 0, { decimals: 2 })}` : undefined, }, { icon: , label: "Portfolio value", value: formatCurrency(totalValue, { decimals: 2 }), muted: !isConnected, }, { icon: , label: "Positions", value: holdings.length.toString(), muted: !isConnected, }, { icon: , label: "Unrealized P&L", value: formatCurrency(totalPnl, { decimals: 2 }), muted: !isConnected, }, ]; const revealTransition = prefersReducedMotion ? "" : "transition-all duration-700"; const sectionRevealClass = prefersReducedMotion ? "opacity-100" : isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6"; const cardRevealClass = prefersReducedMotion ? "opacity-100" : isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"; const hoverLift = prefersReducedMotion ? "" : "transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl hover:shadow-primary/10"; const parallaxOffset = prefersReducedMotion ? 0 : Math.min(scrollY * 0.3, 220); const cardContainer = { hidden: {}, show: { transition: { staggerChildren: 0.12, }, }, }; const cardItem = { hidden: { opacity: 0, y: 14 }, show: { opacity: 1, y: 0 }, }; const summaryAnimate = prefersReducedMotion || isVisible ? "show" : "hidden"; const ctaMotionProps = prefersReducedMotion ? {} : { whileHover: { scale: 1.02 }, whileTap: { scale: 0.97 }, transition: { type: "spring", stiffness: 400, damping: 25 }, }; return (

Portfolio

Your holdings & live positions

Connect your broker to sync holdings. When disconnected, values stay at zero and you will see a prompt to connect.

{isConnected && (brokerStatus?.userName || brokerStatus?.broker) ? (
{brokerStatus?.userName ? ( <> Connected as {brokerStatus.userName} ) : ( <> Connected to {brokerStatus?.broker} )}
) : null}
{isConnected ? ( ) : null}
{summaryCards.map((card) => ( ))}

Holdings

Current positions pulled from your connected broker.

{isConnected ? "Broker connected" : "Not connected"}
{!isAuthed ? ( ) : brokerStatusLoading ? ( ) : !isConnected ? ( ) : holdingsQuery.isLoading ? ( ) : holdingsQuery.isError && holdings.length === 0 ? ( ) : noHoldings ? ( ) : (
{showSessionExpired ? (
Session expired. Showing the last known holdings. Reconnect to refresh.
) : null} {holdings.map((item, idx) => { const qty = Number(item.quantity ?? item.qty ?? 0); const avg = Number(item.average_price ?? item.avg_price ?? 0); const ltp = Number(item.last_price ?? 0); const pnl = Number(item.pnl ?? 0); return ( ); })}
Symbol Qty Avg price LTP P&L
{item.tradingsymbol || item.symbol || "Instrument"} {item.exchange || item.exchange_type || "N/A"}
{qty} {formatCurrency(avg, { decimals: 2 })} {ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"} = 0 ? "text-emerald-500" : "text-red-500"}> {formatCurrency(pnl, { decimals: 2 })}
)}

System arm

Re-arm all active strategies after broker login.

Broker

{isConnected ? "Connected" : "Not connected"}

Armed

{armedCount}

Market

{marketState}

Next execution

{nextExecution ? formatMinuteTimestamp(nextExecution) : "Unknown"}

Engine

{liveness}

{armSummary ? (
System armed. {armSummary.failed_runs?.length ? "Some runs failed to arm." : "All runs armed."}
) : null}

Strategy status

Live status for every configured strategy run.

{systemRuns.length} total
{systemStatusQuery.isLoading ? ( ) : systemRuns.length === 0 ? ( ) : (
{systemRuns.map((run) => ( ))}
Strategy Mode Status Next run Broker Lifecycle
{run.strategy || "Strategy"} {run.mode || "-"} {run.status} {run.next_run ? new Date(run.next_run).toLocaleString() : "-"} {run.broker || "-"} {run.lifecycle || run.status}
)}

Strategy control

Start or stop the Golden Nifty SIP engine from the dashboard.

{liveness} {normalizedStrategyStatus}
Next eligible SIP
{nextEligibleLine}
{eligibilityStatus ? (
{eligibilityStatus}
) : null}
{ const value = Number(event.target.value); setSipAmount(Number.isNaN(value) ? 0 : value); }} />
{ const value = Number(event.target.value); setFrequencyDays(Number.isNaN(value) ? 1 : value); }} />
{isStarting ? "Starting..." : "Start Strategy"}
{marketState === "CLOSED" ? (

Market closed — execution will resume at next session

) : null} {isStrategyRunning ? (

Strategy running — next SIP will execute when eligible

) : null}

Equity curve

Track your account value over time from your demat open date.

setStartDate(e.target.value)} className="w-[180px]" disabled={!isConnected} />
{!isConnected ? ( ) : equityCurveQuery.isLoading && equityCurvePoints.length === 0 ? ( ) : equityCurveQuery.isError && equityCurvePoints.length === 0 ? ( ) : equityCurvePoints.length === 0 ? ( ) : (
new Date(value).toLocaleDateString("en-IN", { month: "short", day: "numeric" })} tickLine={false} axisLine={false} minTickGap={24} /> new Intl.NumberFormat("en-IN", { maximumFractionDigits: 0, notation: "compact" }).format(v as number) } width={80} /> new Date(value as string).toLocaleDateString("en-IN", { year: "numeric", month: "short", day: "numeric", }) } formatter={(val) => formatCurrency(Number(val))} /> } />

Account open date: {equityCurve?.accountOpenDate ? new Date(equityCurve.accountOpenDate).toLocaleDateString("en-IN") : "unknown"}

)}
); } function SummaryCard({ icon, label, value, muted, subText, prefersReducedMotion, }: { icon: React.ReactNode; label: string; value: string; muted?: boolean; subText?: string; prefersReducedMotion: boolean; }) { const cardRef = useRef(null); const [transform, setTransform] = useState({ rotateX: 0, rotateY: 0, scale: 1 }); const hoverClass = prefersReducedMotion ? "" : "transition-all duration-300"; const handleMouseMove = (event: React.MouseEvent) => { if (prefersReducedMotion || !cardRef.current) return; const rect = cardRef.current.getBoundingClientRect(); const x = (event.clientX - rect.left) / rect.width - 0.5; const y = (event.clientY - rect.top) / rect.height - 0.5; setTransform({ rotateX: y * -8, rotateY: x * 8, scale: 1.02, }); }; const handleMouseLeave = () => { if (prefersReducedMotion) return; setTransform({ rotateX: 0, rotateY: 0, scale: 1 }); }; const tiltStyle = prefersReducedMotion ? undefined : { transform: `perspective(900px) rotateX(${transform.rotateX}deg) rotateY(${transform.rotateY}deg) scale(${transform.scale})`, transition: "transform 0.12s ease-out", }; return (
{icon}

{label}

{value}

{subText ?

{subText}

: null}
); } function ZeroState({ message }: { message: string }) { return (

{message}

Once connected, we will pull your latest holdings and live positions securely from your broker.

); }