"use client"; import { useMutation, useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { StatusBadge } from "../page"; import { useSystemStatus } from "@/components/shell"; import { Badge, Button, Card, Empty, ErrorBox, Field, Input, PageHeader, Select, Spinner, Table, Td, Textarea, Th, cx } from "@/components/ui"; import { api, ApiError } from "@/lib/api"; import { fmtIso, fmtNum, fmtPct, signClass } from "@/lib/format"; import type { ParamSpec, RunRow, StrategyMeta } from "@/lib/types"; const DEFAULT_GRIDS: Record> = { tsmom: { lookback: [24, 48, 96], ema_fast: [10, 20], ema_slow: [60, 120] }, ma_ensemble: { slow_mult: [2, 3, 5], target_vol: [0.3, 0.5] }, zscore_mr: { window: [24, 48, 96], entry_z: [1.5, 2.0, 2.5] }, simple_momentum: { lookback: [12, 24, 48] }, equal_risk: { vol_lookback: [24, 48, 96] }, }; function defaultParams(s: StrategyMeta) { return Object.fromEntries(s.params.map((p) => [p.name, p.default])); } export default function LabPage() { const sys = useSystemStatus(); const strategies = useQuery({ queryKey: ["strategies"], queryFn: () => api<{ items: StrategyMeta[] }>("/api/strategies") }); const runs = useQuery({ queryKey: ["runs", 12], queryFn: () => api<{ items: RunRow[] }>("/api/backtests?limit=12"), refetchInterval: 5_000 }); const [selected, setSelected] = useState(null); const strat = strategies.data?.items.find((s) => s.name === selected) ?? null; return ( <>
{strategies.isLoading && } {strategies.error && } {strategies.data?.items.map((s) => ( ))}
{strat ? ( ) : ( Parameters, cost model, holdout, walk-forward and sensitivity options appear here. )} Backtest Explorer →}> {runs.data?.items.length ? ( {runs.data.items.map((r) => ( ))}
StatusStrategySymbolSharpeReturnLabelCreated
{r.strategy}{r.symbol} {r.interval_min}m {fmtNum(r.sharpe)}{fmtPct(r.total_return)}{r.label ?? ""}{fmtIso(r.created_at)}
) : }
); } function ConfigForm({ strat, universe, intervals }: { strat: StrategyMeta; universe: string[]; intervals: number[] }) { const router = useRouter(); const [params, setParams] = useState>(() => defaultParams(strat)); const [symbol, setSymbol] = useState(universe[0] ?? ""); const [interval, setInterval_] = useState(60); const [cash, setCash] = useState(10000); const [cost, setCost] = useState({ taker_fee: 0.004, maker_fee: 0.0025, spread_bps: 5, slippage_bps: 2, max_participation: 0.05 }); const [holdout, setHoldout] = useState(0.2); const [wfOn, setWfOn] = useState(false); const [wfGrid, setWfGrid] = useState(() => JSON.stringify(DEFAULT_GRIDS[strat.name] ?? Object.fromEntries(strat.params.filter((p) => p.type !== "bool").slice(0, 2).map((p) => [p.name, [p.default]])), null, 1)); const [wfFolds, setWfFolds] = useState(4); const [wfAnchored, setWfAnchored] = useState(true); const [sensOn, setSensOn] = useState(false); const [sens, setSens] = useState(() => { const nums = strat.params.filter((p) => p.type === "int" || p.type === "float"); return { x: nums[0]?.name ?? "", xv: nums[0] ? String(nums[0].default) : "", y: nums[1]?.name ?? "", yv: nums[1] ? String(nums[1].default) : "" }; }); const [label, setLabel] = useState(""); const [err, setErr] = useState(null); const validation = useMemo(() => { const out: string[] = []; for (const p of strat.params) { const v = params[p.name]; if ((p.type === "int" || p.type === "float") && typeof v === "number") { if (p.min != null && v < p.min) out.push(`${p.name} must be ≥ ${p.min}`); if (p.max != null && v > p.max) out.push(`${p.name} must be ≤ ${p.max}`); if (p.type === "int" && !Number.isInteger(v)) out.push(`${p.name} must be an integer`); } } return out; }, [strat, params]); const launch = useMutation({ mutationFn: async () => { const body: Record = { strategy: strat.name, symbol, interval_min: interval, params, cost_model: cost, initial_cash: cash, holdout_frac: holdout, label: label || null, stress: true }; if (wfOn) { let grid: unknown; try { grid = JSON.parse(wfGrid); } catch { throw new Error("walk-forward grid is not valid JSON"); } body.walk_forward = { grid, n_folds: wfFolds, anchored: wfAnchored, train_frac: 0.6 }; body.n_trials = Object.values(grid as Record).reduce((a, v) => a * (Array.isArray(v) ? v.length : 1), 1); } if (sensOn && sens.x && sens.y) { const parse = (s: string) => s.split(",").map((x) => x.trim()).filter(Boolean).map((x) => (Number.isFinite(Number(x)) ? Number(x) : x)); body.sensitivity = { x: sens.x, x_values: parse(sens.xv), y: sens.y, y_values: parse(sens.yv), metric: "sharpe" }; } return api<{ id: string }>("/api/backtests", { method: "POST", json: body }); }, onSuccess: (r) => router.push(`/backtests/${r.id}`), onError: (e) => setErr(e instanceof ApiError ? e.message : String(e)), }); return ( {strat.hypothesis} Needs ≥ {strat.min_bars} closed bars. {strat.allows_short ? "May short." : "Long-only (spot)."}}>
{ e.preventDefault(); setErr(null); launch.mutate(); }} className="flex flex-col gap-4">
setCash(Number(e.target.value))} /> setHoldout(Number(e.target.value))} />
Strategy parameters
{strat.params.map((p) => setParams((prev) => ({ ...prev, [p.name]: v }))} />)} {strat.params.length === 0 &&

No parameters.

}
{validation.length > 0 &&
    {validation.map((v) =>
  • {v}
  • )}
}
Cost model
setCost({ ...cost, taker_fee: Number(e.target.value) })} /> setCost({ ...cost, maker_fee: Number(e.target.value) })} /> setCost({ ...cost, spread_bps: Number(e.target.value) })} /> setCost({ ...cost, slippage_bps: Number(e.target.value) })} /> setCost({ ...cost, max_participation: Number(e.target.value) })} />
{wfOn && (