Files
ftdt-quant-lab/dashboard-next/src/app/page.tsx
T
ramseshk 6552511978 Hallmark Cobalt: unified light palette + Ubuntu fonts
Layout: Ubuntu + Ubuntu Mono (next/font/google), light mode
CSS: Hallmark Cobalt palette — cool paper bg, hairlines,
  electric cobalt primary, slate secondary
Cards: white bg, hairline borders, muted type badges
Header/tabs: Ubuntu Mono labels, Ubuntu tab buttons
Removed: dark mode, Inter/JetBrains Mono, purple gradients
2026-08-06 04:22:50 +00:00

399 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 QuantReport from "@/components/QuantReport";
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-[#f8f9fb]">
<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>
{/* QF-Lib Quant Report — Hallmark Cobalt inline */}
<div className="mt-6 border-t border-[#e0e4ec] pt-4">
<QuantReport
strategyName={detailName}
backtestId={historical[detailName]?.name || `${detailName.replace(/\s+/g, "_").toLowerCase()}.json`}
/>
</div>
{/* Live L2 Order Book + Trade Tape */}
{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-[#f8f9fb]">
{/* Header — Hallmark Cobalt */}
<header className="sticky top-0 z-50 border-b border-[#e0e4ec] bg-[#f8f9fb]/95 backdrop-blur-sm">
<div className="flex items-center justify-between px-6 h-12 max-w-[1440px] mx-auto">
<div className="flex items-center gap-5">
<span className="text-[11px] font-medium tracking-[0.04em] text-[#1a1c23]" style={{fontFamily:"'Ubuntu Mono', monospace"}}>
{tab === "live" ? "Live Testnet" : tab === "paper" ? "Paper Mainnet" : "Historical"}
</span>
</div>
<div className="flex items-center gap-4">
<span className="flex items-center gap-1.5">
<span className={`w-1.5 h-1.5 rounded-full ${liveConn ? "bg-[#0ea5e9]" : "bg-[#e5e7eb]"}`} />
<span className="text-[9px] text-[#6e7381] font-medium tracking-[0.03em]" style={{fontFamily:"'Ubuntu Mono', monospace"}}>
{liveConn ? "CONNECTED" : "OFFLINE"} · {liveData?.status ?? "···"}
</span>
</span>
</div>
</div>
</header>
{/* Tabs — Hallmark Cobalt */}
<div className="border-b border-[#e0e4ec] bg-[#f8f9fb]/95 sticky top-12 z-40">
<div className="flex max-w-[1440px] mx-auto px-6">
{(["live", "paper", "historical"] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
style={{fontFamily:"'Ubuntu', sans-serif"}}
className={`relative px-4 py-2.5 text-xs font-medium tracking-[0.02em] transition-colors cursor-pointer
${tab === t
? "text-[#1a1c23] after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[2px] after:bg-[#0ea5e9]"
: "text-[#6e7381] hover:text-[#1a1c23]"
}`}
>
{t === "live" ? "Live" : t === "paper" ? "Paper" : "Historical"}
</button>
))}
</div>
</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>
);
}