thigal_test

This commit is contained in:
Thigazhezhilan J 2026-03-22 14:37:23 +05:30
parent 465a66fd53
commit aa789d739c
6 changed files with 103 additions and 54 deletions

View File

@ -1,26 +1,16 @@
const API_BASE = "/api"; import { apiRequest } from "@/lib/queryClient";
export async function startStrategy(data) { export async function startStrategy(data) {
const res = await fetch(`${API_BASE}/strategy/start`, { const res = await apiRequest("POST", "/strategy/start", data);
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(data),
});
return res.json(); return res.json();
} }
export async function stopStrategy() { export async function stopStrategy() {
const res = await fetch(`${API_BASE}/strategy/stop`, { const res = await apiRequest("POST", "/strategy/stop");
method: "POST",
credentials: "include",
});
return res.json(); return res.json();
} }
export async function getStrategyStatus() { export async function getStrategyStatus() {
const res = await fetch(`${API_BASE}/strategy/status`, { const res = await apiRequest("GET", "/strategy/status");
credentials: "include",
});
return res.json(); return res.json();
} }

View File

@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { apiRequest } from "@/lib/queryClient";
type StrategyEvent = { type StrategyEvent = {
seq?: number; seq?: number;
@ -110,9 +111,7 @@ export default function StrategyTimeline() {
useEffect(() => { useEffect(() => {
const fetchLogs = async () => { const fetchLogs = async () => {
try { try {
const res = await fetch(`/api/logs?since_seq=${latestSeqRef.current}`, { const res = await apiRequest("GET", `/logs?since_seq=${latestSeqRef.current}`);
credentials: "include",
});
const data = await res.json(); const data = await res.json();
const events = Array.isArray(data?.events) ? data.events : []; const events = Array.isArray(data?.events) ? data.events : [];
const normalized = events.map(normalizeLog); const normalized = events.map(normalizeLog);

View File

@ -250,7 +250,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
data-testid="button-learn-more" data-testid="button-learn-more"
onClick={() => navigate("/learn-more")} onClick={() => navigate("/learn-more")}
> >
Learn More Learn More Test
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -5,11 +5,12 @@ const API_BASE_URL =
((typeof window !== "undefined" && ((typeof window !== "undefined" &&
(window.location.hostname === "localhost" || (window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1")) as any window.location.hostname === "127.0.0.1")) as any
? "http://localhost:8000" ? "http://localhost:8000/api"
: undefined); : undefined);
const NORMALIZED_API_BASE_URL = API_BASE_URL const NORMALIZED_API_BASE_URL = API_BASE_URL
? API_BASE_URL.replace(/\/+$/, "") ? API_BASE_URL.replace(/\/+$/, "")
: ""; : "";
const REQUEST_TIMEOUT_MS = 12000;
function resolveApiUrl(url: string) { function resolveApiUrl(url: string) {
if (url.startsWith("http://") || url.startsWith("https://")) { if (url.startsWith("http://") || url.startsWith("https://")) {
@ -18,7 +19,8 @@ function resolveApiUrl(url: string) {
if (!NORMALIZED_API_BASE_URL) { if (!NORMALIZED_API_BASE_URL) {
return url; return url;
} }
return `${NORMALIZED_API_BASE_URL}${url}`; const normalizedPath = url.startsWith("/") ? url : `/${url}`;
return `${NORMALIZED_API_BASE_URL}${normalizedPath}`;
} }
async function throwIfResNotOk(res: Response) { async function throwIfResNotOk(res: Response) {
@ -28,12 +30,31 @@ async function throwIfResNotOk(res: Response) {
} }
} }
async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) {
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
return await fetch(input, {
...init,
signal: controller.signal,
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new Error("Request timed out. Please try again.");
}
throw error;
} finally {
globalThis.clearTimeout(timeoutId);
}
}
export async function apiRequest( export async function apiRequest(
method: string, method: string,
url: string, url: string,
data?: unknown | undefined, data?: unknown | undefined,
): Promise<Response> { ): Promise<Response> {
const res = await fetch(resolveApiUrl(url), { const res = await fetchWithTimeout(resolveApiUrl(url), {
method, method,
headers: data ? { "Content-Type": "application/json" } : {}, headers: data ? { "Content-Type": "application/json" } : {},
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
@ -50,7 +71,7 @@ export const getQueryFn: <T>(options: {
}) => QueryFunction<T> = }) => QueryFunction<T> =
({ on401: unauthorizedBehavior }) => ({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => { async ({ queryKey }) => {
const res = await fetch(resolveApiUrl(queryKey.join("/") as string), { const res = await fetchWithTimeout(resolveApiUrl(queryKey.join("/") as string), {
credentials: "include", credentials: "include",
}); });

View File

@ -246,12 +246,7 @@ function PaperTradingPortfolio() {
let timer: number; let timer: number;
const poll = async () => { const poll = async () => {
try { try {
const res = await fetch("/paper/mtm", { const res = await apiRequest("GET", "/paper/mtm");
credentials: "include",
});
if (!res.ok) {
return;
}
const data: PaperMtmResponse = await res.json(); const data: PaperMtmResponse = await res.json();
setMtmPositions(Array.isArray(data.positions) ? data.positions : []); setMtmPositions(Array.isArray(data.positions) ? data.positions : []);
if (typeof data.equity === "number") { if (typeof data.equity === "number") {
@ -307,12 +302,7 @@ function PaperTradingPortfolio() {
useEffect(() => { useEffect(() => {
const fetchMarketStatus = async () => { const fetchMarketStatus = async () => {
try { try {
const res = await fetch("/market/status", { const res = await apiRequest("GET", "/market/status");
credentials: "include",
});
if (!res.ok) {
return;
}
const data: MarketStatusResponse = await res.json(); const data: MarketStatusResponse = await res.json();
setMarketStatus(data); setMarketStatus(data);
} catch { } catch {
@ -340,10 +330,7 @@ function PaperTradingPortfolio() {
setIsResetting(true); setIsResetting(true);
try { try {
const res = await fetch("/paper/reset", { method: "POST" }); await apiRequest("POST", "/paper/reset");
if (!res.ok) {
throw new Error("Reset failed");
}
setMtmPositions([]); setMtmPositions([]);
setMtmEquity(null); setMtmEquity(null);
setMtmInitialCash(null); setMtmInitialCash(null);
@ -353,16 +340,21 @@ function PaperTradingPortfolio() {
setMtmPnlPoints([]); setMtmPnlPoints([]);
skipFirstPnlPointRef.current = true; skipFirstPnlPointRef.current = true;
setInitialCash(""); setInitialCash("");
fundsQuery.refetch(); await Promise.all([
positionsQuery.refetch(); fundsQuery.refetch(),
ordersQuery.refetch(); positionsQuery.refetch(),
await refreshStatus(); ordersQuery.refetch(),
refreshStatus(),
]);
setTimelineKey((value) => value + 1); setTimelineKey((value) => value + 1);
setIsResetting(false);
} catch (error) { } catch (error) {
setIsResetting(false); toast({
alert("Reset failed. Check backend logs."); title: "Reset failed",
description: error instanceof Error ? error.message : "Please try again.",
});
console.error(error); console.error(error);
} finally {
setIsResetting(false);
} }
}; };
@ -395,9 +387,26 @@ function PaperTradingPortfolio() {
description: "Paper trading is the only available mode right now.", description: "Paper trading is the only available mode right now.",
}); });
} }
if (result?.status === "started" || result?.status === "restarted") {
toast({
title: "Paper strategy started",
description: "The simulator is now running.",
});
}
} catch (error) {
toast({
title: "Start failed",
description: error instanceof Error ? error.message : "Please try again.",
});
console.error(error);
} finally { } finally {
setIsStarting(false); setIsStarting(false);
await refreshStatus(); await Promise.all([
refreshStatus(),
fundsQuery.refetch(),
positionsQuery.refetch(),
ordersQuery.refetch(),
]);
} }
}; };
@ -405,9 +414,24 @@ function PaperTradingPortfolio() {
setIsStopping(true); setIsStopping(true);
try { try {
await stopStrategy(); await stopStrategy();
toast({
title: "Paper strategy stopped",
description: "The simulator has been stopped.",
});
} catch (error) {
toast({
title: "Stop failed",
description: error instanceof Error ? error.message : "Please try again.",
});
console.error(error);
} finally { } finally {
setIsStopping(false); setIsStopping(false);
await refreshStatus(); await Promise.all([
refreshStatus(),
fundsQuery.refetch(),
positionsQuery.refetch(),
ordersQuery.refetch(),
]);
} }
}; };
@ -418,8 +442,12 @@ function PaperTradingPortfolio() {
? "WAITING" ? "WAITING"
: "STOPPED"; : "STOPPED";
const canStart = typeof initialCash === "number" && initialCash >= 10000; const canStart = typeof initialCash === "number" && initialCash >= 10000;
const canStop =
normalizedStrategyStatus === "RUNNING" ||
normalizedStrategyStatus === "WAITING";
const strategyLocked = canStop || isStarting || isStopping || isResetting;
const canAddCash = const canAddCash =
addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0; canStop && addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0;
const strategyBadgeClass = const strategyBadgeClass =
normalizedStrategyStatus === "RUNNING" normalizedStrategyStatus === "RUNNING"
? "bg-green-500 text-white" ? "bg-green-500 text-white"
@ -568,7 +596,7 @@ function PaperTradingPortfolio() {
type="number" type="number"
min={10000} min={10000}
value={initialCash} value={initialCash}
disabled={normalizedStrategyStatus === "RUNNING"} disabled={strategyLocked}
onChange={(event) => { onChange={(event) => {
const raw = event.target.value; const raw = event.target.value;
if (raw === "") { if (raw === "") {
@ -588,6 +616,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}
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>
@ -601,7 +630,7 @@ function PaperTradingPortfolio() {
min={1} min={1}
step={100} step={100}
value={addCashAmount} value={addCashAmount}
disabled={!addCashEnabled || isAddingCash} disabled={!canStop || !addCashEnabled || isAddingCash || isStarting || isStopping || isResetting}
onChange={(event) => { onChange={(event) => {
const raw = event.target.value; const raw = event.target.value;
if (raw === "") { if (raw === "") {
@ -634,6 +663,7 @@ function PaperTradingPortfolio() {
min={0} min={0}
step={100} step={100}
value={sipAmount} value={sipAmount}
disabled={strategyLocked}
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);
@ -650,6 +680,7 @@ function PaperTradingPortfolio() {
min={1} min={1}
step={1} step={1}
value={frequencyValue} value={frequencyValue}
disabled={strategyLocked}
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);
@ -661,6 +692,7 @@ function PaperTradingPortfolio() {
<select <select
id="paper-frequency-unit" id="paper-frequency-unit"
value={frequencyUnit} value={frequencyUnit}
disabled={strategyLocked}
onChange={(event) => onChange={(event) =>
setFrequencyUnit(event.target.value as "minutes" | "days") setFrequencyUnit(event.target.value as "minutes" | "days")
} }
@ -680,18 +712,22 @@ function PaperTradingPortfolio() {
<MotionButton <MotionButton
{...ctaMotionProps} {...ctaMotionProps}
onClick={handleStart} onClick={handleStart}
disabled={!canStart || isStarting || isResetting || normalizedStrategyStatus === "RUNNING"} disabled={!canStart || isStarting || isStopping || isResetting || canStop}
className="shimmer" className="shimmer"
> >
{isStarting ? "Starting..." : "Start Paper Strategy"} {isStarting ? "Starting..." : "Start Paper Strategy"}
</MotionButton> </MotionButton>
<Button variant="outline" onClick={handleStop} disabled={isStopping}> <Button
variant="outline"
onClick={handleStop}
disabled={!canStop || isStarting || isStopping || isResetting}
>
{isStopping ? "Stopping..." : "Stop Paper Strategy"} {isStopping ? "Stopping..." : "Stop Paper Strategy"}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
onClick={handleReset} onClick={handleReset}
disabled={isResetting} disabled={isStarting || isStopping || isResetting}
> >
{isResetting ? "Resetting..." : "Reset Paper Account"} {isResetting ? "Resetting..." : "Reset Paper Account"}
</Button> </Button>

View File

@ -3,6 +3,9 @@ import react from "@vitejs/plugin-react";
import path from "path"; import path from "path";
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
const proxyTarget =
process.env.VITE_PROXY_TARGET?.trim() || "http://localhost:8000";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
@ -32,7 +35,7 @@ export default defineConfig({
allowedHosts: ["*","app.quantfortune.com"], allowedHosts: ["*","app.quantfortune.com"],
proxy: { proxy: {
"/api": { "/api": {
target: "https://api.quantfortune.com/", target: proxyTarget,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },