From 4f17fb166df520f071d0dc26500109ecff43101a Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Mon, 6 Apr 2026 11:31:29 +0530 Subject: [PATCH] Show live broker positions in portfolio --- src/components/landing/PortfolioSection.tsx | 185 +++++++++++++++----- 1 file changed, 144 insertions(+), 41 deletions(-) diff --git a/src/components/landing/PortfolioSection.tsx b/src/components/landing/PortfolioSection.tsx index e03830c8..1502bfea 100644 --- a/src/components/landing/PortfolioSection.tsx +++ b/src/components/landing/PortfolioSection.tsx @@ -47,6 +47,10 @@ type HoldingsResponse = { holdings: any[]; }; +type PositionsResponse = { + positions: any[]; +}; + type MarketStatusResponse = { status?: "OPEN" | "CLOSED"; checked_at?: string; @@ -202,6 +206,49 @@ function getHoldingValue(item: any) { return firstNumber(item?.holding_value, getEffectiveQuantity(item) * getLastPrice(item)); } +function getPortfolioItemKey(item: any, idx: number) { + return `${item.tradingsymbol || item.symbol || item.instrument_token || item.exchange || "item"}-${idx}`; +} + +function renderPortfolioRows(items: any[]) { + return items.map((item, idx) => { + 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 ( + + +
+ + {item.tradingsymbol || item.symbol || "Instrument"} + + {item.exchange || item.exchange_type || "N/A"} + {item.product ? {item.product} : null} +
+ + +
+ {qty} + {t1Qty > 0 && settledQty <= 0 ? ( + T1 pending + ) : null} +
+ + {formatCurrency(avg, { decimals: 2 })} + {ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"} + + = 0 ? "text-emerald-500" : "text-red-500"}> + {formatCurrency(pnl, { decimals: 2 })} + + + + ); + }); +} + function getAvailableFundsValue(funds?: FundsResponse["funds"] | null) { return firstNumber( funds?.available?.live_balance, @@ -251,6 +298,7 @@ export default function PortfolioSection() { const [sessionExpired, setSessionExpired] = useState(false); const [reconnectAttempted, setReconnectAttempted] = useState(false); const [cachedHoldings, setCachedHoldings] = useState([]); + const [cachedPositions, setCachedPositions] = useState([]); const [cachedFunds, setCachedFunds] = useState(null); const [cachedEquityCurve, setCachedEquityCurve] = useState(null); const { @@ -341,9 +389,11 @@ export default function PortfolioSection() { setSessionExpired(false); setReconnectAttempted(false); setCachedHoldings([]); + setCachedPositions([]); setCachedFunds(null); setCachedEquityCurve(null); queryClient.removeQueries({ queryKey: ["/broker/holdings"] }); + queryClient.removeQueries({ queryKey: ["/broker/positions"] }); queryClient.removeQueries({ queryKey: ["/broker/funds"] }); queryClient.removeQueries({ queryKey: ["/broker/equity-curve"] }); await refetchBrokerStatus(); @@ -551,6 +601,27 @@ export default function PortfolioSection() { }, }); + const positionsQuery = useQuery({ + queryKey: ["/broker/positions"], + queryFn: async () => { + const res = await apiRequest("GET", "/broker/positions"); + return res.json(); + }, + enabled: !!brokerStatus?.connected, + retry: 1, + retryDelay: 600, + onSuccess: (data) => { + setCachedPositions(data?.positions || []); + setSessionExpired(false); + setReconnectAttempted(false); + }, + onError: () => { + if (brokerStatus?.connected) { + setSessionExpired(true); + } + }, + }); + const refreshBrokerData = useCallback( async ({ includeEquityCurve = false }: { includeEquityCurve?: boolean } = {}) => { if (!brokerStatus?.connected) { @@ -558,6 +629,7 @@ export default function PortfolioSection() { } const tasks = [ holdingsQuery.refetch(), + positionsQuery.refetch(), fundsQuery.refetch(), refetchBrokerStatus(), ]; @@ -569,6 +641,7 @@ export default function PortfolioSection() { [ brokerStatus?.connected, holdingsQuery.refetch, + positionsQuery.refetch, fundsQuery.refetch, equityCurveQuery.refetch, refetchBrokerStatus, @@ -578,8 +651,10 @@ export default function PortfolioSection() { const isConnected = !!brokerStatus?.connected; const isAuthed = brokerStatus !== null; const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings; + const positions = positionsQuery.data ? positionsQuery.data.positions : cachedPositions; const fundsSnapshot = fundsQuery.data?.funds ?? cachedFunds; const noHoldings = holdings.length === 0; + const noPositions = positions.length === 0; useEffect(() => { if (!isConnected) { setSessionExpired(false); @@ -635,7 +710,7 @@ export default function PortfolioSection() { const availableFunds = getAvailableFundsValue(fundsSnapshot); const { totalValue, totalPnl } = useMemo(() => { - return holdings.reduce( + return [...holdings, ...positions].reduce( (acc, item) => { return { totalValue: acc.totalValue + getHoldingValue(item), @@ -644,12 +719,16 @@ export default function PortfolioSection() { }, { totalValue: 0, totalPnl: 0 }, ); - }, [holdings]); + }, [holdings, positions]); const activeHoldingCount = useMemo( () => holdings.filter((item) => getEffectiveQuantity(item) > 0).length, [holdings], ); + const activePositionCount = useMemo( + () => positions.filter((item) => getEffectiveQuantity(item) !== 0).length, + [positions], + ); const equityCurve = equityCurveQuery.data ?? cachedEquityCurve; const equityCurvePoints = equityCurve?.points ?? []; @@ -1003,11 +1082,15 @@ export default function PortfolioSection() { label: "Portfolio value", value: formatCurrency(totalValue, { decimals: 2 }), muted: !isConnected, + subText: + isConnected && (activeHoldingCount > 0 || activePositionCount > 0) + ? `Holdings: ${activeHoldingCount} | Positions: ${activePositionCount}` + : undefined, }, { icon: , label: "Positions", - value: activeHoldingCount.toString(), + value: activePositionCount.toString(), muted: !isConnected, }, { @@ -1127,11 +1210,11 @@ export default function PortfolioSection() { @@ -1188,7 +1271,7 @@ export default function PortfolioSection() {

Holdings

- Current positions pulled from your connected broker. + Settled holdings synced from your connected broker.

@@ -1235,41 +1318,61 @@ export default function PortfolioSection() { - {holdings.map((item, idx) => { - 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 ( - - -
- - {item.tradingsymbol || item.symbol || "Instrument"} - - {item.exchange || item.exchange_type || "N/A"} -
- - -
- {qty} - {t1Qty > 0 && settledQty <= 0 ? ( - T1 pending - ) : null} -
- - {formatCurrency(avg, { decimals: 2 })} - {ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"} - - = 0 ? "text-emerald-500" : "text-red-500"}> - {formatCurrency(pnl, { decimals: 2 })} - - - - ); - })} + {renderPortfolioRows(holdings)} + + + + )} + + +
+
+
+

Live positions

+

+ Same-day broker positions appear here before they move into holdings. +

+
+ + {activePositionCount} open + +
+ + {!isAuthed ? ( + + ) : brokerStatusLoading ? ( + + ) : !isConnected ? ( + + ) : positionsQuery.isLoading ? ( + + ) : positionsQuery.isError && positions.length === 0 ? ( + + ) : noPositions ? ( + + ) : ( +
+ + + + + + + + + + + + {renderPortfolioRows(positions)}
SymbolQtyAvg priceLTPP&L