Mirror live strategy controls in paper portfolio
This commit is contained in:
parent
b0c1ea3651
commit
434be478e4
@ -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);
|
||||||
} catch {
|
if (!freshStartRequested && Number.isFinite(savedAmount) && savedAmount > 0) {
|
||||||
setStrategyStatus("STOPPED");
|
setSipAmount(savedAmount);
|
||||||
setNextSipTs(null);
|
|
||||||
}
|
}
|
||||||
}, []);
|
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 {
|
||||||
|
setStrategyDetails(null);
|
||||||
|
setStrategyStatus("STOPPED");
|
||||||
|
}
|
||||||
|
}, [freshStartRequested]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
@ -212,35 +297,21 @@ function PaperTradingPortfolio() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const refreshEngineStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await apiRequest("GET", "/engine/status");
|
||||||
|
const data: EngineStatus = await res.json();
|
||||||
|
setEngineStatus(data);
|
||||||
|
} catch {
|
||||||
|
setEngineStatus(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!nextSipTs) {
|
void refreshEngineStatus();
|
||||||
setNextSipCountdown(null);
|
const id = window.setInterval(refreshEngineStatus, 5000);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updateCountdown = () => {
|
|
||||||
const target = new Date(nextSipTs).getTime();
|
|
||||||
if (Number.isNaN(target)) {
|
|
||||||
setNextSipCountdown(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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,8 +370,7 @@ 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();
|
||||||
@ -308,13 +378,20 @@ function PaperTradingPortfolio() {
|
|||||||
} 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>
|
</Badge>
|
||||||
)}
|
{priceStale ? (
|
||||||
|
<Badge variant="outline" className="border-amber-400/50 bg-amber-400/15 text-amber-200">
|
||||||
|
Price stale
|
||||||
|
</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">
|
||||||
|
{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}
|
||||||
disabled={!canStart || isStarting || isStopping || isResetting || canStop}
|
disabled={!canStartFresh || isStarting || isResuming || isStopping || isResetting || !canArmStrategy || isStrategyActive}
|
||||||
className="shimmer"
|
className="shimmer"
|
||||||
>
|
>
|
||||||
{isStarting ? "Starting..." : "Start Paper Strategy"}
|
{isStarting ? "Starting..." : "Start Strategy"}
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
|
) : null}
|
||||||
|
{showRestartStrategy ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setFreshStartRequested(true)}
|
||||||
|
disabled={isStarting || isResuming || isStopping || isResetting}
|
||||||
|
>
|
||||||
|
Restart Strategy
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{isStrategyActive ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={!canStop || isStarting || isStopping || isResetting}
|
disabled={!canStop || isStarting || isResuming || isStopping || isResetting}
|
||||||
>
|
>
|
||||||
{isStopping ? "Stopping..." : "Stop Paper Strategy"}
|
{isStopping ? "Stopping..." : "Stop Strategy"}
|
||||||
</Button>
|
</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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user