Mirror live strategy controls in paper portfolio

This commit is contained in:
Thigazhezhilan J 2026-03-30 01:53:20 +05:30
parent b0c1ea3651
commit 434be478e4

View File

@ -10,13 +10,25 @@ import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
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 { import {
ChartContainer, ChartContainer,
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } from "@/components/ui/chart";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { getStrategyStatus, startStrategy, stopStrategy } from "@/api/strategy"; import {
getStrategyStatus,
resumeStrategy,
startStrategy,
stopStrategy,
} from "@/api/strategy";
import StrategyTimeline from "@/components/StrategyTimeline"; import StrategyTimeline from "@/components/StrategyTimeline";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
@ -80,8 +92,62 @@ type MarketStatusResponse = {
checked_at?: string; checked_at?: string;
}; };
type EngineStatus = {
state?: string;
run_id?: string | null;
last_heartbeat_ts?: string | null;
last_execution_ts?: string | null;
next_eligible_ts?: string | null;
};
type StrategyStatusResponse = {
status?: string;
last_updated?: string | null;
last_execution_ts?: string | null;
next_eligible_ts?: string | null;
run_id?: string | null;
can_resume?: boolean;
can_restart?: boolean;
config?: {
strategy?: string | null;
sip_amount?: number | null;
sip_frequency?: {
value?: number | null;
unit?: string | null;
} | null;
mode?: string | null;
broker?: string | null;
active?: boolean | null;
} | null;
};
const MotionButton = motion(Button); const MotionButton = motion(Button);
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 formatCurrency(value: number, decimals = 2) { function formatCurrency(value: number, decimals = 2) {
return new Intl.NumberFormat("en-IN", { return new Intl.NumberFormat("en-IN", {
style: "currency", style: "currency",
@ -115,11 +181,12 @@ function PaperTradingPortfolio() {
const [frequencyValue, setFrequencyValue] = useState(10); const [frequencyValue, setFrequencyValue] = useState(10);
const [frequencyUnit, setFrequencyUnit] = useState<"minutes" | "days">("minutes"); const [frequencyUnit, setFrequencyUnit] = useState<"minutes" | "days">("minutes");
const [strategyStatus, setStrategyStatus] = useState("STOPPED"); const [strategyStatus, setStrategyStatus] = useState("STOPPED");
const [nextSipTs, setNextSipTs] = useState<string | null>(null); const [strategyDetails, setStrategyDetails] = useState<StrategyStatusResponse | null>(null);
const [nextSipCountdown, setNextSipCountdown] = useState<string | null>(null);
const [isStarting, setIsStarting] = useState(false); const [isStarting, setIsStarting] = useState(false);
const [isResuming, setIsResuming] = useState(false);
const [isStopping, setIsStopping] = useState(false); const [isStopping, setIsStopping] = useState(false);
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
const [freshStartRequested, setFreshStartRequested] = useState(false);
const [addCashEnabled, setAddCashEnabled] = useState(false); const [addCashEnabled, setAddCashEnabled] = useState(false);
const [addCashAmount, setAddCashAmount] = useState<number | "">(""); const [addCashAmount, setAddCashAmount] = useState<number | "">("");
const [isAddingCash, setIsAddingCash] = useState(false); const [isAddingCash, setIsAddingCash] = useState(false);
@ -132,9 +199,9 @@ function PaperTradingPortfolio() {
const [mtmPnlPoints, setMtmPnlPoints] = useState< const [mtmPnlPoints, setMtmPnlPoints] = useState<
{ date: string; pnl: number }[] { date: string; pnl: number }[]
>([]); >([]);
const [engineStatus, setEngineStatus] = useState<EngineStatus | null>(null);
const [priceStale, setPriceStale] = useState(false); const [priceStale, setPriceStale] = useState(false);
const [marketStatus, setMarketStatus] = useState<MarketStatusResponse | null>(null); const [marketStatus, setMarketStatus] = useState<MarketStatusResponse | null>(null);
const [timelineKey, setTimelineKey] = useState(0);
const skipFirstPnlPointRef = useRef(true); const skipFirstPnlPointRef = useRef(true);
const fundsQuery = useQuery<PaperFundsResponse>({ const fundsQuery = useQuery<PaperFundsResponse>({
@ -186,14 +253,32 @@ function PaperTradingPortfolio() {
const refreshStatus = useCallback(async () => { const refreshStatus = useCallback(async () => {
try { try {
const status = await getStrategyStatus(); const status = (await getStrategyStatus()) as StrategyStatusResponse;
setStrategyDetails(status ?? null);
setStrategyStatus(status?.status ?? "STOPPED"); setStrategyStatus(status?.status ?? "STOPPED");
setNextSipTs(status?.next_eligible_ts ?? null); const savedAmount = Number(status?.config?.sip_amount);
if (!freshStartRequested && Number.isFinite(savedAmount) && savedAmount > 0) {
setSipAmount(savedAmount);
}
const savedFrequency = status?.config?.sip_frequency;
const savedFrequencyValue = Number(savedFrequency?.value);
const savedFrequencyUnit =
savedFrequency?.unit === "minutes"
? "minutes"
: savedFrequency?.unit === "days"
? "days"
: null;
if (!freshStartRequested && Number.isFinite(savedFrequencyValue) && savedFrequencyValue >= 1) {
setFrequencyValue(savedFrequencyValue);
}
if (!freshStartRequested && savedFrequencyUnit) {
setFrequencyUnit(savedFrequencyUnit);
}
} catch { } catch {
setStrategyDetails(null);
setStrategyStatus("STOPPED"); setStrategyStatus("STOPPED");
setNextSipTs(null);
} }
}, []); }, [freshStartRequested]);
useEffect(() => { useEffect(() => {
refreshStatus(); refreshStatus();
@ -212,35 +297,21 @@ function PaperTradingPortfolio() {
} }
}, []); }, []);
useEffect(() => { const refreshEngineStatus = useCallback(async () => {
if (!nextSipTs) { try {
setNextSipCountdown(null); const res = await apiRequest("GET", "/engine/status");
return; const data: EngineStatus = await res.json();
setEngineStatus(data);
} catch {
setEngineStatus(null);
} }
const updateCountdown = () => { }, []);
const target = new Date(nextSipTs).getTime();
if (Number.isNaN(target)) { useEffect(() => {
setNextSipCountdown(null); void refreshEngineStatus();
return; const id = window.setInterval(refreshEngineStatus, 5000);
}
const diff = target - Date.now();
if (diff <= 0) {
setNextSipCountdown("now");
return;
}
const totalSeconds = Math.floor(diff / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const hh = hours > 0 ? `${hours}h ` : "";
const mm = `${minutes}`.padStart(2, "0");
const ss = `${seconds}`.padStart(2, "0");
setNextSipCountdown(`${hh}${mm}:${ss}`);
};
updateCountdown();
const id = window.setInterval(updateCountdown, 1000);
return () => window.clearInterval(id); return () => window.clearInterval(id);
}, [nextSipTs]); }, [refreshEngineStatus]);
useEffect(() => { useEffect(() => {
let timer: number; let timer: number;
@ -299,22 +370,28 @@ function PaperTradingPortfolio() {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, []); }, []);
useEffect(() => { const refreshMarketStatus = useCallback(async () => {
const fetchMarketStatus = async () => { try {
try { const res = await apiRequest("GET", "/market/status");
const res = await apiRequest("GET", "/market/status"); const data: MarketStatusResponse = await res.json();
const data: MarketStatusResponse = await res.json(); setMarketStatus(data);
setMarketStatus(data); } catch {
} catch { setMarketStatus(null);
setMarketStatus(null); }
}
};
fetchMarketStatus();
const id = window.setInterval(fetchMarketStatus, 5000);
return () => window.clearInterval(id);
}, []); }, []);
useEffect(() => {
void refreshMarketStatus();
const id = window.setInterval(refreshMarketStatus, 5000);
return () => window.clearInterval(id);
}, [refreshMarketStatus]);
useEffect(() => {
if (!freshStartRequested && typeof mtmInitialCash === "number" && mtmInitialCash > 0) {
setInitialCash(mtmInitialCash);
}
}, [freshStartRequested, mtmInitialCash]);
const handleReset = async () => { const handleReset = async () => {
const ok = window.confirm( const ok = window.confirm(
"This will RESET the entire paper account.\n\n" + "This will RESET the entire paper account.\n\n" +
@ -345,8 +422,10 @@ function PaperTradingPortfolio() {
positionsQuery.refetch(), positionsQuery.refetch(),
ordersQuery.refetch(), ordersQuery.refetch(),
refreshStatus(), refreshStatus(),
refreshEngineStatus(),
refreshMarketStatus(),
]); ]);
setTimelineKey((value) => value + 1); setFreshStartRequested(false);
} catch (error) { } catch (error) {
toast({ toast({
title: "Reset failed", title: "Reset failed",
@ -388,6 +467,7 @@ function PaperTradingPortfolio() {
}); });
} }
if (result?.status === "started" || result?.status === "restarted") { if (result?.status === "started" || result?.status === "restarted") {
setFreshStartRequested(false);
toast({ toast({
title: "Paper strategy started", title: "Paper strategy started",
description: "The simulator is now running.", description: "The simulator is now running.",
@ -401,8 +481,56 @@ function PaperTradingPortfolio() {
console.error(error); console.error(error);
} finally { } finally {
setIsStarting(false); setIsStarting(false);
await Promise.all([ await Promise.allSettled([
refreshStatus(), refreshStatus(),
refreshEngineStatus(),
refreshMarketStatus(),
fundsQuery.refetch(),
positionsQuery.refetch(),
ordersQuery.refetch(),
]);
}
};
const handleResume = async () => {
setIsResuming(true);
try {
const result = await resumeStrategy();
if (result?.status === "already_running") {
toast({
title: "Strategy already running",
description: "The paper strategy is already active.",
});
} else if (result?.status === "resumed") {
setFreshStartRequested(false);
toast({
title: "Paper strategy resumed",
description: "The previous paper run has been resumed with its saved schedule.",
});
} else if (result?.status === "no_resumable_run") {
setFreshStartRequested(true);
toast({
title: "No resumable strategy",
description: "Start a fresh paper cycle instead.",
});
} else {
toast({
title: "Resume failed",
description: result?.message || result?.status || "Unable to resume strategy.",
});
}
} catch (error) {
toast({
title: "Resume failed",
description: error instanceof Error ? error.message : "Please try again.",
});
console.error(error);
} finally {
setIsResuming(false);
await Promise.allSettled([
refreshStatus(),
refreshEngineStatus(),
refreshMarketStatus(),
fundsQuery.refetch(), fundsQuery.refetch(),
positionsQuery.refetch(), positionsQuery.refetch(),
ordersQuery.refetch(), ordersQuery.refetch(),
@ -414,6 +542,7 @@ function PaperTradingPortfolio() {
setIsStopping(true); setIsStopping(true);
try { try {
await stopStrategy(); await stopStrategy();
setFreshStartRequested(false);
toast({ toast({
title: "Paper strategy stopped", title: "Paper strategy stopped",
description: "The simulator has been stopped.", description: "The simulator has been stopped.",
@ -426,8 +555,10 @@ function PaperTradingPortfolio() {
console.error(error); console.error(error);
} finally { } finally {
setIsStopping(false); setIsStopping(false);
await Promise.all([ await Promise.allSettled([
refreshStatus(), refreshStatus(),
refreshEngineStatus(),
refreshMarketStatus(),
fundsQuery.refetch(), fundsQuery.refetch(),
positionsQuery.refetch(), positionsQuery.refetch(),
ordersQuery.refetch(), ordersQuery.refetch(),
@ -441,29 +572,100 @@ function PaperTradingPortfolio() {
: strategyStatus === "WAITING" : strategyStatus === "WAITING"
? "WAITING" ? "WAITING"
: "STOPPED"; : "STOPPED";
const canStart = typeof initialCash === "number" && initialCash >= 10000; const isStrategyActive =
const canStop =
normalizedStrategyStatus === "RUNNING" || normalizedStrategyStatus === "RUNNING" ||
normalizedStrategyStatus === "WAITING"; normalizedStrategyStatus === "WAITING";
const strategyLocked = canStop || isStarting || isStopping || isResetting; const canResumeSavedStrategy = !!strategyDetails?.can_resume;
const showResumeStrategy =
!isStrategyActive && canResumeSavedStrategy && !freshStartRequested;
const showRestartStrategy =
!isStrategyActive && !!strategyDetails?.can_restart && !freshStartRequested;
const showFreshStartStrategy =
!isStrategyActive && (!canResumeSavedStrategy || freshStartRequested);
const canEditStrategyConfig = showFreshStartStrategy;
const canStartFresh = typeof initialCash === "number" && initialCash >= 10000;
const canStop = isStrategyActive;
const strategyLocked =
!canEditStrategyConfig || isStarting || isResuming || isStopping || isResetting;
const canAddCash = const canAddCash =
canStop && addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0; canStop && addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0;
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 livenessBadgeLabel =
liveness === "ACTIVE"
? "Engine active"
: liveness === "STALLED"
? "Engine stalled"
: liveness === "DEAD"
? "Engine dead"
: "Engine stopped";
const strategyBadgeClass = const strategyBadgeClass =
normalizedStrategyStatus === "RUNNING" normalizedStrategyStatus === "RUNNING"
? "bg-green-500 text-white" ? "border-emerald-500/50 bg-emerald-500/10 text-emerald-400"
: normalizedStrategyStatus === "WAITING" : normalizedStrategyStatus === "WAITING"
? "bg-yellow-500 text-white" ? "border-amber-400/50 bg-amber-400/10 text-amber-300"
: "bg-gray-400 text-white"; : "border-red-500/40 bg-red-500/10 text-red-400";
const strategyBadgeLabel =
normalizedStrategyStatus === "RUNNING"
? "Strategy running"
: normalizedStrategyStatus === "WAITING"
? "Strategy waiting"
: "Strategy stopped";
const marketState = marketStatus?.status ?? "UNKNOWN"; const marketState = marketStatus?.status ?? "UNKNOWN";
const marketBadgeClass = const canArmStrategy = liveness === "ACTIVE" || liveness === "STOPPED";
marketState === "OPEN" const nextEligibleTs = engineStatus?.next_eligible_ts
? "border-emerald-500/50 bg-emerald-500/15 text-emerald-300" ? new Date(engineStatus.next_eligible_ts)
: marketState === "CLOSED" : null;
? "border-amber-400/50 bg-amber-400/15 text-amber-200" const nextEligibleValid = nextEligibleTs && !Number.isNaN(nextEligibleTs.getTime());
: "border-slate-400/40 bg-slate-400/15 text-slate-200"; const eligibleSeconds = nextEligibleValid
const priceBadgeClass = priceStale ? (nextEligibleTs.getTime() - Date.now()) / 1000
? "border-amber-400/50 bg-amber-400/15 text-amber-200" : Infinity;
: "border-emerald-500/50 bg-emerald-500/15 text-emerald-300"; 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 summary = [ const summary = [
{ label: "Initial Cash", value: mtmInitialCash ?? 0 }, { label: "Initial Cash", value: mtmInitialCash ?? 0 },
@ -547,11 +749,19 @@ function PaperTradingPortfolio() {
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge variant="secondary">Paper trading (simulated)</Badge> <Badge variant="secondary">Paper trading (simulated)</Badge>
<Button variant="outline" onClick={() => { <Button
fundsQuery.refetch(); variant="outline"
positionsQuery.refetch(); onClick={() => {
ordersQuery.refetch(); void Promise.allSettled([
}}> fundsQuery.refetch(),
positionsQuery.refetch(),
ordersQuery.refetch(),
refreshStatus(),
refreshEngineStatus(),
refreshMarketStatus(),
]);
}}
>
Refresh Refresh
</Button> </Button>
</div> </div>
@ -560,35 +770,35 @@ function PaperTradingPortfolio() {
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden"> <div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50"> <div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-semibold">Paper Strategy Control</p> <p className="text-sm font-semibold">Strategy control</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Strategy: Golden Nifty (Paper) · Assets: NIFTYBEES + GOLDBEES Start or stop the Golden Nifty SIP engine from the dashboard.
</p> </p>
{nextSipTs && (
<p className="text-xs text-muted-foreground">
Next SIP {nextSipCountdown ? `in ${nextSipCountdown}` : ""} ({new Date(nextSipTs).toLocaleTimeString()})
</p>
)}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs ${strategyBadgeClass}`}> <Badge variant="outline" className={livenessBadgeClass}>
{normalizedStrategyStatus} {livenessBadgeLabel}
</span>
<Badge variant="outline" className={marketBadgeClass}>
{marketState === "OPEN"
? "Market OPEN"
: marketState === "CLOSED"
? "Market CLOSED"
: "Market UNKNOWN"}
</Badge> </Badge>
{priceStale && ( <Badge variant="outline" className={strategyBadgeClass}>
<Badge variant="outline" className={priceBadgeClass}> {strategyBadgeLabel}
Price STALE </Badge>
{priceStale ? (
<Badge variant="outline" className="border-amber-400/50 bg-amber-400/15 text-amber-200">
Price stale
</Badge> </Badge>
)} ) : null}
</div> </div>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="rounded-lg border border-border/60 bg-background/40 px-4 py-3">
<div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
Next eligible SIP
</div>
<div className="text-sm font-semibold text-foreground">{nextEligibleLine}</div>
{eligibilityStatus ? (
<div className={`text-xs ${eligibilityClass}`}>{eligibilityStatus}</div>
) : null}
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="paper-initial-cash">Initial Cash (Paper)</Label> <Label htmlFor="paper-initial-cash">Initial Cash (Paper)</Label>
<Input <Input
@ -616,7 +826,7 @@ function PaperTradingPortfolio() {
<Checkbox <Checkbox
id="paper-add-cash-toggle" id="paper-add-cash-toggle"
checked={addCashEnabled} checked={addCashEnabled}
disabled={!canStop || isAddingCash || isStarting || isStopping || isResetting} disabled={!canStop || isAddingCash || isStarting || isResuming || isStopping || isResetting}
onCheckedChange={(value) => setAddCashEnabled(value === true)} onCheckedChange={(value) => setAddCashEnabled(value === true)}
/> />
<Label htmlFor="paper-add-cash-toggle">Add cash during run</Label> <Label htmlFor="paper-add-cash-toggle">Add cash during run</Label>
@ -630,7 +840,7 @@ function PaperTradingPortfolio() {
min={1} min={1}
step={100} step={100}
value={addCashAmount} value={addCashAmount}
disabled={!canStop || !addCashEnabled || isAddingCash || isStarting || isStopping || isResetting} disabled={!canStop || !addCashEnabled || isAddingCash || isStarting || isResuming || isStopping || isResetting}
onChange={(event) => { onChange={(event) => {
const raw = event.target.value; const raw = event.target.value;
if (raw === "") { if (raw === "") {
@ -671,7 +881,7 @@ function PaperTradingPortfolio() {
/> />
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_220px]">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="paper-frequency">Frequency</Label> <Label htmlFor="paper-frequency">Frequency</Label>
<Input <Input
@ -689,18 +899,21 @@ function PaperTradingPortfolio() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="paper-frequency-unit">Unit</Label> <Label htmlFor="paper-frequency-unit">Unit</Label>
<select <Select
id="paper-frequency-unit"
value={frequencyUnit} value={frequencyUnit}
disabled={strategyLocked} disabled={strategyLocked}
onChange={(event) => onValueChange={(value) =>
setFrequencyUnit(event.target.value as "minutes" | "days") setFrequencyUnit(value as "minutes" | "days")
} }
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 md:text-sm"
> >
<option value="minutes">Minutes (testing)</option> <SelectTrigger id="paper-frequency-unit">
<option value="days">Days (long-term)</option> <SelectValue placeholder="Select unit" />
</select> </SelectTrigger>
<SelectContent>
<SelectItem value="days">Days</SelectItem>
<SelectItem value="minutes">Minutes (testing)</SelectItem>
</SelectContent>
</Select>
</div> </div>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
@ -708,40 +921,62 @@ function PaperTradingPortfolio() {
</p> </p>
</div> </div>
</div> </div>
{showResumeStrategy ? (
<div className="text-xs text-muted-foreground">
Resume uses the previously saved paper SIP configuration. Choose restart to begin a fresh cycle.
</div>
) : null}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<MotionButton {showResumeStrategy ? (
{...ctaMotionProps} <MotionButton
onClick={handleStart} {...ctaMotionProps}
disabled={!canStart || isStarting || isStopping || isResetting || canStop} onClick={handleResume}
className="shimmer" disabled={isResuming || !canArmStrategy}
> className="shimmer"
{isStarting ? "Starting..." : "Start Paper Strategy"} >
</MotionButton> {isResuming ? "Resuming..." : "Resume Strategy"}
<Button </MotionButton>
variant="outline" ) : null}
onClick={handleStop} {showFreshStartStrategy ? (
disabled={!canStop || isStarting || isStopping || isResetting} <MotionButton
> {...ctaMotionProps}
{isStopping ? "Stopping..." : "Stop Paper Strategy"} onClick={handleStart}
</Button> disabled={!canStartFresh || isStarting || isResuming || isStopping || isResetting || !canArmStrategy || isStrategyActive}
className="shimmer"
>
{isStarting ? "Starting..." : "Start Strategy"}
</MotionButton>
) : null}
{showRestartStrategy ? (
<Button
variant="outline"
onClick={() => setFreshStartRequested(true)}
disabled={isStarting || isResuming || isStopping || isResetting}
>
Restart Strategy
</Button>
) : null}
{isStrategyActive ? (
<Button
variant="outline"
onClick={handleStop}
disabled={!canStop || isStarting || isResuming || isStopping || isResetting}
>
{isStopping ? "Stopping..." : "Stop Strategy"}
</Button>
) : null}
<Button <Button
variant="destructive" variant="destructive"
onClick={handleReset} onClick={handleReset}
disabled={isStarting || isStopping || isResetting} disabled={isStarting || isResuming || isStopping || isResetting}
> >
{isResetting ? "Resetting..." : "Reset Paper Account"} {isResetting ? "Resetting..." : "Reset Paper Account"}
</Button> </Button>
</div> </div>
{marketState === "CLOSED" ? ( <StrategyTimeline compact />
<p className="text-xs text-muted-foreground">
Market CLOSED - orders will execute when the next session opens.
</p>
) : null}
</div> </div>
</div> </div>
<StrategyTimeline key={timelineKey} />
<motion.div <motion.div
className="grid gap-4 md:grid-cols-3" className="grid gap-4 md:grid-cols-3"
variants={cardContainer} variants={cardContainer}