arbitrage-engine/frontend/app/strategy-plaza/deprecated/page.tsx

197 lines
7.7 KiB
TypeScript
Raw Permalink 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 { useEffect, useState, useCallback } from "react";
import { authFetch, useAuth } from "@/lib/auth";
import Link from "next/link";
import {
TrendingUp, TrendingDown, Clock, Activity, RotateCcw
} from "lucide-react";
interface DeprecatedStrategy {
strategy_id: string;
display_name: string;
symbol: string;
status: string;
started_at: number;
deprecated_at: number | null;
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;
pnl_usdt_24h: number;
last_trade_at: number | null;
}
function formatTime(ms: number | null): string {
if (!ms) return "—";
return new Date(ms).toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
export default function DeprecatedStrategiesPage() {
useAuth();
const [strategies, setStrategies] = useState<DeprecatedStrategy[]>([]);
const [loading, setLoading] = useState(true);
const [restoring, setRestoring] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
const res = await authFetch("/api/strategies?include_deprecated=true");
const data = await res.json();
const deprecated = (data.strategies || []).filter(
(s: DeprecatedStrategy) => s.status === "deprecated"
);
setStrategies(deprecated);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleRestore = async (sid: string, name: string) => {
if (!confirm(`确认重新启用策略「${name}」?将继续使用原有余额和历史数据。`)) return;
setRestoring(sid);
try {
await authFetch(`/api/strategies/${sid}/restore`, { method: "POST" });
await fetchData();
} catch (e) {
console.error(e);
} finally {
setRestoring(null);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-64">
<div className="text-slate-400 text-sm animate-pulse">...</div>
</div>
);
}
return (
<div className="p-4 max-w-5xl mx-auto">
<div className="flex items-center justify-between mb-5">
<div>
<h1 className="text-lg font-bold text-slate-800"></h1>
<p className="text-slate-500 text-xs mt-0.5"></p>
</div>
<Link
href="/strategy-plaza"
className="text-xs text-blue-600 hover:underline"
>
广
</Link>
</div>
{strategies.length === 0 ? (
<div className="text-center text-slate-400 text-sm py-16"></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{strategies.map((s) => {
const isProfit = s.net_usdt >= 0;
const is24hProfit = s.pnl_usdt_24h >= 0;
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
return (
<div key={s.strategy_id} className="rounded-xl border border-slate-200 bg-white overflow-hidden opacity-80">
{/* Header */}
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-slate-600 text-sm">{s.display_name}</h3>
<span className="text-[10px] text-slate-400 bg-slate-200 px-1.5 py-0.5 rounded-full"></span>
</div>
<span className="text-[10px] text-slate-400">{s.symbol.replace("USDT", "")}</span>
</div>
{/* PnL */}
<div className="px-4 pt-3 pb-2">
<div className="flex items-end justify-between mb-2">
<div>
<div className="text-[10px] text-slate-400 mb-0.5"></div>
<div className="text-xl font-bold text-slate-700">
{s.current_balance.toLocaleString()}
<span className="text-xs font-normal text-slate-400 ml-1">USDT</span>
</div>
</div>
<div className="text-right">
<div className="text-[10px] text-slate-400 mb-0.5"></div>
<div className={`text-lg font-bold ${isProfit ? "text-emerald-600" : "text-red-500"}`}>
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
</div>
</div>
</div>
{/* Balance bar */}
<div className="mb-3">
<div className="flex justify-between text-[10px] text-slate-400 mb-1">
<span>{balancePct}%</span>
<span>{s.initial_balance.toLocaleString()} USDT </span>
</div>
<div className="w-full bg-slate-100 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full ${isProfit ? "bg-emerald-400" : "bg-red-300"}`}
style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }}
/>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-2 mb-3">
<div className="bg-slate-50 rounded-lg p-2 text-center">
<div className="text-[10px] text-slate-400"></div>
<div className="text-sm font-bold text-slate-600">{s.win_rate}%</div>
</div>
<div className="bg-slate-50 rounded-lg p-2 text-center">
<div className="text-[10px] text-slate-400">R</div>
<div className={`text-sm font-bold ${s.net_r >= 0 ? "text-emerald-600" : "text-red-500"}`}>
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
</div>
</div>
<div className="bg-slate-50 rounded-lg p-2 text-center">
<div className="text-[10px] text-slate-400"></div>
<div className="text-sm font-bold text-slate-700">{s.trade_count}</div>
</div>
</div>
</div>
{/* Footer */}
<div className="px-4 py-2.5 border-t border-slate-100 flex items-center justify-between bg-slate-50/60">
<div className="flex items-center gap-1">
{is24hProfit ? <TrendingUp size={12} className="text-emerald-500" /> : <TrendingDown size={12} className="text-red-400" />}
<span className="text-[10px] text-slate-500">
{formatTime(s.deprecated_at)}
</span>
</div>
<button
onClick={() => handleRestore(s.strategy_id, s.display_name)}
disabled={restoring === s.strategy_id}
className="flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 disabled:opacity-50 transition-colors font-medium"
>
<RotateCcw size={11} />
{restoring === s.strategy_id ? "启用中..." : "重新启用"}
</button>
</div>
</div>
);
})}
</div>
)}
</div>
);
}