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("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(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("/api/settings/runtime") .then((r) => { setRuntime(r); setMode(r.mode === "LIVE" ? "LIVE" : "SIM"); }) .catch(() => undefined); } useEffect(() => { apiFetch("/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 = { 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("/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 (

系统设置

{tab === "strategy" ? (

无开仓时间窗;可切换 OKX / 币安行情(SIM 本地撮合);周六日可跳过开仓;出场按净盈利或到期全平。

{stratOk ?
{stratOk}
: null} {err && tab === "strategy" ?
{err}
: null}

交易所

有持仓时不可切换。切换后自动套用该所合约并重连公共行情。

资金

setInitialEquity(Number(e.target.value))} />

保存后将权益重置为该金额(须无持仓)。默认 10000。

setLeverage(Number(e.target.value))} />
setPerpQty(Number(e.target.value))} />
setOptQty(Number(e.target.value))} />

选约

setMinHours(Number(e.target.value))} />
setMinOptLev(Number(e.target.value))} />

关闭时不限制 |ATM − 现价|;开启后超过下方点数则不开仓。

setMaxAtmOff(Number(e.target.value))} />

仅在上方开关开启时生效。默认 3;币安行权价步进较粗时可能长时间无开仓机会。

出场与风控

{exitMode === "fixed_usdt" ? (
setNetTarget(Number(e.target.value))} />
) : (
setPremMult(Number(e.target.value))} />
)}
setCloseDevPct(Number(e.target.value))} />
setRest(Number(e.target.value))} />
setOrderInterval(Number(e.target.value))} />

LIVE 私有下单/查单间隔,默认 1s(非高频建议 ≥1)。范围 0.2–30。保存后立即生效。

setFee(Number(e.target.value))} />
) : null} {tab === "runtime" ? (

SIM = 本地撮合;LIVE = 当前所选交易所真下单(OKX / 币安均已接入)。密钥只写入服务器{" "} .env,接口不回传明文。OKX 期权常需账户有 USDC;币安多为 USDT。

{runtimeOk ?
{runtimeOk}
: null} {err && tab === "runtime" ?
{err}
: null}

模式

当前:{runtime?.mode || "—"} · 交易所 {runtime?.exchange || "—"} ·{" "} {runtime?.mode === "LIVE" ? runtime.live_ready ? "LIVE 就绪" : `未就绪(${runtime.live_ready_reason})` : "SIM"} 。有持仓时不可切换模式。

OKX API

setOkxKey(e.target.value)} />
setOkxSecret(e.target.value)} />
setOkxPass(e.target.value)} />

币安 API

setBnKey(e.target.value)} />
setBnSecret(e.target.value)} />

留空保存=不覆盖已有密钥。交易所选币安且 LIVE 时用此密钥真下单(无 Passphrase)。

) : null} {tab === "account" ? (
{err ?
{err}
: null} {ok ?
{ok}
: null}
setNewUsername(e.target.value)} required />
setCurrentPassword(e.target.value)} required />
setNewPassword(e.target.value)} required />
setConfirmPassword(e.target.value)} required />
) : null}
); }