Files
ftdt-quant-lab/dashboard-next/src/app/page.tsx
T
ramseshk d31d301822 Clean dashboard: remove footer links + positions panel
Removed:
  - Three footer link cards (ftdt.io, Quant Lab, Git Repo)
  - Open Positions & Orders collapsible panel
  - Unused imports (Activity, Database, TrendingUp, Collapsible)

Positions already shown live on each strategy card (Pos: 0.0000).
Dashboard is now cleaner: strategies grid + L2 Terminal button only.
2026-08-05 09:55:56 +00:00

385 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ChevronDown, ChevronRight, TrendingDown, ArrowLeft } from "lucide-react";
import { StrategyCard } from "@/components/strategy-card";
import { EquityChart } from "@/components/equity-chart";
import { PositionsPanel } from "@/components/positions-panel";
import { OBIDetail } from "@/components/obi-detail";
import OrderBookDepthMap from "@/components/orderbook-depth-map";
import L2Terminal from "@/components/L2Terminal";
import { useLiveMetrics, usePaperMetrics, fetchHistorical, fetchBacktestDetail, recalcBacktest } from "@/lib/api";
import type { Strategy, BacktestSummary, BacktestFull, Trade, Position, Order } from "@/lib/types";
type Tab = "live" | "paper" | "historical";
const STRAT_COLORS = ["#22c55e","#3b82f6","#a855f7","#f59e0b","#ef4444","#06b6d4","#ec4899","#84cc16","#6366f1","#14b8a6","#f97316","#8b5cf6"];
export default function Dashboard() {
const [tab, setTab] = useState<Tab>("live");
const { data: liveData, connected: liveConn } = useLiveMetrics();
const { data: paperData } = usePaperMetrics();
const [historical, setHistorical] = useState<Record<string, BacktestSummary>>({});
const [detailOpen, setDetailOpen] = useState(false);
const [l2TerminalOpen, setL2TerminalOpen] = useState(false);
const [detailName, setDetailName] = useState("");
const [detailTab, setDetailTab] = useState<Tab>("live");
const [filter, setFilter] = useState("ALL");
const [btFull, setBtFull] = useState<BacktestFull | null>(null);
const [feeOn, setFeeOn] = useState(true);
const [feeTier, setFeeTier] = useState(0);
const [stakingTier, setStakingTier] = useState("none");
const [posOpen, setPosOpen] = useState(false);
const [tickerFilter, setTickerFilter] = useState("ALL");
useEffect(() => { fetchHistorical().then(setHistorical); }, []);
const strategies = tab === "live" ? liveData?.strategies ?? {}
: tab === "paper" ? paperData?.strategies ?? {}
: {};
const handleCardClick = useCallback(async (name: string, t: Tab) => {
setDetailName(name);
setDetailTab(t);
setBtFull(null);
setDetailOpen(true);
if (t === "historical") {
const ht = historical[name];
if (ht) {
try {
const full = await fetchBacktestDetail(ht.name);
setBtFull(full);
} catch { /* ignore */ }
}
}
}, [historical]);
const handleFeeRecalc = useCallback(async () => {
if (!detailName || detailTab !== "historical") return;
const ht = historical[detailName];
if (!ht) return;
try {
const full = await recalcBacktest(ht.name, feeTier, stakingTier);
setBtFull(full);
} catch { /* ignore */ }
}, [detailName, detailTab, feeTier, stakingTier, historical]);
const detailStrat = detailTab === "historical" ? null
: (tab === "paper" ? paperData : liveData)?.strategies?.[detailName] ?? null;
let detailEquity: { t: number; v: number }[] = [];
let detailTrades: Trade[] = [];
let detailPositions: Position[] = [];
let detailOrders: Order[] = [];
if (detailTab === "live" && liveData) {
if (detailName) {
detailEquity = (liveData.strategy_equity ?? {})[detailName] ?? liveData.equity_history ?? [];
}
detailTrades = (liveData.trades ?? []).filter((t) => t.strategy === detailName);
detailPositions = (liveData.open_positions ?? []).filter((p) => p.strategy === detailName);
detailOrders = liveData.open_orders ?? [];
} else if (detailTab === "paper" && paperData) {
if (detailName) {
detailEquity = (paperData.strategy_equity ?? {})[detailName] ?? paperData.equity_history ?? [];
}
detailTrades = (paperData.per_strategy_trades ?? {})[detailName] ?? [];
detailPositions = (paperData.open_positions ?? []).filter((p) => p.strategy === detailName);
detailOrders = paperData.open_orders ?? [];
} else if (detailTab === "historical" && btFull) {
detailEquity = (btFull.equity_curve ?? []).map((e) => ({
t: typeof e.t === "string" ? Math.floor(new Date(e.t).getTime() / 1000) : e.t,
v: e.v,
}));
detailTrades = btFull.trades ?? [];
}
if (detailOpen) {
return (
<div className="min-h-screen bg-background">
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-xl">
<div className="flex items-center justify-between px-6 py-3 max-w-[1440px] mx-auto">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" className="h-8 gap-2" onClick={() => setDetailOpen(false)}>
<ArrowLeft className="w-4 h-4" />
<span className="text-xs">Back</span>
</Button>
<div>
<h1 className="text-sm font-bold tracking-tight">{detailName}</h1>
<div className="flex items-center gap-2">
{detailStrat && (
<>
<Badge variant="outline" className="text-[10px]">{detailStrat.type}</Badge>
<Badge variant={detailStrat.status === "running" ? "default" : "secondary"} className="text-[10px]">
{detailStrat.status?.toUpperCase()}
</Badge>
<span className="text-[10px] text-muted-foreground">{detailStrat.instrument}</span>
</>
)}
{detailTab === "historical" && btFull && (
<span className="text-[10px] text-muted-foreground">
30d · {btFull.total_trades} trades · {btFull.fee_model ?? "taker"} model
</span>
)}
</div>
</div>
</div>
{detailTab === "historical" && (
<div className="flex items-center gap-2">
<label className="text-[10px] text-muted-foreground flex items-center gap-1">
<input type="checkbox" checked={feeOn} onChange={(e) => setFeeOn(e.target.checked)} className="rounded" />
Fees
</label>
<select value={feeTier} onChange={(e) => setFeeTier(Number(e.target.value))} className="text-[10px] bg-card border border-border rounded px-2 py-1 text-foreground h-6">
{["Tier 0 (0.045/0.015%)","Tier 1 >$5M (0.040/0.012%)","Tier 2 >$25M (0.035/0.008%)","Tier 3 >$100M (0.030/0.004%)","Tier 4 >$500M (0.028/0.000%)","Tier 5 >$2B (0.026/0.000%)","Tier 6 >$7B (0.024/0.000%)"].map((t, i) => <option key={i} value={i}>{t}</option>)}
</select>
<select value={stakingTier} onChange={(e) => setStakingTier(e.target.value)} className="text-[10px] bg-card border border-border rounded px-2 py-1 text-foreground h-6">
{["No Stake","Wood (×0.95)","Bronze (×0.90)","Silver (×0.85)","Gold (×0.80)","Platinum (×0.70)","Diamond (×0.60)"].map((t, i) => <option key={i} value={["none","wood","bronze","silver","gold","platinum","diamond"][i]}>{t}</option>)}
</select>
<Button size="sm" variant="outline" className="text-[10px] h-6 px-2" onClick={handleFeeRecalc}>
Recalc
</Button>
</div>
)}
</div>
</header>
<div className="max-w-[1440px] mx-auto px-6 py-6 space-y-6">
{/* OBI Strategy: 3D Depth Map View */}
{detailTab === "live" && detailName.includes("Order Book Imbalance") && detailStrat && liveData && (
<OBIDetail
strategy={detailStrat}
strategyName={detailName}
equityData={detailEquity}
trades={detailTrades}
liveData={liveData}
color={STRAT_COLORS[Object.keys(strategies).indexOf(detailName) % STRAT_COLORS.length] ?? "#22c55e"}
/>
)}
{/* Regular detail for non-OBI strategies */}
{!(detailTab === "live" && detailName.includes("Order Book Imbalance")) && (
<>
{detailStrat && (
<p className="text-xs text-muted-foreground leading-relaxed p-4 bg-muted/50 rounded-lg border border-border">
{detailStrat.description || "No description available."}
</p>
)}
{detailTab === "historical" && btFull && (
<p className="text-xs text-muted-foreground leading-relaxed p-4 bg-muted/50 rounded-lg border border-border">
{btFull.strategy} {btFull.num_periods} periods, {btFull.total_trades} trades,
total fees ${btFull.fees_total?.toFixed(2)}, model: {btFull.fee_model ?? "taker"}
</p>
)}
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2">
{detailStrat ? (
[
{ l: "PnL", v: `$${detailStrat.pnl?.toFixed(4)}` },
{ l: "PnL%", v: `${detailStrat.pnl_pct >= 0 ? "+" : ""}${detailStrat.pnl_pct?.toFixed(2)}%`, up: detailStrat.pnl_pct >= 0 },
{ l: "Trades", v: String(detailStrat.trades_today ?? 0) },
{ l: "Win Rate", v: `${Math.round((detailStrat.win_rate ?? 0) * 100)}%` },
{ l: "Fees", v: `$${(detailStrat.fee_paid ?? 0).toFixed(4)}`, up: false },
{ l: "Position", v: (detailStrat.position ?? 0).toFixed(4) },
].map(({ l, v, up }) => (
<div key={l} className="p-3 rounded-lg border border-border bg-card/50">
<p className="text-[9px] text-muted-foreground uppercase tracking-wider mb-1">{l}</p>
<p className={`text-sm font-mono font-semibold ${up === true ? "text-green-500" : up === false && l !== "Position" ? "text-red-500" : ""}`}>{v}</p>
</div>
))
) : detailTab === "historical" && btFull ? (
[
{ l: feeOn ? "Net PnL" : "Gross PnL", v: `${(feeOn ? (btFull.pnl_net_pct ?? btFull.pnl_pct ?? 0) : (btFull.pnl_gross_pct ?? btFull.pnl_pct ?? 0)) >= 0 ? "+" : ""}${(feeOn ? (btFull.pnl_net_pct ?? btFull.pnl_pct ?? 0) : (btFull.pnl_gross_pct ?? btFull.pnl_pct ?? 0)).toFixed(2)}%`, up: (feeOn ? (btFull.pnl_net_pct ?? btFull.pnl_pct ?? 0) : (btFull.pnl_gross_pct ?? btFull.pnl_pct ?? 0)) >= 0 },
{ l: "Sharpe", v: (btFull.sharpe ?? 0).toFixed(2) },
{ l: "Sortino", v: (btFull.sortino ?? 0).toFixed(2) },
{ l: "Max DD", v: `${((btFull.max_dd ?? 0) * 100).toFixed(1)}%`, up: false },
{ l: "Win Rate", v: `${Math.round((btFull.win_rate ?? 0) * 100)}%` },
{ l: "Fees", v: `$${(btFull.fees_total ?? 0).toFixed(2)}`, up: false },
].map(({ l, v, up }) => (
<div key={l} className="p-3 rounded-lg border border-border bg-card/50">
<p className="text-[9px] text-muted-foreground uppercase tracking-wider mb-1">{l}</p>
<p className={`text-sm font-mono font-semibold ${up === true ? "text-green-500" : up === false ? "text-red-500" : ""}`}>{v}</p>
</div>
))
) : (
<div className="col-span-6 text-center text-muted-foreground text-xs py-8">Loading...</div>
)}
</div>
{detailEquity.length > 0 && (
<div className="rounded-lg border border-border overflow-hidden h-[320px]">
<EquityChart data={detailEquity} color={STRAT_COLORS[Object.keys(strategies).indexOf(detailName) % STRAT_COLORS.length] ?? "#22c55e"} height={320} />
</div>
)}
{detailTab !== "historical" && (detailPositions.length > 0 || detailOrders.length > 0) && (
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-3 pb-2 border-b border-border">
Open Positions & Orders {detailName ? `for ${detailName}` : ""}
</h4>
<PositionsPanel positions={detailPositions} orders={detailOrders} />
</div>
)}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-3 pb-2 border-b border-border">
Trade History {detailTrades.length > 0 ? `(${detailTrades.length})` : ""}
</h4>
{detailTrades.length > 0 ? (
<div className="overflow-x-auto rounded-lg border border-border">
<Table>
<TableHeader>
<TableRow className="border-border hover:bg-transparent">
<TableHead className="text-[9px] h-7">Time</TableHead>
<TableHead className="text-[9px] h-7">Side</TableHead>
<TableHead className="text-[9px] h-7">Size</TableHead>
<TableHead className="text-[9px] h-7">Price</TableHead>
<TableHead className="text-[9px] h-7 text-right">PnL</TableHead>
<TableHead className="text-[9px] h-7 text-right">Fee</TableHead>
<TableHead className="text-[9px] h-7">Reason</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detailTrades.slice(-200).reverse().map((t, i) => {
const tp = detailTab === "historical"
? (feeOn ? (t.pnl_net ?? t.pnl ?? 0) : (t.pnl_gross ?? t.pnl ?? 0))
: (t.pnl ?? 0);
return (
<TableRow key={i} className="border-border/50 hover:bg-muted/30">
<TableCell className="text-[10px] py-1.5 font-mono whitespace-nowrap">{(t.time ?? "").substring(0, 16)}</TableCell>
<TableCell className="text-[10px] py-1.5">
<Badge variant="outline" className={`text-[9px] h-4 px-1.5 border-0 ${(t.side ?? "").indexOf("BUY") >= 0 ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}>
{t.side ?? "—"}
</Badge>
</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">{t.size}</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">${(t.price ?? 0).toFixed(1)}</TableCell>
<TableCell className={`text-[10px] py-1.5 font-mono text-right ${tp >= 0 ? "text-green-500" : "text-red-500"}`}>
{tp >= 0 ? "+" : ""}${Math.abs(tp).toFixed(4)}
</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono text-right text-red-400">${(t.fee ?? 0).toFixed(4)}</TableCell>
<TableCell className="text-[10px] py-1.5 text-muted-foreground max-w-[300px] truncate" title={t.reason}>
{t.reason ?? "—"}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<p className="text-xs text-muted-foreground text-center py-12">No trades recorded yet</p>
)}
</div>
{/* Live L2 Order Book + Trade Tape (all strategies, live tab only) */}
{detailTab === "live" && (
<div className="mt-6">
<OrderBookDepthMap coin="BTC" height={480} topRatio={0.55} />
</div>
)}
<div className="h-8" />
</>
)}
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background">
<header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-xl">
<div className="flex items-center justify-between px-6 py-3 max-w-[1440px] mx-auto">
<div className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${liveConn ? "bg-green-500 animate-pulse" : "bg-red-500"}`} />
<div>
<h1 className="text-sm font-bold tracking-tight">FTDT Quant Lab</h1>
<p className="text-[10px] text-muted-foreground">
{tab === "live" ? `Live Testnet · Equity $${liveData?.total_equity?.toFixed(2) ?? "—"}`
: tab === "paper" ? `Paper Mainnet · Equity $${paperData?.total_equity?.toLocaleString() ?? "—"}`
: "Historical · Mainnet Real Data"}
</p>
</div>
</div>
<Badge variant={liveConn ? "default" : "destructive"} className="text-[10px] h-5">
{liveConn ? "LIVE" : "OFFLINE"}
</Badge>
</div>
</header>
<div className="border-b border-border bg-background/80 backdrop-blur-xl sticky top-[49px] z-40">
<Tabs value={tab} onValueChange={(v) => setTab(v as Tab)} className="max-w-[1440px] mx-auto px-6">
<TabsList className="h-10 bg-transparent border-0 gap-0 p-0">
<TabsTrigger value="live" className="data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none px-5 text-xs h-10">
Live<Badge variant="outline" className="ml-1.5 text-[9px] h-4 px-1.5 bg-amber-500/10 text-amber-400 border-0">Testnet</Badge>
</TabsTrigger>
<TabsTrigger value="paper" className="data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none px-5 text-xs h-10">
Paper<Badge variant="outline" className="ml-1.5 text-[9px] h-4 px-1.5 bg-purple-500/10 text-purple-400 border-0">$100K Mainnet</Badge>
</TabsTrigger>
<TabsTrigger value="historical" className="data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none px-5 text-xs h-10">
Historical<Badge variant="outline" className="ml-1.5 text-[9px] h-4 px-1.5 bg-purple-500/10 text-purple-400 border-0">Real Data</Badge>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<main className="max-w-[1440px] mx-auto px-6 py-6">
{/* Ticker filter for Historical tab */}
{tab === "historical" && (
<div className="flex items-center gap-2 mb-4 flex-wrap">
<span className="text-[9px] text-muted-foreground uppercase tracking-wider mr-1">Ticker:</span>
{["ALL", "BTC", "ETH", "HYPE", "VVV"].map((t) => (
<button key={t} onClick={() => setTickerFilter(t)}
className={`text-[10px] px-3 py-1 rounded-md border transition-colors ${tickerFilter === t ? "bg-primary text-primary-foreground border-primary" : "bg-card text-muted-foreground border-border hover:border-primary/50"}`}>
{t}
</button>
))}
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 mb-6">
<AnimatePresence mode="popLayout">
{Object.entries(strategies).map(([name, s], i) => (
<motion.div key={`${tab}-${name}`} layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.2, delay: i * 0.03 }}>
<StrategyCard name={name} strategy={s} tab={tab} onClick={() => handleCardClick(name, tab)} />
</motion.div>
))}
</AnimatePresence>
{tab === "historical" && Object.entries(historical).filter(([, b]) => tickerFilter === "ALL" || b.coin === tickerFilter).map(([name, b], i) => (
<motion.div key={name} layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: i * 0.03 }}>
<StrategyCard name={name} tab="historical" onClick={() => handleCardClick(name, "historical")}
coin={String(b.coin ?? "?")}
badge={`30d · Mainnet`}
stats={[{ label: "Sharpe", value: b.sharpe.toFixed(2) }, { label: "Max DD", value: `${(b.max_dd * 100).toFixed(1)}%`, negative: true }, { label: "Win", value: `${Math.round(b.win_rate * 100)}%` }]}
pnlPct={b.pnl_pct} status="REAL DATA" />
</motion.div>
))}
</div>
{/* L2 Terminal launcher */}
<button onClick={() => setL2TerminalOpen(true)} className="flex items-center gap-2 px-4 py-2 mb-4 border border-[#1A1A2E] bg-[#0A0A10] hover:bg-[#111122] rounded transition-colors">
<span className="text-[11px] font-mono text-gray-300"> L2 Depth Map</span>
<span className="text-[9px] text-gray-600">ws://hyperliquid · {liveConn ? "LIVE" : "OFFLINE"}</span>
</button>
</main>
{/* Fullscreen L2 Terminal */}
{l2TerminalOpen && (
<div className="fixed inset-0 z-[200] bg-black">
<button
onClick={() => setL2TerminalOpen(false)}
className="absolute top-2 right-4 z-[201] text-gray-400 hover:text-white text-xs font-mono bg-[#111] px-3 py-1 rounded border border-[#333]"
>
Close L2 Terminal
</button>
<L2Terminal coin="BTC" className="w-full h-full" />
</div>
)}
</div>
);
}