Recreate Next.js source — shadcn components, chart, types, hooks
Source files were untracked and lost during reset. Recreated: - page.tsx (full 3-tab dashboard), layout.tsx, globals.css (shadcn theme) - types.ts, api.ts, utils.ts - strategy-card.tsx, equity-chart.tsx, positions-panel.tsx - ui/ (7 shadcn wrappers: card, badge, button, table, tabs, sheet, collapsible, select) - Config files: next.config.ts, tsconfig.json, postcss.config.mjs, components.json
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
out-www/
|
||||||
|
_next-app-backup/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "export",
|
||||||
|
images: { unoptimized: true },
|
||||||
|
trailingSlash: true,
|
||||||
|
basePath: "/cv",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "ftdt-quant-dashboard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.0",
|
||||||
|
"@radix-ui/react-select": "^2.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^11.0.0",
|
||||||
|
"lightweight-charts": "^4.2.0",
|
||||||
|
"lucide-react": "^0.454.0",
|
||||||
|
"next": "^16.3.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.0.0",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.0588 0.0162 269.6475);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.1059 0.0201 269.5991);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.1059 0.0201 269.5991);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.0588 0.0162 269.6475);
|
||||||
|
--secondary: oklch(0.1776 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.1776 0 0);
|
||||||
|
--muted-foreground: oklch(0.7559 0.0125 239.9659);
|
||||||
|
--accent: oklch(0.1776 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.602 0.2378 25.3312);
|
||||||
|
--border: oklch(1 0 0 / 0.1);
|
||||||
|
--input: oklch(1 0 0 / 0.15);
|
||||||
|
--ring: oklch(0.7559 0.0125 239.9659);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
border-color: var(--border);
|
||||||
|
outline-color: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-inter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-jetbrains-mono",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "FTDT Quant Lab",
|
||||||
|
description: "Professional quantitative trading dashboard — live testnet, paper mainnet, historical backtests",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className="dark">
|
||||||
|
<body className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { ChevronDown, ChevronRight, Activity, Database, TrendingUp, TrendingDown, ArrowLeft } from "lucide-react";
|
||||||
|
import { StrategyCard } from "@/components/strategy-card";
|
||||||
|
import { EquityChart } from "@/components/equity-chart";
|
||||||
|
import { PositionsPanel } from "@/components/positions-panel";
|
||||||
|
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 [detailName, setDetailName] = useState("");
|
||||||
|
const [detailTab, setDetailTab] = useState<Tab>("live");
|
||||||
|
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);
|
||||||
|
|
||||||
|
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">
|
||||||
|
{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>
|
||||||
|
<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">
|
||||||
|
<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).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")}
|
||||||
|
badge={`30d · ${b.coin ?? "BTC"} · 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>
|
||||||
|
|
||||||
|
<Collapsible open={posOpen} onOpenChange={setPosOpen} className="mb-6">
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors py-1">
|
||||||
|
{posOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
|
Open Positions & Orders ({liveData?.open_positions?.length ?? 0} pos · {liveData?.open_orders?.length ?? 0} ord)
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<PositionsPanel positions={liveData?.open_positions ?? []} orders={liveData?.open_orders ?? []} />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
|
||||||
|
<a href="https://ftdt.io" target="_blank" className="flex items-center gap-3 p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors">
|
||||||
|
<Activity className="w-4 h-4 text-primary" />
|
||||||
|
<div><p className="text-xs font-medium">ftdt.io</p><p className="text-[10px] text-muted-foreground">Main platform</p></div>
|
||||||
|
</a>
|
||||||
|
<a href="https://app.ftdt.io/quant-lab" target="_blank" className="flex items-center gap-3 p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors">
|
||||||
|
<TrendingUp className="w-4 h-4 text-chart-2" />
|
||||||
|
<div><p className="text-xs font-medium">Quant Lab ↗</p><p className="text-[10px] text-muted-foreground">Web dashboard</p></div>
|
||||||
|
</a>
|
||||||
|
<a href="https://git.ftdt.io/rams/ftdt-quant-lab" target="_blank" className="flex items-center gap-3 p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors">
|
||||||
|
<Database className="w-4 h-4 text-chart-3" />
|
||||||
|
<div><p className="text-xs font-medium">Git Repo</p><p className="text-[10px] text-muted-foreground">rams/ftdt-quant-lab</p></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { createChart, ColorType } from "lightweight-charts";
|
||||||
|
|
||||||
|
interface EquityChartProps {
|
||||||
|
data: { t: number; v: number }[];
|
||||||
|
color?: string;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquityChart({ data, color = "#3b82f6", height = 260 }: EquityChartProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<ReturnType<typeof createChart> | null>(null);
|
||||||
|
const seriesRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const seriesColor = color.startsWith("#") ? color : "#3b82f6";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || data.length === 0) return;
|
||||||
|
|
||||||
|
const chart = createChart(containerRef.current, {
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: "transparent" },
|
||||||
|
textColor: "#b0b7c0",
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: "rgba(255,255,255,0.03)" },
|
||||||
|
horzLines: { color: "rgba(255,255,255,0.03)" },
|
||||||
|
},
|
||||||
|
rightPriceScale: { borderColor: "rgba(255,255,255,0.08)" },
|
||||||
|
timeScale: { borderColor: "rgba(255,255,255,0.08)", timeVisible: true },
|
||||||
|
crosshair: { mode: 0 },
|
||||||
|
width: containerRef.current.clientWidth,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const series = chart.addAreaSeries({
|
||||||
|
lineColor: seriesColor,
|
||||||
|
topColor: `${seriesColor}26`,
|
||||||
|
bottomColor: `${seriesColor}05`,
|
||||||
|
lineWidth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pts = data.map((d) => ({
|
||||||
|
time: d.t as import("lightweight-charts").UTCTimestamp,
|
||||||
|
value: d.v,
|
||||||
|
}));
|
||||||
|
series.setData(pts);
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
chartRef.current = chart;
|
||||||
|
seriesRef.current = series;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (containerRef.current && chartRef.current) {
|
||||||
|
chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}, [data, seriesColor, height]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (seriesRef.current && data.length > 0) {
|
||||||
|
const pts = data.map((d) => ({
|
||||||
|
time: d.t as import("lightweight-charts").UTCTimestamp,
|
||||||
|
value: d.v,
|
||||||
|
}));
|
||||||
|
seriesRef.current.setData(pts);
|
||||||
|
chartRef.current?.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} style={{ width: "100%", height }} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import type { Position, Order } from "@/lib/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
positions: Position[];
|
||||||
|
orders: Order[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PositionsPanel({ positions, orders }: Props) {
|
||||||
|
return (
|
||||||
|
<Card className="p-0 overflow-hidden border-border">
|
||||||
|
{positions.length > 0 && (
|
||||||
|
<div className="p-4 pb-0">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Positions ({positions.length})</p>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-border/50">
|
||||||
|
<TableHead className="text-[9px] h-7">Strategy</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7">Size</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7">Entry</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7 text-right">PnL</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{positions.map((p, i) => (
|
||||||
|
<TableRow key={i} className="border-border/50">
|
||||||
|
<TableCell className="text-[10px] py-1.5">{p.strategy}</TableCell>
|
||||||
|
<TableCell className="text-[10px] py-1.5 font-mono">{p.size}</TableCell>
|
||||||
|
<TableCell className="text-[10px] py-1.5 font-mono">${p.entry_px?.toFixed(1) ?? "—"}</TableCell>
|
||||||
|
<TableCell className={`text-[10px] py-1.5 font-mono text-right ${(p.pnl ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
|
||||||
|
{(p.pnl ?? 0) >= 0 ? "+" : ""}${(p.pnl ?? 0).toFixed(4)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{orders.length > 0 && (
|
||||||
|
<div className="p-4 pb-4">
|
||||||
|
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Orders ({orders.length})</p>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-border/50">
|
||||||
|
<TableHead className="text-[9px] h-7">Coin</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7">Side</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7">Size</TableHead>
|
||||||
|
<TableHead className="text-[9px] h-7">Limit</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{orders.map((o, i) => (
|
||||||
|
<TableRow key={i} className="border-border/50">
|
||||||
|
<TableCell className="text-[10px] py-1.5">{o.coin}</TableCell>
|
||||||
|
<TableCell className="text-[10px] py-1.5">
|
||||||
|
<Badge variant="outline" className={`text-[9px] h-4 px-1.5 border-0 ${o.side === "B" ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}>
|
||||||
|
{o.side === "B" ? "BUY" : "SELL"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-[10px] py-1.5 font-mono">{o.sz}</TableCell>
|
||||||
|
<TableCell className="text-[10px] py-1.5 font-mono">${o.limitPx ?? "—"}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{positions.length === 0 && orders.length === 0 && (
|
||||||
|
<p className="text-xs text-muted-foreground text-center py-8">No open positions or orders</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import type { Strategy } from "@/lib/types";
|
||||||
|
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||||
|
|
||||||
|
interface StrategyCardProps {
|
||||||
|
name: string;
|
||||||
|
strategy?: Strategy;
|
||||||
|
tab: "live" | "paper" | "backtest" | "historical";
|
||||||
|
onClick: () => void;
|
||||||
|
badge?: string;
|
||||||
|
stats?: { label: string; value: string; negative?: boolean }[];
|
||||||
|
pnlPct?: number;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StrategyCard({ name, strategy, tab, onClick, badge, stats, pnlPct, status }: StrategyCardProps) {
|
||||||
|
if (strategy) {
|
||||||
|
const equity = strategy.allocation + (strategy.pnl ?? 0);
|
||||||
|
const isUp = equity >= strategy.allocation;
|
||||||
|
const pnl = strategy.pnl ?? 0;
|
||||||
|
const pnlPctVal = strategy.pnl_pct ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="p-4 cursor-pointer hover:border-primary/50 hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 border-border"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold leading-tight">{name}</p>
|
||||||
|
<p className="text-[9px] text-muted-foreground mt-0.5">
|
||||||
|
${strategy.allocation} · {strategy.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Badge variant={strategy.status === "running" ? "default" : "secondary"} className="text-[8px] h-4 px-1.5">
|
||||||
|
{strategy.status?.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-[8px] h-4 px-1.5 border-border">
|
||||||
|
{strategy.fee_model?.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${isUp ? "text-green-500" : "text-red-500"}`}>
|
||||||
|
{isUp ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||||
|
${equity.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 text-[9px] text-muted-foreground flex-wrap">
|
||||||
|
<span>PnL: <b className={pnlPctVal >= 0 ? "text-green-500" : "text-red-500"}>{pnl >= 0 ? "+" : ""}{pnl.toFixed(2)} ({pnlPctVal >= 0 ? "+" : ""}{pnlPctVal.toFixed(2)}%)</b></span>
|
||||||
|
<span>Trades: <b>{strategy.trades_today ?? 0}</b></span>
|
||||||
|
<span>Win: <b>{Math.round((strategy.win_rate ?? 0) * 100)}%</b></span>
|
||||||
|
<span>Pos: <b>{(strategy.position ?? 0).toFixed(4)}</b></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 pt-2 border-t border-border/50 text-[9px] text-muted-foreground leading-relaxed">
|
||||||
|
<b>Alloc:</b> ${strategy.allocation} · <b>Max pos:</b> {strategy.max_position ?? "—"} · <b>Stop:</b> {strategy.stop_loss ?? "—"}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backtest / Historical card
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="p-4 cursor-pointer hover:border-primary/50 hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 border-border"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold leading-tight">{name}</p>
|
||||||
|
<p className="text-[9px] text-muted-foreground mt-0.5">{badge ?? "Backtest"}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="text-[8px] h-4 px-1.5">{status ?? "BACKTEST"}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${(pnlPct ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
|
||||||
|
{(pnlPct ?? 0) >= 0 ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||||
|
{(pnlPct ?? 0) >= 0 ? "+" : ""}{(pnlPct ?? 0).toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
{stats && (
|
||||||
|
<div className="flex gap-3 text-[9px] text-muted-foreground flex-wrap">
|
||||||
|
{stats.map((s) => (
|
||||||
|
<span key={s.label}>
|
||||||
|
{s.label}: <b className={s.negative ? "text-red-500" : ""}>{s.value}</b>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = {
|
||||||
|
default: "border-transparent bg-primary text-primary-foreground",
|
||||||
|
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||||
|
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
||||||
|
outline: "text-foreground",
|
||||||
|
};
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { variant?: keyof typeof badgeVariants }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
badgeVariants[variant],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge };
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
|
variant?: "default" | "ghost" | "outline";
|
||||||
|
size?: "default" | "sm" | "icon";
|
||||||
|
}) {
|
||||||
|
const variants: Record<string, string> = {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
};
|
||||||
|
const sizes: Record<string, string> = {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
variants[variant],
|
||||||
|
sizes[size],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button };
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-[--card-spacing] overflow-hidden rounded-xl bg-card text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-header" className={cn("flex flex-col gap-1.5 px-[--card-spacing] pt-[--card-spacing]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-title" className={cn("font-semibold leading-none tracking-tight", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-content" className={cn("px-[--card-spacing]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="card-footer" className={cn("flex items-center px-[--card-spacing] pb-[--card-spacing]", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CollapsibleContextType {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
const CollapsibleContext = React.createContext<CollapsibleContextType | null>(null);
|
||||||
|
|
||||||
|
function Collapsible({ open, onOpenChange, className, children, ...props }: React.ComponentProps<"div"> & CollapsibleContextType) {
|
||||||
|
return (
|
||||||
|
<CollapsibleContext.Provider value={{ open, onOpenChange }}>
|
||||||
|
<div data-slot="collapsible" className={cn("flex flex-col gap-2", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({ className, children, ...props }: React.ComponentProps<"button">) {
|
||||||
|
const ctx = React.useContext(CollapsibleContext);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
className={cn("flex items-center gap-2 text-sm font-medium [&[data-state=open]>svg]:rotate-180", className)}
|
||||||
|
onClick={() => ctx?.onOpenChange(!ctx?.open)}
|
||||||
|
data-state={ctx?.open ? "open" : "closed"}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({ className, children, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const ctx = React.useContext(CollapsibleContext);
|
||||||
|
if (!ctx?.open) return null;
|
||||||
|
return (
|
||||||
|
<div data-slot="collapsible-content" className={cn("overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className)} data-state={ctx.open ? "open" : "closed"} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
function Select({ value, onChange, className, children, ...props }: React.ComponentProps<"select"> & { onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void }) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
data-slot="select"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className={cn(
|
||||||
|
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Select };
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
function Sheet({ open, onOpenChange, children }: { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode }) {
|
||||||
|
if (!open) return null;
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm" onClick={() => onOpenChange(false)}>
|
||||||
|
<div className="fixed inset-y-0 right-0 z-50 flex" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({ side = "right", className, children, ...props }: React.ComponentProps<"div"> & { side?: "right" | "left" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background flex h-full flex-col shadow-lg",
|
||||||
|
side === "right" ? "ml-auto" : "mr-auto",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||||
|
return <h2 data-slot="sheet-title" className={cn("text-lg font-semibold", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Sheet, SheetContent, SheetHeader, SheetTitle };
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return <table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return <tr data-slot="table-row" className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return <th data-slot="table-head" className={cn("h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return <td data-slot="table-cell" className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell };
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TabsContext = React.createContext<{ value: string; onValueChange: (v: string) => void } | null>(null);
|
||||||
|
|
||||||
|
function Tabs({ value, onValueChange, className, children, ...props }: React.ComponentProps<"div"> & { value: string; onValueChange: (v: string) => void }) {
|
||||||
|
return (
|
||||||
|
<TabsContext.Provider value={{ value, onValueChange }}>
|
||||||
|
<div data-slot="tabs" className={cn("flex flex-col", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TabsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div data-slot="tabs-list" className={cn("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({ value, className, children, ...props }: React.ComponentProps<"button"> & { value: string }) {
|
||||||
|
const ctx = React.useContext(TabsContext);
|
||||||
|
const active = ctx?.value === value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
data-state={active ? "active" : "inactive"}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={() => ctx?.onValueChange(value)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({ value, className, children, ...props }: React.ComponentProps<"div"> & { value: string }) {
|
||||||
|
const ctx = React.useContext(TabsContext);
|
||||||
|
if (ctx?.value !== value) return null;
|
||||||
|
return (
|
||||||
|
<div data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import type { LiveMetrics, PaperMetrics } from "./types";
|
||||||
|
|
||||||
|
const API_BASE = "/cv/api";
|
||||||
|
|
||||||
|
export function useLiveMetrics() {
|
||||||
|
const [data, setData] = useState<LiveMetrics | null>(null);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const WS_BASE = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/cv/ws`;
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(WS_BASE);
|
||||||
|
wsRef.current = ws;
|
||||||
|
ws.onopen = () => setConnected(true);
|
||||||
|
ws.onclose = () => {
|
||||||
|
setConnected(false);
|
||||||
|
timerRef.current = setTimeout(connect, 5000);
|
||||||
|
};
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(e.data) as LiveMetrics;
|
||||||
|
setData(d);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
timerRef.current = setTimeout(connect, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect();
|
||||||
|
return () => {
|
||||||
|
wsRef.current?.close();
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, connected };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePaperMetrics() {
|
||||||
|
const [data, setData] = useState<PaperMetrics | null>(null);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const base = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/cv/ws/paper`;
|
||||||
|
const connect = () => {
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(base);
|
||||||
|
wsRef.current = ws;
|
||||||
|
ws.onopen = () => setConnected(true);
|
||||||
|
ws.onclose = () => {
|
||||||
|
setConnected(false);
|
||||||
|
setTimeout(connect, 5000);
|
||||||
|
};
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(e.data) as PaperMetrics;
|
||||||
|
setData(d);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
setTimeout(connect, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
connect();
|
||||||
|
return () => wsRef.current?.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, connected };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchHistorical(): Promise<Record<string, import("./types").BacktestSummary>> {
|
||||||
|
const res = await fetch(`${API_BASE}/backtests/historical`);
|
||||||
|
const list: import("./types").BacktestSummary[] = await res.json();
|
||||||
|
const byStrat: Record<string, import("./types").BacktestSummary> = {};
|
||||||
|
for (const b of list) {
|
||||||
|
if (!byStrat[b.strategy]) byStrat[b.strategy] = b;
|
||||||
|
}
|
||||||
|
return byStrat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBacktestDetail(name: string): Promise<import("./types").BacktestFull> {
|
||||||
|
const res = await fetch(`${API_BASE}/backtest/historical/${encodeURIComponent(name)}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recalcBacktest(
|
||||||
|
name: string,
|
||||||
|
feeTier: number,
|
||||||
|
stakingTier: string,
|
||||||
|
): Promise<import("./types").BacktestFull> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE}/backtest/${encodeURIComponent(name)}/recalc?fee_tier=${feeTier}&staking_tier=${stakingTier}`,
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// ═══════════ FTDT Quant Lab — Type Definitions ═══════════
|
||||||
|
|
||||||
|
export interface Strategy {
|
||||||
|
allocation: number;
|
||||||
|
instrument: string;
|
||||||
|
pnl: number;
|
||||||
|
pnl_pct: number;
|
||||||
|
position: number;
|
||||||
|
trades_today: number;
|
||||||
|
wins: number;
|
||||||
|
win_rate: number;
|
||||||
|
status: "running" | "idle";
|
||||||
|
size: number;
|
||||||
|
fee_paid: number;
|
||||||
|
fee_model: "maker" | "taker";
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
signals: { signal: string; ts: number }[];
|
||||||
|
max_position?: number;
|
||||||
|
stop_loss?: number;
|
||||||
|
entry_price?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trade {
|
||||||
|
time: string;
|
||||||
|
strategy: string;
|
||||||
|
side: string;
|
||||||
|
size: number;
|
||||||
|
price: number;
|
||||||
|
pnl: number;
|
||||||
|
fee: number;
|
||||||
|
reason?: string;
|
||||||
|
pnl_net?: number;
|
||||||
|
pnl_gross?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
strategy: string;
|
||||||
|
size: number;
|
||||||
|
entry_px: number;
|
||||||
|
pnl?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
coin: string;
|
||||||
|
side: "B" | "A";
|
||||||
|
sz: number;
|
||||||
|
limitPx?: string;
|
||||||
|
cloid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiveMetrics {
|
||||||
|
timestamp: number;
|
||||||
|
wallet: string;
|
||||||
|
total_equity: number;
|
||||||
|
base_equity: number;
|
||||||
|
total_pnl: number;
|
||||||
|
total_pnl_pct: number;
|
||||||
|
reserve: number;
|
||||||
|
equity_history: { t: number; v: number }[];
|
||||||
|
strategy_equity: Record<string, { t: number; v: number }[]>;
|
||||||
|
strategies: Record<string, Strategy>;
|
||||||
|
trades: Trade[];
|
||||||
|
status: string;
|
||||||
|
testnet_up: boolean;
|
||||||
|
open_positions: Position[];
|
||||||
|
open_orders: Order[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaperMetrics {
|
||||||
|
timestamp: number;
|
||||||
|
total_equity: number;
|
||||||
|
base_equity: number;
|
||||||
|
total_pnl: number;
|
||||||
|
total_pnl_pct: number;
|
||||||
|
regime: string;
|
||||||
|
equity_history: { t: number; v: number }[];
|
||||||
|
strategy_equity: Record<string, { t: number; v: number }[]>;
|
||||||
|
strategies: Record<string, Strategy>;
|
||||||
|
per_strategy_trades: Record<string, Trade[]>;
|
||||||
|
open_positions: Position[];
|
||||||
|
open_orders: Order[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestSummary {
|
||||||
|
name: string;
|
||||||
|
strategy: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
sharpe: number;
|
||||||
|
sortino: number;
|
||||||
|
pnl_pct: number;
|
||||||
|
max_dd: number;
|
||||||
|
win_rate: number;
|
||||||
|
total_trades: number;
|
||||||
|
coin?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BacktestFull {
|
||||||
|
name?: string;
|
||||||
|
strategy: string;
|
||||||
|
pnl_net?: number;
|
||||||
|
pnl_gross?: number;
|
||||||
|
pnl_pct?: number;
|
||||||
|
pnl_net_pct?: number;
|
||||||
|
pnl_gross_pct?: number;
|
||||||
|
pnl: number;
|
||||||
|
sharpe: number;
|
||||||
|
sortino: number;
|
||||||
|
max_dd: number;
|
||||||
|
win_rate: number;
|
||||||
|
total_trades: number;
|
||||||
|
fees_total: number;
|
||||||
|
num_periods: number;
|
||||||
|
fee_model: string;
|
||||||
|
equity_curve: { t: number | string; v: number }[];
|
||||||
|
trades: Trade[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"out",
|
||||||
|
"out-www",
|
||||||
|
"_next-app-backup"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user