import { useEffect, useState } from "react"; import { apiFetch, MarketSnapshot, PlanState } from "../api/client"; function fmt(n: number | null | undefined, d = 2) { if (n == null || Number.isNaN(n)) return "—"; return n.toFixed(d); } function pnlClass(n: number | null | undefined) { if (n == null || Number.isNaN(n) || n === 0) return ""; return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss"; } /** OKX 期权到期:YYMMDD 当日 08:00 UTC(上海 16:00) */ function expiryMsFromYmd(ymd: string | null | undefined): number | null { if (!ymd || ymd.length !== 6) return null; const yy = 2000 + Number(ymd.slice(0, 2)); const mm = Number(ymd.slice(2, 4)); const dd = Number(ymd.slice(4, 6)); if (![yy, mm, dd].every((x) => Number.isFinite(x))) return null; return Date.UTC(yy, mm - 1, dd, 8, 0, 0); } function formatCountdown(msLeft: number): string { if (msLeft <= 0) return "已到期"; const s = Math.floor(msLeft / 1000); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; if (d > 0) return `${d}天 ${h}时 ${String(m).padStart(2, "0")}分`; if (h > 0) return `${h}时 ${String(m).padStart(2, "0")}分 ${String(sec).padStart(2, "0")}秒`; return `${m}分 ${String(sec).padStart(2, "0")}秒`; } const PHASE_ZH: Record = { idle: "空闲", wait_signal: "等待信号", opening: "开仓中", open: "持仓中", closing: "平仓中", resting: "休息中", paused: "已暂停", stopped: "已停开", outside_window: "窗外", liquidity_wait: "流动性等待", weekend_skip: "周末跳过", }; export default function PlanPage() { const [snap, setSnap] = useState(null); const [plan, setPlan] = useState(null); const [err, setErr] = useState(""); const [busy, setBusy] = useState(""); const [nowMs, setNowMs] = useState(() => Date.now()); async function refresh() { try { const [m, p] = await Promise.all([ apiFetch("/api/market/snapshot"), apiFetch("/api/plan/state"), ]); setSnap(m); setPlan(p); setErr(""); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } } useEffect(() => { refresh(); const t = window.setInterval(refresh, 1500); return () => window.clearInterval(t); }, []); useEffect(() => { const t = window.setInterval(() => setNowMs(Date.now()), 1000); return () => window.clearInterval(t); }, []); async function act(path: string, label: string) { setBusy(label); setErr(""); try { await apiFetch(path, { method: "POST", body: "{}" }); await refresh(); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setBusy(""); } } const bias = snap?.ask_compare?.bias; const biasTag = bias === "call_ask_gt_put" ? ( 买 Call + 永续空 ) : bias === "put_ask_gt_call" ? ( 买 Put + 永续多 ) : ( 等待 / 相等 ); const pos = plan?.position; const open = !!pos?.has_position; const exitMode = plan?.exit_mode ?? "fixed_usdt"; const exitTarget = plan?.exit_target_usdt ?? plan?.net_profit_target ?? 15; const netPnl = pos?.net_pnl ?? 0; const movePct = pos?.move_pct ?? 0; const exitRuleLabel = exitMode === "premium_multiple" ? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}` : `固定 ${fmt(plan?.net_profit_target ?? 15)} U`; const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—"; const expiryMs = pos?.expiry_ms ?? expiryMsFromYmd(pos?.expiry_ymd) ?? null; const countdown = open && expiryMs != null ? formatCountdown(expiryMs - nowMs) : "—"; const countdownUrgent = open && expiryMs != null && expiryMs - nowMs > 0 && expiryMs - nowMs < 6 * 3600_000; return (

自动对冲计划

SIM 本地撮合 · 行情{" "} {(snap?.exchange || "okx").toUpperCase()} {snap?.perp_inst_id ? ` · ${snap.perp_inst_id}` : ""} · 净盈利达标或到期全平

{err ?
{err}
: null}
{busy ? {busy}… : null}

策略

状态 {plan?.running ? ( 启动中 ) : ( 已停 )} {" "} · {phaseLabel} · 已完成 {plan?.rounds_done ?? 0} 组
选约条件 剩余≥{fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥ {fmt(plan?.min_option_leverage, 0)}x
休息 {plan?.rest_left_sec ? `${plan.rest_left_sec}s / ${plan.rest_seconds}s` : "—"}
权益 / 杠杆 {fmt(plan?.ledger?.equity)} / 可用 {fmt(plan?.ledger?.available)} ·{" "} {fmt(plan?.leverage, 0)}x
当前组 {pos?.group_id || "—"}
信号方向 {biasTag}
出场规则 {exitRuleLabel}
净盈利 / 目标 {open ? fmt(netPnl) : "—"} {" / "} {open || exitMode === "fixed_usdt" ? fmt(exitTarget) : "—"}
永续浮盈 {fmt(pos?.perp_upl)}
期权浮盈 {fmt(pos?.option_upl)}
{plan?.last_error ? (
最近错误 {plan.last_error}
) : null}
{open ? ( <>
{pos?.perp_inst_id || "ETH-USDT-SWAP"} {pos?.perp_side === "long" ? "做多" : "做空"}
永续持仓 组 {pos?.group_id} 数量 {fmt(pos?.perp_qty_eth, 2)} ETH {fmt(pos?.leverage ?? plan?.leverage, 0)}x
开仓价 {fmt(pos?.perp_entry_px)}
可平价 {fmt(pos?.perp_mark_px)}
浮盈亏 {fmt(pos?.perp_upl)}
保证金 {fmt(pos?.perp_margin)}
名义价值 {fmt(pos?.perp_notional)}
波动比例 {fmt(movePct, 2)}%
{pos?.option_inst_id || "期权"} {pos?.option_side === "call" ? "看涨 Call" : "看跌 Put"}
{countdown}
期权持仓 到期 {pos?.expiry_ymd || "—"} 行权 {fmt(pos?.strike, 0)} {fmt(pos?.option_qty_eth, 2)} ETH · {fmt(pos?.option_qty_contracts, 0)} 张
开仓均价 {fmt(pos?.option_entry_px)}
标记/买一 {fmt(pos?.option_mark_px)}
浮盈亏 {fmt(pos?.option_upl)}
初始权利金 {fmt(pos?.initial_premium)}
) : ( <>
永续持仓 · 暂无
期权持仓 · 暂无
)}

永续行情

买一 {fmt(snap?.perp?.bid)}
卖一 {fmt(snap?.perp?.ask)}
指数 {fmt(snap?.index_px)}

期权 ATM {snap?.pair ? `@ ${snap.pair.strike}` : ""}

Call 卖一/买一 {fmt(snap?.call?.ask)} / {fmt(snap?.call?.bid)}
Put 卖一/买一 {fmt(snap?.put?.ask)} / {fmt(snap?.put?.bid)}
距现价 {(() => { const strike = snap?.pair?.strike; const px = snap?.index_px ?? (snap?.perp?.bid != null && snap?.perp?.ask != null ? (snap.perp.bid + snap.perp.ask) / 2 : null); if (strike == null || px == null) return "—"; const d = strike - px; const sign = d > 0 ? "+" : ""; return `${sign}${d.toFixed(0)}(最近可用行权价)`; })()}
到期 {snap?.pair?.expiry_ymd || "—"}
); }