import React, { useEffect, useMemo, useRef, useState } from "react"; import { ColorType, CrosshairMode, LineSeries, createChart, type IChartApi, type LineData, type Time, } from "lightweight-charts"; import { ArrowDownRight, ArrowUpRight, RefreshCw, Wallet } from "lucide-react"; import type { LiveStrategyState, StrategyMarket, TradeLogEntry, } from "../shared/strategyTypes"; async function api(path: string, init?: RequestInit): Promise { const response = await fetch(path, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, }); if (!response.ok) { const body = (await response.json().catch(() => undefined)) as { error?: string } | undefined; throw new Error(body?.error ?? `Request failed: ${response.status}`); } return (await response.json()) as T; } type SortKey = "currentValue" | "unrealizedPnlPct" | "name" | "lastUpdated"; export function LivePage() { const [market, setMarket] = useState("us"); const [strategies, setStrategies] = useState([]); const [selectedName, setSelectedName] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [sortKey, setSortKey] = useState("unrealizedPnlPct"); const [filterText, setFilterText] = useState(""); const refresh = (mk: StrategyMarket = market) => { setLoading(true); setError(""); api(`/api/strategies/live?market=${mk}`) .then((res) => { setStrategies(res); if (res.length > 0) { setSelectedName((prev) => prev && res.some((s) => s.name === prev) ? prev : res[0].name, ); } else { setSelectedName(null); } }) .catch((err) => setError(err instanceof Error ? err.message : "加载失败")) .finally(() => setLoading(false)); }; useEffect(() => { refresh(market); // eslint-disable-next-line react-hooks/exhaustive-deps }, [market]); const sorted = useMemo(() => { const list = [...strategies]; const ft = filterText.trim().toLowerCase(); const filtered = ft ? list.filter( (s) => s.name.toLowerCase().includes(ft) || s.label.toLowerCase().includes(ft), ) : list; filtered.sort((a, b) => { switch (sortKey) { case "currentValue": return (b.currentValue ?? 0) - (a.currentValue ?? 0); case "unrealizedPnlPct": return (b.unrealizedPnlPct ?? 0) - (a.unrealizedPnlPct ?? 0); case "name": return a.label.localeCompare(b.label); case "lastUpdated": return (b.lastUpdated ?? "").localeCompare(a.lastUpdated ?? ""); } }); return filtered; }, [strategies, sortKey, filterText]); const focused = useMemo( () => sorted.find((s) => s.name === selectedName) ?? sorted[0], [sorted, selectedName], ); return (
{error &&
{error}
}
setFilterText(e.target.value)} />
{focused ? ( ) : (

未选择策略

)}
); } function LiveStrategyDetail({ strategy }: { strategy: LiveStrategyState }) { const m = strategy; return ( <>

{m.name}

{m.label}

{m.market.toUpperCase()} · 初始 {money(m.initialCapital)} · 更新于 {m.lastUpdated ?? "-"} {m.priceAsOf && m.priceAsOf !== m.lastUpdated ? ` · 价格 ${m.priceAsOf}` : ""}

= 0 ? "positive" : "negative"}`}> {money(m.currentValue)} {m.unrealizedPnl >= 0 ? "+" : ""} {money(m.unrealizedPnl)} ({pct(m.unrealizedPnlPct)})
{m.metrics && } {m.metrics && } {m.metrics && }
); } function SummaryCell({ label, value, positive, negative, }: { label: string; value: string; positive?: boolean; negative?: boolean; }) { const cls = positive ? "positive" : negative ? "negative" : ""; return (
{value} {label}
); } function LiveEquityChart({ equityCurve, initialCapital, }: { equityCurve: { date: string; value: number }[]; initialCapital: number; }) { const containerRef = useRef(null); const chartRef = useRef(null); useEffect(() => { if (!containerRef.current) return; const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: 280, layout: { background: { type: ColorType.Solid, color: "#0d1219" }, textColor: "#b0b0b0", attributionLogo: false, }, grid: { vertLines: { color: "#1a2030" }, horzLines: { color: "#1a2030" }, }, crosshair: { mode: CrosshairMode.Normal }, timeScale: { borderColor: "#1d2530", timeVisible: false }, rightPriceScale: { borderColor: "#1d2530" }, }); chartRef.current = chart; const eqSeries = chart.addSeries(LineSeries, { color: "#4db6ff", lineWidth: 2, priceLineVisible: false, }); const data: LineData