9fd2a842af
Block new opens while flat legs wait; emergency close bypasses the check. Co-authored-by: Cursor <cursoragent@cursor.com>
301 lines
9.6 KiB
TypeScript
301 lines
9.6 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
||
import {
|
||
changeCredentials,
|
||
getUsername,
|
||
setSession,
|
||
apiFetch,
|
||
StrategySettings,
|
||
} from "../api/client";
|
||
|
||
type Tab = "strategy" | "account";
|
||
|
||
export default function SettingsPage() {
|
||
const [tab, setTab] = useState<Tab>("strategy");
|
||
|
||
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [err, setErr] = useState("");
|
||
const [ok, setOk] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const [fee, setFee] = useState(0.0005);
|
||
const [exitPct, setExitPct] = useState(2);
|
||
const [rest, setRest] = useState(300);
|
||
const [leverage, setLeverage] = useState(3);
|
||
const [minHours, setMinHours] = useState(12);
|
||
const [minOptLev, setMinOptLev] = useState(100);
|
||
const [closeDevPct, setCloseDevPct] = useState(30);
|
||
const [perpQty, setPerpQty] = useState(1);
|
||
const [optQty, setOptQty] = useState(2);
|
||
const [stratOk, setStratOk] = useState("");
|
||
|
||
useEffect(() => {
|
||
apiFetch<StrategySettings>("/api/settings/strategy")
|
||
.then((s) => {
|
||
setFee(s.fee_rate);
|
||
setExitPct(s.exit_move_pct ?? 2);
|
||
setRest(s.rest_seconds);
|
||
setLeverage(s.leverage ?? 3);
|
||
setMinHours(s.min_option_hours ?? 12);
|
||
setMinOptLev(s.min_option_leverage ?? 100);
|
||
setCloseDevPct(s.close_bid_mark_max_pct ?? 30);
|
||
setPerpQty(s.perp_qty_eth ?? 1);
|
||
setOptQty(s.option_qty_eth ?? 2);
|
||
})
|
||
.catch(() => undefined);
|
||
}, []);
|
||
|
||
async function onSaveCreds(e: FormEvent) {
|
||
e.preventDefault();
|
||
setErr("");
|
||
setOk("");
|
||
if (newPassword !== confirmPassword) {
|
||
setErr("两次输入的新密码不一致");
|
||
return;
|
||
}
|
||
if (newPassword.length < 4) {
|
||
setErr("新密码至少 4 位");
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
const res = await changeCredentials({
|
||
current_password: currentPassword,
|
||
new_username: newUsername.trim(),
|
||
new_password: newPassword,
|
||
});
|
||
setSession(res.token, res.username);
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
setOk("用户名/密码已更新");
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onSaveStrategy(e: FormEvent) {
|
||
e.preventDefault();
|
||
setStratOk("");
|
||
setErr("");
|
||
try {
|
||
await apiFetch("/api/settings/strategy", {
|
||
method: "PUT",
|
||
body: JSON.stringify({
|
||
fee_rate: fee,
|
||
exit_move_pct: exitPct,
|
||
rest_seconds: rest,
|
||
leverage,
|
||
min_option_hours: minHours,
|
||
min_option_leverage: minOptLev,
|
||
close_bid_mark_max_pct: closeDevPct,
|
||
perp_qty_eth: perpQty,
|
||
option_qty_eth: optQty,
|
||
}),
|
||
});
|
||
setStratOk("策略参数已保存");
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
}
|
||
}
|
||
|
||
return (
|
||
<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">
|
||
<p style={{ color: "var(--muted)", marginTop: 0 }}>
|
||
无开仓时间窗;期权按剩余时长选到期 → 平值 → 校验杠杆;波动按百分比全平。
|
||
</p>
|
||
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
|
||
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
|
||
<form onSubmit={onSaveStrategy}>
|
||
<h3 style={{ margin: "0 0 10px", fontSize: 14, color: "var(--muted)" }}>资金</h3>
|
||
<div className="field">
|
||
<label htmlFor="lev">永续杠杆倍数</label>
|
||
<input
|
||
id="lev"
|
||
className="mono"
|
||
type="number"
|
||
step="1"
|
||
min="1"
|
||
value={leverage}
|
||
onChange={(e) => setLeverage(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
|
||
<h3 style={{ margin: "18px 0 10px", fontSize: 14, color: "var(--muted)" }}>策略</h3>
|
||
<div className="field">
|
||
<label htmlFor="hours">期权最小剩余小时</label>
|
||
<input
|
||
id="hours"
|
||
className="mono"
|
||
type="number"
|
||
step="1"
|
||
min="1"
|
||
value={minHours}
|
||
onChange={(e) => setMinHours(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="olev">期权杠杆下限(现价÷卖一权利金)</label>
|
||
<input
|
||
id="olev"
|
||
className="mono"
|
||
type="number"
|
||
step="1"
|
||
min="1"
|
||
value={minOptLev}
|
||
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="exit">波动出场比例(%)</label>
|
||
<input
|
||
id="exit"
|
||
className="mono"
|
||
type="number"
|
||
step="0.1"
|
||
min="0.1"
|
||
value={exitPct}
|
||
onChange={(e) => setExitPct(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="closeDev">平仓买一/标记最大偏差(%)</label>
|
||
<input
|
||
id="closeDev"
|
||
className="mono"
|
||
type="number"
|
||
step="1"
|
||
min="1"
|
||
value={closeDevPct}
|
||
onChange={(e) => setCloseDevPct(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="perp">永续 ETH 数量</label>
|
||
<input
|
||
id="perp"
|
||
className="mono"
|
||
type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
value={perpQty}
|
||
onChange={(e) => setPerpQty(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="opt">期权 ETH 数量</label>
|
||
<input
|
||
id="opt"
|
||
className="mono"
|
||
type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
value={optQty}
|
||
onChange={(e) => setOptQty(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="rest">轮间休息秒数</label>
|
||
<input
|
||
id="rest"
|
||
className="mono"
|
||
type="number"
|
||
step="1"
|
||
value={rest}
|
||
onChange={(e) => setRest(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="fee">手续费率</label>
|
||
<input
|
||
id="fee"
|
||
className="mono"
|
||
type="number"
|
||
step="0.0001"
|
||
value={fee}
|
||
onChange={(e) => setFee(Number(e.target.value))}
|
||
/>
|
||
</div>
|
||
<button className="btn" type="submit">
|
||
保存策略
|
||
</button>
|
||
</form>
|
||
</div>
|
||
) : (
|
||
<div className="card">
|
||
{err ? <div className="err">{err}</div> : null}
|
||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||
<form onSubmit={onSaveCreds}>
|
||
<div className="field">
|
||
<label htmlFor="user">新用户名</label>
|
||
<input
|
||
id="user"
|
||
value={newUsername}
|
||
onChange={(e) => setNewUsername(e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="cur">当前密码</label>
|
||
<input
|
||
id="cur"
|
||
type="password"
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="np">新密码</label>
|
||
<input
|
||
id="np"
|
||
type="password"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="cp">确认新密码</label>
|
||
<input
|
||
id="cp"
|
||
type="password"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
<button className="btn" type="submit" disabled={loading}>
|
||
{loading ? "保存中…" : "保存账号"}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|