Restructure Plan UI for semi-auto tabs and T-quote ladder.
Hide amplitude card when filter off; compact semi form with rules; monitor tab keeps positions and market. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from ..market import get_gateway
|
||||
from .auth import require_user
|
||||
@@ -18,6 +18,19 @@ async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
return snap
|
||||
|
||||
|
||||
@router.get("/option-ladder")
|
||||
async def market_option_ladder(
|
||||
_user: Annotated[str, Depends(require_user)],
|
||||
wings: int = Query(default=4, ge=1, le=12),
|
||||
) -> dict:
|
||||
"""半自动页 T 型报价:ATM 上下各 wings 档。"""
|
||||
gw = get_gateway()
|
||||
ladder = getattr(gw, "option_ladder", None)
|
||||
if not callable(ladder):
|
||||
raise HTTPException(status_code=501, detail="当前会话不支持 option-ladder")
|
||||
return ladder(wings=wings)
|
||||
|
||||
|
||||
@router.post("/realign")
|
||||
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
"""手动重对齐次日到期 ATM 合约(运维/调试用)。"""
|
||||
|
||||
@@ -1058,6 +1058,99 @@ class StrategySession:
|
||||
d["ask_compare"] = ac
|
||||
return d
|
||||
|
||||
def option_ladder(self, *, wings: int = 4) -> dict[str, Any]:
|
||||
"""
|
||||
T 型报价:当前监控到期附近若干档 Call/Put 盘口。
|
||||
行:虚值 Call(上行)/ ATM / 虚值 Put(下行);列:Call 卖一·杠杆 | 行权价 | Put 卖一·杠杆。
|
||||
"""
|
||||
s = self.settings
|
||||
wings = max(1, min(12, int(wings)))
|
||||
idx = self.ex.fetch_index(s.index_inst_id)
|
||||
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
|
||||
if mark is None or float(mark) <= 0:
|
||||
return {"ok": False, "detail": "无标的价", "rows": [], "index_px": None}
|
||||
underlying = float(mark)
|
||||
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
||||
from .selection import _complete_by_expiry, pick_atm_strike
|
||||
|
||||
eligible = list_eligible_expiry_ymds(contracts, min_hours=1.0)
|
||||
ymd = None
|
||||
if self._pair is not None and self._pair.expiry_ymd:
|
||||
ymd = str(self._pair.expiry_ymd)
|
||||
if not ymd and eligible:
|
||||
ymd = eligible[0]
|
||||
if not ymd:
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": "无合格到期",
|
||||
"rows": [],
|
||||
"index_px": underlying,
|
||||
}
|
||||
complete = _complete_by_expiry(contracts)
|
||||
if ymd not in complete:
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": f"到期 {ymd} 无完整对",
|
||||
"rows": [],
|
||||
"index_px": underlying,
|
||||
"expiry_ymd": ymd,
|
||||
}
|
||||
_ems, strikes_map = complete[ymd]
|
||||
strikes = sorted(float(k) for k in strikes_map.keys())
|
||||
atm = pick_atm_strike(strikes, underlying)
|
||||
if atm is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": "无 ATM",
|
||||
"rows": [],
|
||||
"index_px": underlying,
|
||||
"expiry_ymd": ymd,
|
||||
}
|
||||
atm_i = min(range(len(strikes)), key=lambda i: abs(strikes[i] - float(atm)))
|
||||
lo = max(0, atm_i - wings)
|
||||
hi = min(len(strikes), atm_i + wings + 1)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for k in strikes[lo:hi]:
|
||||
legs = strikes_map[k]
|
||||
call_id = legs.get("C")
|
||||
put_id = legs.get("P")
|
||||
cq = self.ex.quote(str(call_id)) if call_id else None
|
||||
pq = self.ex.quote(str(put_id)) if put_id else None
|
||||
c_ask = float(cq.ask) if cq and cq.ask is not None else None
|
||||
p_ask = float(pq.ask) if pq and pq.ask is not None else None
|
||||
c_bid = float(cq.bid) if cq and cq.bid is not None else None
|
||||
p_bid = float(pq.bid) if pq and pq.bid is not None else None
|
||||
off = float(k) - underlying
|
||||
if abs(float(k) - float(atm)) < 1e-9:
|
||||
tag = "atm"
|
||||
elif float(k) > underlying + 1e-9:
|
||||
tag = "otm_call" # Call 虚值 / Put 实值
|
||||
else:
|
||||
tag = "otm_put" # Put 虚值 / Call 实值
|
||||
rows.append(
|
||||
{
|
||||
"strike": float(k),
|
||||
"offset": round(off, 2),
|
||||
"tag": tag,
|
||||
"call_ask": c_ask,
|
||||
"call_bid": c_bid,
|
||||
"call_lev": option_leverage(underlying, c_ask),
|
||||
"put_ask": p_ask,
|
||||
"put_bid": p_bid,
|
||||
"put_lev": option_leverage(underlying, p_ask),
|
||||
"call_inst_id": call_id,
|
||||
"put_inst_id": put_id,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"detail": "",
|
||||
"index_px": underlying,
|
||||
"expiry_ymd": ymd,
|
||||
"atm_strike": float(atm),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
async def _refresh_loop(self) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(30 if self._pair is not None else 10)
|
||||
|
||||
+387
-188
@@ -122,6 +122,23 @@ export default function PlanPage() {
|
||||
const [semiPerpU, setSemiPerpU] = useState(1);
|
||||
const [semiOptU, setSemiOptU] = useState(4);
|
||||
const [semiDirty, setSemiDirty] = useState(false);
|
||||
const [planTab, setPlanTab] = useState<"semi" | "monitor">("semi");
|
||||
const [ladder, setLadder] = useState<{
|
||||
ok?: boolean;
|
||||
detail?: string;
|
||||
index_px?: number | null;
|
||||
expiry_ymd?: string;
|
||||
atm_strike?: number;
|
||||
rows?: Array<{
|
||||
strike: number;
|
||||
offset: number;
|
||||
tag: string;
|
||||
call_ask: number | null;
|
||||
call_lev: number | null;
|
||||
put_ask: number | null;
|
||||
put_lev: number | null;
|
||||
}>;
|
||||
} | null>(null);
|
||||
|
||||
const semiParamsBody = () => ({
|
||||
semi_view_side: semiView,
|
||||
@@ -149,12 +166,26 @@ export default function PlanPage() {
|
||||
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
||||
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
||||
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
|
||||
const m = p.semi_moneyness;
|
||||
setSemiMny(m === "itm" || m === "atm" || m === "otm" ? m : "otm");
|
||||
const mny = p.semi_moneyness;
|
||||
setSemiMny(mny === "itm" || mny === "atm" || mny === "otm" ? mny : "otm");
|
||||
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
|
||||
setSemiPerpU(Number(p.semi_perp_unit ?? 1));
|
||||
setSemiOptU(Number(p.semi_option_unit ?? 4));
|
||||
}
|
||||
if (
|
||||
p.semi_auto_enabled &&
|
||||
String(p.hedge_mode || "") !== "option_option"
|
||||
) {
|
||||
try {
|
||||
setLadder(
|
||||
await apiFetch<NonNullable<typeof ladder>>(
|
||||
"/api/market/option-ladder?wings=4",
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
/* ignore ladder errors */
|
||||
}
|
||||
}
|
||||
setErr("");
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
@@ -276,6 +307,8 @@ export default function PlanPage() {
|
||||
snap?.hedge_mode === "option_option" ||
|
||||
pos?.hedge_mode === "option_option" ||
|
||||
!!pos?.option2_inst_id;
|
||||
const semiOn = !!plan?.semi_auto_enabled && !isOo;
|
||||
const showAmpCard = plan?.oo_amplitude_filter_enabled === true;
|
||||
const exitMode = plan?.exit_mode ?? "fixed_usdt";
|
||||
const riskBased =
|
||||
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
||||
@@ -573,209 +606,351 @@ 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 }}>
|
||||
人工定方向/行权类型/配比 → 授权后机器盯开平 → 平完停。虚值杠杆门≥180(默认200)。顺向:开仓指数±点数且净利>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>
|
||||
{semiOn ? (
|
||||
<div className="tabs plan-main-tabs" role="tablist" aria-label="计划页">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={planTab === "semi" ? "tab active" : "tab"}
|
||||
aria-selected={planTab === "semi"}
|
||||
onClick={() => setPlanTab("semi")}
|
||||
>
|
||||
半自动单
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={planTab === "monitor" ? "tab active" : "tab"}
|
||||
aria-selected={planTab === "monitor"}
|
||||
onClick={() => setPlanTab("monitor")}
|
||||
>
|
||||
持仓监控
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{semiOn && planTab === "semi" ? (
|
||||
<div className="plan-semi-tab">
|
||||
<section className="card plan-semi-card plan-semi-compact" aria-label="半自动本单">
|
||||
<div className="plan-semi-head">
|
||||
<h3 className="plan-panel-title">半自动 · 本单</h3>
|
||||
<span className="mono meta">
|
||||
{plan.semi_armed ? "已授权" : "未授权"}
|
||||
{" · "}
|
||||
{semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值"}
|
||||
{" · "}
|
||||
{fmt(semiPerpU, 0)}:{fmt(semiOptU, 0)}
|
||||
</span>
|
||||
</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" ? (
|
||||
<details className="plan-semi-rules">
|
||||
<summary>规则说明</summary>
|
||||
<ul>
|
||||
<li>人工定方向、行权类型、永续:期权配比与出场 →「授权开下一单」后机器盯选约/开/平。</li>
|
||||
<li>平仓后停在等待授权,不自动连开;与全自动循环无关。</li>
|
||||
<li>虚值:偏离 ≤ 设定点数;杠杆门 ≥180(默认 200)。实值/平值用本单最低杠杆。</li>
|
||||
<li>顺向:开仓指数 ± 波动点数,且组合净利 > 0 → 双腿全平(先期权后永续)。</li>
|
||||
<li>逆向兑现:组合净利 ≥ 净利基数 × k → 全平。</li>
|
||||
<li>以损定仓时配比为单位再乘 k;手动仓按配比直接开。</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div className="settings-fields plan-semi-fields">
|
||||
<div className="field">
|
||||
<label htmlFor="semiOtmOff">虚值最大偏离(点)</label>
|
||||
<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="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="semiOtmOff"
|
||||
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
|
||||
id="semiMove"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={open || !!busy || !!plan.semi_armed}
|
||||
value={semiOtmOff}
|
||||
value={semiMove}
|
||||
onChange={(e) => {
|
||||
setSemiOtmOff(Number(e.target.value));
|
||||
setSemiMove(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiExitU">净利基数</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>
|
||||
) : 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 className="mono meta plan-semi-summary">
|
||||
{(() => {
|
||||
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`;
|
||||
})()}
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiMove">期权出场 · 波动点数</label>
|
||||
<input
|
||||
id="semiMove"
|
||||
className="mono"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
<div className="plan-semi-btns">
|
||||
<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}
|
||||
value={semiMove}
|
||||
onChange={(e) => {
|
||||
setSemiMove(Number(e.target.value));
|
||||
setSemiDirty(true);
|
||||
}}
|
||||
/>
|
||||
onClick={() => void armSemi(true)}
|
||||
>
|
||||
授权开下一单
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost"
|
||||
type="button"
|
||||
disabled={open || !!busy || !plan.semi_armed}
|
||||
onClick={() => void armSemi(false)}
|
||||
>
|
||||
取消授权
|
||||
</button>
|
||||
</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);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="card plan-t-quote" aria-label="T型报价">
|
||||
<div className="plan-semi-head">
|
||||
<h3 className="plan-panel-title">T 型报价 · 实值 / 平值 / 虚值</h3>
|
||||
<span className="mono meta">
|
||||
{ladder?.expiry_ymd
|
||||
? `到期 ${ladder.expiry_ymd}`
|
||||
: "—"}
|
||||
{ladder?.atm_strike != null
|
||||
? ` · ATM ${Math.round(ladder.atm_strike)}`
|
||||
: ""}
|
||||
{ladder?.index_px != null
|
||||
? ` · 指数 ${fmtExPx("index", ladder.index_px)}`
|
||||
: ""}
|
||||
</span>
|
||||
</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);
|
||||
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 }}>
|
||||
<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>
|
||||
{!ladder?.ok ? (
|
||||
<p className="meta">{ladder?.detail || "暂无报价链"}</p>
|
||||
) : (
|
||||
<div className="plan-t-wrap">
|
||||
<table className="plan-t-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Call卖一</th>
|
||||
<th>Call杠杆</th>
|
||||
<th>行权价</th>
|
||||
<th>类型</th>
|
||||
<th>Put杠杆</th>
|
||||
<th>Put卖一</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...(ladder.rows || [])]
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((r) => {
|
||||
const tagZh =
|
||||
r.tag === "atm"
|
||||
? "平值"
|
||||
: r.tag === "otm_call"
|
||||
? "Call虚/Put实"
|
||||
: "Put虚/Call实";
|
||||
const hi =
|
||||
(semiMny === "otm" &&
|
||||
((semiView === "long" && r.tag === "otm_call") ||
|
||||
(semiView === "short" && r.tag === "otm_put")) &&
|
||||
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
||||
(semiMny === "atm" && r.tag === "atm") ||
|
||||
(semiMny === "itm" &&
|
||||
((semiView === "long" &&
|
||||
(r.tag === "otm_put" || r.tag === "atm")) ||
|
||||
(semiView === "short" &&
|
||||
(r.tag === "otm_call" || r.tag === "atm"))));
|
||||
return (
|
||||
<tr
|
||||
key={r.strike}
|
||||
className={
|
||||
r.tag === "atm"
|
||||
? "plan-t-atm"
|
||||
: hi
|
||||
? "plan-t-pick"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<td className="mono">
|
||||
{r.call_ask != null
|
||||
? fmtExPx("option", r.call_ask)
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="mono">
|
||||
{r.call_lev != null
|
||||
? `${r.call_lev.toFixed(0)}x`
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="mono plan-t-k">
|
||||
{Math.round(r.strike)}
|
||||
<span className="meta">
|
||||
{" "}
|
||||
({r.offset >= 0 ? "+" : ""}
|
||||
{fmt(r.offset, 0)})
|
||||
</span>
|
||||
</td>
|
||||
<td>{tagZh}</td>
|
||||
<td className="mono">
|
||||
{r.put_lev != null
|
||||
? `${r.put_lev.toFixed(0)}x`
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="mono">
|
||||
{r.put_ask != null
|
||||
? fmtExPx("option", r.put_ask)
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!semiOn || planTab === "monitor" ? (
|
||||
<div className="plan-shell">
|
||||
<div className="plan-board">
|
||||
<div className="card plan-strategy">
|
||||
@@ -798,7 +973,18 @@ export default function PlanPage() {
|
||||
<div className="kv">
|
||||
<span>模式</span>
|
||||
<span className="mono">
|
||||
{riskBased ? (
|
||||
{semiOn ? (
|
||||
<span className="status-running">
|
||||
半自动 ·{" "}
|
||||
{semiMny === "itm"
|
||||
? "实值"
|
||||
: semiMny === "atm"
|
||||
? "平值"
|
||||
: "虚值"}{" "}
|
||||
· 配比 {fmt(semiPerpU, 0)}:{fmt(semiOptU, 0)}
|
||||
{plan?.semi_armed ? " · 已授权" : " · 未授权"}
|
||||
</span>
|
||||
) : riskBased ? (
|
||||
<span className="status-running">{sizingModeLabel}</span>
|
||||
) : (
|
||||
sizingModeLabel
|
||||
@@ -808,7 +994,17 @@ export default function PlanPage() {
|
||||
<div className="kv">
|
||||
<span>选约条件</span>
|
||||
<span className="mono">
|
||||
{isOo ? (
|
||||
{semiOn ? (
|
||||
<>
|
||||
剩余≥{fmt(semiMinH, 0)}h · 杠杆≥{fmt(semiMinLev, 0)}x
|
||||
{semiMny === "otm"
|
||||
? ` · 虚值≤${fmt(semiOtmOff, 0)}点`
|
||||
: ""}
|
||||
{" · "}
|
||||
顺向±{fmt(semiMove, 0)}点且净利>0 / 净利≥
|
||||
{fmt(semiExitU, 1)}×k
|
||||
</>
|
||||
) : isOo ? (
|
||||
ooSelectLabel
|
||||
) : (
|
||||
<>
|
||||
@@ -1326,7 +1522,8 @@ export default function PlanPage() {
|
||||
</div>
|
||||
|
||||
<div className="plan-market">
|
||||
<div className="card">
|
||||
{showAmpCard ? (
|
||||
<div className="card plan-card-compact">
|
||||
<h3 className="plan-panel-title">指数 / 振幅</h3>
|
||||
<div className="kv">
|
||||
<span>指数</span>
|
||||
@@ -1375,6 +1572,7 @@ export default function PlanPage() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!isOo ? (
|
||||
<div className="card">
|
||||
<h3 className="plan-panel-title">永续行情</h3>
|
||||
@@ -1617,6 +1815,7 @@ export default function PlanPage() {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -468,6 +468,144 @@ input {
|
||||
color: #5ec8ff;
|
||||
}
|
||||
|
||||
.plan-main-tabs {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.plan-semi-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.plan-semi-compact {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.plan-semi-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.plan-semi-head .plan-panel-title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.plan-semi-rules {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.plan-semi-rules summary {
|
||||
cursor: pointer;
|
||||
color: #5ec8ff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plan-semi-rules ul {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 1.2em;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.plan-semi-fields {
|
||||
margin-top: 0 !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.plan-semi-fields .field label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.plan-semi-fields .field input,
|
||||
.plan-semi-fields .field select {
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.plan-semi-summary {
|
||||
margin-top: 6px !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.plan-semi-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.plan-semi-btns .btn {
|
||||
min-height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.plan-t-quote {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.plan-t-wrap {
|
||||
overflow-x: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.plan-t-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.plan-t-table th,
|
||||
.plan-t-table td {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plan-t-table th:nth-child(3),
|
||||
.plan-t-table td:nth-child(3),
|
||||
.plan-t-table th:nth-child(4),
|
||||
.plan-t-table td:nth-child(4) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plan-t-table th {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.plan-t-atm {
|
||||
background: rgba(94, 200, 255, 0.08);
|
||||
}
|
||||
|
||||
.plan-t-pick {
|
||||
background: rgba(14, 203, 129, 0.08);
|
||||
}
|
||||
|
||||
.plan-t-k {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.plan-card-compact {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.plan-card-compact .plan-panel-title {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.app-container {
|
||||
padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
Reference in New Issue
Block a user