"use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Trash2 } from "lucide-react"; import Link from "next/link"; import { useMemo, useState } from "react"; import { StatusBadge } from "../page"; import { LinesChart, type LineSpec } from "@/components/charts"; import { Button, Card, Empty, ErrorBox, PageHeader, Spinner, Table, Td, Th } from "@/components/ui"; import { api } from "@/lib/api"; import { fmtIso, fmtNum, fmtPct, fmtPctPlain, fmtRatio, signClass } from "@/lib/format"; import type { EquityPoint, Metrics, RunRow } from "@/lib/types"; const COLORS = ["#4f8cff", "#22c55e", "#f59e0b", "#ef4444", "#a855f7", "#14b8a6", "#f472b6", "#eab308"]; type CompareItem = { id: string; strategy: string; symbol: string; interval_min: number; params: Record; metrics: Metrics; equity: EquityPoint[]; label: string | null }; export default function BacktestsPage() { const qc = useQueryClient(); const runs = useQuery({ queryKey: ["runs", 200], queryFn: () => api<{ items: RunRow[] }>("/api/backtests?limit=200"), refetchInterval: (q) => (q.state.data?.items.some((r) => r.status === "queued" || r.status === "running") ? 2_000 : 15_000), }); const [sel, setSel] = useState([]); const compare = useQuery({ queryKey: ["compare", sel], queryFn: () => api<{ items: CompareItem[] }>("/api/backtests/compare", { method: "POST", json: sel }), enabled: sel.length >= 2 }); const del = useMutation({ mutationFn: (id: string) => api(`/api/backtests/${id}`, { method: "DELETE" }), onSuccess: () => qc.invalidateQueries({ queryKey: ["runs"] }) }); const toggle = (id: string) => setSel((s) => (s.includes(id) ? s.filter((x) => x !== id) : s.length < 8 ? [...s, id] : s)); const series: LineSpec[] = useMemo(() => (compare.data?.items ?? []).map((it, i) => { const e0 = it.equity[0]?.equity ?? 1; return { id: it.id, label: `${it.strategy} ${it.symbol} ${it.interval_min}m`, color: COLORS[i % COLORS.length], points: it.equity.map((p) => ({ ts: p.ts, value: (p.equity / e0 - 1) * 100 })) }; }), [compare.data]); return ( <> {sel.length >= 2 && ( setSel([])}>Clear}> {compare.isLoading && } {compare.error && } {compare.data && ( <> {compare.data.items.map((it, i) => ( ))}
RunParamsCAGRVolSharpeSortinoMax DDCalmarTurnoverTradesFee drag
{it.strategy} {it.symbol} {it.interval_min}m{it.label && ({it.label})} {JSON.stringify(it.params)} {fmtPct(it.metrics.cagr)}{fmtPctPlain(it.metrics.volatility)}{fmtRatio(it.metrics.sharpe)}{fmtRatio(it.metrics.sortino)} {fmtPct(it.metrics.max_drawdown)}{fmtRatio(it.metrics.calmar)}{fmtRatio(it.metrics.turnover_annual, 1)}×{it.metrics.n_trades}{fmtPctPlain(it.metrics.fee_drag, 2)}
)}
)} New backtest →}> {runs.isLoading && } {runs.error && } {runs.data && (runs.data.items.length ? ( {runs.data.items.map((r) => ( ))}
CompareStatusStrategySymbolIntervalSharpeReturnMax DDTradesLabelCreatedDelete
toggle(r.id)} /> {r.strategy} {r.symbol}{r.interval_min}m {fmtNum(r.sharpe)}{fmtPct(r.total_return)}{fmtPct(r.max_drawdown)}{r.n_trades ?? "—"} {r.label ?? ""}{r.error && {r.error}} {fmtIso(r.created_at)}
) : Launch one from the Strategy Lab.)}
); }