thigal_test
This commit is contained in:
parent
465a66fd53
commit
aa789d739c
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user