Add live testing controls and portfolio refresh fixes
This commit is contained in:
parent
aa93225ef7
commit
2097489053
@ -11,6 +11,13 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import StrategyTimeline from "@/components/StrategyTimeline";
|
import StrategyTimeline from "@/components/StrategyTimeline";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
@ -52,13 +59,19 @@ type FundsResponse = {
|
|||||||
withdrawable?: number;
|
withdrawable?: number;
|
||||||
utilized?: number;
|
utilized?: number;
|
||||||
balance?: number;
|
balance?: number;
|
||||||
|
available?: {
|
||||||
|
live_balance?: number;
|
||||||
|
cash?: number;
|
||||||
|
opening_balance?: number;
|
||||||
|
};
|
||||||
|
raw?: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type EquityCurveResponse = {
|
type EquityCurveResponse = {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
accountOpenDate?: string;
|
exactFrom?: string | null;
|
||||||
points: { date: string; value: number }[];
|
points: { date: string; value: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,6 +86,7 @@ type EngineStatus = {
|
|||||||
type SessionUser = Pick<User, "id" | "username">;
|
type SessionUser = Pick<User, "id" | "username">;
|
||||||
|
|
||||||
const MotionButton = motion(Button);
|
const MotionButton = motion(Button);
|
||||||
|
const BROKER_DATA_REFRESH_MS = 30_000;
|
||||||
|
|
||||||
function formatCurrency(amount: number, options?: { decimals?: number }) {
|
function formatCurrency(amount: number, options?: { decimals?: number }) {
|
||||||
const decimals = options?.decimals ?? 2;
|
const decimals = options?.decimals ?? 2;
|
||||||
@ -113,6 +127,65 @@ function formatRelativeSeconds(seconds: number) {
|
|||||||
return `in ${parts.join(" ")}`;
|
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() {
|
function usePrefersReducedMotion() {
|
||||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||||
|
|
||||||
@ -266,27 +339,14 @@ export default function PortfolioSection() {
|
|||||||
formatDateInput(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)),
|
formatDateInput(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)),
|
||||||
);
|
);
|
||||||
const [sipAmount, setSipAmount] = useState(5000);
|
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 [strategyStatus, setStrategyStatus] = useState("STOPPED");
|
||||||
const [isStarting, setIsStarting] = useState(false);
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
const [isStopping, setIsStopping] = useState(false);
|
const [isStopping, setIsStopping] = useState(false);
|
||||||
const [engineStatus, setEngineStatus] = useState<EngineStatus | null>(null);
|
const [engineStatus, setEngineStatus] = useState<EngineStatus | null>(null);
|
||||||
const [marketStatus, setMarketStatus] = useState<MarketStatusResponse | null>(null);
|
const [marketStatus, setMarketStatus] = useState<MarketStatusResponse | null>(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 () => {
|
const refreshStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const status = await getStrategyStatus();
|
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 isConnected = !!brokerStatus?.connected;
|
||||||
const isAuthed = brokerStatus !== null;
|
const isAuthed = brokerStatus !== null;
|
||||||
const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings;
|
const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings;
|
||||||
@ -425,12 +509,7 @@ export default function PortfolioSection() {
|
|||||||
setReconnectAttempted(true);
|
setReconnectAttempted(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await refetchBrokerStatus();
|
await refreshBrokerData({ includeEquityCurve: true });
|
||||||
await Promise.all([
|
|
||||||
holdingsQuery.refetch(),
|
|
||||||
fundsQuery.refetch(),
|
|
||||||
equityCurveQuery.refetch(),
|
|
||||||
]);
|
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -439,36 +518,54 @@ export default function PortfolioSection() {
|
|||||||
sessionExpired,
|
sessionExpired,
|
||||||
reconnectAttempted,
|
reconnectAttempted,
|
||||||
isConnected,
|
isConnected,
|
||||||
refetchBrokerStatus,
|
refreshBrokerData,
|
||||||
holdingsQuery,
|
|
||||||
fundsQuery,
|
|
||||||
equityCurveQuery,
|
|
||||||
]);
|
]);
|
||||||
const availableFunds =
|
|
||||||
fundsSnapshot?.balance ??
|
useEffect(() => {
|
||||||
fundsSnapshot?.net ??
|
if (!isConnected) {
|
||||||
fundsSnapshot?.withdrawable ??
|
return;
|
||||||
fundsSnapshot?.cash ??
|
}
|
||||||
fundsSnapshot?.raw?.net ??
|
|
||||||
fundsSnapshot?.raw?.available?.live_balance ??
|
const refreshIfVisible = () => {
|
||||||
fundsSnapshot?.raw?.available?.opening_balance ??
|
if (document.visibilityState !== "visible") {
|
||||||
0;
|
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(() => {
|
const { totalValue, totalPnl } = useMemo(() => {
|
||||||
return holdings.reduce(
|
return holdings.reduce(
|
||||||
(acc, item) => {
|
(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 {
|
return {
|
||||||
totalValue: acc.totalValue + qty * last,
|
totalValue: acc.totalValue + getHoldingValue(item),
|
||||||
totalPnl: acc.totalPnl + pnl,
|
totalPnl: acc.totalPnl + getDisplayPnl(item),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ totalValue: 0, totalPnl: 0 },
|
{ totalValue: 0, totalPnl: 0 },
|
||||||
);
|
);
|
||||||
}, [holdings]);
|
}, [holdings]);
|
||||||
|
|
||||||
|
const activeHoldingCount = useMemo(
|
||||||
|
() => holdings.filter((item) => getEffectiveQuantity(item) > 0).length,
|
||||||
|
[holdings],
|
||||||
|
);
|
||||||
|
|
||||||
const equityCurve = equityCurveQuery.data ?? cachedEquityCurve;
|
const equityCurve = equityCurveQuery.data ?? cachedEquityCurve;
|
||||||
const equityCurvePoints = equityCurve?.points ?? [];
|
const equityCurvePoints = equityCurve?.points ?? [];
|
||||||
const showSessionExpired = sessionExpired && isConnected;
|
const showSessionExpired = sessionExpired && isConnected;
|
||||||
@ -569,11 +666,8 @@ export default function PortfolioSection() {
|
|||||||
}, [brokerStatus, requireLogin]);
|
}, [brokerStatus, requireLogin]);
|
||||||
|
|
||||||
const handleRefreshBrokerData = useCallback(() => {
|
const handleRefreshBrokerData = useCallback(() => {
|
||||||
holdingsQuery.refetch();
|
void refreshBrokerData({ includeEquityCurve: true });
|
||||||
fundsQuery.refetch();
|
}, [refreshBrokerData]);
|
||||||
equityCurveQuery.refetch();
|
|
||||||
refetchBrokerStatus();
|
|
||||||
}, [equityCurveQuery, fundsQuery, holdingsQuery, refetchBrokerStatus]);
|
|
||||||
|
|
||||||
const handleDisconnectBroker = useCallback(() => {
|
const handleDisconnectBroker = useCallback(() => {
|
||||||
disconnectBrokerMutation.mutate();
|
disconnectBrokerMutation.mutate();
|
||||||
@ -601,8 +695,8 @@ export default function PortfolioSection() {
|
|||||||
strategy_name: "Golden Nifty",
|
strategy_name: "Golden Nifty",
|
||||||
sip_amount: sipAmount,
|
sip_amount: sipAmount,
|
||||||
sip_frequency: {
|
sip_frequency: {
|
||||||
value: frequencyDays,
|
value: frequencyValue,
|
||||||
unit: "days",
|
unit: frequencyUnit,
|
||||||
},
|
},
|
||||||
mode: "LIVE",
|
mode: "LIVE",
|
||||||
});
|
});
|
||||||
@ -632,7 +726,10 @@ export default function PortfolioSection() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsStarting(false);
|
setIsStarting(false);
|
||||||
await refreshStatus();
|
await Promise.allSettled([
|
||||||
|
refreshStatus(),
|
||||||
|
refreshBrokerData({ includeEquityCurve: true }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -642,7 +739,10 @@ export default function PortfolioSection() {
|
|||||||
await stopStrategy();
|
await stopStrategy();
|
||||||
} finally {
|
} finally {
|
||||||
setIsStopping(false);
|
setIsStopping(false);
|
||||||
await refreshStatus();
|
await Promise.allSettled([
|
||||||
|
refreshStatus(),
|
||||||
|
refreshBrokerData({ includeEquityCurve: true }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -666,7 +766,7 @@ export default function PortfolioSection() {
|
|||||||
{
|
{
|
||||||
icon: <BarChart3 className="h-5 w-5" />,
|
icon: <BarChart3 className="h-5 w-5" />,
|
||||||
label: "Positions",
|
label: "Positions",
|
||||||
value: holdings.length.toString(),
|
value: activeHoldingCount.toString(),
|
||||||
muted: !isConnected,
|
muted: !isConnected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -888,10 +988,12 @@ export default function PortfolioSection() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/60">
|
<tbody className="divide-y divide-border/60">
|
||||||
{holdings.map((item, idx) => {
|
{holdings.map((item, idx) => {
|
||||||
const qty = Number(item.quantity ?? item.qty ?? 0);
|
const qty = getEffectiveQuantity(item);
|
||||||
const avg = Number(item.average_price ?? item.avg_price ?? 0);
|
const settledQty = getSettledQuantity(item);
|
||||||
const ltp = Number(item.last_price ?? 0);
|
const t1Qty = getT1Quantity(item);
|
||||||
const pnl = Number(item.pnl ?? 0);
|
const avg = getAveragePrice(item);
|
||||||
|
const ltp = getLastPrice(item);
|
||||||
|
const pnl = getDisplayPnl(item);
|
||||||
return (
|
return (
|
||||||
<tr key={`${item.tradingsymbol || item.instrument_token || idx}`}>
|
<tr key={`${item.tradingsymbol || item.instrument_token || idx}`}>
|
||||||
<td className="px-6 py-3">
|
<td className="px-6 py-3">
|
||||||
@ -902,7 +1004,14 @@ export default function PortfolioSection() {
|
|||||||
<Badge variant="outline">{item.exchange || item.exchange_type || "N/A"}</Badge>
|
<Badge variant="outline">{item.exchange || item.exchange_type || "N/A"}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">{qty}</td>
|
<td className="px-6 py-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{qty}</span>
|
||||||
|
{t1Qty > 0 && settledQty <= 0 ? (
|
||||||
|
<span className="text-xs text-amber-300">T1 pending</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-3">{formatCurrency(avg, { decimals: 2 })}</td>
|
<td className="px-6 py-3">{formatCurrency(avg, { decimals: 2 })}</td>
|
||||||
<td className="px-6 py-3">{ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"}</td>
|
<td className="px-6 py-3">{ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"}</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-6 py-3">
|
||||||
@ -972,21 +1081,43 @@ export default function PortfolioSection() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
|
||||||
<Label htmlFor="strategy-frequency">Frequency (days)</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="strategy-frequency">Frequency</Label>
|
||||||
id="strategy-frequency"
|
<Input
|
||||||
type="number"
|
id="strategy-frequency"
|
||||||
min={1}
|
type="number"
|
||||||
step={1}
|
min={1}
|
||||||
value={frequencyDays}
|
step={1}
|
||||||
onChange={(event) => {
|
value={frequencyValue}
|
||||||
const value = Number(event.target.value);
|
onChange={(event) => {
|
||||||
setFrequencyDays(Number.isNaN(value) ? 1 : value);
|
const value = Number(event.target.value);
|
||||||
}}
|
setFrequencyValue(Number.isNaN(value) ? 1 : value);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Unit</Label>
|
||||||
|
<Select
|
||||||
|
value={frequencyUnit}
|
||||||
|
onValueChange={(value) => setFrequencyUnit(value as "days" | "minutes")}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select unit" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
<SelectItem value="minutes">Minutes (testing)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{frequencyUnit === "minutes" ? (
|
||||||
|
<div className="text-xs text-amber-300">
|
||||||
|
Minutes mode is for live testing only. The engine checks every few seconds in this mode.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<MotionButton
|
<MotionButton
|
||||||
{...ctaMotionProps}
|
{...ctaMotionProps}
|
||||||
@ -1012,7 +1143,7 @@ export default function PortfolioSection() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold">Equity curve</p>
|
<p className="text-sm font-semibold">Equity curve</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Track your account value over time from your demat open date.
|
Track exact stored daily broker snapshots from the day recording began.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -1024,7 +1155,6 @@ export default function PortfolioSection() {
|
|||||||
type="date"
|
type="date"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
max={formatDateInput(new Date())}
|
max={formatDateInput(new Date())}
|
||||||
min={linkedAtInput}
|
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
className="w-[180px]"
|
className="w-[180px]"
|
||||||
disabled={!isConnected}
|
disabled={!isConnected}
|
||||||
@ -1039,7 +1169,7 @@ export default function PortfolioSection() {
|
|||||||
) : equityCurveQuery.isError && equityCurvePoints.length === 0 ? (
|
) : equityCurveQuery.isError && equityCurvePoints.length === 0 ? (
|
||||||
<ZeroState message="Could not load equity curve." />
|
<ZeroState message="Could not load equity curve." />
|
||||||
) : equityCurvePoints.length === 0 ? (
|
) : equityCurvePoints.length === 0 ? (
|
||||||
<ZeroState message="Could not load equity curve." />
|
<ZeroState message="No exact equity snapshots yet. The curve starts from the first recorded daily snapshot." />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-80">
|
<div className="h-80">
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
@ -1088,9 +1218,15 @@ export default function PortfolioSection() {
|
|||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
Account open date: {equityCurve?.accountOpenDate
|
Exact daily values from{" "}
|
||||||
? new Date(equityCurve.accountOpenDate).toLocaleDateString("en-IN")
|
{equityCurve?.exactFrom
|
||||||
: "unknown"}
|
? new Date(equityCurve.exactFrom).toLocaleDateString("en-IN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
: "the first recorded snapshot"}
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user