Extend semi-auto with ITM/ATM/OTM, offset, and 1:4 sizing.

Moneyness and perp:option units are plan-only; OTM enforces leverage >=180.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 11:42:57 +08:00
parent 4585dba3c3
commit 457838ef64
17 changed files with 428 additions and 41 deletions
+8
View File
@@ -277,6 +277,10 @@ export type PlanState = {
semi_perp_exit_unit?: number;
semi_min_option_hours?: number;
semi_min_option_leverage?: number;
semi_moneyness?: "itm" | "atm" | "otm";
semi_otm_max_offset?: number;
semi_perp_unit?: number;
semi_option_unit?: number;
semi_net_exit_target?: number;
can_open: boolean;
open_capacity?: {
@@ -421,6 +425,10 @@ export type StrategySettings = {
semi_perp_exit_unit?: number;
semi_min_option_hours?: number;
semi_min_option_leverage?: number;
semi_moneyness?: "itm" | "atm" | "otm";
semi_otm_max_offset?: number;
semi_perp_unit?: number;
semi_option_unit?: number;
close_bid_mark_max_pct?: number;
residual_min_premium_pct?: number;
residual_close_check_sec?: number;
+101 -18
View File
@@ -116,9 +116,25 @@ export default function PlanPage() {
const [semiMove, setSemiMove] = useState(50);
const [semiExitU, setSemiExitU] = useState(5);
const [semiMinH, setSemiMinH] = useState(30);
const [semiMinLev, setSemiMinLev] = useState(100);
const [semiMinLev, setSemiMinLev] = useState(200);
const [semiMny, setSemiMny] = useState<"itm" | "atm" | "otm">("otm");
const [semiOtmOff, setSemiOtmOff] = useState(25);
const [semiPerpU, setSemiPerpU] = useState(1);
const [semiOptU, setSemiOptU] = useState(4);
const [semiDirty, setSemiDirty] = useState(false);
const semiParamsBody = () => ({
semi_view_side: semiView,
semi_option_move_points: semiMove,
semi_perp_exit_unit: semiExitU,
semi_min_option_hours: semiMinH,
semi_min_option_leverage: semiMinLev,
semi_moneyness: semiMny,
semi_otm_max_offset: semiOtmOff,
semi_perp_unit: semiPerpU,
semi_option_unit: semiOptU,
});
async function refresh() {
try {
const [m, p] = await Promise.all([
@@ -132,7 +148,12 @@ export default function PlanPage() {
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));
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
const m = p.semi_moneyness;
setSemiMny(m === "itm" || m === "atm" || m === "otm" ? m : "otm");
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
setSemiPerpU(Number(p.semi_perp_unit ?? 1));
setSemiOptU(Number(p.semi_option_unit ?? 4));
}
setErr("");
} catch (e) {
@@ -146,13 +167,7 @@ export default function PlanPage() {
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,
}),
body: JSON.stringify(semiParamsBody()),
});
setPlan(p);
setSemiDirty(false);
@@ -170,13 +185,7 @@ export default function PlanPage() {
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,
}),
body: JSON.stringify(semiParamsBody()),
});
setSemiDirty(false);
}
@@ -568,7 +577,7 @@ export default function PlanPage() {
<section className="card plan-semi-card" aria-label="半自动本单">
<h3 className="plan-panel-title"> · </h3>
<p className="meta" style={{ marginTop: 0 }}>
/ ±&gt;0×k
// 180200±&gt;0×k
</p>
<div className="settings-fields" style={{ marginTop: 8 }}>
<div className="field">
@@ -587,6 +596,76 @@ export default function PlanPage() {
<option value="short"> · Put + </option>
</select>
</div>
<div className="field">
<label htmlFor="semiMny"></label>
<select
id="semiMny"
className="mono"
disabled={open || !!busy || !!plan.semi_armed}
value={semiMny}
onChange={(e) => {
const v = e.target.value;
setSemiMny(
v === "itm" || v === "atm" || v === "otm" ? v : "otm",
);
setSemiDirty(true);
}}
>
<option value="itm"></option>
<option value="atm"></option>
<option value="otm"></option>
</select>
</div>
{semiMny === "otm" ? (
<div className="field">
<label htmlFor="semiOtmOff"></label>
<input
id="semiOtmOff"
className="mono"
type="number"
step="1"
min="1"
disabled={open || !!busy || !!plan.semi_armed}
value={semiOtmOff}
onChange={(e) => {
setSemiOtmOff(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
) : null}
<div className="field">
<label htmlFor="semiPerpU"></label>
<input
id="semiPerpU"
className="mono"
type="number"
step="0.1"
min="0.01"
disabled={open || !!busy || !!plan.semi_armed}
value={semiPerpU}
onChange={(e) => {
setSemiPerpU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiOptU"></label>
<input
id="semiOptU"
className="mono"
type="number"
step="0.1"
min="0.01"
disabled={open || !!busy || !!plan.semi_armed}
value={semiOptU}
onChange={(e) => {
setSemiOptU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiMove"> · </label>
<input
@@ -661,7 +740,11 @@ export default function PlanPage() {
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 ? "已授权盯开" : "未授权"}`;
const mnyZh =
semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值";
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · ${mnyZh}${
semiMny === "otm" ? `${fmt(semiOtmOff, 0)}` : ""
} · 配比 ${fmt(semiPerpU, 2)}:${fmt(semiOptU, 2)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`;
})()}
</div>
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}>