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:
@@ -268,6 +268,16 @@ export type PlanState = {
|
||||
max_atm_open_offset?: number;
|
||||
fixed_direction_enabled?: boolean;
|
||||
fixed_perp_side?: "long" | "short";
|
||||
semi_auto_enabled?: boolean;
|
||||
semi_armed?: boolean;
|
||||
semi_view_side?: "long" | "short";
|
||||
semi_option_side?: "call" | "put";
|
||||
semi_perp_side?: "long" | "short";
|
||||
semi_option_move_points?: number;
|
||||
semi_perp_exit_unit?: number;
|
||||
semi_min_option_hours?: number;
|
||||
semi_min_option_leverage?: number;
|
||||
semi_net_exit_target?: number;
|
||||
can_open: boolean;
|
||||
open_capacity?: {
|
||||
leverage?: number;
|
||||
@@ -404,6 +414,13 @@ export type StrategySettings = {
|
||||
max_atm_open_offset?: number;
|
||||
fixed_direction_enabled?: boolean;
|
||||
fixed_perp_side?: "long" | "short";
|
||||
semi_auto_enabled?: boolean;
|
||||
semi_armed?: boolean;
|
||||
semi_view_side?: "long" | "short";
|
||||
semi_option_move_points?: number;
|
||||
semi_perp_exit_unit?: number;
|
||||
semi_min_option_hours?: number;
|
||||
semi_min_option_leverage?: number;
|
||||
close_bid_mark_max_pct?: number;
|
||||
residual_min_premium_pct?: number;
|
||||
residual_close_check_sec?: number;
|
||||
|
||||
@@ -28,6 +28,8 @@ const CLOSE_REASON_ZH: Record<string, string> = {
|
||||
premium_multiple: "权利金倍数达标·双腿全平",
|
||||
target_perp_only: "净盈利达标·只平永续(期权归档)",
|
||||
residual_premium_close: "残留期权·权利金回收中途平",
|
||||
semi_target_points: "半自动·标的到点且组合净利>0·双腿全平",
|
||||
semi_perp_exit: "半自动·净利基数达标·双腿全平",
|
||||
expiry: "到期结算",
|
||||
emergency: "紧急全平",
|
||||
manual: "手动平仓",
|
||||
|
||||
+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">
|
||||
|
||||
@@ -89,6 +89,7 @@ export default function SettingsPage() {
|
||||
const [perpQty, setPerpQty] = useState(1);
|
||||
const [optQty, setOptQty] = useState(2);
|
||||
const [showManualTrade, setShowManualTrade] = useState(false);
|
||||
const [semiAutoOn, setSemiAutoOn] = useState(false);
|
||||
const [sizingMode, setSizingMode] = useState<"manual" | "risk_based">("manual");
|
||||
const [riskLeverageBasis, setRiskLeverageBasis] = useState<
|
||||
"actual" | "selection"
|
||||
@@ -214,6 +215,7 @@ export default function SettingsPage() {
|
||||
setPerpQty(s.perp_qty_eth ?? 1);
|
||||
setOptQty(s.option_qty_eth ?? 2);
|
||||
setShowManualTrade(s.show_manual_trade_buttons === true);
|
||||
setSemiAutoOn(s.semi_auto_enabled === true);
|
||||
setSizingMode(s.sizing_mode === "risk_based" ? "risk_based" : "manual");
|
||||
setRiskLeverageBasis(
|
||||
s.risk_leverage_basis === "actual" ? "actual" : "selection",
|
||||
@@ -393,6 +395,7 @@ export default function SettingsPage() {
|
||||
close_bid_mark_max_pct: closeDevPct,
|
||||
residual_min_premium_pct: residualMinPremPct,
|
||||
show_manual_trade_buttons: showManualTrade,
|
||||
semi_auto_enabled: oo ? false : semiAutoOn,
|
||||
sizing_mode: oo ? "risk_based" : sizingMode,
|
||||
risk_leverage_basis: riskLeverageBasis,
|
||||
risk_loss_mode: oo ? "percent" : riskLossMode,
|
||||
@@ -1582,6 +1585,22 @@ export default function SettingsPage() {
|
||||
<section className="settings-section">
|
||||
<h3>节奏与费用</h3>
|
||||
<div className="settings-fields">
|
||||
{!isOo ? (
|
||||
<div className="field">
|
||||
<label htmlFor="semiAuto">半自动模式</label>
|
||||
<select
|
||||
id="semiAuto"
|
||||
className="mono"
|
||||
value={semiAutoOn ? "on" : "off"}
|
||||
onChange={(e) =>
|
||||
setSemiAutoOn(e.target.value === "on")
|
||||
}
|
||||
>
|
||||
<option value="off">关闭(全自动循环)</option>
|
||||
<option value="on">开启(首页授权单笔)</option>
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="field">
|
||||
<label htmlFor="rest">轮间休息秒数</label>
|
||||
<input
|
||||
@@ -1770,6 +1789,9 @@ export default function SettingsPage() {
|
||||
) : null}
|
||||
{stratSub === "pace" ? (
|
||||
<>
|
||||
<li>
|
||||
半自动:开启后首页出现本单面板;人工定方向/目标并「授权开下一单」,机器盯选约→开→平;平完停,不自动连开。期期不可用。
|
||||
</li>
|
||||
<li>
|
||||
实盘下单最小间隔:LIVE 私有下单/查单间隔,默认 1s。范围
|
||||
0.2–30。
|
||||
|
||||
@@ -459,6 +459,15 @@ input {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.plan-semi-card {
|
||||
margin-bottom: 12px;
|
||||
border-color: rgba(94, 200, 255, 0.35);
|
||||
}
|
||||
|
||||
.plan-semi-card .plan-panel-title {
|
||||
color: #5ec8ff;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.app-container {
|
||||
padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
Reference in New Issue
Block a user