Add 永期半自动: human arm, machine open/close, then stop.
Settings toggle, Plan panel, Fleet monitor, exit locks and armed TOCTOU gates; docs and dual audits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+216
-11
@@ -82,6 +82,7 @@ function fmtHoldDuration(openAtMs: number | null | undefined, now: number) {
|
||||
const PHASE_ZH: Record<string, string> = {
|
||||
idle: "空闲",
|
||||
wait_signal: "等待信号",
|
||||
wait_human: "等待人工授权",
|
||||
opening: "开仓中",
|
||||
open: "持仓中",
|
||||
closing: "平仓中",
|
||||
@@ -111,6 +112,12 @@ export default function PlanPage() {
|
||||
const [busy, setBusy] = useState("");
|
||||
const [residualBusy, setResidualBusy] = useState("");
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const [semiView, setSemiView] = useState<"long" | "short">("long");
|
||||
const [semiMove, setSemiMove] = useState(50);
|
||||
const [semiExitU, setSemiExitU] = useState(5);
|
||||
const [semiMinH, setSemiMinH] = useState(30);
|
||||
const [semiMinLev, setSemiMinLev] = useState(100);
|
||||
const [semiDirty, setSemiDirty] = useState(false);
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
@@ -120,12 +127,71 @@ export default function PlanPage() {
|
||||
]);
|
||||
setSnap(m);
|
||||
setPlan(p);
|
||||
if (!semiDirty) {
|
||||
setSemiView(p.semi_view_side === "short" ? "short" : "long");
|
||||
setSemiMove(Number(p.semi_option_move_points ?? 50));
|
||||
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
||||
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
||||
setSemiMinLev(Number(p.semi_min_option_leverage ?? 100));
|
||||
}
|
||||
setErr("");
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSemiParams() {
|
||||
setBusy("semi-save");
|
||||
setErr("");
|
||||
try {
|
||||
const p = await apiFetch<PlanState>("/api/plan/semi/params", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
semi_view_side: semiView,
|
||||
semi_option_move_points: semiMove,
|
||||
semi_perp_exit_unit: semiExitU,
|
||||
semi_min_option_hours: semiMinH,
|
||||
semi_min_option_leverage: semiMinLev,
|
||||
}),
|
||||
});
|
||||
setPlan(p);
|
||||
setSemiDirty(false);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
async function armSemi(armed: boolean) {
|
||||
setBusy(armed ? "semi-arm" : "semi-disarm");
|
||||
setErr("");
|
||||
try {
|
||||
if (semiDirty) {
|
||||
await apiFetch<PlanState>("/api/plan/semi/params", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
semi_view_side: semiView,
|
||||
semi_option_move_points: semiMove,
|
||||
semi_perp_exit_unit: semiExitU,
|
||||
semi_min_option_hours: semiMinH,
|
||||
semi_min_option_leverage: semiMinLev,
|
||||
}),
|
||||
});
|
||||
setSemiDirty(false);
|
||||
}
|
||||
const p = await apiFetch<PlanState>("/api/plan/semi/arm", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ armed }),
|
||||
});
|
||||
setPlan(p);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy("");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const t = window.setInterval(refresh, 1500);
|
||||
@@ -171,26 +237,36 @@ export default function PlanPage() {
|
||||
}
|
||||
|
||||
const bias = snap?.ask_compare?.bias;
|
||||
const biasTag =
|
||||
bias === "strike_below_spot" ||
|
||||
bias === "call_ask_gt_put" ||
|
||||
bias === "fixed_short_call" ? (
|
||||
const pos = plan?.position;
|
||||
const open = !!pos?.has_position;
|
||||
// 持仓中展示本组成交方向,勿用监控 ATM 的实时盘口信号(会漂)
|
||||
const heldOpt = String(pos?.option_side || "").toLowerCase();
|
||||
const heldPerp = String(pos?.perp_side || "").toLowerCase();
|
||||
const biasTag = open ? (
|
||||
heldOpt === "call" || heldPerp === "short" ? (
|
||||
<span className="tag up">买 Call + 永续空</span>
|
||||
) : bias === "strike_above_spot" ||
|
||||
bias === "put_ask_gt_call" ||
|
||||
bias === "fixed_long_put" ? (
|
||||
) : heldOpt === "put" || heldPerp === "long" ? (
|
||||
<span className="tag down">买 Put + 永续多</span>
|
||||
) : (
|
||||
<span className="tag">等待 / 相等</span>
|
||||
);
|
||||
<span className="tag">持仓中</span>
|
||||
)
|
||||
) : bias === "strike_below_spot" ||
|
||||
bias === "call_ask_gt_put" ||
|
||||
bias === "fixed_short_call" ? (
|
||||
<span className="tag up">买 Call + 永续空</span>
|
||||
) : bias === "strike_above_spot" ||
|
||||
bias === "put_ask_gt_call" ||
|
||||
bias === "fixed_long_put" ? (
|
||||
<span className="tag down">买 Put + 永续多</span>
|
||||
) : (
|
||||
<span className="tag">等待 / 相等</span>
|
||||
);
|
||||
|
||||
const pos = plan?.position;
|
||||
const isOo =
|
||||
plan?.hedge_mode === "option_option" ||
|
||||
snap?.hedge_mode === "option_option" ||
|
||||
pos?.hedge_mode === "option_option" ||
|
||||
!!pos?.option2_inst_id;
|
||||
const open = !!pos?.has_position;
|
||||
const exitMode = plan?.exit_mode ?? "fixed_usdt";
|
||||
const riskBased =
|
||||
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
||||
@@ -488,6 +564,135 @@ export default function PlanPage() {
|
||||
{busy ? <span className="meta">{busy}…</span> : null}
|
||||
</div>
|
||||
|
||||
{plan?.semi_auto_enabled && !isOo ? (
|
||||
<section className="card plan-semi-card" aria-label="半自动本单">
|
||||
<h3 className="plan-panel-title">半自动 · 本单</h3>
|
||||
<p className="meta" style={{ marginTop: 0 }}>
|
||||
人工定方向与目标 → 授权后机器盯选约/开平 → 平完停,再授权下一单。选约:≥剩余时长与杠杆、实值或平值。顺向:开仓指数±点数且组合净利>0;逆向兑现:净利≥基数×k。平仓先期权后永续。
|
||||
</p>
|
||||
<div className="settings-fields" style={{ marginTop: 8 }}>
|
||||
<div className="field">
|
||||
<label htmlFor="semiView">方向(看法)</label>
|
||||
<select
|
||||
id="semiView"
|
||||
className="mono"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiView}
|
||||
onChange={(e) => {
|
||||
setSemiView(e.target.value === "short" ? "short" : "long");
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
>
|
||||
<option value="long">多 · 买 Call + 永续空</option>
|
||||
<option value="short">空 · 买 Put + 永续多</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiMove">期权出场 · 波动点数</label>
|
||||
<input
|
||||
id="semiMove"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiMove}
|
||||
onChange={(e) => {
|
||||
setSemiMove(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiExitU">净利出场基数(U,×k)</label>
|
||||
<input
|
||||
id="semiExitU"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiExitU}
|
||||
onChange={(e) => {
|
||||
setSemiExitU(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiMinH">选约 · 最短剩余(h)</label>
|
||||
<input
|
||||
id="semiMinH"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiMinH}
|
||||
onChange={(e) => {
|
||||
setSemiMinH(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiMinLev">选约 · 最低杠杆</label>
|
||||
<input
|
||||
id="semiMinLev"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiMinLev}
|
||||
onChange={(e) => {
|
||||
setSemiMinLev(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mono meta" style={{ marginTop: 8 }}>
|
||||
{(() => {
|
||||
const idx = snap?.index_px;
|
||||
if (idx == null || !Number.isFinite(Number(idx))) {
|
||||
return `目标净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · 指数 —`;
|
||||
}
|
||||
const n = Number(idx);
|
||||
const tgt =
|
||||
semiView === "long" ? n + Number(semiMove) : n - Number(semiMove);
|
||||
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`;
|
||||
})()}
|
||||
</div>
|
||||
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={open || !!busy || !semiDirty}
|
||||
onClick={() => void saveSemiParams()}
|
||||
>
|
||||
保存本单参数
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
onClick={() => void armSemi(true)}
|
||||
>
|
||||
授权开下一单
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={open || !!busy || !plan.semi_armed}
|
||||
onClick={() => void armSemi(false)}
|
||||
>
|
||||
取消授权
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className="plan-shell">
|
||||
<div className="plan-board">
|
||||
<div className="card plan-strategy">
|
||||
|
||||
Reference in New Issue
Block a user