"use client";
import { useEffect, useState, useCallback } from "react";
import { authFetch } from "@/lib/auth";
import { useAuth } from "@/lib/auth";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
TrendingUp,
TrendingDown,
Clock,
Activity,
AlertCircle,
CheckCircle,
PauseCircle,
Plus,
Settings,
Trash2,
PlusCircle,
} from "lucide-react";
interface StrategyCard {
strategy_id: string;
display_name: string;
symbol: string;
status: "running" | "paused" | "error";
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;
std_r: number;
last_trade_at: number | null;
}
function formatDuration(ms: number): string {
const totalSec = Math.floor((Date.now() - ms) / 1000);
const d = Math.floor(totalSec / 86400);
const h = Math.floor((totalSec % 86400) / 3600);
const m = Math.floor((totalSec % 3600) / 60);
if (d > 0) return `${d}天 ${h}小时`;
if (h > 0) return `${h}小时 ${m}分`;
return `${m}分钟`;
}
function formatTime(ms: number | null): string {
if (!ms) return "—";
const d = new Date(ms);
return d.toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
function StatusBadge({ status }: { status: string }) {
if (status === "running") {
return (
运行中
);
}
if (status === "paused") {
return (
已暂停
);
}
return (
异常
);
}
// ── AddBalanceModal ────────────────────────────────────────────────────────────
function AddBalanceModal({
strategy,
onClose,
onSuccess,
}: {
strategy: StrategyCard;
onClose: () => void;
onSuccess: () => void;
}) {
const [amount, setAmount] = useState(1000);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async () => {
if (amount <= 0) { setError("金额必须大于0"); return; }
setSubmitting(true);
setError("");
try {
const res = await authFetch(`/api/strategies/${strategy.strategy_id}/add-balance`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount }),
});
if (!res.ok) throw new Error("追加失败");
onSuccess();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : "未知错误");
} finally {
setSubmitting(false);
}
};
return (
追加余额
策略:{strategy.display_name}
{error &&
{error}
}
);
}
// ── StrategyCardComponent ──────────────────────────────────────────────────────
function StrategyCardComponent({
s,
onDeprecate,
onAddBalance,
}: {
s: StrategyCard;
onDeprecate: (s: StrategyCard) => void;
onAddBalance: (s: StrategyCard) => void;
}) {
const isProfit = s.net_usdt >= 0;
const is24hProfit = s.pnl_usdt_24h >= 0;
const balancePct = ((s.current_balance / s.initial_balance) * 100).toFixed(1);
const symbolShort = s.symbol?.replace("USDT", "") || "";
return (
{/* Header */}
{s.display_name}
{symbolShort && (
{symbolShort}
)}
{formatDuration(s.started_at)}
{/* Main PnL */}
当前余额
{s.current_balance.toLocaleString()}
USDT
累计盈亏
{isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U
{/* Balance Bar */}
{balancePct}%
{s.initial_balance.toLocaleString()} USDT 初始
{/* Stats Row */}
胜率
= 50 ? "text-emerald-600" : s.win_rate >= 45 ? "text-amber-600" : "text-red-500"}`}>
{s.win_rate}%
净R
= 0 ? "text-emerald-600" : "text-red-500"}`}>
{s.net_r >= 0 ? "+" : ""}{s.net_r}R
{/* Avg win/loss */}
平均赢
+{s.avg_win_r}R
平均亏
{s.avg_loss_r}R
{/* Footer */}
{is24hProfit ? (
) : (
)}
24h {is24hProfit ? "+" : ""}{s.pnl_usdt_24h.toLocaleString()} U
{s.open_positions > 0 ? (
{s.open_positions}仓持仓中
) : (
上次: {formatTime(s.last_trade_at)}
)}
{/* Action Buttons */}
调整参数
);
}
// ── Main Page ─────────────────────────────────────────────────────────────────
export default function StrategyPlazaPage() {
useAuth();
const router = useRouter();
const [strategies, setStrategies] = useState([]);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState(null);
const [addBalanceTarget, setAddBalanceTarget] = useState(null);
type SymbolFilter = "ALL" | "BTCUSDT" | "ETHUSDT" | "XRPUSDT" | "SOLUSDT";
type StatusFilter = "all" | "running" | "paused" | "error";
type PnlFilter = "all" | "positive" | "negative";
type SortKey = "recent" | "net_usdt_desc" | "net_usdt_asc" | "pnl24h_desc" | "pnl24h_asc";
const [symbolFilter, setSymbolFilter] = useState("ALL");
const [statusFilter, setStatusFilter] = useState("all");
const [pnlFilter, setPnlFilter] = useState("all");
const [sortKey, setSortKey] = useState("net_usdt_desc");
const fetchData = useCallback(async () => {
try {
const res = await authFetch("/api/strategies");
const data = await res.json();
setStrategies(data.strategies || []);
setLastUpdated(new Date());
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [fetchData]);
const handleDeprecate = async (s: StrategyCard) => {
if (!confirm(`确认废弃策略「${s.display_name}」?\n\n废弃后策略停止运行,数据永久保留,可在废弃列表中重新启用。`)) return;
try {
await authFetch(`/api/strategies/${s.strategy_id}/deprecate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: true }),
});
await fetchData();
} catch (e) {
console.error(e);
}
};
const filteredStrategies = strategies
.filter((s) => {
if (symbolFilter !== "ALL" && s.symbol !== symbolFilter) return false;
if (statusFilter !== "all" && s.status !== statusFilter) return false;
if (pnlFilter === "positive" && s.net_usdt <= 0) return false;
if (pnlFilter === "negative" && s.net_usdt >= 0) return false;
return true;
})
.sort((a, b) => {
switch (sortKey) {
case "net_usdt_desc":
return b.net_usdt - a.net_usdt;
case "net_usdt_asc":
return a.net_usdt - b.net_usdt;
case "pnl24h_desc":
return b.pnl_usdt_24h - a.pnl_usdt_24h;
case "pnl24h_asc":
return a.pnl_usdt_24h - b.pnl_usdt_24h;
case "recent":
default: {
const aTs = a.last_trade_at ?? a.started_at ?? 0;
const bTs = b.last_trade_at ?? b.started_at ?? 0;
return bTs - aTs;
}
}
});
if (loading) {
return (
);
}
return (
{/* Header */}
{lastUpdated && (
{lastUpdated.toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })}
)}
{/* Filters & Sorting */}
{/* 左侧:币种 + 盈亏过滤 */}
币种:
{(["ALL", "BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] as SymbolFilter[]).map((sym) => {
const label = sym === "ALL" ? "全部" : sym.replace("USDT", "");
const active = symbolFilter === sym;
return (
);
})}
盈亏:
{[
{ key: "all" as PnlFilter, label: "全部" },
{ key: "positive" as PnlFilter, label: "仅盈利" },
{ key: "negative" as PnlFilter, label: "仅亏损" },
].map((opt) => {
const active = pnlFilter === opt.key;
return (
);
})}
{/* 右侧:状态 + 排序 */}
状态:
{[
{ key: "all" as StatusFilter, label: "全部" },
{ key: "running" as StatusFilter, label: "运行中" },
{ key: "paused" as StatusFilter, label: "已暂停" },
{ key: "error" as StatusFilter, label: "异常" },
].map((opt) => {
const active = statusFilter === opt.key;
return (
);
})}
排序:
{/* Strategy Cards */}
{filteredStrategies.map((s) => (
))}
{filteredStrategies.length === 0 && (
暂无运行中的策略
)}
{/* Add Balance Modal */}
{addBalanceTarget && (
setAddBalanceTarget(null)}
onSuccess={fetchData}
/>
)}
);
}