Implement P1 local matcher/ledger and P2 strategy engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 17:03:46 +08:00
parent 0fca5f025e
commit 51b8bb8f8a
30 changed files with 2086 additions and 150 deletions
+156 -60
View File
@@ -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卖一 &gt; Put卖一 +</span>
<span className="tag up"> Call + </span>
) : bias === "put_ask_gt_call" ? (
<span className="tag down">Put卖一 &gt; 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>