From 18506f2a44aa05109ec9dc8050cce55ea2866176 Mon Sep 17 00:00:00 2001 From: dev-worker Date: Mon, 2 Mar 2026 16:19:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20P1/P2/P3=E5=89=A9=E4=BD=996=E9=A1=B9?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-3: 前端持仓USDT从config读riskUsd(不再硬编码*2) P1-4: 平仓兜底不盲目取最后成交,无明确平仓记录则延后结算 P2-1: LISTEN连接断线自动重建+重新LISTEN P2-2: 余额风控LOW_BALANCE自动恢复(余额回升则解除暂停) P2-3: fetch_pending_signals改用asyncio.to_thread避免阻塞事件循环 P3-1: dashboard页面改用新auth体系(authFetch+useAuth+/api/auth/me) --- backend/live_executor.py | 2 +- backend/position_sync.py | 4 ++- backend/risk_guard.py | 15 ++++++++-- frontend/app/dashboard/page.tsx | 49 ++++++++++++++++++++------------- frontend/app/live/page.tsx | 4 ++- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/backend/live_executor.py b/backend/live_executor.py index 11e3ba6..c25a50f 100644 --- a/backend/live_executor.py +++ b/backend/live_executor.py @@ -667,7 +667,7 @@ async def main(): work_conn = ensure_db_conn(work_conn) # 获取待处理信号(NOTIFY + 轮询双保险) - signals = fetch_pending_signals(work_conn) + signals = await asyncio.to_thread(fetch_pending_signals, work_conn) for sig in signals: # 补充TP/SL参数 diff --git a/backend/position_sync.py b/backend/position_sync.py index 18b9d48..484e50e 100644 --- a/backend/position_sync.py +++ b/backend/position_sync.py @@ -444,7 +444,9 @@ async def check_closed_positions(session, conn): if total_qty > 0: exit_price = sum(float(t["price"]) * float(t["qty"]) for t in close_trades) / total_qty elif trades_data: - exit_price = float(trades_data[-1].get("price", exit_price)) + # fallback: 不盲目取最后一条(可能是开仓成交),延后本轮结算 + logger.warning(f"[{symbol}] 未找到明确平仓成交,延后结算") + continue # 汇总手续费(开仓后200ms起算,避免含其他策略成交) for t in trades_data: diff --git a/backend/risk_guard.py b/backend/risk_guard.py index 931c622..fa00a6e 100644 --- a/backend/risk_guard.py +++ b/backend/risk_guard.py @@ -548,10 +548,19 @@ async def main(): # 4. 余额检查 balance = await check_balance(session) - if balance < RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE: - if not risk_state.block_new_entries: + threshold = RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE + if balance < threshold: + if risk_state.circuit_break_reason != "LOW_BALANCE": risk_state.block_new_entries = True - logger.warning(f"🟡 余额不足: ${balance:.2f} < ${RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE:.2f},暂停开仓") + risk_state.status = "warning" + risk_state.circuit_break_reason = "LOW_BALANCE" + logger.warning(f"🟡 余额不足: ${balance:.2f} < ${threshold:.2f},暂停开仓") + else: + if risk_state.circuit_break_reason == "LOW_BALANCE": + risk_state.block_new_entries = False + risk_state.status = "normal" + risk_state.circuit_break_reason = None + logger.info(f"✅ 余额恢复: ${balance:.2f} >= ${threshold:.2f},解除暂停") # 5. 持仓超时检查 await check_hold_timeout(session, conn) diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index da3c5f0..716902c 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { authFetch, useAuth } from "@/lib/auth"; interface UserInfo { id: number; @@ -12,36 +13,46 @@ interface UserInfo { export default function DashboardPage() { const router = useRouter(); + const { isLoggedIn, loading, logout } = useAuth(); const [user, setUser] = useState(null); const [discordId, setDiscordId] = useState(""); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(""); useEffect(() => { - const token = localStorage.getItem("arb_token"); - if (!token) { router.push("/login"); return; } - fetch("/api/user/me", { headers: { Authorization: `Bearer ${token}` } }) - .then(r => { if (!r.ok) { router.push("/login"); return null; } return r.json(); }) - .then(d => { if (d) { setUser(d); setDiscordId(d.discord_id || ""); } }); - }, [router]); + if (loading) return; + if (!isLoggedIn) { router.push("/login"); return; } + authFetch("/api/auth/me") + .then(r => r.ok ? r.json() : Promise.reject()) + .then(d => { + setUser({ + id: d.id, + email: d.email, + discord_id: d.discord_id || null, + tier: d.subscription?.tier || "free", + expires_at: d.subscription?.expires_at || null, + }); + setDiscordId(d.discord_id || ""); + }) + .catch(() => router.push("/login")); + }, [loading, isLoggedIn, router]); const bindDiscord = async () => { setSaving(true); setMsg(""); - const token = localStorage.getItem("arb_token"); - const r = await fetch("/api/user/bind-discord", { - method: "POST", - headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ discord_id: discordId }), - }); - const d = await r.json(); - setMsg(r.ok ? "✅ 绑定成功" : d.detail || "绑定失败"); + try { + const r = await authFetch("/api/user/bind-discord", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ discord_id: discordId }), + }); + const d = await r.json(); + setMsg(r.ok ? "\u2705 绑定成功" : d.detail || "绑定失败"); + if (r.ok && user) setUser({ ...user, discord_id: discordId }); + } catch { setMsg("绑定失败"); } setSaving(false); - if (r.ok && user) setUser({ ...user, discord_id: discordId }); }; - const logout = () => { localStorage.removeItem("arb_token"); router.push("/"); }; - - if (!user) return
加载中...
; + if (loading || !user) return
加载中...
; const tierLabel: Record = { free: "免费版", pro: "Pro", premium: "Premium" }; @@ -80,7 +91,7 @@ export default function DashboardPage() { {saving ? "保存中..." : "绑定"} - {msg &&

{msg}

} + {msg &&

{msg}

}

如何获取Discord ID:设置 → 外观 → 开发者模式 → 右键个人头像 → 复制用户ID

diff --git a/frontend/app/live/page.tsx b/frontend/app/live/page.tsx index fa3fcd0..016c591 100644 --- a/frontend/app/live/page.tsx +++ b/frontend/app/live/page.tsx @@ -238,11 +238,13 @@ function L3_Positions() { const [positions, setPositions] = useState([]); const [wsPrices, setWsPrices] = useState>({}); const [recon, setRecon] = useState(null); + const [riskUsd, setRiskUsd] = useState(2); useEffect(() => { const f = async () => { try { const r = await authFetch(`/api/live/positions?strategy=${LIVE_STRATEGY}`); if (r.ok) { const j = await r.json(); setPositions(j.data||[]); } } catch {} try { const r = await authFetch("/api/live/reconciliation"); if (r.ok) setRecon(await r.json()); } catch {} + try { const r = await authFetch("/api/live/config"); if (r.ok) { const cfg = await r.json(); setRiskUsd(parseFloat(cfg?.risk_per_trade_usd?.value ?? "2")); } } catch {} }; f(); const iv = setInterval(f, 5000); return () => clearInterval(iv); }, []); @@ -284,7 +286,7 @@ function L3_Positions() { const fullR = rd > 0 ? (p.direction==="LONG"?(cp-entry)/rd:(entry-cp)/rd) : 0; const tp1R = rd > 0 ? (p.direction==="LONG"?((p.tp1_price||0)-entry)/rd:(entry-(p.tp1_price||0))/rd) : 0; const unrealR = p.tp1_hit ? 0.5*tp1R+0.5*fullR : fullR; - const unrealUsdt = unrealR * 2; + const unrealUsdt = unrealR * riskUsd; const holdColor = holdMin>=60?"text-red-500 font-bold":holdMin>=45?"text-amber-500":"text-slate-400"; const dist = liqDist[p.symbol]; const distColor = dist !== undefined ? (dist < 8 ? "bg-slate-900 text-white" : dist < 12 ? "bg-red-50 text-red-700" : dist < 20 ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700") : "bg-slate-50 text-slate-400";