From 4d5ebbb1b3bb300a83f9b09d51bbe96ed1bab2bb Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 06:32:29 +0000 Subject: [PATCH] feat: strategy-plaza frontend - cards overview + detail tabs --- frontend/app/strategy-plaza/[id]/page.tsx | 328 ++++++++++++++++++++++ frontend/app/strategy-plaza/page.tsx | 253 +++++++++++++++++ frontend/components/Sidebar.tsx | 5 +- 3 files changed, 582 insertions(+), 4 deletions(-) create mode 100644 frontend/app/strategy-plaza/[id]/page.tsx create mode 100644 frontend/app/strategy-plaza/page.tsx diff --git a/frontend/app/strategy-plaza/[id]/page.tsx b/frontend/app/strategy-plaza/[id]/page.tsx new file mode 100644 index 0000000..7a00198 --- /dev/null +++ b/frontend/app/strategy-plaza/[id]/page.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useParams, useSearchParams, useRouter } from "next/navigation"; +import { authFetch } from "@/lib/auth"; +import Link from "next/link"; +import { + ArrowLeft, + CheckCircle, + PauseCircle, + AlertCircle, + Clock, + TrendingUp, + TrendingDown, +} from "lucide-react"; + +// ─── 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; + std_r: number; + cvd_windows?: string; + description?: string; +} + +interface Signal { + ts: number; + symbol: string; + score: number; + signal: string | null; + price: number; + factors: any; +} + +interface Trade { + id: number; + symbol: string; + direction: string; + score: number; + entry_price: number; + exit_price: number | null; + tp1_price: number; + tp2_price: number; + sl_price: number; + tp1_hit: boolean; + pnl_r: number; + risk_distance: number; + entry_ts: number; + exit_ts: number | null; + status: string; +} + +// ─── Helpers ────────────────────────────────────────────────────── + +function bjt(ms: number) { + const d = new Date(ms + 8 * 3600 * 1000); + return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; +} + +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); + if (d > 0) return `${d}天${h}h`; + const m = Math.floor((s % 3600) / 60); + if (h > 0) return `${h}h${m}m`; + return `${m}m`; +} + +function StatusBadge({ status }: { status: string }) { + if (status === "running") return 运行中; + if (status === "paused") return 已暂停; + return 异常; +} + +// ─── Sub-views ──────────────────────────────────────────────────── + +function SignalsView({ strategyId }: { strategyId: string }) { + const [signals, setSignals] = useState([]); + const [loading, setLoading] = useState(true); + + const fetch_ = useCallback(async () => { + try { + const r = await authFetch(`/api/strategy-plaza/${strategyId}/signals?limit=40`); + const d = await r.json(); + setSignals(d.signals || []); + } catch {} + setLoading(false); + }, [strategyId]); + + useEffect(() => { fetch_(); const iv = setInterval(fetch_, 15000); return () => clearInterval(iv); }, [fetch_]); + + if (loading) return
加载信号中...
; + + return ( +
+ + + + + + + + + + + + + + {signals.map((s, i) => { + const fc = s.factors && (typeof s.factors === "string" ? JSON.parse(s.factors) : s.factors); + return ( + + + + + + + + + + ); + })} + +
时间(北京)币种价格分数信号CVD 30mCVD 4h
{bjt(s.ts)}{s.symbol.replace("USDT", "")}{s.price?.toLocaleString()} + = 75 ? "text-emerald-400" : s.score >= 50 ? "text-yellow-400" : "text-gray-500"}`}> + {s.score} + + + {s.signal ? ( + + {s.signal} + + ) : } + {fc?.cvd_30m?.toFixed(0) ?? "—"}{fc?.cvd_4h?.toFixed(0) ?? "—"}
+ {signals.length === 0 &&
暂无信号数据
} +
+ ); +} + +function TradesView({ strategyId }: { strategyId: string }) { + const [trades, setTrades] = useState([]); + const [loading, setLoading] = useState(true); + + const fetch_ = useCallback(async () => { + try { + const r = await authFetch(`/api/strategy-plaza/${strategyId}/trades?limit=50`); + const d = await r.json(); + setTrades(d.trades || []); + } catch {} + setLoading(false); + }, [strategyId]); + + useEffect(() => { fetch_(); }, [fetch_]); + + if (loading) return
加载交易记录中...
; + + return ( +
+ + + + + + + + + + + + + + + + {trades.map((t) => { + const isWin = t.pnl_r > 0; + const isActive = !t.exit_ts; + return ( + + + + + + + + + + + + ); + })} + +
入场时间出场时间币种方向入场价出场价盈亏R盈亏U状态
{bjt(t.entry_ts)}{t.exit_ts ? bjt(t.exit_ts) : 持仓中}{t.symbol.replace("USDT", "")} + + {t.direction === "LONG" ? "多" : "空"} + + {t.entry_price?.toLocaleString()}{t.exit_price?.toLocaleString() ?? "—"} + {isActive ? "活跃" : `${isWin ? "+" : ""}${t.pnl_r?.toFixed(3)}R`} + + {isActive ? "—" : `${isWin ? "+" : ""}${Math.round(t.pnl_r * 200)}U`} + + + {isActive ? "活跃" : isWin ? "盈利" : "亏损"} + +
+ {trades.length === 0 &&
暂无交易记录
} +
+ ); +} + +// ─── 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(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
加载中...
; + if (!summary) return
策略不存在
; + + const isProfit = summary.net_usdt >= 0; + + return ( +
+ {/* Back */} + + + 返回策略广场 + + + {/* Header */} +
+
+
+

{summary.display_name}

+ {summary.description &&

{summary.description}

} +
+ + + + 运行 {fmtDur(summary.started_at)} + + {summary.cvd_windows && ( + CVD {summary.cvd_windows} + )} +
+
+
+
+ {isProfit ? "+" : ""}{summary.net_usdt.toLocaleString()} U +
+
{summary.current_balance.toLocaleString()} / {summary.initial_balance.toLocaleString()} USDT
+
+
+ + {/* Stats */} +
+ {[ + { label: "胜率", value: `${summary.win_rate}%`, color: summary.win_rate >= 50 ? "text-emerald-400" : summary.win_rate >= 45 ? "text-yellow-400" : "text-red-400" }, + { label: "净R", value: `${summary.net_r >= 0 ? "+" : ""}${summary.net_r}R`, color: summary.net_r >= 0 ? "text-emerald-400" : "text-red-400" }, + { label: "24h盈亏", value: `${summary.pnl_usdt_24h >= 0 ? "+" : ""}${summary.pnl_usdt_24h}U`, color: summary.pnl_usdt_24h >= 0 ? "text-emerald-400" : "text-red-400" }, + { label: "总交易数", value: summary.trade_count, color: "text-white" }, + ].map(({ label, value, color }) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ + {/* Tabs */} +
+ {["signals", "paper"].map((t) => ( + + ))} +
+ + {/* Tab Content */} +
+ {tab === "signals" ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/app/strategy-plaza/page.tsx b/frontend/app/strategy-plaza/page.tsx new file mode 100644 index 0000000..8108d3c --- /dev/null +++ b/frontend/app/strategy-plaza/page.tsx @@ -0,0 +1,253 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { authFetch } from "@/lib/auth"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; +import { + TrendingUp, + TrendingDown, + Minus, + Clock, + Activity, + AlertCircle, + CheckCircle, + PauseCircle, +} from "lucide-react"; + +interface StrategyCard { + id: string; + display_name: 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 ( + + + 异常 + + ); +} + +function StrategyCardComponent({ s }: { s: StrategyCard }) { + 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 ( + +
+ {/* Header */} +
+
+

+ {s.display_name} +

+
+ + + + 已运行 {formatDuration(s.started_at)} + +
+
+
+
+ {isProfit ? "+" : ""}{s.net_usdt.toLocaleString()} U +
+
{balancePct}% 余额
+
+
+ + {/* Balance Bar */} +
+
+ 当前余额 + {s.current_balance.toLocaleString()} / {s.initial_balance.toLocaleString()} USDT +
+
+
= s.initial_balance ? "bg-emerald-500" : "bg-red-500"}`} + style={{ width: `${Math.min(100, Math.max(0, (s.current_balance / s.initial_balance) * 100))}%` }} + /> +
+
+ + {/* Stats Grid */} +
+
+
胜率
+
= 50 ? "text-emerald-400" : s.win_rate >= 45 ? "text-yellow-400" : "text-red-400"}`}> + {s.win_rate}% +
+
+
+
净R
+
= 0 ? "text-emerald-400" : "text-red-400"}`}> + {s.net_r >= 0 ? "+" : ""}{s.net_r}R +
+
+
+
交易数
+
{s.trade_count}
+
+
+ + {/* P&L Ratio */} +
+
+
平均赢
+
+{s.avg_win_r}R
+
+
+
平均亏
+
{s.avg_loss_r}R
+
+
+ + {/* 24h & Last Trade */} +
+
+ {is24hProfit ? ( + + ) : ( + + )} + + 24h {is24hProfit ? "+" : ""}{s.pnl_usdt_24h.toLocaleString()} U ({is24hProfit ? "+" : ""}{s.pnl_r_24h}R) + +
+
+ + {s.open_positions > 0 ? ( + {s.open_positions}仓持仓中 + ) : ( + 上次: {formatTime(s.last_trade_at)} + )} +
+
+
+ + ); +} + +export default function StrategyPlazaPage() { + const { token } = useAuth(); + const [strategies, setStrategies] = useState([]); + const [loading, setLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + + const fetchData = useCallback(async () => { + try { + const res = await authFetch("/api/strategy-plaza"); + 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]); + + if (loading) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

策略广场

+

+ 点击策略卡片查看信号引擎和模拟盘详情 +

+
+
+ {lastUpdated ? ( + <> + + {lastUpdated.toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })} 更新 + + ) : null} +
+
+ + {/* Strategy Cards */} +
+ {strategies.map((s) => ( + + ))} +
+ + {strategies.length === 0 && ( +
暂无策略数据
+ )} +
+ ); +} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index c82d310..1228219 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -14,10 +14,7 @@ const navItems = [ { href: "/", label: "仪表盘", icon: LayoutDashboard }, { href: "/trades", label: "成交流", icon: Activity }, { href: "/live", label: "⚡ 实盘交易", icon: Bolt, section: "── 实盘 ──" }, - { href: "/signals-v53", label: "V5.3 信号引擎", icon: Zap, section: "── V5.3 ──" }, - { href: "/paper-v53", label: "V5.3 模拟盘", icon: LineChart }, - { href: "/signals-v53fast", label: "V5.3 Fast 信号", icon: Zap, section: "── V5.3 Fast ──" }, - { href: "/paper-v53fast", label: "V5.3 Fast 模拟盘", icon: LineChart }, + { href: "/strategy-plaza", label: "策略广场", icon: Zap, section: "── 策略 ──" }, { href: "/server", label: "服务器", icon: Monitor }, { href: "/about", label: "说明", icon: Info }, ];