Implement P1 local matcher/ledger and P2 strategy engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
const TOKEN_KEY = "eth_hedge_token";
|
||||
const USER_KEY = "eth_hedge_user";
|
||||
|
||||
/** 始终同源(经 dc.hyf2.cc 反代),不再暴露可改 API 地址。 */
|
||||
export function getApiBase(): string {
|
||||
return window.location.origin;
|
||||
}
|
||||
@@ -113,3 +112,39 @@ type Quote = {
|
||||
ask_sz: number | null;
|
||||
mark_px: number | null;
|
||||
};
|
||||
|
||||
export type PlanState = {
|
||||
running: boolean;
|
||||
phase: string;
|
||||
rounds_done: number;
|
||||
max_rounds: number;
|
||||
window_key: string | null;
|
||||
rest_left_sec: number;
|
||||
rest_seconds: number;
|
||||
exit_move_points: number;
|
||||
can_open: boolean;
|
||||
last_error: string | null;
|
||||
position: {
|
||||
has_position: boolean;
|
||||
group_id?: string;
|
||||
perp_side?: string;
|
||||
option_side?: string;
|
||||
perp_upl?: number;
|
||||
option_upl?: number;
|
||||
index_px?: number | null;
|
||||
entry_index_px?: number;
|
||||
move_points?: number;
|
||||
initial_premium?: number;
|
||||
premium_gap?: number;
|
||||
};
|
||||
ledger: { equity: number; available: number; reserved: number };
|
||||
};
|
||||
|
||||
export type StrategySettings = {
|
||||
fee_rate: number;
|
||||
exit_move_points: number;
|
||||
rest_seconds: number;
|
||||
max_rounds: number;
|
||||
initial_equity: number;
|
||||
ledger: { equity: number; available: number };
|
||||
};
|
||||
|
||||
+156
-60
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiFetch, MarketSnapshot } from "../api/client";
|
||||
import { apiFetch, MarketSnapshot, PlanState } from "../api/client";
|
||||
|
||||
function fmt(n: number | null | undefined, d = 2) {
|
||||
if (n == null || Number.isNaN(n)) return "—";
|
||||
@@ -8,112 +8,208 @@ function fmt(n: number | null | undefined, d = 2) {
|
||||
|
||||
export default function PlanPage() {
|
||||
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
|
||||
const [plan, setPlan] = useState<PlanState | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
const [busy, setBusy] = useState("");
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [m, p] = await Promise.all([
|
||||
apiFetch<MarketSnapshot>("/api/market/snapshot"),
|
||||
apiFetch<PlanState>("/api/plan/state"),
|
||||
]);
|
||||
setSnap(m);
|
||||
setPlan(p);
|
||||
setErr("");
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
refresh();
|
||||
const t = window.setInterval(refresh, 1500);
|
||||
return () => window.clearInterval(t);
|
||||
}, []);
|
||||
|
||||
async function act(path: string, label: string) {
|
||||
setBusy(label);
|
||||
setErr("");
|
||||
try {
|
||||
await apiFetch(path, { method: "POST", body: "{}" });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
const bias = snap?.ask_compare?.bias;
|
||||
const biasTag =
|
||||
bias === "call_ask_gt_put" ? (
|
||||
<span className="tag up">Call卖一 > Put卖一 → 永续多+期权空</span>
|
||||
<span className="tag up">买 Call + 永续空</span>
|
||||
) : bias === "put_ask_gt_call" ? (
|
||||
<span className="tag down">Put卖一 > Call卖一 → 永续空+期权多</span>
|
||||
<span className="tag down">买 Put + 永续多</span>
|
||||
) : (
|
||||
<span className="tag">等待 / 相等</span>
|
||||
);
|
||||
|
||||
const pos = plan?.position;
|
||||
const exitN = plan?.exit_move_points ?? 30;
|
||||
const move = pos?.move_points ?? 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>自动对冲计划</h2>
|
||||
<p style={{ color: "var(--muted)", marginTop: -8 }}>
|
||||
P0 行情只读 · 策略开平仓待拍板后接入
|
||||
SIM 本地撮合 · 期权只买 · 标的波动 N 点可配
|
||||
</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 12 }}>
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
disabled={!!busy || plan?.running}
|
||||
onClick={() => act("/api/plan/start", "start")}
|
||||
>
|
||||
启动策略
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={!!busy || !plan?.running}
|
||||
onClick={() => act("/api/plan/pause", "pause")}
|
||||
>
|
||||
暂停
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={!!busy}
|
||||
onClick={() => act("/api/sim/open-group", "open")}
|
||||
>
|
||||
模拟开一组
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={!!busy}
|
||||
onClick={() => act("/api/sim/close-group", "close")}
|
||||
>
|
||||
模拟全平
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={!!busy}
|
||||
onClick={() => act("/api/plan/emergency-close", "emg")}
|
||||
>
|
||||
紧急全平
|
||||
</button>
|
||||
{busy ? <span className="meta">{busy}…</span> : null}
|
||||
</div>
|
||||
|
||||
<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>策略</span>
|
||||
<span className="mono">
|
||||
{snap?.pair
|
||||
? `${snap.pair.expiry_ymd} @ ${snap.pair.strike}`
|
||||
: "—"}
|
||||
{plan?.running ? "运行中" : "已停"} · {plan?.phase || "—"} · 轮次{" "}
|
||||
{plan?.rounds_done ?? 0}/{plan?.max_rounds ?? 3}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>选向</span>
|
||||
{biasTag}
|
||||
<span>开仓窗</span>
|
||||
<span className="mono">
|
||||
{plan?.can_open ? "可开" : "禁止新开"} · 窗 {plan?.window_key || "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>休息</span>
|
||||
<span className="mono">
|
||||
{plan?.rest_left_sec ? `${plan.rest_left_sec}s / ${plan.rest_seconds}s` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>权益</span>
|
||||
<span className="mono">{fmt(plan?.ledger?.equity)} / 可用 {fmt(plan?.ledger?.available)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>当前组</span>
|
||||
<span className="mono">{pos?.group_id || "—"}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>方向</span>
|
||||
<span className="mono">
|
||||
{pos?.has_position
|
||||
? `永续${pos.perp_side} + 买${pos.option_side?.toUpperCase()}`
|
||||
: "—"}{" "}
|
||||
{biasTag}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>初始权利金</span>
|
||||
<span className="mono">{fmt(pos?.initial_premium)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>永续浮盈 / 距覆盖</span>
|
||||
<span className="mono">
|
||||
{fmt(pos?.perp_upl)} / {fmt(pos?.premium_gap)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>N 点进度</span>
|
||||
<span className="mono">
|
||||
{fmt(move, 1)} / {fmt(exitN, 0)}
|
||||
</span>
|
||||
</div>
|
||||
{plan?.last_error ? (
|
||||
<div className="kv">
|
||||
<span>最近错误</span>
|
||||
<span className="err" style={{ margin: 0 }}>
|
||||
{plan.last_error}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid-2">
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>永续 ETH-USDT-SWAP</h3>
|
||||
<h3 style={{ marginTop: 0 }}>永续</h3>
|
||||
<div className="kv">
|
||||
<span>买一</span>
|
||||
<span className="mono">{fmt(snap?.perp?.bid)} × {fmt(snap?.perp?.bid_sz, 2)}</span>
|
||||
<span className="mono">{fmt(snap?.perp?.bid)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>卖一</span>
|
||||
<span className="mono">{fmt(snap?.perp?.ask)} × {fmt(snap?.perp?.ask_sz, 2)}</span>
|
||||
<span className="mono">{fmt(snap?.perp?.ask)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>标记</span>
|
||||
<span className="mono">{fmt(snap?.perp?.mark_px)}</span>
|
||||
<span>指数</span>
|
||||
<span className="mono">{fmt(snap?.index_px)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>期权 ATM</h3>
|
||||
<h3 style={{ marginTop: 0 }}>
|
||||
期权 ATM {snap?.pair ? `@ ${snap.pair.strike}` : ""}
|
||||
</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>Call 卖一/买一</span>
|
||||
<span className="mono">
|
||||
{fmt(snap?.call?.ask)} / {fmt(snap?.call?.bid)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>Put</span>
|
||||
<span className="mono" style={{ fontSize: 12 }}>
|
||||
{snap?.pair?.put_inst_id || "—"}
|
||||
<span>Put 卖一/买一</span>
|
||||
<span className="mono">
|
||||
{fmt(snap?.put?.ask)} / {fmt(snap?.put?.bid)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>到期</span>
|
||||
<span className="mono">{snap?.pair?.expiry_ymd || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+153
-56
@@ -1,5 +1,11 @@
|
||||
import { FormEvent, useState } from "react";
|
||||
import { changeCredentials, getUsername, setSession } from "../api/client";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import {
|
||||
changeCredentials,
|
||||
getUsername,
|
||||
setSession,
|
||||
apiFetch,
|
||||
StrategySettings,
|
||||
} from "../api/client";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
||||
@@ -10,7 +16,24 @@ export default function SettingsPage() {
|
||||
const [ok, setOk] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSave(e: FormEvent) {
|
||||
const [fee, setFee] = useState(0.0005);
|
||||
const [exitPts, setExitPts] = useState(30);
|
||||
const [rest, setRest] = useState(300);
|
||||
const [maxRounds, setMaxRounds] = useState(3);
|
||||
const [stratOk, setStratOk] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<StrategySettings>("/api/settings/strategy")
|
||||
.then((s) => {
|
||||
setFee(s.fee_rate);
|
||||
setExitPts(s.exit_move_points);
|
||||
setRest(s.rest_seconds);
|
||||
setMaxRounds(s.max_rounds);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
async function onSaveCreds(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setErr("");
|
||||
setOk("");
|
||||
@@ -41,60 +64,134 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveStrategy(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setStratOk("");
|
||||
setErr("");
|
||||
try {
|
||||
await apiFetch("/api/settings/strategy", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
fee_rate: fee,
|
||||
exit_move_points: exitPts,
|
||||
rest_seconds: rest,
|
||||
max_rounds: maxRounds,
|
||||
}),
|
||||
});
|
||||
setStratOk("策略参数已保存");
|
||||
} catch (ex) {
|
||||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ maxWidth: 560 }}>
|
||||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||
<p style={{ color: "var(--muted)" }}>修改登录用户名与密码(写入服务器 .env,立即生效)。</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||||
<form onSubmit={onSave}>
|
||||
<div className="field">
|
||||
<label htmlFor="user">新用户名</label>
|
||||
<input
|
||||
id="user"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cur">当前密码</label>
|
||||
<input
|
||||
id="cur"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="np">新密码</label>
|
||||
<input
|
||||
id="np"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cp">确认新密码</label>
|
||||
<input
|
||||
id="cp"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit" disabled={loading}>
|
||||
{loading ? "保存中…" : "保存"}
|
||||
</button>
|
||||
</form>
|
||||
<div style={{ display: "grid", gap: 16, maxWidth: 560 }}>
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>策略设置</h2>
|
||||
<p style={{ color: "var(--muted)" }}>
|
||||
标的波动 N 点全平、轮次休息、费率(滑点=1×费率)。
|
||||
</p>
|
||||
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
|
||||
<form onSubmit={onSaveStrategy}>
|
||||
<div className="field">
|
||||
<label htmlFor="exit">EXIT_MOVE_POINTS(标的波动点数)</label>
|
||||
<input
|
||||
id="exit"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
value={exitPts}
|
||||
onChange={(e) => setExitPts(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="rest">REST_SECONDS(轮间休息秒)</label>
|
||||
<input
|
||||
id="rest"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
value={rest}
|
||||
onChange={(e) => setRest(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="rounds">MAX_ROUNDS</label>
|
||||
<input
|
||||
id="rounds"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
value={maxRounds}
|
||||
onChange={(e) => setMaxRounds(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="fee">FEE_RATE</label>
|
||||
<input
|
||||
id="fee"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={fee}
|
||||
onChange={(e) => setFee(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit">
|
||||
保存策略
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>登录账号</h2>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
||||
<form onSubmit={onSaveCreds}>
|
||||
<div className="field">
|
||||
<label htmlFor="user">新用户名</label>
|
||||
<input
|
||||
id="user"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cur">当前密码</label>
|
||||
<input
|
||||
id="cur"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="np">新密码</label>
|
||||
<input
|
||||
id="np"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="cp">确认新密码</label>
|
||||
<input
|
||||
id="cp"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button className="btn" type="submit" disabled={loading}>
|
||||
{loading ? "保存中…" : "保存账号"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,64 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiFetch } from "../api/client";
|
||||
|
||||
type Summary = {
|
||||
groups: number;
|
||||
wins: number;
|
||||
win_rate: number;
|
||||
total_pnl: number;
|
||||
total_fees: number;
|
||||
total_slip: number;
|
||||
close_reasons: Record<string, number>;
|
||||
equity_curve: { group_id: string; realized_pnl: number }[];
|
||||
};
|
||||
|
||||
export default function StatsPage() {
|
||||
const [s, setS] = useState<Summary | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<Summary>("/api/stats/summary")
|
||||
.then(setS)
|
||||
.catch((e) => setErr(e instanceof Error ? e.message : String(e)));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>统计</h2>
|
||||
<p style={{ color: "var(--muted)" }}>胜率 / 盈亏 / 手续费曲线将在有成交后展示。</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
{!s ? (
|
||||
<p style={{ color: "var(--muted)" }}>加载中…</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="kv">
|
||||
<span>组数 / 胜率</span>
|
||||
<span className="mono">
|
||||
{s.groups} / {(s.win_rate * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>总盈亏</span>
|
||||
<span className="mono">{s.total_pnl.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>总手续费 / 滑点</span>
|
||||
<span className="mono">
|
||||
{s.total_fees.toFixed(2)} / {s.total_slip.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>平仓原因</span>
|
||||
<span className="mono">{JSON.stringify(s.close_reasons)}</span>
|
||||
</div>
|
||||
<h3>按组盈亏</h3>
|
||||
{s.equity_curve.map((x) => (
|
||||
<div key={x.group_id} className="kv">
|
||||
<span className="mono">{x.group_id}</span>
|
||||
<span className="mono">{x.realized_pnl.toFixed(2)}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,91 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiFetch } from "../api/client";
|
||||
|
||||
type Group = {
|
||||
group_id: string;
|
||||
status: string;
|
||||
bias: string | null;
|
||||
option_side: string | null;
|
||||
perp_side: string | null;
|
||||
initial_premium: number;
|
||||
realized_pnl: number;
|
||||
close_reason: string | null;
|
||||
open_at_ms: number | null;
|
||||
close_at_ms: number | null;
|
||||
};
|
||||
|
||||
type Fill = {
|
||||
id: number;
|
||||
leg: string;
|
||||
action: string;
|
||||
side: string;
|
||||
fill_px: number;
|
||||
fee: number;
|
||||
qty_eth: number;
|
||||
};
|
||||
|
||||
export default function TradesPage() {
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [fills, setFills] = useState<Fill[]>([]);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<{ groups: Group[] }>("/api/trades/groups")
|
||||
.then((r) => setGroups(r.groups))
|
||||
.catch((e) => setErr(e instanceof Error ? e.message : String(e)));
|
||||
}, []);
|
||||
|
||||
async function openGroup(id: string) {
|
||||
setSelected(id);
|
||||
try {
|
||||
const r = await apiFetch<{ fills: Fill[] }>(`/api/trades/groups/${id}`);
|
||||
setFills(r.fills);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>交易记录</h2>
|
||||
<p style={{ color: "var(--muted)" }}>按组成交明细将在 P1/P2 接入。</p>
|
||||
{err ? <div className="err">{err}</div> : null}
|
||||
<div className="card" style={{ marginBottom: 12 }}>
|
||||
{groups.length === 0 ? (
|
||||
<p style={{ color: "var(--muted)" }}>暂无成交组</p>
|
||||
) : (
|
||||
groups.map((g) => (
|
||||
<div
|
||||
key={g.group_id}
|
||||
className="kv"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => openGroup(g.group_id)}
|
||||
>
|
||||
<span className="mono">
|
||||
{g.group_id} · {g.status} · {g.perp_side}/{g.option_side}
|
||||
</span>
|
||||
<span className="mono">
|
||||
PnL {Number(g.realized_pnl || 0).toFixed(2)} · {g.close_reason || "—"}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{selected ? (
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>{selected} 成交明细</h3>
|
||||
{fills.map((f) => (
|
||||
<div key={f.id} className="kv">
|
||||
<span className="mono">
|
||||
{f.leg} {f.action} {f.side}
|
||||
</span>
|
||||
<span className="mono">
|
||||
px {f.fill_px.toFixed(4)} · qty {f.qty_eth} · fee {f.fee.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user