Files
eth_hedge_sim/frontend/src/pages/Settings.tsx
T
dekun dbc86a1ce6 Add Binance live trading, anti-stuck open/close recovery, and configurable rate limits.
OKX/Binance LIVE share half_open and option_closed_perp_pending repair paths; private REST throttles default to 1s and are tunable in settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 21:35:10 +08:00

704 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FormEvent, useEffect, useState } from "react";
import {
changeCredentials,
getUsername,
setSession,
apiFetch,
StrategySettings,
RuntimeSettings,
} from "../api/client";
type Tab = "strategy" | "runtime" | "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 [exitMode, setExitMode] = useState<"fixed_usdt" | "premium_multiple">(
"fixed_usdt",
);
const [netTarget, setNetTarget] = useState(15);
const [premMult, setPremMult] = useState(1);
const [rest, setRest] = useState(300);
const [orderInterval, setOrderInterval] = useState(1);
const [skipWeekends, setSkipWeekends] = useState(true);
const [leverage, setLeverage] = useState(3);
const [minHours, setMinHours] = useState(12);
const [minOptLev, setMinOptLev] = useState(100);
const [atmOffOn, setAtmOffOn] = useState(false);
const [maxAtmOff, setMaxAtmOff] = useState(3);
const [closeDevPct, setCloseDevPct] = useState(30);
const [perpQty, setPerpQty] = useState(1);
const [optQty, setOptQty] = useState(2);
const [initialEquity, setInitialEquity] = useState(10000);
const [exchange, setExchange] = useState<"okx" | "binance">("okx");
const [stratOk, setStratOk] = useState("");
const [runtime, setRuntime] = useState<RuntimeSettings | null>(null);
const [mode, setMode] = useState<"SIM" | "LIVE">("SIM");
const [okxKey, setOkxKey] = useState("");
const [okxSecret, setOkxSecret] = useState("");
const [okxPass, setOkxPass] = useState("");
const [bnKey, setBnKey] = useState("");
const [bnSecret, setBnSecret] = useState("");
const [runtimeOk, setRuntimeOk] = useState("");
function loadRuntime() {
apiFetch<RuntimeSettings>("/api/settings/runtime")
.then((r) => {
setRuntime(r);
setMode(r.mode === "LIVE" ? "LIVE" : "SIM");
})
.catch(() => undefined);
}
useEffect(() => {
apiFetch<StrategySettings>("/api/settings/strategy")
.then((s) => {
setFee(s.fee_rate);
setExitMode(s.exit_mode === "premium_multiple" ? "premium_multiple" : "fixed_usdt");
setNetTarget(s.net_profit_target ?? 15);
setPremMult(s.premium_exit_multiple ?? 1);
setRest(s.rest_seconds);
setOrderInterval(s.live_order_interval_sec ?? 1);
setSkipWeekends(s.skip_weekends !== false);
setLeverage(s.leverage ?? 3);
setMinHours(s.min_option_hours ?? 12);
setMinOptLev(s.min_option_leverage ?? 100);
setAtmOffOn(s.atm_open_offset_enabled === true);
setMaxAtmOff(s.max_atm_open_offset ?? 3);
setCloseDevPct(s.close_bid_mark_max_pct ?? 30);
setPerpQty(s.perp_qty_eth ?? 1);
setOptQty(s.option_qty_eth ?? 2);
setInitialEquity(s.initial_equity ?? 10000);
setExchange(s.exchange === "binance" ? "binance" : "okx");
})
.catch(() => undefined);
loadRuntime();
}, []);
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_mode: exitMode,
net_profit_target: netTarget,
premium_exit_multiple: premMult,
rest_seconds: rest,
live_order_interval_sec: orderInterval,
skip_weekends: skipWeekends,
leverage,
min_option_hours: minHours,
min_option_leverage: minOptLev,
atm_open_offset_enabled: atmOffOn,
max_atm_open_offset: maxAtmOff,
close_bid_mark_max_pct: closeDevPct,
perp_qty_eth: perpQty,
option_qty_eth: optQty,
initial_equity: initialEquity,
exchange,
}),
});
setStratOk(
"策略参数已保存(切换交易所/改模拟资金需无持仓;切换后会重连行情)",
);
} catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex));
}
}
async function onSaveRuntime(e: FormEvent) {
e.preventDefault();
setErr("");
setRuntimeOk("");
const goingLive = mode === "LIVE" && runtime?.mode !== "LIVE";
if (goingLive) {
const typed = window.prompt('切换到 LIVE 实盘:请输入 LIVE 确认(将真实下单)');
if (typed !== "LIVE") {
setErr("已取消:须输入 LIVE 才能切换到实盘");
return;
}
}
setLoading(true);
try {
const body: Record<string, unknown> = {
mode,
confirm_live: goingLive,
};
if (okxKey.trim()) body.okx_api_key = okxKey.trim();
if (okxSecret.trim()) body.okx_api_secret = okxSecret.trim();
if (okxPass.trim()) body.okx_api_passphrase = okxPass.trim();
if (bnKey.trim()) body.binance_api_key = bnKey.trim();
if (bnSecret.trim()) body.binance_api_secret = bnSecret.trim();
const r = await apiFetch<RuntimeSettings>("/api/settings/runtime", {
method: "PUT",
body: JSON.stringify(body),
});
setRuntime(r);
setMode(r.mode === "LIVE" ? "LIVE" : "SIM");
setOkxKey("");
setOkxSecret("");
setOkxPass("");
setBnKey("");
setBnSecret("");
setRuntimeOk(
r.mode === "LIVE"
? r.live_ready
? "已切换 LIVE,密钥已写入 .env"
: `已切 LIVE,但未就绪:${r.live_ready_reason}`
: "已切换 SIM,配置已写入 .env",
);
} catch (ex) {
setErr(ex instanceof Error ? ex.message : String(ex));
} finally {
setLoading(false);
}
}
return (
<div className="settings-page">
<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 === "runtime" ? "tab active" : "tab"}
onClick={() => {
setTab("runtime");
loadRuntime();
}}
>
</button>
<button
type="button"
className={tab === "account" ? "tab active" : "tab"}
onClick={() => setTab("account")}
>
</button>
</div>
{tab === "strategy" ? (
<div className="card settings-card">
<p className="settings-lead">
OKX / SIM
</p>
{stratOk ? <div className="settings-ok">{stratOk}</div> : null}
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
<form onSubmit={onSaveStrategy}>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="exch">SIM </label>
<select
id="exch"
className="mono"
value={exchange}
onChange={(e) =>
setExchange(e.target.value === "binance" ? "binance" : "okx")
}
>
<option value="okx">OKXUSDC </option>
<option value="binance">USDT + </option>
</select>
<p className="settings-hint">
</p>
</div>
</div>
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="equity">USDT</label>
<input
id="equity"
className="mono"
type="number"
step="100"
min="1000"
value={initialEquity}
onChange={(e) => setInitialEquity(Number(e.target.value))}
/>
<p className="settings-hint">
10000
</p>
</div>
<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>
<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>
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<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="atmoffOn"> ATM </label>
<select
id="atmoffOn"
className="mono"
value={atmOffOn ? "1" : "0"}
onChange={(e) => setAtmOffOn(e.target.value === "1")}
>
<option value="0"></option>
<option value="1"></option>
</select>
<p className="settings-hint">
|ATM |
</p>
</div>
<div className="field">
<label htmlFor="atmoff"> ATM </label>
<input
id="atmoff"
className="mono"
type="number"
step="0.5"
min="0"
disabled={!atmOffOn}
value={maxAtmOff}
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
/>
<p className="settings-hint">
3
</p>
</div>
</div>
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="exitMode"></label>
<select
id="exitMode"
className="mono"
value={exitMode}
onChange={(e) =>
setExitMode(
e.target.value === "premium_multiple"
? "premium_multiple"
: "fixed_usdt",
)
}
>
<option value="fixed_usdt">USDT</option>
<option value="premium_multiple"></option>
</select>
</div>
{exitMode === "fixed_usdt" ? (
<div className="field">
<label htmlFor="netTarget">USDT</label>
<input
id="netTarget"
className="mono"
type="number"
step="0.1"
min="0.1"
value={netTarget}
onChange={(e) => setNetTarget(Number(e.target.value))}
/>
</div>
) : (
<div className="field">
<label htmlFor="premMult">1 = </label>
<input
id="premMult"
className="mono"
type="number"
step="0.1"
min="0.1"
value={premMult}
onChange={(e) => setPremMult(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="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="orderInterval"></label>
<input
id="orderInterval"
className="mono"
type="number"
step="0.1"
min="0.2"
max="30"
value={orderInterval}
onChange={(e) => setOrderInterval(Number(e.target.value))}
/>
<p className="settings-hint">
LIVE / 1s 1
0.230
</p>
</div>
<div className="field">
<label htmlFor="skipWe"></label>
<select
id="skipWe"
className="mono"
value={skipWeekends ? "1" : "0"}
onChange={(e) => setSkipWeekends(e.target.value === "1")}
>
<option value="1"></option>
<option value="0"></option>
</select>
</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>
</div>
</section>
<div className="settings-actions">
<button className="btn" type="submit">
</button>
</div>
</form>
</div>
) : null}
{tab === "runtime" ? (
<div className="card settings-card">
<p className="settings-lead">
SIM = LIVE = OKX / {" "}
<span className="mono">.env</span>OKX USDC USDT
</p>
{runtimeOk ? <div className="settings-ok">{runtimeOk}</div> : null}
{err && tab === "runtime" ? <div className="err">{err}</div> : null}
<form onSubmit={onSaveRuntime}>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="mode"></label>
<select
id="mode"
className="mono"
value={mode}
onChange={(e) =>
setMode(e.target.value === "LIVE" ? "LIVE" : "SIM")
}
>
<option value="SIM">SIM </option>
<option value="LIVE">LIVE </option>
</select>
<p className="settings-hint">
{runtime?.mode || "—"} · {runtime?.exchange || "—"} ·{" "}
{runtime?.mode === "LIVE"
? runtime.live_ready
? "LIVE 就绪"
: `未就绪(${runtime.live_ready_reason})`
: "SIM"}
</p>
</div>
</div>
</section>
<section className="settings-section">
<h3>OKX API</h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="okxKey">API Key</label>
<input
id="okxKey"
className="mono"
type="password"
autoComplete="off"
placeholder={
runtime?.okx_api_key_masked
? `已配置 ${runtime.okx_api_key_masked}`
: "未配置"
}
value={okxKey}
onChange={(e) => setOkxKey(e.target.value)}
/>
</div>
<div className="field">
<label htmlFor="okxSecret">Secret</label>
<input
id="okxSecret"
className="mono"
type="password"
autoComplete="off"
placeholder={
runtime?.okx_api_secret_masked
? `已配置 ${runtime.okx_api_secret_masked}`
: "未配置"
}
value={okxSecret}
onChange={(e) => setOkxSecret(e.target.value)}
/>
</div>
<div className="field">
<label htmlFor="okxPass">Passphrase</label>
<input
id="okxPass"
className="mono"
type="password"
autoComplete="off"
placeholder={
runtime?.okx_api_passphrase_masked
? `已配置 ${runtime.okx_api_passphrase_masked}`
: "未配置"
}
value={okxPass}
onChange={(e) => setOkxPass(e.target.value)}
/>
</div>
</div>
</section>
<section className="settings-section">
<h3> API</h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="bnKey">API Key</label>
<input
id="bnKey"
className="mono"
type="password"
autoComplete="off"
placeholder={
runtime?.binance_api_key_masked
? `已配置 ${runtime.binance_api_key_masked}`
: "未配置"
}
value={bnKey}
onChange={(e) => setBnKey(e.target.value)}
/>
</div>
<div className="field">
<label htmlFor="bnSecret">Secret</label>
<input
id="bnSecret"
className="mono"
type="password"
autoComplete="off"
placeholder={
runtime?.binance_api_secret_masked
? `已配置 ${runtime.binance_api_secret_masked}`
: "未配置"
}
value={bnSecret}
onChange={(e) => setBnSecret(e.target.value)}
/>
</div>
<p className="settings-hint">
= LIVE
Passphrase
</p>
</div>
</section>
<div className="settings-actions">
<button className="btn" type="submit" disabled={loading}>
{loading ? "保存中…" : "保存模式与密钥"}
</button>
</div>
</form>
</div>
) : null}
{tab === "account" ? (
<div className="card settings-card settings-card-narrow">
{err ? <div className="err">{err}</div> : null}
{ok ? <div className="settings-ok">{ok}</div> : null}
<form onSubmit={onSaveCreds} className="settings-account-form">
<div className="settings-fields">
<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>
</div>
<div className="settings-actions">
<button className="btn" type="submit" disabled={loading}>
{loading ? "保存中…" : "保存账号"}
</button>
</div>
</form>
</div>
) : null}
</div>
);
}