UI: centered nav title, settings tabs, running status, manual buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,11 @@ class StrategyEngine:
|
|||||||
rest_left = 0
|
rest_left = 0
|
||||||
if rest_until:
|
if rest_until:
|
||||||
rest_left = max(0, int((int(rest_until) - time.time() * 1000) / 1000))
|
rest_left = max(0, int((int(rest_until) - time.time() * 1000) / 1000))
|
||||||
|
last_error = row["last_error"]
|
||||||
|
# 清掉已修复的旧序列化错误残留
|
||||||
|
if last_error and "PriceResult" in str(last_error) and "__dict__" in str(last_error):
|
||||||
|
self._set_state(last_error=None)
|
||||||
|
last_error = None
|
||||||
return {
|
return {
|
||||||
"running": bool(row["running"]),
|
"running": bool(row["running"]),
|
||||||
"phase": row["phase"],
|
"phase": row["phase"],
|
||||||
@@ -51,7 +56,7 @@ class StrategyEngine:
|
|||||||
"rest_seconds": rest_sec,
|
"rest_seconds": rest_sec,
|
||||||
"exit_move_points": exit_pts,
|
"exit_move_points": exit_pts,
|
||||||
"can_open": can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm),
|
"can_open": can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm),
|
||||||
"last_error": row["last_error"],
|
"last_error": last_error,
|
||||||
"position": upl,
|
"position": upl,
|
||||||
"ledger": self.ledger.snapshot(),
|
"ledger": self.ledger.snapshot(),
|
||||||
}
|
}
|
||||||
@@ -67,16 +72,16 @@ class StrategyEngine:
|
|||||||
sql = f"UPDATE strategy_state SET {', '.join(cols)} WHERE id=1"
|
sql = f"UPDATE strategy_state SET {', '.join(cols)} WHERE id=1"
|
||||||
self.db.execute(sql, tuple(vals))
|
self.db.execute(sql, tuple(vals))
|
||||||
|
|
||||||
|
async def pause(self) -> dict[str, Any]:
|
||||||
|
self._set_state(running=0, phase="paused", last_error=None)
|
||||||
|
return self.state()
|
||||||
|
|
||||||
async def start(self) -> dict[str, Any]:
|
async def start(self) -> dict[str, Any]:
|
||||||
self._set_state(running=1, last_error=None, phase="idle")
|
self._set_state(running=1, last_error=None, phase="idle")
|
||||||
if self._task is None or self._task.done():
|
if self._task is None or self._task.done():
|
||||||
self._task = asyncio.create_task(self._loop(), name="strategy-engine")
|
self._task = asyncio.create_task(self._loop(), name="strategy-engine")
|
||||||
return self.state()
|
return self.state()
|
||||||
|
|
||||||
async def pause(self) -> dict[str, Any]:
|
|
||||||
self._set_state(running=0, phase="paused")
|
|
||||||
return self.state()
|
|
||||||
|
|
||||||
async def emergency_close(self) -> dict[str, Any]:
|
async def emergency_close(self) -> dict[str, Any]:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
r = self.matcher.close_group(reason="emergency")
|
r = self.matcher.close_group(reason="emergency")
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>eth_hedge_sim</title>
|
<title>OKX 自动化对冲系统</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
const user = getUsername();
|
const user = getUsername();
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<nav className="topnav">
|
<header className="topnav">
|
||||||
<div className="brand">eth_hedge_sim</div>
|
<div className="topnav-side left">
|
||||||
|
<div className="brand">OKX 自动化对冲系统</div>
|
||||||
|
</div>
|
||||||
|
<nav className="topnav-center">
|
||||||
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/plan" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
自动对冲计划
|
自动对冲计划
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -25,7 +28,8 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
<NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/settings" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
系统设置
|
系统设置
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div className="spacer" />
|
</nav>
|
||||||
|
<div className="topnav-side right">
|
||||||
<span className="meta mono">{user}</span>
|
<span className="meta mono">{user}</span>
|
||||||
<button
|
<button
|
||||||
className="btn ghost"
|
className="btn ghost"
|
||||||
@@ -37,7 +41,8 @@ function Shell({ children }: { children: ReactNode }) {
|
|||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</div>
|
||||||
|
</header>
|
||||||
<main className="page">{children}</main>
|
<main className="page">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="login-wrap">
|
<div className="login-wrap">
|
||||||
<form className="login-box" onSubmit={onSubmit}>
|
<form className="login-box" onSubmit={onSubmit}>
|
||||||
<h1>eth_hedge_sim</h1>
|
<h1>OKX 自动化对冲系统</h1>
|
||||||
<p>模拟盘登录</p>
|
<p>模拟盘登录</p>
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function PlanPage() {
|
|||||||
disabled={!!busy}
|
disabled={!!busy}
|
||||||
onClick={() => act("/api/sim/open-group", "open")}
|
onClick={() => act("/api/sim/open-group", "open")}
|
||||||
>
|
>
|
||||||
模拟开一组
|
手动开一组
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn ghost"
|
className="btn ghost"
|
||||||
@@ -98,7 +98,7 @@ export default function PlanPage() {
|
|||||||
disabled={!!busy}
|
disabled={!!busy}
|
||||||
onClick={() => act("/api/sim/close-group", "close")}
|
onClick={() => act("/api/sim/close-group", "close")}
|
||||||
>
|
>
|
||||||
模拟全平
|
手动全平
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn ghost"
|
className="btn ghost"
|
||||||
@@ -115,8 +115,12 @@ export default function PlanPage() {
|
|||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>策略</span>
|
<span>策略</span>
|
||||||
<span className="mono">
|
<span className="mono">
|
||||||
{plan?.running ? "运行中" : "已停"} · {plan?.phase || "—"} · 轮次{" "}
|
{plan?.running ? (
|
||||||
{plan?.rounds_done ?? 0}/{plan?.max_rounds ?? 3}
|
<span className="status-running">启动中</span>
|
||||||
|
) : (
|
||||||
|
<span className="status-stopped">已停</span>
|
||||||
|
)}{" "}
|
||||||
|
· {plan?.phase || "—"} · 轮次 {plan?.rounds_done ?? 0}/{plan?.max_rounds ?? 3}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
StrategySettings,
|
StrategySettings,
|
||||||
} from "../api/client";
|
} from "../api/client";
|
||||||
|
|
||||||
|
type Tab = "strategy" | "account";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const [tab, setTab] = useState<Tab>("strategy");
|
||||||
|
|
||||||
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
@@ -85,13 +89,32 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "grid", gap: 16, maxWidth: 560 }}>
|
<div style={{ maxWidth: 560 }}>
|
||||||
|
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||||
|
<div className="tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={tab === "strategy" ? "tab active" : "tab"}
|
||||||
|
onClick={() => setTab("strategy")}
|
||||||
|
>
|
||||||
|
策略设置
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={tab === "account" ? "tab active" : "tab"}
|
||||||
|
onClick={() => setTab("account")}
|
||||||
|
>
|
||||||
|
登录账户
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === "strategy" ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 style={{ marginTop: 0 }}>策略设置</h2>
|
<p style={{ color: "var(--muted)", marginTop: 0 }}>
|
||||||
<p style={{ color: "var(--muted)" }}>
|
|
||||||
标的波动 N 点全平、轮次休息、费率(滑点=1×费率)。
|
标的波动 N 点全平、轮次休息、费率(滑点=1×费率)。
|
||||||
</p>
|
</p>
|
||||||
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
|
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
|
||||||
|
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
|
||||||
<form onSubmit={onSaveStrategy}>
|
<form onSubmit={onSaveStrategy}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="exit">EXIT_MOVE_POINTS(标的波动点数)</label>
|
<label htmlFor="exit">EXIT_MOVE_POINTS(标的波动点数)</label>
|
||||||
@@ -142,9 +165,8 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2 style={{ marginTop: 0 }}>登录账号</h2>
|
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||||||
<form onSubmit={onSaveCreds}>
|
<form onSubmit={onSaveCreds}>
|
||||||
@@ -192,6 +214,7 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topnav {
|
.topnav {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
@@ -61,11 +62,31 @@ input {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topnav-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-side.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
margin-right: 12px;
|
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topnav a {
|
.topnav a {
|
||||||
@@ -73,6 +94,7 @@ input {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topnav a.active {
|
.topnav a.active {
|
||||||
@@ -80,6 +102,38 @@ input {
|
|||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-running {
|
||||||
|
color: var(--up);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stopped {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.topnav .spacer {
|
.topnav .spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user