Show OTM option quotes in option-option mode.
Align session to amplitude Call/Put pair and replace perp/ATM market panels. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,12 +43,20 @@ class OptionPair:
|
||||
strike: float
|
||||
call_inst_id: str
|
||||
put_inst_id: str
|
||||
put_strike: float | None = None # 期期:Put 行权价;None=与 strike 同(ATM)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
put_k = (
|
||||
float(self.put_strike)
|
||||
if self.put_strike is not None
|
||||
else float(self.strike)
|
||||
)
|
||||
return {
|
||||
"expiry_ymd": self.expiry_ymd,
|
||||
"expiry_ms": self.expiry_ms,
|
||||
"strike": self.strike,
|
||||
"call_strike": float(self.strike),
|
||||
"put_strike": put_k,
|
||||
"call_inst_id": self.call_inst_id,
|
||||
"put_inst_id": self.put_inst_id,
|
||||
}
|
||||
|
||||
+210
-11
@@ -63,6 +63,26 @@ def _held_option_inst_id() -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _held_option_legs() -> tuple[str | None, str | None]:
|
||||
"""期期持仓:返回 (call_inst, put_inst);非期期或无仓则 put 为空。"""
|
||||
try:
|
||||
from ..models.db import get_db
|
||||
|
||||
row = get_db().fetchone(
|
||||
"""SELECT status, hedge_mode, option_inst_id, option2_inst_id
|
||||
FROM positions WHERE id=1"""
|
||||
)
|
||||
if not row or row["status"] not in ("open", "half_open"):
|
||||
return None, None
|
||||
call_id = str(row["option_inst_id"] or "").strip() or None
|
||||
put_id = str(row["option2_inst_id"] or "").strip() or None
|
||||
if str(row["hedge_mode"] or "").strip().lower() != "option_option":
|
||||
return call_id, None
|
||||
return call_id, put_id
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def _as_bool_setting(raw: str | None, default: bool) -> bool:
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
@@ -236,6 +256,7 @@ class StrategySession:
|
||||
self.settings = settings or get_settings()
|
||||
self.ex = exchange or get_exchange()
|
||||
self._pair: OptionPair | None = None
|
||||
self._oo_amp: dict[str, Any] | None = None
|
||||
self._refresh_task: asyncio.Task[None] | None = None
|
||||
self._started = False
|
||||
|
||||
@@ -250,9 +271,11 @@ class StrategySession:
|
||||
ids: list[str] = [s.perp_inst_id]
|
||||
if p is not None:
|
||||
ids.extend([p.call_inst_id, p.put_inst_id])
|
||||
held = _held_option_inst_id()
|
||||
held, held2 = _held_option_legs()
|
||||
if held:
|
||||
ids.append(held)
|
||||
if held2:
|
||||
ids.append(held2)
|
||||
# 去重保序
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
@@ -309,19 +332,59 @@ class StrategySession:
|
||||
|
||||
def align_to_held_position(self) -> OptionPair | None:
|
||||
"""有活跃仓时:监控对锁定为持仓合约的到期/行权价。"""
|
||||
held = _held_option_inst_id()
|
||||
call_id, put_id = _held_option_legs()
|
||||
held = call_id or _held_option_inst_id()
|
||||
if not held:
|
||||
return None
|
||||
pair = pair_from_option_inst(held)
|
||||
if pair is None:
|
||||
logger.warning("cannot rebuild pair from held option %s", held)
|
||||
return None
|
||||
mark = self._mark_for_atm() or float(pair.strike)
|
||||
mark = self._mark_for_atm()
|
||||
idx = None
|
||||
try:
|
||||
idx = self.ex.fetch_index(self.settings.index_inst_id)
|
||||
except Exception:
|
||||
pass
|
||||
if put_id and call_id:
|
||||
# 期期:双腿分别钉住
|
||||
try:
|
||||
from ..models.db import get_db
|
||||
|
||||
row = get_db().fetchone(
|
||||
"SELECT strike2 FROM positions WHERE id=1"
|
||||
)
|
||||
except Exception:
|
||||
row = None
|
||||
cpair = pair_from_option_inst(call_id)
|
||||
ppair = pair_from_option_inst(put_id)
|
||||
if cpair is None:
|
||||
logger.warning("cannot rebuild call pair from held %s", call_id)
|
||||
return None
|
||||
put_strike = None
|
||||
if row and row["strike2"] is not None:
|
||||
put_strike = float(row["strike2"])
|
||||
elif ppair is not None:
|
||||
put_strike = float(ppair.strike)
|
||||
pair = OptionPair(
|
||||
expiry_ymd=cpair.expiry_ymd,
|
||||
expiry_ms=cpair.expiry_ms,
|
||||
strike=float(cpair.strike),
|
||||
call_inst_id=call_id,
|
||||
put_inst_id=put_id,
|
||||
put_strike=put_strike,
|
||||
)
|
||||
logger.info(
|
||||
"pin OO watch call=%s put=%s C@%.0f P@%.0f",
|
||||
call_id,
|
||||
put_id,
|
||||
pair.strike,
|
||||
float(put_strike or pair.strike),
|
||||
)
|
||||
return self._apply_pair(
|
||||
pair, mark=float(mark or pair.strike), idx=idx
|
||||
)
|
||||
pair = pair_from_option_inst(held)
|
||||
if pair is None:
|
||||
logger.warning("cannot rebuild pair from held option %s", held)
|
||||
return None
|
||||
mark = mark or float(pair.strike)
|
||||
logger.info(
|
||||
"pin watch to held option %s strike=%.0f expiry=%s",
|
||||
held,
|
||||
@@ -334,6 +397,8 @@ class StrategySession:
|
||||
# 重启/刷新时若仍有仓,绝不切到新 ATM
|
||||
if _has_open_position():
|
||||
return self.align_to_held_position()
|
||||
if _hedge_mode() == "option_option":
|
||||
return self.align_oo_instruments()
|
||||
s = self.settings
|
||||
idx = self.ex.fetch_index(s.index_inst_id)
|
||||
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
|
||||
@@ -376,6 +441,58 @@ class StrategySession:
|
||||
)
|
||||
return self._apply_pair(pair, mark=float(mark), idx=idx)
|
||||
|
||||
def align_oo_instruments(self) -> OptionPair | None:
|
||||
"""期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅不足仍对齐候选)。"""
|
||||
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
||||
from .oo_selection import select_oo_pair
|
||||
|
||||
if _has_open_position():
|
||||
return self.align_to_held_position()
|
||||
s = self.settings
|
||||
amp_pct, amp_hours, min_hours, _min_lev = _oo_settings()
|
||||
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 mark <= 0:
|
||||
raise RuntimeError("无法获取标的标记/指数价格,无法选期期虚值")
|
||||
underlying = float(mark)
|
||||
amp = fetch_amplitude_hl_for_runtime(amp_hours)
|
||||
if amp is None:
|
||||
self._oo_amp = None
|
||||
raise RuntimeError("无法获取振幅 K 线高低点")
|
||||
self._oo_amp = {
|
||||
"high": float(amp.high),
|
||||
"low": float(amp.low),
|
||||
"mid": float(amp.mid),
|
||||
"range_pct": float(amp.range_pct),
|
||||
"hours": float(amp_hours),
|
||||
"min_pct": float(amp_pct),
|
||||
"ok": float(amp.range_pct) + 1e-12 >= float(amp_pct),
|
||||
}
|
||||
contracts = self.ex.list_option_contracts(s.option_inst_family)
|
||||
skip = _skip_expiry_ymds_for_next()
|
||||
picked = select_oo_pair(
|
||||
contracts,
|
||||
spot=underlying,
|
||||
high=float(amp.high),
|
||||
low=float(amp.low),
|
||||
min_hours=float(min_hours),
|
||||
skip_expiry_ymds=skip,
|
||||
)
|
||||
if picked is None:
|
||||
raise RuntimeError(
|
||||
f"未找到剩余≥{min_hours}h 的虚值 Call@高/Put@低"
|
||||
)
|
||||
ymd, ems, ck, pk, call_inst, put_inst = picked
|
||||
pair = OptionPair(
|
||||
expiry_ymd=ymd,
|
||||
expiry_ms=int(ems),
|
||||
strike=float(ck),
|
||||
call_inst_id=call_inst,
|
||||
put_inst_id=put_inst,
|
||||
put_strike=float(pk),
|
||||
)
|
||||
return self._apply_pair(pair, mark=underlying, idx=idx)
|
||||
|
||||
def pick_for_open(self) -> OpenPick | None:
|
||||
if _hedge_mode() == "option_option":
|
||||
return self._pick_for_open_oo()
|
||||
@@ -452,14 +569,24 @@ class StrategySession:
|
||||
min_lev,
|
||||
)
|
||||
return None
|
||||
# 监控用:用 Call 行权价构造假 pair(两腿不同 strike,call/put inst 正确)
|
||||
# 监控用:Call/Put 不同行权价
|
||||
pair = OptionPair(
|
||||
expiry_ymd=ymd,
|
||||
expiry_ms=int(ems),
|
||||
strike=float(ck),
|
||||
call_inst_id=call_inst,
|
||||
put_inst_id=put_inst,
|
||||
put_strike=float(pk),
|
||||
)
|
||||
self._oo_amp = {
|
||||
"high": float(amp.high),
|
||||
"low": float(amp.low),
|
||||
"mid": float(amp.mid),
|
||||
"range_pct": float(amp.range_pct),
|
||||
"hours": float(amp_hours),
|
||||
"min_pct": float(amp_pct),
|
||||
"ok": True,
|
||||
}
|
||||
self._apply_pair(pair, mark=underlying, idx=idx)
|
||||
if hasattr(self.ex, "cache"):
|
||||
from ..exchange.book_cache import BookCache
|
||||
@@ -655,6 +782,8 @@ class StrategySession:
|
||||
return None
|
||||
|
||||
def atm_needs_realign(self, mark_px: float | None = None) -> bool:
|
||||
if _hedge_mode() == "option_option":
|
||||
return self.oo_needs_realign()
|
||||
if self._pair is None:
|
||||
return True
|
||||
min_hours, _, _, _ = _strategy_floats()
|
||||
@@ -679,19 +808,78 @@ class StrategySession:
|
||||
return True
|
||||
return abs(float(self._pair.strike) - float(mark)) >= _ATM_DRIFT_POINTS
|
||||
|
||||
def oo_needs_realign(self) -> bool:
|
||||
if self._pair is None:
|
||||
return True
|
||||
_amp_pct, amp_hours, min_hours, _ = _oo_settings()
|
||||
if (
|
||||
hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms)
|
||||
+ 1e-9
|
||||
< min_hours
|
||||
):
|
||||
return True
|
||||
skip = _skip_expiry_ymds_for_next()
|
||||
if str(self._pair.expiry_ymd or "") in skip:
|
||||
return True
|
||||
try:
|
||||
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
||||
from .oo_selection import select_oo_pair
|
||||
|
||||
mark = self._mark_for_atm()
|
||||
if mark is None or mark <= 0:
|
||||
return False
|
||||
amp = fetch_amplitude_hl_for_runtime(amp_hours)
|
||||
if amp is None:
|
||||
return False
|
||||
self._oo_amp = {
|
||||
"high": float(amp.high),
|
||||
"low": float(amp.low),
|
||||
"mid": float(amp.mid),
|
||||
"range_pct": float(amp.range_pct),
|
||||
"hours": float(amp_hours),
|
||||
"min_pct": float(_amp_pct),
|
||||
"ok": float(amp.range_pct) + 1e-12 >= float(_amp_pct),
|
||||
}
|
||||
contracts = self.ex.list_option_contracts(self.settings.option_inst_family)
|
||||
picked = select_oo_pair(
|
||||
contracts,
|
||||
spot=float(mark),
|
||||
high=float(amp.high),
|
||||
low=float(amp.low),
|
||||
min_hours=float(min_hours),
|
||||
skip_expiry_ymds=skip,
|
||||
)
|
||||
if picked is None:
|
||||
return False
|
||||
_ymd, _ems, _ck, _pk, call_inst, put_inst = picked
|
||||
return (
|
||||
call_inst != self._pair.call_inst_id
|
||||
or put_inst != self._pair.put_inst_id
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("oo_needs_realign failed")
|
||||
return False
|
||||
|
||||
async def ensure_atm_async(self, *, force: bool = False) -> OptionPair | None:
|
||||
if _has_open_position():
|
||||
# 持仓期间:钉住持仓行权价(禁止漂到新 ATM)
|
||||
held = _held_option_inst_id()
|
||||
# 持仓期间:钉住持仓行权价(禁止漂到新 ATM/虚值)
|
||||
call_id, put_id = _held_option_legs()
|
||||
held = call_id or _held_option_inst_id()
|
||||
if held and (
|
||||
self._pair is None
|
||||
or held not in (self._pair.call_inst_id, self._pair.put_inst_id)
|
||||
or (
|
||||
put_id
|
||||
and put_id
|
||||
not in (self._pair.call_inst_id, self._pair.put_inst_id)
|
||||
)
|
||||
):
|
||||
return await asyncio.to_thread(self.align_to_held_position)
|
||||
return self._pair
|
||||
if force or self.atm_needs_realign():
|
||||
logger.info(
|
||||
"ATM realign force=%s old_strike=%s old_exp=%s",
|
||||
"%s realign force=%s old_strike=%s old_exp=%s",
|
||||
"OO" if _hedge_mode() == "option_option" else "ATM",
|
||||
force,
|
||||
self._pair.strike if self._pair else None,
|
||||
self._pair.expiry_ymd if self._pair else None,
|
||||
@@ -706,6 +894,17 @@ class StrategySession:
|
||||
d = self.ex.snapshot_dict(self.settings.perp_inst_id)
|
||||
d["exchange"] = getattr(self.ex, "name", self.settings.exchange)
|
||||
d["perp_inst_id"] = self.settings.perp_inst_id
|
||||
hm = _hedge_mode()
|
||||
d["hedge_mode"] = hm
|
||||
if self._pair is not None:
|
||||
pd = self._pair.to_dict()
|
||||
d["pair"] = pd
|
||||
if self._oo_amp is not None:
|
||||
d["oo_amplitude"] = dict(self._oo_amp)
|
||||
if hm == "option_option":
|
||||
ac = dict(d.get("ask_compare") or {})
|
||||
ac["bias"] = "option_option"
|
||||
d["ask_compare"] = ac
|
||||
return d
|
||||
|
||||
async def _refresh_loop(self) -> None:
|
||||
|
||||
@@ -204,9 +204,21 @@ export type MarketSnapshot = {
|
||||
index_px: number | null;
|
||||
exchange?: string;
|
||||
perp_inst_id?: string;
|
||||
hedge_mode?: "perp_option" | "option_option";
|
||||
oo_amplitude?: {
|
||||
high?: number;
|
||||
low?: number;
|
||||
mid?: number;
|
||||
range_pct?: number;
|
||||
hours?: number;
|
||||
min_pct?: number;
|
||||
ok?: boolean;
|
||||
} | null;
|
||||
pair: {
|
||||
expiry_ymd: string;
|
||||
strike: number;
|
||||
call_strike?: number;
|
||||
put_strike?: number;
|
||||
call_inst_id: string;
|
||||
put_inst_id: string;
|
||||
} | null;
|
||||
|
||||
+158
-59
@@ -187,6 +187,7 @@ export default function PlanPage() {
|
||||
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;
|
||||
@@ -851,6 +852,11 @@ export default function PlanPage() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : isOo ? (
|
||||
<>
|
||||
<div className="pos-empty">Call 持仓 · 暂无</div>
|
||||
<div className="pos-empty">Put 持仓 · 暂无</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="pos-empty">永续持仓 · 暂无</div>
|
||||
@@ -861,43 +867,102 @@ export default function PlanPage() {
|
||||
</div>
|
||||
|
||||
<div className="plan-market">
|
||||
<div className="card">
|
||||
<h3 className="plan-panel-title">永续行情</h3>
|
||||
<div className="kv">
|
||||
<span>买一</span>
|
||||
<span className="mono">{fmtExPx("perp", snap?.perp?.bid)}</span>
|
||||
{isOo ? (
|
||||
<div className="card">
|
||||
<h3 className="plan-panel-title">指数 / 振幅</h3>
|
||||
<div className="kv">
|
||||
<span>指数</span>
|
||||
<span className="mono">{fmtExPx("index", snap?.index_px)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>
|
||||
高点
|
||||
{snap?.oo_amplitude?.hours != null
|
||||
? `(${fmt(snap.oo_amplitude.hours, 0)}h)`
|
||||
: ""}
|
||||
</span>
|
||||
<span className="mono">
|
||||
{fmtExPx("index", snap?.oo_amplitude?.high)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>低点</span>
|
||||
<span className="mono">
|
||||
{fmtExPx("index", snap?.oo_amplitude?.low)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>振幅</span>
|
||||
<span className="mono">
|
||||
{snap?.oo_amplitude?.range_pct != null
|
||||
? `${fmt(snap.oo_amplitude.range_pct, 2)}%`
|
||||
: "—"}
|
||||
{snap?.oo_amplitude?.min_pct != null
|
||||
? ` · 门限≥${fmt(snap.oo_amplitude.min_pct, 1)}%`
|
||||
: ""}
|
||||
{snap?.oo_amplitude?.ok === false
|
||||
? " · 不足"
|
||||
: snap?.oo_amplitude?.ok === true
|
||||
? " · 达标"
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>卖一</span>
|
||||
<span className="mono">{fmtExPx("perp", snap?.perp?.ask)}</span>
|
||||
) : (
|
||||
<div className="card">
|
||||
<h3 className="plan-panel-title">永续行情</h3>
|
||||
<div className="kv">
|
||||
<span>买一</span>
|
||||
<span className="mono">{fmtExPx("perp", snap?.perp?.bid)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>卖一</span>
|
||||
<span className="mono">{fmtExPx("perp", snap?.perp?.ask)}</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>市价</span>
|
||||
<span className="mono">
|
||||
{fmtExPx(
|
||||
"perp",
|
||||
snap?.perp?.mark_px ??
|
||||
(snap?.perp?.bid != null && snap?.perp?.ask != null
|
||||
? (snap.perp.bid + snap.perp.ask) / 2
|
||||
: null),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>指数</span>
|
||||
<span className="mono">{fmtExPx("index", snap?.index_px)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>市价</span>
|
||||
<span className="mono">
|
||||
{fmtExPx(
|
||||
"perp",
|
||||
snap?.perp?.mark_px ??
|
||||
(snap?.perp?.bid != null && snap?.perp?.ask != null
|
||||
? (snap.perp.bid + snap.perp.ask) / 2
|
||||
: null),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>指数</span>
|
||||
<span className="mono">{fmtExPx("index", snap?.index_px)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="card">
|
||||
<h3 className="plan-panel-title">
|
||||
{open ? "持仓期权" : "期权 ATM"}
|
||||
{snap?.pair
|
||||
? ` @ ${
|
||||
snap.pair.strike != null
|
||||
? Math.round(Number(snap.pair.strike))
|
||||
: "—"
|
||||
}`
|
||||
: ""}
|
||||
{isOo
|
||||
? open
|
||||
? "持仓虚值"
|
||||
: "期期虚值"
|
||||
: open
|
||||
? "持仓期权"
|
||||
: "期权 ATM"}
|
||||
{isOo && snap?.pair
|
||||
? ` · C@${Math.round(
|
||||
Number(
|
||||
snap.pair.call_strike ?? snap.pair.strike ?? 0,
|
||||
),
|
||||
)} / P@${Math.round(
|
||||
Number(
|
||||
snap.pair.put_strike ?? snap.pair.strike ?? 0,
|
||||
),
|
||||
)}`
|
||||
: snap?.pair
|
||||
? ` @ ${
|
||||
snap.pair.strike != null
|
||||
? Math.round(Number(snap.pair.strike))
|
||||
: "—"
|
||||
}`
|
||||
: ""}
|
||||
</h3>
|
||||
{(() => {
|
||||
const under =
|
||||
@@ -921,17 +986,29 @@ export default function PlanPage() {
|
||||
: open && heldSide === "put"
|
||||
? putLev
|
||||
: null;
|
||||
const callK = snap?.pair?.call_strike ?? snap?.pair?.strike;
|
||||
const putK = snap?.pair?.put_strike ?? snap?.pair?.strike;
|
||||
return (
|
||||
<>
|
||||
<div className="kv">
|
||||
<span>Call {sideLabel}</span>
|
||||
<span>
|
||||
Call {sideLabel}
|
||||
{isOo && callK != null
|
||||
? ` @${Math.round(Number(callK))}`
|
||||
: ""}
|
||||
</span>
|
||||
<span className="mono">
|
||||
{fmtTopEx("option", callPx, callSz)}
|
||||
{!open ? ` · 杠杆 ${callLev}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<span>Put {sideLabel}</span>
|
||||
<span>
|
||||
Put {sideLabel}
|
||||
{isOo && putK != null
|
||||
? ` @${Math.round(Number(putK))}`
|
||||
: ""}
|
||||
</span>
|
||||
<span className="mono">
|
||||
{fmtTopEx("option", putPx, putSz)}
|
||||
{!open ? ` · 杠杆 ${putLev}` : ""}
|
||||
@@ -940,7 +1017,7 @@ export default function PlanPage() {
|
||||
<div className="kv">
|
||||
<span>实际杠杆</span>
|
||||
<span className="mono">
|
||||
{open
|
||||
{open && !isOo
|
||||
? heldLev ||
|
||||
(heldSide
|
||||
? "—"
|
||||
@@ -951,29 +1028,51 @@ export default function PlanPage() {
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<div className="kv">
|
||||
<span>距现价</span>
|
||||
<span className="mono">
|
||||
{(() => {
|
||||
const strike = snap?.pair?.strike;
|
||||
const px =
|
||||
snap?.index_px ??
|
||||
(snap?.perp?.bid != null && snap?.perp?.ask != null
|
||||
? (snap.perp.bid + snap.perp.ask) / 2
|
||||
: null);
|
||||
if (strike == null || px == null) return "—";
|
||||
const abs = Math.abs(strike - px);
|
||||
const sign = strike - px > 0 ? "+" : "";
|
||||
const delta = `${sign}${(strike - px).toFixed(0)}`;
|
||||
if (!plan?.atm_open_offset_enabled) {
|
||||
return `${delta} · 偏差限制关`;
|
||||
}
|
||||
const lim = plan?.max_atm_open_offset ?? 3;
|
||||
const ok = abs <= lim + 1e-9;
|
||||
return `${delta} · 开仓${ok ? "可" : "不可"}(|Δ|≤${lim})`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
{isOo ? (
|
||||
<div className="kv">
|
||||
<span>相对现价</span>
|
||||
<span className="mono">
|
||||
{(() => {
|
||||
const px = snap?.index_px;
|
||||
const ck = snap?.pair?.call_strike ?? snap?.pair?.strike;
|
||||
const pk = snap?.pair?.put_strike;
|
||||
if (px == null || ck == null) return "—";
|
||||
const cOff = Number(ck) - Number(px);
|
||||
const pOff =
|
||||
pk != null ? Number(pk) - Number(px) : null;
|
||||
const fmtOff = (n: number) =>
|
||||
`${n > 0 ? "+" : ""}${n.toFixed(0)}`;
|
||||
return pOff != null
|
||||
? `C ${fmtOff(cOff)} · P ${fmtOff(pOff)}`
|
||||
: `C ${fmtOff(cOff)}`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="kv">
|
||||
<span>距现价</span>
|
||||
<span className="mono">
|
||||
{(() => {
|
||||
const strike = snap?.pair?.strike;
|
||||
const px =
|
||||
snap?.index_px ??
|
||||
(snap?.perp?.bid != null && snap?.perp?.ask != null
|
||||
? (snap.perp.bid + snap.perp.ask) / 2
|
||||
: null);
|
||||
if (strike == null || px == null) return "—";
|
||||
const abs = Math.abs(strike - px);
|
||||
const sign = strike - px > 0 ? "+" : "";
|
||||
const delta = `${sign}${(strike - px).toFixed(0)}`;
|
||||
if (!plan?.atm_open_offset_enabled) {
|
||||
return `${delta} · 偏差限制关`;
|
||||
}
|
||||
const lim = plan?.max_atm_open_offset ?? 3;
|
||||
const ok = abs <= lim + 1e-9;
|
||||
return `${delta} · 开仓${ok ? "可" : "不可"}(|Δ|≤${lim})`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="kv">
|
||||
<span>到期</span>
|
||||
<span className="mono">{snap?.pair?.expiry_ymd || "—"}</span>
|
||||
|
||||
Reference in New Issue
Block a user