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:
@@ -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 = {
|
||||
|
||||
@@ -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")}
|
||||
>
|
||||
手动开一组
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user