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)
|
work_conn = ensure_db_conn(work_conn)
|
||||||
|
|
||||||
# 获取待处理信号(NOTIFY + 轮询双保险)
|
# 获取待处理信号(NOTIFY + 轮询双保险)
|
||||||
signals = fetch_pending_signals(work_conn)
|
signals = await asyncio.to_thread(fetch_pending_signals, work_conn)
|
||||||
|
|
||||||
for sig in signals:
|
for sig in signals:
|
||||||
# 补充TP/SL参数
|
# 补充TP/SL参数
|
||||||
|
|||||||
@ -444,7 +444,9 @@ async def check_closed_positions(session, conn):
|
|||||||
if total_qty > 0:
|
if total_qty > 0:
|
||||||
exit_price = sum(float(t["price"]) * float(t["qty"]) for t in close_trades) / total_qty
|
exit_price = sum(float(t["price"]) * float(t["qty"]) for t in close_trades) / total_qty
|
||||||
elif trades_data:
|
elif trades_data:
|
||||||
exit_price = float(trades_data[-1].get("price", exit_price))
|
# fallback: ä¸ç²ç®åæå䏿¡ï¼å¯è½æ¯å¼ä»æäº¤ï¼ï¼å»¶åæ¬è½®ç»ç®
|
||||||
|
logger.warning(f"[{symbol}] æªæ¾å°æç¡®å¹³ä»æäº¤ï¼å»¶åç»ç®")
|
||||||
|
continue
|
||||||
|
|
||||||
# 汇总手续费(开仓后200ms起算,避免含其他策略成交)
|
# 汇总手续费(开仓后200ms起算,避免含其他策略成交)
|
||||||
for t in trades_data:
|
for t in trades_data:
|
||||||
|
|||||||
@ -548,10 +548,19 @@ async def main():
|
|||||||
|
|
||||||
# 4. 余额检查
|
# 4. 余额检查
|
||||||
balance = await check_balance(session)
|
balance = await check_balance(session)
|
||||||
if balance < RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE:
|
threshold = RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE
|
||||||
if not risk_state.block_new_entries:
|
if balance < threshold:
|
||||||
|
if risk_state.circuit_break_reason != "LOW_BALANCE":
|
||||||
risk_state.block_new_entries = True
|
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. 持仓超时检查
|
# 5. 持仓超时检查
|
||||||
await check_hold_timeout(session, conn)
|
await check_hold_timeout(session, conn)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { authFetch, useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
id: number;
|
id: number;
|
||||||
@ -12,36 +13,46 @@ interface UserInfo {
|
|||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isLoggedIn, loading, logout } = useAuth();
|
||||||
const [user, setUser] = useState<UserInfo | null>(null);
|
const [user, setUser] = useState<UserInfo | null>(null);
|
||||||
const [discordId, setDiscordId] = useState("");
|
const [discordId, setDiscordId] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [msg, setMsg] = useState("");
|
const [msg, setMsg] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("arb_token");
|
if (loading) return;
|
||||||
if (!token) { router.push("/login"); return; }
|
if (!isLoggedIn) { router.push("/login"); return; }
|
||||||
fetch("/api/user/me", { headers: { Authorization: `Bearer ${token}` } })
|
authFetch("/api/auth/me")
|
||||||
.then(r => { if (!r.ok) { router.push("/login"); return null; } return r.json(); })
|
.then(r => r.ok ? r.json() : Promise.reject())
|
||||||
.then(d => { if (d) { setUser(d); setDiscordId(d.discord_id || ""); } });
|
.then(d => {
|
||||||
}, [router]);
|
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 () => {
|
const bindDiscord = async () => {
|
||||||
setSaving(true); setMsg("");
|
setSaving(true); setMsg("");
|
||||||
const token = localStorage.getItem("arb_token");
|
try {
|
||||||
const r = await fetch("/api/user/bind-discord", {
|
const r = await authFetch("/api/user/bind-discord", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ discord_id: discordId }),
|
body: JSON.stringify({ discord_id: discordId }),
|
||||||
});
|
});
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
setMsg(r.ok ? "✅ 绑定成功" : d.detail || "绑定失败");
|
setMsg(r.ok ? "\u2705 绑定成功" : d.detail || "绑定失败");
|
||||||
setSaving(false);
|
|
||||||
if (r.ok && user) setUser({ ...user, discord_id: discordId });
|
if (r.ok && user) setUser({ ...user, discord_id: discordId });
|
||||||
|
} catch { setMsg("绑定失败"); }
|
||||||
|
setSaving(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => { localStorage.removeItem("arb_token"); router.push("/"); };
|
if (loading || !user) return <div className="text-slate-500 p-8">加载中...</div>;
|
||||||
|
|
||||||
if (!user) return <div className="text-slate-500 p-8">加载中...</div>;
|
|
||||||
|
|
||||||
const tierLabel: Record<string, string> = { free: "免费版", pro: "Pro", premium: "Premium" };
|
const tierLabel: Record<string, string> = { free: "免费版", pro: "Pro", premium: "Premium" };
|
||||||
|
|
||||||
@ -80,7 +91,7 @@ export default function DashboardPage() {
|
|||||||
{saving ? "保存中..." : "绑定"}
|
{saving ? "保存中..." : "绑定"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<p className="text-slate-400 text-xs">如何获取Discord ID:设置 → 外观 → 开发者模式 → 右键个人头像 → 复制用户ID</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -238,11 +238,13 @@ function L3_Positions() {
|
|||||||
const [positions, setPositions] = useState<any[]>([]);
|
const [positions, setPositions] = useState<any[]>([]);
|
||||||
const [wsPrices, setWsPrices] = useState<Record<string,number>>({});
|
const [wsPrices, setWsPrices] = useState<Record<string,number>>({});
|
||||||
const [recon, setRecon] = useState<any>(null);
|
const [recon, setRecon] = useState<any>(null);
|
||||||
|
const [riskUsd, setRiskUsd] = useState(2);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const f = async () => {
|
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/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/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);
|
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 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 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 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 holdColor = holdMin>=60?"text-red-500 font-bold":holdMin>=45?"text-amber-500":"text-slate-400";
|
||||||
const dist = liqDist[p.symbol];
|
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";
|
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