fix: P1/P2/P3剩余6项全部修复

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)
This commit is contained in:
dev-worker 2026-03-02 16:19:03 +00:00
parent 27a51b4d19
commit 18506f2a44
5 changed files with 49 additions and 25 deletions

View File

@ -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参数

View File

@ -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:

View File

@ -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)

View File

@ -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<UserInfo | null>(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", {
try {
const r = await authFetch("/api/user/bind-discord", {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ discord_id: discordId }),
});
const d = await r.json();
setMsg(r.ok ? "✅ 绑定成功" : d.detail || "绑定失败");
setSaving(false);
setMsg(r.ok ? "\u2705 绑定成功" : d.detail || "绑定失败");
if (r.ok && user) setUser({ ...user, discord_id: discordId });
} catch { setMsg("绑定失败"); }
setSaving(false);
};
const logout = () => { localStorage.removeItem("arb_token"); router.push("/"); };
if (!user) return <div className="text-slate-500 p-8">...</div>;
if (loading || !user) return <div className="text-slate-500 p-8">...</div>;
const tierLabel: Record<string, string> = { free: "免费版", pro: "Pro", premium: "Premium" };
@ -80,7 +91,7 @@ export default function DashboardPage() {
{saving ? "保存中..." : "绑定"}
</button>
</div>
{msg && <p className={`text-sm ${msg.startsWith("") ? "text-emerald-400" : "text-red-400"}`}>{msg}</p>}
{msg && <p className={`text-sm ${msg.startsWith("\u2705") ? "text-emerald-400" : "text-red-400"}`}>{msg}</p>}
<p className="text-slate-400 text-xs">Discord ID ID</p>
</div>

View File

@ -238,11 +238,13 @@ function L3_Positions() {
const [positions, setPositions] = useState<any[]>([]);
const [wsPrices, setWsPrices] = useState<Record<string,number>>({});
const [recon, setRecon] = useState<any>(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";