Compare commits
No commits in common. "b3b798ff2f86ab46e1f5fd28f8dc750c6b398abd" and "d10bb3dd7879c9cb232788f21af753b294fa91f7" have entirely different histories.
b3b798ff2f
...
d10bb3dd78
@ -6,10 +6,9 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "npm run typecheck",
|
||||
"preview": "vite preview",
|
||||
"start": "vite --host 0.0.0.0 --port 3001"
|
||||
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
||||
21
src/api/strategy.js
Normal file
21
src/api/strategy.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export async function startStrategy(data) {
|
||||
const res = await apiRequest("POST", "/strategy/start", data);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function stopStrategy() {
|
||||
const res = await apiRequest("POST", "/strategy/stop");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function resumeStrategy() {
|
||||
const res = await apiRequest("POST", "/strategy/resume");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getStrategyStatus() {
|
||||
const res = await apiRequest("GET", "/strategy/status");
|
||||
return res.json();
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export type StrategyFrequencyUnit = "days" | "minutes";
|
||||
|
||||
export type StrategyStartRequest = {
|
||||
strategy_name: string;
|
||||
sip_amount: number;
|
||||
sip_frequency: {
|
||||
value: number;
|
||||
unit: StrategyFrequencyUnit;
|
||||
};
|
||||
mode: "LIVE" | "PAPER";
|
||||
initial_cash?: number;
|
||||
};
|
||||
|
||||
export type StrategyActionResponse = {
|
||||
status: string;
|
||||
run_id?: string | null;
|
||||
message?: string;
|
||||
redirect_url?: string | null;
|
||||
broker?: string | null;
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
export 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?: StrategyFrequencyUnit | null;
|
||||
} | null;
|
||||
mode?: string | null;
|
||||
broker?: string | null;
|
||||
active?: boolean | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function startStrategy(data: StrategyStartRequest): Promise<StrategyActionResponse> {
|
||||
const res = await apiRequest("POST", "/strategy/start", data);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function stopStrategy(): Promise<StrategyActionResponse> {
|
||||
const res = await apiRequest("POST", "/strategy/stop");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function resumeStrategy(): Promise<StrategyActionResponse> {
|
||||
const res = await apiRequest("POST", "/strategy/resume");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getStrategyStatus(): Promise<StrategyStatusResponse> {
|
||||
const res = await apiRequest("GET", "/strategy/status");
|
||||
return res.json();
|
||||
}
|
||||
@ -33,7 +33,9 @@ type StrategyTimelineProps = {
|
||||
|
||||
function parseTimestamp(value?: string | null) {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value);
|
||||
const hasTimezone = /Z|[+-]\d{2}:?\d{2}$/.test(value);
|
||||
const normalized = hasTimezone ? value : `${value}Z`;
|
||||
const parsed = new Date(normalized);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
@ -197,7 +199,7 @@ function VerboseStrategyTimeline() {
|
||||
prev.map((entry) => entry.seq).filter((seq) => typeof seq === "number"),
|
||||
);
|
||||
const next = normalized.filter(
|
||||
(entry: StrategyEvent) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
||||
(entry) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
||||
);
|
||||
return [...prev, ...next];
|
||||
});
|
||||
@ -206,7 +208,7 @@ function VerboseStrategyTimeline() {
|
||||
latestSeqRef.current = data.latest_seq;
|
||||
} else {
|
||||
const lastSeq = normalized.reduce(
|
||||
(max: number, entry: StrategyEvent) => Math.max(max, entry.seq ?? 0),
|
||||
(max, entry) => Math.max(max, entry.seq ?? 0),
|
||||
latestSeqRef.current,
|
||||
);
|
||||
latestSeqRef.current = lastSeq;
|
||||
@ -304,7 +306,7 @@ function VerboseStrategyTimeline() {
|
||||
<div className="mt-1 text-xs text-green-300/80">Reason: {reason}</div>
|
||||
) : null}
|
||||
<div className="mt-3 space-y-3">
|
||||
{run.events.map((entry: StrategyEvent, index: number) => {
|
||||
{run.events.map((entry, index) => {
|
||||
const badge = getBadge(entry);
|
||||
const timestamp = entry.ts ?? entry.timestamp;
|
||||
const message = entry.message ?? entry.event ?? "Unknown event";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import FinalCTA from "../landing/FinalCTA";
|
||||
|
||||
export default function FinalCTAExample() {
|
||||
return <FinalCTA onExploreStrategies={() => {}} />;
|
||||
return <FinalCTA />;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import HeroSection from "../landing/HeroSection";
|
||||
|
||||
export default function HeroSectionExample() {
|
||||
return <HeroSection onExploreStrategies={() => {}} />;
|
||||
return <HeroSection />;
|
||||
}
|
||||
|
||||
@ -252,7 +252,7 @@ export default function AuthDialogs({ layout = "desktop" }: AuthDialogsProps) {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="link"
|
||||
className="w-full text-sm"
|
||||
onClick={() => {
|
||||
setResetEmail(loginForm.email.trim());
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { GOLDEN_NIFTY_PATH } from "@/lib/routes";
|
||||
|
||||
export default function Footer() {
|
||||
const links: Record<string, { label: string; href: string; newTab?: boolean }[]> = {
|
||||
const links = {
|
||||
Product: [
|
||||
{ label: "Strategies", href: GOLDEN_NIFTY_PATH, newTab: true },
|
||||
{ label: "Pricing", href: "#" },
|
||||
|
||||
@ -52,10 +52,8 @@ type PositionsResponse = {
|
||||
};
|
||||
|
||||
type MarketStatusResponse = {
|
||||
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
||||
reason?: string;
|
||||
status?: "OPEN" | "CLOSED";
|
||||
checked_at?: string;
|
||||
next_open_at?: string | null;
|
||||
};
|
||||
|
||||
type FundsResponse = {
|
||||
@ -197,16 +195,6 @@ function getNetQuantity(item: any) {
|
||||
return getEffectiveQuantity(item);
|
||||
}
|
||||
|
||||
function formatFrequencyLabel(value?: number | null, unit?: string | null) {
|
||||
const safeValue = Number(value);
|
||||
if (!Number.isFinite(safeValue) || safeValue <= 0 || !unit) {
|
||||
return null;
|
||||
}
|
||||
const normalizedUnit = unit === "minutes" ? "minute" : unit === "days" ? "day" : unit;
|
||||
const pluralizedUnit = safeValue === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
||||
return `${safeValue} ${pluralizedUnit}`;
|
||||
}
|
||||
|
||||
function getAveragePrice(item: any) {
|
||||
return firstNumber(item?.average_price, item?.avg_price);
|
||||
}
|
||||
@ -448,6 +436,16 @@ export default function PortfolioSection() {
|
||||
enabled: !!brokerStatus?.connected,
|
||||
retry: 1,
|
||||
retryDelay: 600,
|
||||
onSuccess: (data) => {
|
||||
setCachedHoldings(data?.holdings || []);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
},
|
||||
onError: () => {
|
||||
if (brokerStatus?.connected) {
|
||||
setSessionExpired(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const fundsQuery = useQuery<FundsResponse>({
|
||||
@ -459,6 +457,16 @@ export default function PortfolioSection() {
|
||||
enabled: !!brokerStatus?.connected,
|
||||
retry: 1,
|
||||
retryDelay: 600,
|
||||
onSuccess: (data) => {
|
||||
setCachedFunds(data?.funds ?? null);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
},
|
||||
onError: () => {
|
||||
if (brokerStatus?.connected) {
|
||||
setSessionExpired(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [startDate, setStartDate] = useState(() =>
|
||||
@ -599,6 +607,16 @@ export default function PortfolioSection() {
|
||||
enabled: !!brokerStatus?.connected,
|
||||
retry: 1,
|
||||
retryDelay: 600,
|
||||
onSuccess: (data) => {
|
||||
setCachedEquityCurve(data ?? null);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
},
|
||||
onError: () => {
|
||||
if (brokerStatus?.connected) {
|
||||
setSessionExpired(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const positionsQuery = useQuery<PositionsResponse>({
|
||||
@ -610,6 +628,16 @@ export default function PortfolioSection() {
|
||||
enabled: !!brokerStatus?.connected,
|
||||
retry: 1,
|
||||
retryDelay: 600,
|
||||
onSuccess: (data) => {
|
||||
setCachedPositions(data?.positions || []);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
},
|
||||
onError: () => {
|
||||
if (brokerStatus?.connected) {
|
||||
setSessionExpired(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const refreshBrokerData = useCallback(
|
||||
@ -617,7 +645,7 @@ export default function PortfolioSection() {
|
||||
if (!brokerStatus?.connected) {
|
||||
return;
|
||||
}
|
||||
const tasks: Promise<unknown>[] = [
|
||||
const tasks = [
|
||||
holdingsQuery.refetch(),
|
||||
positionsQuery.refetch(),
|
||||
fundsQuery.refetch(),
|
||||
@ -640,58 +668,11 @@ export default function PortfolioSection() {
|
||||
|
||||
const isConnected = !!brokerStatus?.connected;
|
||||
const isAuthed = brokerStatus !== null;
|
||||
const holdings = holdingsQuery.data?.holdings ?? cachedHoldings;
|
||||
const positions = positionsQuery.data?.positions ?? cachedPositions;
|
||||
const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings;
|
||||
const positions = positionsQuery.data ? positionsQuery.data.positions : cachedPositions;
|
||||
const fundsSnapshot = fundsQuery.data?.funds ?? cachedFunds;
|
||||
const noHoldings = holdings.length === 0;
|
||||
const noPositions = positions.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (holdingsQuery.data) {
|
||||
setCachedHoldings(holdingsQuery.data.holdings ?? []);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
}
|
||||
}, [holdingsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fundsQuery.data) {
|
||||
setCachedFunds(fundsQuery.data.funds ?? null);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
}
|
||||
}, [fundsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (equityCurveQuery.data) {
|
||||
setCachedEquityCurve(equityCurveQuery.data);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
}
|
||||
}, [equityCurveQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (positionsQuery.data) {
|
||||
setCachedPositions(positionsQuery.data.positions ?? []);
|
||||
setSessionExpired(false);
|
||||
setReconnectAttempted(false);
|
||||
}
|
||||
}, [positionsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
brokerStatus?.connected &&
|
||||
(holdingsQuery.isError || fundsQuery.isError || equityCurveQuery.isError || positionsQuery.isError)
|
||||
) {
|
||||
setSessionExpired(true);
|
||||
}
|
||||
}, [
|
||||
brokerStatus?.connected,
|
||||
holdingsQuery.isError,
|
||||
fundsQuery.isError,
|
||||
equityCurveQuery.isError,
|
||||
positionsQuery.isError,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
setSessionExpired(false);
|
||||
@ -781,38 +762,6 @@ export default function PortfolioSection() {
|
||||
: strategyStatus === "WAITING"
|
||||
? "WAITING"
|
||||
: "STOPPED";
|
||||
const savedStrategyAmount = Number(strategyDetails?.config?.sip_amount);
|
||||
const savedStrategyFrequencyValue = Number(strategyDetails?.config?.sip_frequency?.value);
|
||||
const savedStrategyFrequencyUnit =
|
||||
strategyDetails?.config?.sip_frequency?.unit === "minutes"
|
||||
? "minutes"
|
||||
: strategyDetails?.config?.sip_frequency?.unit === "days"
|
||||
? "days"
|
||||
: null;
|
||||
const savedStrategyCadence = formatFrequencyLabel(
|
||||
savedStrategyFrequencyValue,
|
||||
savedStrategyFrequencyUnit,
|
||||
);
|
||||
const savedStrategyAmountLabel =
|
||||
Number.isFinite(savedStrategyAmount) && savedStrategyAmount > 0
|
||||
? formatCurrency(savedStrategyAmount)
|
||||
: null;
|
||||
const savedStrategyMode =
|
||||
strategyDetails?.config?.mode === "LIVE"
|
||||
? "Live"
|
||||
: strategyDetails?.config?.mode === "PAPER"
|
||||
? "Paper"
|
||||
: null;
|
||||
const savedStrategyBroker =
|
||||
strategyDetails?.config?.broker ? formatBrokerName(strategyDetails.config.broker) : null;
|
||||
const savedStrategySummary = [
|
||||
savedStrategyAmountLabel ? `${savedStrategyAmountLabel} SIP` : null,
|
||||
savedStrategyCadence ? `every ${savedStrategyCadence}` : null,
|
||||
savedStrategyMode,
|
||||
savedStrategyBroker,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
const isStrategyActive =
|
||||
normalizedStrategyStatus === "RUNNING" ||
|
||||
normalizedStrategyStatus === "WAITING";
|
||||
@ -1534,12 +1483,6 @@ export default function PortfolioSection() {
|
||||
Minutes mode is for live testing only. The engine checks every few seconds in this mode.
|
||||
</div>
|
||||
) : null}
|
||||
{savedStrategySummary ? (
|
||||
<div className="rounded-lg border border-border/60 bg-background/40 px-3 py-2 text-xs text-muted-foreground">
|
||||
{isStrategyActive ? "Current run" : showResumeStrategy ? "Resume will continue" : "Saved strategy config"}
|
||||
: <span className="font-medium text-foreground"> {savedStrategySummary}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{showResumeStrategy ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Resume uses the previously saved SIP configuration. Choose restart to begin a fresh cycle.
|
||||
|
||||
@ -43,11 +43,8 @@ async function getResponseErrorMessage(res: Response) {
|
||||
if (contentType.includes("application/json")) {
|
||||
try {
|
||||
const data = await res.json();
|
||||
const structuredDetail = typeof data?.detail === "object" && data?.detail !== null ? data.detail : null;
|
||||
const detail =
|
||||
(typeof data?.detail === "string" && data.detail) ||
|
||||
(typeof structuredDetail?.message === "string" && structuredDetail.message) ||
|
||||
(typeof structuredDetail?.detail === "string" && structuredDetail.detail) ||
|
||||
(typeof data?.message === "string" && data.message) ||
|
||||
(typeof data?.error === "string" && data.error);
|
||||
if (detail) {
|
||||
@ -164,7 +161,7 @@ export const queryClient = new QueryClient({
|
||||
queryFn: getQueryFn({ on401: "throw" }),
|
||||
refetchInterval: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5_000,
|
||||
staleTime: Infinity,
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
|
||||
@ -43,6 +43,7 @@ const trustTimeline = [
|
||||
|
||||
export default function About() {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const heroMotion = prefersReducedMotion ? {} : pageEnterMotion;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
@ -51,9 +52,9 @@ export default function About() {
|
||||
<main className="pt-24 pb-16">
|
||||
<motion.section
|
||||
className="relative overflow-hidden"
|
||||
{...(prefersReducedMotion
|
||||
? {}
|
||||
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
||||
initial={heroMotion.initial}
|
||||
animate={heroMotion.animate}
|
||||
transition={heroMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
||||
|
||||
@ -35,6 +35,7 @@ const posts: BlogPost[] = [
|
||||
|
||||
export default function Blog() {
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const heroMotion = prefersReducedMotion ? {} : pageEnterMotion;
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@ -49,9 +50,9 @@ export default function Blog() {
|
||||
<main className="pt-24 pb-16">
|
||||
<motion.section
|
||||
className="relative overflow-hidden px-6 py-16"
|
||||
{...(prefersReducedMotion
|
||||
? {}
|
||||
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
||||
initial={heroMotion.initial}
|
||||
animate={heroMotion.animate}
|
||||
transition={heroMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(59,130,246,0.12),transparent_0)] [background-size:120px_120px] opacity-40" />
|
||||
|
||||
@ -51,6 +51,7 @@ export default function Disclosures() {
|
||||
className="relative overflow-hidden px-6 py-16"
|
||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||
|
||||
@ -59,6 +59,7 @@ export default function LearnMore() {
|
||||
className="relative overflow-hidden px-6 py-16"
|
||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -left-16 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
||||
|
||||
@ -88,10 +88,8 @@ type PaperMtmResponse = {
|
||||
};
|
||||
|
||||
type MarketStatusResponse = {
|
||||
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
||||
reason?: string;
|
||||
status?: "OPEN" | "CLOSED";
|
||||
checked_at?: string;
|
||||
next_open_at?: string | null;
|
||||
};
|
||||
|
||||
type EngineStatus = {
|
||||
@ -168,7 +166,7 @@ function formatSignedCurrency(value: number, decimals = 2) {
|
||||
}
|
||||
|
||||
function formatTimestamp(value?: string) {
|
||||
if (!value) return "-";
|
||||
if (!value) return "—";
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value;
|
||||
|
||||
@ -63,6 +63,7 @@ export default function Privacy() {
|
||||
className="relative overflow-hidden px-6 py-16"
|
||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||
|
||||
@ -51,6 +51,7 @@ export default function Terms() {
|
||||
className="relative overflow-hidden px-6 py-16"
|
||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user