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 ? (
+
+ ) : (
+
+
+
+
+ | Symbol |
+ Qty |
+ Avg price |
+ LTP |
+ P&L |
+
+
+
+ {renderPortfolioRows(positions)}