Add SIM/LIVE switch with API keys saved to .env and OKX live executor.

Enable settings UI for mode/keys, gate strategy start when LIVE is not ready, and stop PM2 from forcing MODE=SIM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 21:08:39 +08:00
parent c2113b1a57
commit e666230d0b
19 changed files with 1367 additions and 33 deletions
+19
View File
@@ -175,6 +175,25 @@ export type PlanState = {
status: string;
}[];
ledger: { equity: number; available: number; reserved: number };
mode?: "SIM" | "LIVE";
sim?: boolean;
live_ready?: boolean;
live_ready_reason?: string;
};
export type RuntimeSettings = {
mode: "SIM" | "LIVE";
exchange: string;
okx_configured: boolean;
binance_configured: boolean;
okx_api_key_masked: string | null;
okx_api_secret_masked: string | null;
okx_api_passphrase_masked: string | null;
binance_api_key_masked: string | null;
binance_api_secret_masked: string | null;
live_ready: boolean;
live_ready_reason: string;
sim: boolean;
};
export type StrategySettings = {
+17 -3
View File
@@ -132,10 +132,13 @@ export default function PlanPage() {
<div>
<h2 style={{ marginTop: 0 }}></h2>
<p style={{ color: "var(--muted)", marginTop: -8 }}>
SIM · {" "}
{plan?.mode === "LIVE" ? "LIVE 实盘下单" : "SIM 本地撮合"} · {" "}
{(snap?.exchange || "okx").toUpperCase()}
{snap?.perp_inst_id ? ` · ${snap.perp_inst_id}` : ""} ·
(/) ·
{plan?.mode === "LIVE" && plan.live_ready === false
? ` · 未就绪: ${plan.live_ready_reason || "请配置 API"}`
: ""}
</p>
{err ? <div className="err">{err}</div> : null}
@@ -143,7 +146,16 @@ export default function PlanPage() {
<button
className="btn"
type="button"
disabled={!!busy || plan?.running}
disabled={
!!busy ||
plan?.running ||
(plan?.mode === "LIVE" && plan.live_ready === false)
}
title={
plan?.mode === "LIVE" && plan.live_ready === false
? plan.live_ready_reason || "LIVE 未就绪"
: undefined
}
onClick={() => act("/api/plan/start", "start")}
>
@@ -159,7 +171,9 @@ export default function PlanPage() {
<button
className="btn ghost"
type="button"
disabled={!!busy}
disabled={
!!busy || (plan?.mode === "LIVE" && plan.live_ready === false)
}
onClick={() => act("/api/sim/open-group", "open")}
>
+223 -3
View File
@@ -5,9 +5,10 @@ import {
setSession,
apiFetch,
StrategySettings,
RuntimeSettings,
} from "../api/client";
type Tab = "strategy" | "account";
type Tab = "strategy" | "runtime" | "account";
export default function SettingsPage() {
const [tab, setTab] = useState<Tab>("strategy");
@@ -40,6 +41,24 @@ export default function SettingsPage() {
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) => {
@@ -61,6 +80,7 @@ export default function SettingsPage() {
setExchange(s.exchange === "binance" ? "binance" : "okx");
})
.catch(() => undefined);
loadRuntime();
}, []);
async function onSaveCreds(e: FormEvent) {
@@ -128,6 +148,54 @@ export default function SettingsPage() {
}
}
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>
@@ -139,6 +207,16 @@ export default function SettingsPage() {
>
</button>
<button
type="button"
className={tab === "runtime" ? "tab active" : "tab"}
onClick={() => {
setTab("runtime");
loadRuntime();
}}
>
</button>
<button
type="button"
className={tab === "account" ? "tab active" : "tab"}
@@ -401,7 +479,149 @@ export default function SettingsPage() {
</div>
</form>
</div>
) : (
) : null}
{tab === "runtime" ? (
<div className="card settings-card">
<p className="settings-lead">
SIM = LIVE = OKX {" "}
<span className="mono">.env</span>
</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">=</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}
@@ -454,7 +674,7 @@ export default function SettingsPage() {
</div>
</form>
</div>
)}
) : null}
</div>
);
}