Add resume strategy functionality and update UI for resuming strategies

This commit is contained in:
Thigazhezhilan J 2026-03-28 13:06:42 +05:30
parent 67b633ae9b
commit 5720cdb63c
3 changed files with 159 additions and 14 deletions

View File

@ -10,6 +10,11 @@ export async function stopStrategy() {
return res.json(); return res.json();
} }
export async function resumeStrategy() {
const res = await apiRequest("POST", "/strategy/resume");
return res.json();
}
export async function getStrategyStatus() { export async function getStrategyStatus() {
const res = await apiRequest("GET", "/strategy/status"); const res = await apiRequest("GET", "/strategy/status");
return res.json(); return res.json();

View File

@ -88,6 +88,10 @@ function getBadge(entry: StrategyEvent) {
return { label: "STOP", className: "border-slate-400/40 bg-slate-400/20 text-slate-200" }; return { label: "STOP", className: "border-slate-400/40 bg-slate-400/20 text-slate-200" };
} }
if (eventName === "STRATEGY_RESUMED") {
return { label: "RESUME", className: "border-cyan-400/40 bg-cyan-400/20 text-cyan-200" };
}
if (eventName === "ORDER_PLACED") { if (eventName === "ORDER_PLACED") {
const side = typeof meta.side === "string" ? meta.side.toUpperCase() : ""; const side = typeof meta.side === "string" ? meta.side.toUpperCase() : "";
if (side === "BUY") { if (side === "BUY") {

View File

@ -6,7 +6,7 @@ import { Wallet, BarChart3, AlertCircle, RefreshCcw, PlugZap } from "lucide-reac
import BrokerConnectDialog from "./BrokerConnectDialog"; import BrokerConnectDialog from "./BrokerConnectDialog";
import LoginRequiredDialog from "./LoginRequiredDialog"; import LoginRequiredDialog from "./LoginRequiredDialog";
import { getQueryFn, apiRequest, queryClient } from "@/lib/queryClient"; import { getQueryFn, apiRequest, queryClient } from "@/lib/queryClient";
import { getStrategyStatus, startStrategy, stopStrategy } from "@/api/strategy"; import { getStrategyStatus, resumeStrategy, startStrategy, stopStrategy } from "@/api/strategy";
import { Badge } from "@/components/ui/badge"; 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";
@ -83,6 +83,27 @@ type EngineStatus = {
next_eligible_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;
};
type SessionUser = Pick<User, "id" | "username">; type SessionUser = Pick<User, "id" | "username">;
const MotionButton = motion(Button); const MotionButton = motion(Button);
@ -342,19 +363,38 @@ export default function PortfolioSection() {
const [frequencyValue, setFrequencyValue] = useState(30); const [frequencyValue, setFrequencyValue] = useState(30);
const [frequencyUnit, setFrequencyUnit] = useState<"days" | "minutes">("days"); const [frequencyUnit, setFrequencyUnit] = useState<"days" | "minutes">("days");
const [strategyStatus, setStrategyStatus] = useState("STOPPED"); const [strategyStatus, setStrategyStatus] = useState("STOPPED");
const [strategyDetails, setStrategyDetails] = useState<StrategyStatusResponse | 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 [freshStartRequested, setFreshStartRequested] = 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 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");
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 (error) { } catch (error) {
setStrategyDetails(null);
setStrategyStatus("STOPPED"); setStrategyStatus("STOPPED");
} }
}, []); }, [freshStartRequested]);
useEffect(() => { useEffect(() => {
refreshStatus(); refreshStatus();
@ -581,6 +621,14 @@ export default function PortfolioSection() {
const isStrategyActive = const isStrategyActive =
normalizedStrategyStatus === "RUNNING" || normalizedStrategyStatus === "RUNNING" ||
normalizedStrategyStatus === "WAITING"; normalizedStrategyStatus === "WAITING";
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 heartbeatAgeSec = engineStatus?.last_heartbeat_ts const heartbeatAgeSec = engineStatus?.last_heartbeat_ts
? (Date.now() - new Date(engineStatus.last_heartbeat_ts).getTime()) / 1000 ? (Date.now() - new Date(engineStatus.last_heartbeat_ts).getTime()) / 1000
@ -741,6 +789,7 @@ export default function PortfolioSection() {
description: "The engine is already active.", description: "The engine is already active.",
}); });
} else if (result?.status === "started" || result?.status === "restarted") { } else if (result?.status === "started" || result?.status === "restarted") {
setFreshStartRequested(false);
toast({ toast({
title: "Live strategy started", title: "Live strategy started",
description: "Golden Nifty is now armed for live Zerodha execution.", description: "Golden Nifty is now armed for live Zerodha execution.",
@ -760,10 +809,70 @@ export default function PortfolioSection() {
} }
}; };
const handleResume = async () => {
if (!(await requireLogin())) {
return;
}
if (!isConnected) {
setConnectPromptOpen(true);
return;
}
if (showSessionExpired || brokerAuthExpired) {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect before resuming the live strategy.",
});
handleReconnectClick();
return;
}
setIsResuming(true);
try {
const result = await resumeStrategy();
if (result?.status === "broker_auth_required") {
toast({
title: "Reconnect broker",
description: "Your Zerodha session has expired. Reconnect and resume again.",
});
await startSavedBrokerReconnect();
return;
}
if (result?.status === "already_running") {
toast({
title: "Strategy already running",
description: "The engine is already active.",
});
} else if (result?.status === "resumed") {
setFreshStartRequested(false);
toast({
title: "Live strategy resumed",
description: "The previous live 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 cycle instead.",
});
} else {
toast({
title: "Resume failed",
description: result?.message || result?.status || "Unable to resume strategy.",
});
}
} finally {
setIsResuming(false);
await Promise.allSettled([
refreshStatus(),
refreshBrokerData({ includeEquityCurve: true }),
]);
}
};
const handleStop = async () => { const handleStop = async () => {
setIsStopping(true); setIsStopping(true);
try { try {
await stopStrategy(); await stopStrategy();
setFreshStartRequested(false);
} finally { } finally {
setIsStopping(false); setIsStopping(false);
await Promise.allSettled([ await Promise.allSettled([
@ -1095,6 +1204,7 @@ export default function PortfolioSection() {
min={0} min={0}
step={100} step={100}
value={sipAmount} value={sipAmount}
disabled={!canEditStrategyConfig}
onChange={(event) => { onChange={(event) => {
const value = Number(event.target.value); const value = Number(event.target.value);
setSipAmount(Number.isNaN(value) ? 0 : value); setSipAmount(Number.isNaN(value) ? 0 : value);
@ -1110,6 +1220,7 @@ export default function PortfolioSection() {
min={1} min={1}
step={1} step={1}
value={frequencyValue} value={frequencyValue}
disabled={!canEditStrategyConfig}
onChange={(event) => { onChange={(event) => {
const value = Number(event.target.value); const value = Number(event.target.value);
setFrequencyValue(Number.isNaN(value) ? 1 : value); setFrequencyValue(Number.isNaN(value) ? 1 : value);
@ -1120,6 +1231,7 @@ export default function PortfolioSection() {
<Label>Unit</Label> <Label>Unit</Label>
<Select <Select
value={frequencyUnit} value={frequencyUnit}
disabled={!canEditStrategyConfig}
onValueChange={(value) => setFrequencyUnit(value as "days" | "minutes")} onValueChange={(value) => setFrequencyUnit(value as "days" | "minutes")}
> >
<SelectTrigger> <SelectTrigger>
@ -1138,7 +1250,23 @@ export default function PortfolioSection() {
Minutes mode is for live testing only. The engine checks every few seconds in this mode. Minutes mode is for live testing only. The engine checks every few seconds in this mode.
</div> </div>
) : null} ) : null}
{showResumeStrategy ? (
<div className="text-xs text-muted-foreground">
Resume uses the previously saved 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">
{showResumeStrategy ? (
<MotionButton
{...ctaMotionProps}
onClick={handleResume}
disabled={isResuming || !canArmStrategy}
className="shimmer"
>
{isResuming ? "Resuming..." : "Resume Strategy"}
</MotionButton>
) : null}
{showFreshStartStrategy ? (
<MotionButton <MotionButton
{...ctaMotionProps} {...ctaMotionProps}
onClick={handleStart} onClick={handleStart}
@ -1147,9 +1275,17 @@ export default function PortfolioSection() {
> >
{isStarting ? "Starting..." : "Start Strategy"} {isStarting ? "Starting..." : "Start Strategy"}
</MotionButton> </MotionButton>
) : null}
{showRestartStrategy ? (
<Button variant="outline" onClick={() => setFreshStartRequested(true)}>
Restart Strategy
</Button>
) : null}
{isStrategyActive ? (
<Button variant="outline" onClick={handleStop} disabled={isStopping}> <Button variant="outline" onClick={handleStop} disabled={isStopping}>
{isStopping ? "Stopping..." : "Stop Strategy"} {isStopping ? "Stopping..." : "Stop Strategy"}
</Button> </Button>
) : null}
</div> </div>
<StrategyTimeline compact /> <StrategyTimeline compact />
</div> </div>