From 209748905363012485d5fbeafa9aa1769f71b5fe Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Wed, 25 Mar 2026 23:30:09 +0530 Subject: [PATCH] Add live testing controls and portfolio refresh fixes --- src/components/landing/PortfolioSection.tsx | 284 +++++++++++++++----- 1 file changed, 210 insertions(+), 74 deletions(-) diff --git a/src/components/landing/PortfolioSection.tsx b/src/components/landing/PortfolioSection.tsx index 89356520..6343b8ce 100644 --- a/src/components/landing/PortfolioSection.tsx +++ b/src/components/landing/PortfolioSection.tsx @@ -11,6 +11,13 @@ 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import StrategyTimeline from "@/components/StrategyTimeline"; import { toast } from "@/hooks/use-toast"; import { @@ -52,13 +59,19 @@ type FundsResponse = { withdrawable?: number; utilized?: number; balance?: number; + available?: { + live_balance?: number; + cash?: number; + opening_balance?: number; + }; + raw?: any; }; }; type EquityCurveResponse = { startDate: string; endDate: string; - accountOpenDate?: string; + exactFrom?: string | null; points: { date: string; value: number }[]; }; @@ -73,6 +86,7 @@ type EngineStatus = { type SessionUser = Pick; const MotionButton = motion(Button); +const BROKER_DATA_REFRESH_MS = 30_000; function formatCurrency(amount: number, options?: { decimals?: number }) { const decimals = options?.decimals ?? 2; @@ -113,6 +127,65 @@ function formatRelativeSeconds(seconds: number) { return `in ${parts.join(" ")}`; } +function firstNumber(...values: unknown[]) { + for (const value of values) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + return 0; +} + +function getSettledQuantity(item: any) { + return firstNumber(item?.settled_quantity, item?.quantity, item?.qty); +} + +function getT1Quantity(item: any) { + return firstNumber(item?.t1_quantity); +} + +function getEffectiveQuantity(item: any) { + return firstNumber(item?.effective_quantity, getSettledQuantity(item) + getT1Quantity(item)); +} + +function getAveragePrice(item: any) { + return firstNumber(item?.average_price, item?.avg_price); +} + +function getLastPrice(item: any) { + return firstNumber(item?.last_price, item?.close_price, getAveragePrice(item)); +} + +function getDisplayPnl(item: any) { + return firstNumber( + item?.display_pnl, + getEffectiveQuantity(item) * (getLastPrice(item) - getAveragePrice(item)), + ); +} + +function getHoldingValue(item: any) { + return firstNumber(item?.holding_value, getEffectiveQuantity(item) * getLastPrice(item)); +} + +function getAvailableFundsValue(funds?: FundsResponse["funds"] | null) { + return firstNumber( + funds?.available?.live_balance, + funds?.raw?.equity?.available?.live_balance, + funds?.raw?.equity?.available?.cash, + funds?.raw?.available?.live_balance, + funds?.balance, + funds?.cash, + funds?.withdrawable, + funds?.net, + funds?.raw?.equity?.net, + funds?.raw?.net, + funds?.available?.opening_balance, + funds?.raw?.equity?.available?.opening_balance, + funds?.raw?.available?.opening_balance, + ); +} + function usePrefersReducedMotion() { const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); @@ -266,27 +339,14 @@ export default function PortfolioSection() { formatDateInput(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)), ); const [sipAmount, setSipAmount] = useState(5000); - const [frequencyDays, setFrequencyDays] = useState(30); + const [frequencyValue, setFrequencyValue] = useState(30); + const [frequencyUnit, setFrequencyUnit] = useState<"days" | "minutes">("days"); 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(); @@ -406,6 +466,30 @@ export default function PortfolioSection() { }, }); + const refreshBrokerData = useCallback( + async ({ includeEquityCurve = false }: { includeEquityCurve?: boolean } = {}) => { + if (!brokerStatus?.connected) { + return; + } + const tasks = [ + holdingsQuery.refetch(), + fundsQuery.refetch(), + refetchBrokerStatus(), + ]; + if (includeEquityCurve) { + tasks.push(equityCurveQuery.refetch()); + } + await Promise.allSettled(tasks); + }, + [ + brokerStatus?.connected, + holdingsQuery.refetch, + fundsQuery.refetch, + equityCurveQuery.refetch, + refetchBrokerStatus, + ], + ); + const isConnected = !!brokerStatus?.connected; const isAuthed = brokerStatus !== null; const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings; @@ -425,12 +509,7 @@ export default function PortfolioSection() { setReconnectAttempted(true); (async () => { try { - await refetchBrokerStatus(); - await Promise.all([ - holdingsQuery.refetch(), - fundsQuery.refetch(), - equityCurveQuery.refetch(), - ]); + await refreshBrokerData({ includeEquityCurve: true }); } catch { return; } @@ -439,36 +518,54 @@ export default function PortfolioSection() { sessionExpired, reconnectAttempted, isConnected, - refetchBrokerStatus, - holdingsQuery, - fundsQuery, - equityCurveQuery, + refreshBrokerData, ]); - const availableFunds = - fundsSnapshot?.balance ?? - fundsSnapshot?.net ?? - fundsSnapshot?.withdrawable ?? - fundsSnapshot?.cash ?? - fundsSnapshot?.raw?.net ?? - fundsSnapshot?.raw?.available?.live_balance ?? - fundsSnapshot?.raw?.available?.opening_balance ?? - 0; + + useEffect(() => { + if (!isConnected) { + return; + } + + const refreshIfVisible = () => { + if (document.visibilityState !== "visible") { + return; + } + void refreshBrokerData(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + void refreshBrokerData(); + } + }; + + const intervalId = window.setInterval(refreshIfVisible, BROKER_DATA_REFRESH_MS); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.clearInterval(intervalId); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [isConnected, refreshBrokerData]); + const availableFunds = getAvailableFundsValue(fundsSnapshot); 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: acc.totalValue + getHoldingValue(item), + totalPnl: acc.totalPnl + getDisplayPnl(item), }; }, { totalValue: 0, totalPnl: 0 }, ); }, [holdings]); + const activeHoldingCount = useMemo( + () => holdings.filter((item) => getEffectiveQuantity(item) > 0).length, + [holdings], + ); + const equityCurve = equityCurveQuery.data ?? cachedEquityCurve; const equityCurvePoints = equityCurve?.points ?? []; const showSessionExpired = sessionExpired && isConnected; @@ -569,11 +666,8 @@ export default function PortfolioSection() { }, [brokerStatus, requireLogin]); const handleRefreshBrokerData = useCallback(() => { - holdingsQuery.refetch(); - fundsQuery.refetch(); - equityCurveQuery.refetch(); - refetchBrokerStatus(); - }, [equityCurveQuery, fundsQuery, holdingsQuery, refetchBrokerStatus]); + void refreshBrokerData({ includeEquityCurve: true }); + }, [refreshBrokerData]); const handleDisconnectBroker = useCallback(() => { disconnectBrokerMutation.mutate(); @@ -601,8 +695,8 @@ export default function PortfolioSection() { strategy_name: "Golden Nifty", sip_amount: sipAmount, sip_frequency: { - value: frequencyDays, - unit: "days", + value: frequencyValue, + unit: frequencyUnit, }, mode: "LIVE", }); @@ -632,7 +726,10 @@ export default function PortfolioSection() { } } finally { setIsStarting(false); - await refreshStatus(); + await Promise.allSettled([ + refreshStatus(), + refreshBrokerData({ includeEquityCurve: true }), + ]); } }; @@ -642,7 +739,10 @@ export default function PortfolioSection() { await stopStrategy(); } finally { setIsStopping(false); - await refreshStatus(); + await Promise.allSettled([ + refreshStatus(), + refreshBrokerData({ includeEquityCurve: true }), + ]); } }; @@ -666,7 +766,7 @@ export default function PortfolioSection() { { icon: , label: "Positions", - value: holdings.length.toString(), + value: activeHoldingCount.toString(), muted: !isConnected, }, { @@ -888,10 +988,12 @@ export default function PortfolioSection() { {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); + const qty = getEffectiveQuantity(item); + const settledQty = getSettledQuantity(item); + const t1Qty = getT1Quantity(item); + const avg = getAveragePrice(item); + const ltp = getLastPrice(item); + const pnl = getDisplayPnl(item); return ( @@ -902,7 +1004,14 @@ export default function PortfolioSection() { {item.exchange || item.exchange_type || "N/A"} - {qty} + +
+ {qty} + {t1Qty > 0 && settledQty <= 0 ? ( + T1 pending + ) : null} +
+ {formatCurrency(avg, { decimals: 2 })} {ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"} @@ -972,21 +1081,43 @@ export default function PortfolioSection() { }} /> -
- - { - const value = Number(event.target.value); - setFrequencyDays(Number.isNaN(value) ? 1 : value); - }} - /> +
+
+ + { + const value = Number(event.target.value); + setFrequencyValue(Number.isNaN(value) ? 1 : value); + }} + /> +
+
+ + +
+ {frequencyUnit === "minutes" ? ( +
+ Minutes mode is for live testing only. The engine checks every few seconds in this mode. +
+ ) : null}

Equity curve

- Track your account value over time from your demat open date. + Track exact stored daily broker snapshots from the day recording began.

@@ -1024,7 +1155,6 @@ export default function PortfolioSection() { type="date" value={startDate} max={formatDateInput(new Date())} - min={linkedAtInput} onChange={(e) => setStartDate(e.target.value)} className="w-[180px]" disabled={!isConnected} @@ -1039,7 +1169,7 @@ export default function PortfolioSection() { ) : equityCurveQuery.isError && equityCurvePoints.length === 0 ? ( ) : equityCurvePoints.length === 0 ? ( - + ) : (

- Account open date: {equityCurve?.accountOpenDate - ? new Date(equityCurve.accountOpenDate).toLocaleDateString("en-IN") - : "unknown"} + Exact daily values from{" "} + {equityCurve?.exactFrom + ? new Date(equityCurve.exactFrom).toLocaleDateString("en-IN", { + year: "numeric", + month: "short", + day: "numeric", + }) + : "the first recorded snapshot"} + .

)}