177 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|