Initial eth_hedge_sim: P0 market, auth UI, one-click deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { getApiBase, login, setApiBase, setSession } from "../api/client";
|
||||
|
||||
export default function LoginPage() {
|
||||
const nav = useNavigate();
|
||||
const [apiBase, setApi] = useState(getApiBase());
|
||||
const [username, setUsername] = useState("admin");
|
||||
const [password, setPassword] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const hint = useMemo(
|
||||
() => "默认同域 API。跨机访问时填写如 http://47.236.184.99:5155",
|
||||
[],
|
||||
);
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setErr("");
|
||||
setLoading(true);
|
||||
try {
|
||||
setApiBase(apiBase);
|
||||
const res = await login(username.trim(), password);
|
||||
setSession(res.token, res.username);
|
||||
nav("/plan", { replace: true });
|
||||
} catch (ex) {
|
||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-wrap">
|
||||
<form className="login-box" onSubmit={onSubmit}>
|
||||
<h1>eth_hedge_sim</h1>
|
||||
<p>模拟盘登录 · 可自定义 API 地址</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
<div className="field">
|
||||
<label htmlFor="api">API 地址</label>
|
||||
<input
|
||||
id="api"
|
||||
className="mono"
|
||||
value={apiBase}
|
||||
onChange={(e) => setApi(e.target.value)}
|
||||
placeholder="http://host:5155"
|
||||
autoComplete="url"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="user">用户名</label>
|
||||
<input
|
||||
id="user"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="pass">密码</label>
|
||||
<input
|
||||
id="pass"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn block" type="submit" disabled={loading}>
|
||||
{loading ? "登录中…" : "登录"}
|
||||
</button>
|
||||
<div className="hint">{hint}</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiFetch, MarketSnapshot } from "../api/client";
|
||||
|
||||
function fmt(n: number | null | undefined, d = 2) {
|
||||
if (n == null || Number.isNaN(n)) return "—";
|
||||
return n.toFixed(d);
|
||||
}
|
||||
|
||||
export default function PlanPage() {
|
||||
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await apiFetch<MarketSnapshot>("/api/market/snapshot");
|
||||
if (alive) {
|
||||
setSnap(data);
|
||||
setErr("");
|
||||
}
|
||||
} catch (e) {
|
||||
if (alive) setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
load();
|
||||
const t = window.setInterval(load, 2000);
|
||||
return () => {
|
||||
alive = false;
|
||||
window.clearInterval(t);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const bias = snap?.ask_compare?.bias;
|
||||
const biasTag =
|
||||
bias === "call_ask_gt_put" ? (
|
||||
<span className="tag up">Call卖一 > Put卖一 → 永续多+期权空</span>
|
||||
) : bias === "put_ask_gt_call" ? (
|
||||
<span className="tag down">Put卖一 > Call卖一 → 永续空+期权多</span>
|
||||
) : (
|
||||
<span className="tag">等待 / 相等</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>自动对冲计划</h2>
|
||||
<p style={{ color: "var(--muted)", marginTop: -8 }}>
|
||||
P0 行情只读 · 策略开平仓待拍板后接入
|
||||
</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
<div className="kv">
|
||||
<span>模式</span>
|
||||
<span className="mono">SIM · 测试环境</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>行情连接</span>
|
||||
<span className="mono">{snap?.connected ? "WS 已连接" : "REST/未连"}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>指数</span>
|
||||
<span className="mono">{fmt(snap?.index_px)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>选约</span>
|
||||
<span className="mono">
|
||||
{snap?.pair
|
||||
? `${snap.pair.expiry_ymd} @ ${snap.pair.strike}`
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>选向</span>
|
||||
{biasTag}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid-2">
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>永续 ETH-USDT-SWAP</h3>
|
||||
<div className="kv">
|
||||
<span>买一</span>
|
||||
<span className="mono">{fmt(snap?.perp?.bid)} × {fmt(snap?.perp?.bid_sz, 2)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>卖一</span>
|
||||
<span className="mono">{fmt(snap?.perp?.ask)} × {fmt(snap?.perp?.ask_sz, 2)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>标记</span>
|
||||
<span className="mono">{fmt(snap?.perp?.mark_px)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>期权 ATM</h3>
|
||||
<div className="kv">
|
||||
<span>Call 卖一</span>
|
||||
<span className="mono">{fmt(snap?.call?.ask)} / 买一 {fmt(snap?.call?.bid)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>Put 卖一</span>
|
||||
<span className="mono">{fmt(snap?.put?.ask)} / 买一 {fmt(snap?.put?.bid)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>Call</span>
|
||||
<span className="mono" style={{ fontSize: 12 }}>
|
||||
{snap?.pair?.call_inst_id || "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>Put</span>
|
||||
<span className="mono" style={{ fontSize: 12 }}>
|
||||
{snap?.pair?.put_inst_id || "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { getApiBase, setApiBase } from "../api/client";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [apiBase, setApi] = useState(getApiBase());
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
function onSave(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setApiBase(apiBase);
|
||||
setSaved(true);
|
||||
window.setTimeout(() => setSaved(false), 1500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ maxWidth: 560 }}>
|
||||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||
<p style={{ color: "var(--muted)" }}>
|
||||
前端可单独指定后端 API;同机部署默认用当前域名端口 5155。
|
||||
</p>
|
||||
<form onSubmit={onSave}>
|
||||
<div className="field">
|
||||
<label htmlFor="api">API 地址</label>
|
||||
<input
|
||||
id="api"
|
||||
className="mono"
|
||||
value={apiBase}
|
||||
onChange={(e) => setApi(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit">
|
||||
保存
|
||||
</button>
|
||||
{saved ? <span style={{ marginLeft: 10, color: "var(--up)" }}>已保存</span> : null}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function StatsPage() {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>统计</h2>
|
||||
<p style={{ color: "var(--muted)" }}>胜率 / 盈亏 / 手续费曲线将在有成交后展示。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function TradesPage() {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>交易记录</h2>
|
||||
<p style={{ color: "var(--muted)" }}>按组成交明细将在 P1/P2 接入。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user