arbitrage-engine/frontend/app/strategy-plaza/[id]/page.tsx

177 lines
7.3 KiB
TypeScript

"use client";
import { useParams, useSearchParams, useRouter } from "next/navigation";
import { useEffect, useState, useCallback } from "react";
import { authFetch } from "@/lib/auth";
import Link from "next/link";
import dynamic from "next/dynamic";
import {
ArrowLeft,
CheckCircle,
PauseCircle,
AlertCircle,
Clock,
} from "lucide-react";
// ─── Dynamic imports for each strategy's pages ───────────────────
const SignalsV53 = dynamic(() => import("@/app/signals-v53/page"), { ssr: false });
const SignalsV53Fast = dynamic(() => import("@/app/signals-v53fast/page"), { ssr: false });
const SignalsV53Middle = dynamic(() => import("@/app/signals-v53middle/page"), { ssr: false });
const PaperV53 = dynamic(() => import("@/app/paper-v53/page"), { ssr: false });
const PaperV53Fast = dynamic(() => import("@/app/paper-v53fast/page"), { ssr: false });
const PaperV53Middle = dynamic(() => import("@/app/paper-v53middle/page"), { ssr: false });
// ─── Types ────────────────────────────────────────────────────────
interface StrategySummary {
id: string;
display_name: string;
status: string;
started_at: number;
initial_balance: number;
current_balance: number;
net_usdt: number;
net_r: number;
trade_count: number;
win_rate: number;
avg_win_r: number;
avg_loss_r: number;
open_positions: number;
pnl_usdt_24h: number;
pnl_r_24h: number;
cvd_windows?: string;
description?: string;
}
// ─── Helpers ──────────────────────────────────────────────────────
function fmtDur(ms: number) {
const s = Math.floor((Date.now() - ms) / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
if (d > 0) return `${d}${h}h`;
if (h > 0) return `${h}h${m}m`;
return `${m}m`;
}
function StatusBadge({ status }: { status: string }) {
if (status === "running") return <span className="flex items-center gap-1 text-xs text-emerald-400"><CheckCircle size={12} /></span>;
if (status === "paused") return <span className="flex items-center gap-1 text-xs text-yellow-400"><PauseCircle size={12} /></span>;
return <span className="flex items-center gap-1 text-xs text-red-400"><AlertCircle size={12} /></span>;
}
// ─── Content router ───────────────────────────────────────────────
function SignalsContent({ strategyId }: { strategyId: string }) {
if (strategyId === "v53") return <SignalsV53 />;
if (strategyId === "v53_fast") return <SignalsV53Fast />;
if (strategyId === "v53_middle") return <SignalsV53Middle />;
return <div className="p-8 text-gray-400">: {strategyId}</div>;
}
function PaperContent({ strategyId }: { strategyId: string }) {
if (strategyId === "v53") return <PaperV53 />;
if (strategyId === "v53_fast") return <PaperV53Fast />;
if (strategyId === "v53_middle") return <PaperV53Middle />;
return <div className="p-8 text-gray-400">: {strategyId}</div>;
}
// ─── Main Page ────────────────────────────────────────────────────
export default function StrategyDetailPage() {
const params = useParams();
const searchParams = useSearchParams();
const router = useRouter();
const strategyId = params?.id as string;
const tab = searchParams?.get("tab") || "signals";
const [summary, setSummary] = useState<StrategySummary | null>(null);
const [loading, setLoading] = useState(true);
const fetchSummary = useCallback(async () => {
try {
const r = await authFetch(`/api/strategy-plaza/${strategyId}/summary`);
if (r.ok) {
const d = await r.json();
setSummary(d);
}
} catch {}
setLoading(false);
}, [strategyId]);
useEffect(() => {
fetchSummary();
const iv = setInterval(fetchSummary, 30000);
return () => clearInterval(iv);
}, [fetchSummary]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-gray-400 animate-pulse">...</div>
</div>
);
}
const isProfit = (summary?.net_usdt ?? 0) >= 0;
return (
<div className="p-4 max-w-full">
{/* Back + Strategy Header */}
<div className="flex items-center gap-3 mb-4">
<Link href="/strategy-plaza" className="flex items-center gap-1 text-gray-400 hover:text-white text-sm transition-colors">
<ArrowLeft size={16} />
广
</Link>
<span className="text-gray-600">/</span>
<span className="text-white font-medium">{summary?.display_name ?? strategyId}</span>
</div>
{/* Summary Bar */}
{summary && (
<div className="flex flex-wrap items-center gap-4 bg-gray-900 border border-gray-700 rounded-xl px-5 py-3 mb-4">
<StatusBadge status={summary.status} />
<span className="text-xs text-gray-400">
<Clock size={10} className="inline mr-1" /> {fmtDur(summary.started_at)}
</span>
{summary.cvd_windows && (
<span className="text-xs text-cyan-400 bg-cyan-900/30 px-2 py-0.5 rounded">CVD {summary.cvd_windows}</span>
)}
<span className="ml-auto flex items-center gap-4 text-sm">
<span className="text-gray-400"> <span className={summary.win_rate >= 50 ? "text-emerald-400 font-bold" : "text-yellow-400 font-bold"}>{summary.win_rate}%</span></span>
<span className="text-gray-400">R <span className={`font-bold ${isProfit ? "text-emerald-400" : "text-red-400"}`}>{summary.net_r >= 0 ? "+" : ""}{summary.net_r}R</span></span>
<span className="text-gray-400"> <span className={`font-bold ${isProfit ? "text-emerald-400" : "text-red-400"}`}>{summary.current_balance.toLocaleString()} U</span></span>
<span className="text-gray-400">24h <span className={`font-bold ${summary.pnl_usdt_24h >= 0 ? "text-emerald-400" : "text-red-400"}`}>{summary.pnl_usdt_24h >= 0 ? "+" : ""}{summary.pnl_usdt_24h} U</span></span>
</span>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 mb-4">
{[
{ key: "signals", label: "📊 信号引擎" },
{ key: "paper", label: "📈 模拟盘" },
].map(({ key, label }) => (
<button
key={key}
onClick={() => router.push(`/strategy-plaza/${strategyId}?tab=${key}`)}
className={`px-5 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === key
? "bg-cyan-600 text-white"
: "bg-gray-800 text-gray-400 hover:text-white hover:bg-gray-700"
}`}
>
{label}
</button>
))}
</div>
{/* Content — direct render of existing pages */}
<div>
{tab === "signals" ? (
<SignalsContent strategyId={strategyId} />
) : (
<PaperContent strategyId={strategyId} />
)}
</div>
</div>
);
}