Add OKX-style funds bar with USDT/USDC convert and transfer.

SIM uses multi-wallet balances; LIVE hits OKX asset/account APIs and spot USDC-USDT swap. Funds strip hides sim labeling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-29 17:06:13 +08:00
parent 1c75db4a19
commit 5c3bd4b654
13 changed files with 1020 additions and 6 deletions
+1 -2
View File
@@ -168,8 +168,7 @@ export default function PlanPage() {
<div className="plan-desktop-head">
<h2 style={{ marginTop: 0 }}></h2>
<p style={{ color: "var(--muted)", marginTop: -8 }}>
{plan?.mode === "LIVE" ? "LIVE 实盘下单" : "SIM 本地撮合"} · {" "}
{(snap?.exchange || "okx").toUpperCase()}
{(snap?.exchange || "okx").toUpperCase()}
{snap?.perp_inst_id ? ` · ${snap.perp_inst_id}` : ""} ·
(/) ·
{plan?.mode === "LIVE" && plan.live_ready === false
+190 -1
View File
@@ -12,7 +12,7 @@ import {
uploadRestoreBackup,
} from "../api/client";
type Tab = "strategy" | "runtime" | "account" | "backup";
type Tab = "strategy" | "runtime" | "funds" | "account" | "backup";
type StratSub =
| "position"
| "select"
@@ -104,6 +104,18 @@ export default function SettingsPage() {
const [restoreFile, setRestoreFile] = useState<File | null>(null);
const [restorePhrase, setRestorePhrase] = useState("");
const [fundsOk, setFundsOk] = useState("");
const [fundsErr, setFundsErr] = useState("");
const [convertDir, setConvertDir] = useState<"usdt_to_usdc" | "usdc_to_usdt">(
"usdt_to_usdc",
);
const [convertAmt, setConvertAmt] = useState(100);
const [xferCcy, setXferCcy] = useState<"USDT" | "USDC">("USDC");
const [xferAmt, setXferAmt] = useState(50);
const [xferFrom, setXferFrom] = useState("funding");
const [xferTo, setXferTo] = useState("trading");
const [usdcRate, setUsdcRate] = useState<number | null>(null);
function loadRuntime() {
apiFetch<RuntimeSettings>("/api/settings/runtime")
.then((r) => {
@@ -426,6 +438,20 @@ export default function SettingsPage() {
>
</button>
<button
type="button"
className={tab === "funds" ? "tab active" : "tab"}
onClick={() => {
setTab("funds");
setFundsErr("");
setFundsOk("");
apiFetch<{ usdc_usdt_rate?: number }>("/api/funds/summary")
.then((r) => setUsdcRate(r.usdc_usdt_rate ?? null))
.catch(() => undefined);
}}
>
</button>
<button
type="button"
className={tab === "account" ? "tab active" : "tab"}
@@ -1031,6 +1057,169 @@ export default function SettingsPage() {
</div>
) : null}
{tab === "funds" ? (
<div className="card settings-card">
{fundsErr ? <div className="err">{fundsErr}</div> : null}
{fundsOk ? <div className="settings-ok">{fundsOk}</div> : null}
<section className="settings-section">
<h3>USDT USDC</h3>
<p className="meta" style={{ marginTop: 0 }}>
OKX USDC-USDT{" "}
{usdcRate != null ? `1 USDC ≈ ${usdcRate.toFixed(4)} USDT` : "—"}
USDT USDC
</p>
<div className="settings-fields">
<div className="field">
<label htmlFor="convertDir"></label>
<select
id="convertDir"
value={convertDir}
onChange={(e) =>
setConvertDir(
e.target.value === "usdc_to_usdt"
? "usdc_to_usdt"
: "usdt_to_usdc",
)
}
>
<option value="usdt_to_usdc">USDT USDC</option>
<option value="usdc_to_usdt">USDC USDT</option>
</select>
</div>
<div className="field">
<label htmlFor="convertAmt"></label>
<input
id="convertAmt"
type="number"
min={0}
step="0.01"
value={convertAmt}
onChange={(e) => setConvertAmt(Number(e.target.value))}
/>
</div>
</div>
<div className="settings-actions">
<button
className="btn"
type="button"
disabled={loading}
onClick={async () => {
setFundsErr("");
setFundsOk("");
setLoading(true);
try {
await apiFetch("/api/funds/convert", {
method: "POST",
body: JSON.stringify({
direction: convertDir,
amount: convertAmt,
}),
});
setFundsOk("兑换成功,顶部资金条将刷新");
} catch (e) {
setFundsErr(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
}}
>
</button>
</div>
</section>
<section className="settings-section">
<h3></h3>
<p className="meta" style={{ marginTop: 0 }}>
USDT/USDC options_funding /
options_trading
</p>
<div className="settings-fields">
<div className="field">
<label htmlFor="xferCcy"></label>
<select
id="xferCcy"
value={xferCcy}
onChange={(e) =>
setXferCcy(e.target.value === "USDT" ? "USDT" : "USDC")
}
>
<option value="USDC">USDC</option>
<option value="USDT">USDT</option>
</select>
</div>
<div className="field">
<label htmlFor="xferFrom"></label>
<select
id="xferFrom"
value={xferFrom}
onChange={(e) => setXferFrom(e.target.value)}
>
<option value="funding"></option>
<option value="trading"></option>
<option value="options_funding"></option>
<option value="options_trading"></option>
</select>
</div>
<div className="field">
<label htmlFor="xferTo"></label>
<select
id="xferTo"
value={xferTo}
onChange={(e) => setXferTo(e.target.value)}
>
<option value="funding"></option>
<option value="trading"></option>
<option value="options_funding"></option>
<option value="options_trading"></option>
</select>
</div>
<div className="field">
<label htmlFor="xferAmt"></label>
<input
id="xferAmt"
type="number"
min={0}
step="0.01"
value={xferAmt}
onChange={(e) => setXferAmt(Number(e.target.value))}
/>
</div>
</div>
<div className="settings-actions">
<button
className="btn"
type="button"
disabled={loading}
onClick={async () => {
setFundsErr("");
setFundsOk("");
setLoading(true);
try {
await apiFetch("/api/funds/transfer", {
method: "POST",
body: JSON.stringify({
ccy: xferCcy,
amount: xferAmt,
from_account: xferFrom,
to_account: xferTo,
}),
});
setFundsOk("划转成功,顶部资金条将刷新");
} catch (e) {
setFundsErr(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
}}
>
</button>
</div>
</section>
</div>
) : null}
{tab === "account" ? (
<div className="card settings-card settings-card-narrow">
{err ? <div className="err">{err}</div> : null}