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 typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from ..market import get_gateway
|
from ..market import get_gateway
|
||||||
from .auth import require_user
|
from .auth import require_user
|
||||||
@@ -18,6 +18,19 @@ async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
|
|||||||
return snap
|
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")
|
@router.post("/realign")
|
||||||
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
|
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||||
"""手动重对齐次日到期 ATM 合约(运维/调试用)。"""
|
"""手动重对齐次日到期 ATM 合约(运维/调试用)。"""
|
||||||
|
|||||||
@@ -1058,6 +1058,99 @@ class StrategySession:
|
|||||||
d["ask_compare"] = ac
|
d["ask_compare"] = ac
|
||||||
return d
|
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:
|
async def _refresh_loop(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(30 if self._pair is not None else 10)
|
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 [semiPerpU, setSemiPerpU] = useState(1);
|
||||||
const [semiOptU, setSemiOptU] = useState(4);
|
const [semiOptU, setSemiOptU] = useState(4);
|
||||||
const [semiDirty, setSemiDirty] = useState(false);
|
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 = () => ({
|
const semiParamsBody = () => ({
|
||||||
semi_view_side: semiView,
|
semi_view_side: semiView,
|
||||||
@@ -149,12 +166,26 @@ export default function PlanPage() {
|
|||||||
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
|
||||||
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
setSemiMinH(Number(p.semi_min_option_hours ?? 30));
|
||||||
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
|
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
|
||||||
const m = p.semi_moneyness;
|
const mny = p.semi_moneyness;
|
||||||
setSemiMny(m === "itm" || m === "atm" || m === "otm" ? m : "otm");
|
setSemiMny(mny === "itm" || mny === "atm" || mny === "otm" ? mny : "otm");
|
||||||
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
|
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
|
||||||
setSemiPerpU(Number(p.semi_perp_unit ?? 1));
|
setSemiPerpU(Number(p.semi_perp_unit ?? 1));
|
||||||
setSemiOptU(Number(p.semi_option_unit ?? 4));
|
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("");
|
setErr("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(e instanceof Error ? e.message : String(e));
|
setErr(e instanceof Error ? e.message : String(e));
|
||||||
@@ -276,6 +307,8 @@ export default function PlanPage() {
|
|||||||
snap?.hedge_mode === "option_option" ||
|
snap?.hedge_mode === "option_option" ||
|
||||||
pos?.hedge_mode === "option_option" ||
|
pos?.hedge_mode === "option_option" ||
|
||||||
!!pos?.option2_inst_id;
|
!!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 exitMode = plan?.exit_mode ?? "fixed_usdt";
|
||||||
const riskBased =
|
const riskBased =
|
||||||
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
plan?.risk_based === true || plan?.sizing_mode === "risk_based";
|
||||||
@@ -573,209 +606,351 @@ export default function PlanPage() {
|
|||||||
{busy ? <span className="meta">{busy}…</span> : null}
|
{busy ? <span className="meta">{busy}…</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{plan?.semi_auto_enabled && !isOo ? (
|
{semiOn ? (
|
||||||
<section className="card plan-semi-card" aria-label="半自动本单">
|
<div className="tabs plan-main-tabs" role="tablist" aria-label="计划页">
|
||||||
<h3 className="plan-panel-title">半自动 · 本单</h3>
|
<button
|
||||||
<p className="meta" style={{ marginTop: 0 }}>
|
type="button"
|
||||||
人工定方向/行权类型/配比 → 授权后机器盯开平 → 平完停。虚值杠杆门≥180(默认200)。顺向:开仓指数±点数且净利>0;逆向:净利≥基数×k。
|
role="tab"
|
||||||
</p>
|
className={planTab === "semi" ? "tab active" : "tab"}
|
||||||
<div className="settings-fields" style={{ marginTop: 8 }}>
|
aria-selected={planTab === "semi"}
|
||||||
<div className="field">
|
onClick={() => setPlanTab("semi")}
|
||||||
<label htmlFor="semiView">方向(看法)</label>
|
>
|
||||||
<select
|
半自动单
|
||||||
id="semiView"
|
</button>
|
||||||
className="mono"
|
<button
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
type="button"
|
||||||
value={semiView}
|
role="tab"
|
||||||
onChange={(e) => {
|
className={planTab === "monitor" ? "tab active" : "tab"}
|
||||||
setSemiView(e.target.value === "short" ? "short" : "long");
|
aria-selected={planTab === "monitor"}
|
||||||
setSemiDirty(true);
|
onClick={() => setPlanTab("monitor")}
|
||||||
}}
|
>
|
||||||
>
|
持仓监控
|
||||||
<option value="long">多 · 买 Call + 永续空</option>
|
</button>
|
||||||
<option value="short">空 · 买 Put + 永续多</option>
|
</div>
|
||||||
</select>
|
) : 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>
|
||||||
<div className="field">
|
<details className="plan-semi-rules">
|
||||||
<label htmlFor="semiMny">行权类型</label>
|
<summary>规则说明</summary>
|
||||||
<select
|
<ul>
|
||||||
id="semiMny"
|
<li>人工定方向、行权类型、永续:期权配比与出场 →「授权开下一单」后机器盯选约/开/平。</li>
|
||||||
className="mono"
|
<li>平仓后停在等待授权,不自动连开;与全自动循环无关。</li>
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
<li>虚值:偏离 ≤ 设定点数;杠杆门 ≥180(默认 200)。实值/平值用本单最低杠杆。</li>
|
||||||
value={semiMny}
|
<li>顺向:开仓指数 ± 波动点数,且组合净利 > 0 → 双腿全平(先期权后永续)。</li>
|
||||||
onChange={(e) => {
|
<li>逆向兑现:组合净利 ≥ 净利基数 × k → 全平。</li>
|
||||||
const v = e.target.value;
|
<li>以损定仓时配比为单位再乘 k;手动仓按配比直接开。</li>
|
||||||
setSemiMny(
|
</ul>
|
||||||
v === "itm" || v === "atm" || v === "otm" ? v : "otm",
|
</details>
|
||||||
);
|
<div className="settings-fields plan-semi-fields">
|
||||||
setSemiDirty(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="itm">实值(含平值)</option>
|
|
||||||
<option value="atm">平值</option>
|
|
||||||
<option value="otm">虚值</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{semiMny === "otm" ? (
|
|
||||||
<div className="field">
|
<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
|
<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"
|
className="mono"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
step="1"
|
||||||
min="1"
|
min="1"
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
value={semiOtmOff}
|
value={semiMove}
|
||||||
onChange={(e) => {
|
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);
|
setSemiDirty(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div className="field">
|
<div className="mono meta plan-semi-summary">
|
||||||
<label htmlFor="semiOptU">期权名义单位(配比)</label>
|
{(() => {
|
||||||
<input
|
const idx = snap?.index_px;
|
||||||
id="semiOptU"
|
if (idx == null || !Number.isFinite(Number(idx))) {
|
||||||
className="mono"
|
return `净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`;
|
||||||
type="number"
|
}
|
||||||
step="0.1"
|
const n = Number(idx);
|
||||||
min="0.01"
|
const tgt =
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
semiView === "long"
|
||||||
value={semiOptU}
|
? n + Number(semiMove)
|
||||||
onChange={(e) => {
|
: n - Number(semiMove);
|
||||||
setSemiOptU(Number(e.target.value));
|
return `${fmtExPx("index", n)}→${fmtExPx("index", tgt)} · 净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`;
|
||||||
setSemiDirty(true);
|
})()}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="plan-semi-btns">
|
||||||
<label htmlFor="semiMove">期权出场 · 波动点数</label>
|
<button
|
||||||
<input
|
className="btn ghost"
|
||||||
id="semiMove"
|
type="button"
|
||||||
className="mono"
|
disabled={open || !!busy || !semiDirty}
|
||||||
type="number"
|
onClick={() => void saveSemiParams()}
|
||||||
step="1"
|
>
|
||||||
min="1"
|
保存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
type="button"
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
disabled={open || !!busy || !!plan.semi_armed}
|
||||||
value={semiMove}
|
onClick={() => void armSemi(true)}
|
||||||
onChange={(e) => {
|
>
|
||||||
setSemiMove(Number(e.target.value));
|
授权开下一单
|
||||||
setSemiDirty(true);
|
</button>
|
||||||
}}
|
<button
|
||||||
/>
|
className="btn ghost"
|
||||||
|
type="button"
|
||||||
|
disabled={open || !!busy || !plan.semi_armed}
|
||||||
|
onClick={() => void armSemi(false)}
|
||||||
|
>
|
||||||
|
取消授权
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
</section>
|
||||||
<label htmlFor="semiExitU">净利出场基数(U,×k)</label>
|
|
||||||
<input
|
<section className="card plan-t-quote" aria-label="T型报价">
|
||||||
id="semiExitU"
|
<div className="plan-semi-head">
|
||||||
className="mono"
|
<h3 className="plan-panel-title">T 型报价 · 实值 / 平值 / 虚值</h3>
|
||||||
type="number"
|
<span className="mono meta">
|
||||||
step="0.1"
|
{ladder?.expiry_ymd
|
||||||
min="0.1"
|
? `到期 ${ladder.expiry_ymd}`
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
: "—"}
|
||||||
value={semiExitU}
|
{ladder?.atm_strike != null
|
||||||
onChange={(e) => {
|
? ` · ATM ${Math.round(ladder.atm_strike)}`
|
||||||
setSemiExitU(Number(e.target.value));
|
: ""}
|
||||||
setSemiDirty(true);
|
{ladder?.index_px != null
|
||||||
}}
|
? ` · 指数 ${fmtExPx("index", ladder.index_px)}`
|
||||||
/>
|
: ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
{!ladder?.ok ? (
|
||||||
<label htmlFor="semiMinH">选约 · 最短剩余(h)</label>
|
<p className="meta">{ladder?.detail || "暂无报价链"}</p>
|
||||||
<input
|
) : (
|
||||||
id="semiMinH"
|
<div className="plan-t-wrap">
|
||||||
className="mono"
|
<table className="plan-t-table">
|
||||||
type="number"
|
<thead>
|
||||||
step="1"
|
<tr>
|
||||||
min="1"
|
<th>Call卖一</th>
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
<th>Call杠杆</th>
|
||||||
value={semiMinH}
|
<th>行权价</th>
|
||||||
onChange={(e) => {
|
<th>类型</th>
|
||||||
setSemiMinH(Number(e.target.value));
|
<th>Put杠杆</th>
|
||||||
setSemiDirty(true);
|
<th>Put卖一</th>
|
||||||
}}
|
</tr>
|
||||||
/>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
<div className="field">
|
{[...(ladder.rows || [])]
|
||||||
<label htmlFor="semiMinLev">选约 · 最低杠杆</label>
|
.slice()
|
||||||
<input
|
.reverse()
|
||||||
id="semiMinLev"
|
.map((r) => {
|
||||||
className="mono"
|
const tagZh =
|
||||||
type="number"
|
r.tag === "atm"
|
||||||
step="1"
|
? "平值"
|
||||||
min="1"
|
: r.tag === "otm_call"
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
? "Call虚/Put实"
|
||||||
value={semiMinLev}
|
: "Put虚/Call实";
|
||||||
onChange={(e) => {
|
const hi =
|
||||||
setSemiMinLev(Number(e.target.value));
|
(semiMny === "otm" &&
|
||||||
setSemiDirty(true);
|
((semiView === "long" && r.tag === "otm_call") ||
|
||||||
}}
|
(semiView === "short" && r.tag === "otm_put")) &&
|
||||||
/>
|
Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
|
||||||
</div>
|
(semiMny === "atm" && r.tag === "atm") ||
|
||||||
</div>
|
(semiMny === "itm" &&
|
||||||
<div className="mono meta" style={{ marginTop: 8 }}>
|
((semiView === "long" &&
|
||||||
{(() => {
|
(r.tag === "otm_put" || r.tag === "atm")) ||
|
||||||
const idx = snap?.index_px;
|
(semiView === "short" &&
|
||||||
if (idx == null || !Number.isFinite(Number(idx))) {
|
(r.tag === "otm_call" || r.tag === "atm"))));
|
||||||
return `目标净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · 指数 —`;
|
return (
|
||||||
}
|
<tr
|
||||||
const n = Number(idx);
|
key={r.strike}
|
||||||
const tgt =
|
className={
|
||||||
semiView === "long" ? n + Number(semiMove) : n - Number(semiMove);
|
r.tag === "atm"
|
||||||
const mnyZh =
|
? "plan-t-atm"
|
||||||
semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值";
|
: hi
|
||||||
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · ${mnyZh}${
|
? "plan-t-pick"
|
||||||
semiMny === "otm" ? `≤${fmt(semiOtmOff, 0)}点` : ""
|
: undefined
|
||||||
} · 配比 ${fmt(semiPerpU, 2)}:${fmt(semiOptU, 2)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`;
|
}
|
||||||
})()}
|
>
|
||||||
</div>
|
<td className="mono">
|
||||||
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}>
|
{r.call_ask != null
|
||||||
<button
|
? fmtExPx("option", r.call_ask)
|
||||||
className="btn ghost"
|
: "—"}
|
||||||
type="button"
|
</td>
|
||||||
disabled={open || !!busy || !semiDirty}
|
<td className="mono">
|
||||||
onClick={() => void saveSemiParams()}
|
{r.call_lev != null
|
||||||
>
|
? `${r.call_lev.toFixed(0)}x`
|
||||||
保存本单参数
|
: "—"}
|
||||||
</button>
|
</td>
|
||||||
<button
|
<td className="mono plan-t-k">
|
||||||
className="btn"
|
{Math.round(r.strike)}
|
||||||
type="button"
|
<span className="meta">
|
||||||
disabled={open || !!busy || !!plan.semi_armed}
|
{" "}
|
||||||
onClick={() => void armSemi(true)}
|
({r.offset >= 0 ? "+" : ""}
|
||||||
>
|
{fmt(r.offset, 0)})
|
||||||
授权开下一单
|
</span>
|
||||||
</button>
|
</td>
|
||||||
<button
|
<td>{tagZh}</td>
|
||||||
className="btn ghost"
|
<td className="mono">
|
||||||
type="button"
|
{r.put_lev != null
|
||||||
disabled={open || !!busy || !plan.semi_armed}
|
? `${r.put_lev.toFixed(0)}x`
|
||||||
onClick={() => void armSemi(false)}
|
: "—"}
|
||||||
>
|
</td>
|
||||||
取消授权
|
<td className="mono">
|
||||||
</button>
|
{r.put_ask != null
|
||||||
</div>
|
? fmtExPx("option", r.put_ask)
|
||||||
</section>
|
: "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{!semiOn || planTab === "monitor" ? (
|
||||||
<div className="plan-shell">
|
<div className="plan-shell">
|
||||||
<div className="plan-board">
|
<div className="plan-board">
|
||||||
<div className="card plan-strategy">
|
<div className="card plan-strategy">
|
||||||
@@ -798,7 +973,18 @@ export default function PlanPage() {
|
|||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>模式</span>
|
<span>模式</span>
|
||||||
<span className="mono">
|
<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>
|
<span className="status-running">{sizingModeLabel}</span>
|
||||||
) : (
|
) : (
|
||||||
sizingModeLabel
|
sizingModeLabel
|
||||||
@@ -808,7 +994,17 @@ export default function PlanPage() {
|
|||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>选约条件</span>
|
<span>选约条件</span>
|
||||||
<span className="mono">
|
<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
|
ooSelectLabel
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -1326,7 +1522,8 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="plan-market">
|
<div className="plan-market">
|
||||||
<div className="card">
|
{showAmpCard ? (
|
||||||
|
<div className="card plan-card-compact">
|
||||||
<h3 className="plan-panel-title">指数 / 振幅</h3>
|
<h3 className="plan-panel-title">指数 / 振幅</h3>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>指数</span>
|
<span>指数</span>
|
||||||
@@ -1375,6 +1572,7 @@ export default function PlanPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
{!isOo ? (
|
{!isOo ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3 className="plan-panel-title">永续行情</h3>
|
<h3 className="plan-panel-title">永续行情</h3>
|
||||||
@@ -1617,6 +1815,7 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,6 +468,144 @@ input {
|
|||||||
color: #5ec8ff;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px));
|
padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px));
|
||||||
|
|||||||
Reference in New Issue
Block a user