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:
parent
27a51b4d19
commit
18506f2a44
@ -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参数
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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", {
|
||||
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 <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>
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user