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
|
strike: float
|
||||||
call_inst_id: str
|
call_inst_id: str
|
||||||
put_inst_id: str
|
put_inst_id: str
|
||||||
|
put_strike: float | None = None # 期期:Put 行权价;None=与 strike 同(ATM)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
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 {
|
return {
|
||||||
"expiry_ymd": self.expiry_ymd,
|
"expiry_ymd": self.expiry_ymd,
|
||||||
"expiry_ms": self.expiry_ms,
|
"expiry_ms": self.expiry_ms,
|
||||||
"strike": self.strike,
|
"strike": self.strike,
|
||||||
|
"call_strike": float(self.strike),
|
||||||
|
"put_strike": put_k,
|
||||||
"call_inst_id": self.call_inst_id,
|
"call_inst_id": self.call_inst_id,
|
||||||
"put_inst_id": self.put_inst_id,
|
"put_inst_id": self.put_inst_id,
|
||||||
}
|
}
|
||||||
|
|||||||
+210
-11
@@ -63,6 +63,26 @@ def _held_option_inst_id() -> str | None:
|
|||||||
return 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:
|
def _as_bool_setting(raw: str | None, default: bool) -> bool:
|
||||||
if raw is None or raw == "":
|
if raw is None or raw == "":
|
||||||
return default
|
return default
|
||||||
@@ -236,6 +256,7 @@ class StrategySession:
|
|||||||
self.settings = settings or get_settings()
|
self.settings = settings or get_settings()
|
||||||
self.ex = exchange or get_exchange()
|
self.ex = exchange or get_exchange()
|
||||||
self._pair: OptionPair | None = None
|
self._pair: OptionPair | None = None
|
||||||
|
self._oo_amp: dict[str, Any] | None = None
|
||||||
self._refresh_task: asyncio.Task[None] | None = None
|
self._refresh_task: asyncio.Task[None] | None = None
|
||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
@@ -250,9 +271,11 @@ class StrategySession:
|
|||||||
ids: list[str] = [s.perp_inst_id]
|
ids: list[str] = [s.perp_inst_id]
|
||||||
if p is not None:
|
if p is not None:
|
||||||
ids.extend([p.call_inst_id, p.put_inst_id])
|
ids.extend([p.call_inst_id, p.put_inst_id])
|
||||||
held = _held_option_inst_id()
|
held, held2 = _held_option_legs()
|
||||||
if held:
|
if held:
|
||||||
ids.append(held)
|
ids.append(held)
|
||||||
|
if held2:
|
||||||
|
ids.append(held2)
|
||||||
# 去重保序
|
# 去重保序
|
||||||
out: list[str] = []
|
out: list[str] = []
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
@@ -309,19 +332,59 @@ class StrategySession:
|
|||||||
|
|
||||||
def align_to_held_position(self) -> OptionPair | None:
|
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:
|
if not held:
|
||||||
return None
|
return None
|
||||||
pair = pair_from_option_inst(held)
|
mark = self._mark_for_atm()
|
||||||
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)
|
|
||||||
idx = None
|
idx = None
|
||||||
try:
|
try:
|
||||||
idx = self.ex.fetch_index(self.settings.index_inst_id)
|
idx = self.ex.fetch_index(self.settings.index_inst_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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(
|
logger.info(
|
||||||
"pin watch to held option %s strike=%.0f expiry=%s",
|
"pin watch to held option %s strike=%.0f expiry=%s",
|
||||||
held,
|
held,
|
||||||
@@ -334,6 +397,8 @@ class StrategySession:
|
|||||||
# 重启/刷新时若仍有仓,绝不切到新 ATM
|
# 重启/刷新时若仍有仓,绝不切到新 ATM
|
||||||
if _has_open_position():
|
if _has_open_position():
|
||||||
return self.align_to_held_position()
|
return self.align_to_held_position()
|
||||||
|
if _hedge_mode() == "option_option":
|
||||||
|
return self.align_oo_instruments()
|
||||||
s = self.settings
|
s = self.settings
|
||||||
idx = self.ex.fetch_index(s.index_inst_id)
|
idx = self.ex.fetch_index(s.index_inst_id)
|
||||||
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
|
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)
|
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:
|
def pick_for_open(self) -> OpenPick | None:
|
||||||
if _hedge_mode() == "option_option":
|
if _hedge_mode() == "option_option":
|
||||||
return self._pick_for_open_oo()
|
return self._pick_for_open_oo()
|
||||||
@@ -452,14 +569,24 @@ class StrategySession:
|
|||||||
min_lev,
|
min_lev,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
# 监控用:用 Call 行权价构造假 pair(两腿不同 strike,call/put inst 正确)
|
# 监控用:Call/Put 不同行权价
|
||||||
pair = OptionPair(
|
pair = OptionPair(
|
||||||
expiry_ymd=ymd,
|
expiry_ymd=ymd,
|
||||||
expiry_ms=int(ems),
|
expiry_ms=int(ems),
|
||||||
strike=float(ck),
|
strike=float(ck),
|
||||||
call_inst_id=call_inst,
|
call_inst_id=call_inst,
|
||||||
put_inst_id=put_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)
|
self._apply_pair(pair, mark=underlying, idx=idx)
|
||||||
if hasattr(self.ex, "cache"):
|
if hasattr(self.ex, "cache"):
|
||||||
from ..exchange.book_cache import BookCache
|
from ..exchange.book_cache import BookCache
|
||||||
@@ -655,6 +782,8 @@ class StrategySession:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def atm_needs_realign(self, mark_px: float | None = None) -> bool:
|
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:
|
if self._pair is None:
|
||||||
return True
|
return True
|
||||||
min_hours, _, _, _ = _strategy_floats()
|
min_hours, _, _, _ = _strategy_floats()
|
||||||
@@ -679,19 +808,78 @@ class StrategySession:
|
|||||||
return True
|
return True
|
||||||
return abs(float(self._pair.strike) - float(mark)) >= _ATM_DRIFT_POINTS
|
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:
|
async def ensure_atm_async(self, *, force: bool = False) -> OptionPair | None:
|
||||||
if _has_open_position():
|
if _has_open_position():
|
||||||
# 持仓期间:钉住持仓行权价(禁止漂到新 ATM)
|
# 持仓期间:钉住持仓行权价(禁止漂到新 ATM/虚值)
|
||||||
held = _held_option_inst_id()
|
call_id, put_id = _held_option_legs()
|
||||||
|
held = call_id or _held_option_inst_id()
|
||||||
if held and (
|
if held and (
|
||||||
self._pair is None
|
self._pair is None
|
||||||
or held not in (self._pair.call_inst_id, self._pair.put_inst_id)
|
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 await asyncio.to_thread(self.align_to_held_position)
|
||||||
return self._pair
|
return self._pair
|
||||||
if force or self.atm_needs_realign():
|
if force or self.atm_needs_realign():
|
||||||
logger.info(
|
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,
|
force,
|
||||||
self._pair.strike if self._pair else None,
|
self._pair.strike if self._pair else None,
|
||||||
self._pair.expiry_ymd 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 = self.ex.snapshot_dict(self.settings.perp_inst_id)
|
||||||
d["exchange"] = getattr(self.ex, "name", self.settings.exchange)
|
d["exchange"] = getattr(self.ex, "name", self.settings.exchange)
|
||||||
d["perp_inst_id"] = self.settings.perp_inst_id
|
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
|
return d
|
||||||
|
|
||||||
async def _refresh_loop(self) -> None:
|
async def _refresh_loop(self) -> None:
|
||||||
|
|||||||
@@ -204,9 +204,21 @@ export type MarketSnapshot = {
|
|||||||
index_px: number | null;
|
index_px: number | null;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
perp_inst_id?: 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: {
|
pair: {
|
||||||
expiry_ymd: string;
|
expiry_ymd: string;
|
||||||
strike: number;
|
strike: number;
|
||||||
|
call_strike?: number;
|
||||||
|
put_strike?: number;
|
||||||
call_inst_id: string;
|
call_inst_id: string;
|
||||||
put_inst_id: string;
|
put_inst_id: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|||||||
+158
-59
@@ -187,6 +187,7 @@ export default function PlanPage() {
|
|||||||
const pos = plan?.position;
|
const pos = plan?.position;
|
||||||
const isOo =
|
const isOo =
|
||||||
plan?.hedge_mode === "option_option" ||
|
plan?.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 open = !!pos?.has_position;
|
const open = !!pos?.has_position;
|
||||||
@@ -851,6 +852,11 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : isOo ? (
|
||||||
|
<>
|
||||||
|
<div className="pos-empty">Call 持仓 · 暂无</div>
|
||||||
|
<div className="pos-empty">Put 持仓 · 暂无</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="pos-empty">永续持仓 · 暂无</div>
|
<div className="pos-empty">永续持仓 · 暂无</div>
|
||||||
@@ -861,43 +867,102 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="plan-market">
|
<div className="plan-market">
|
||||||
<div className="card">
|
{isOo ? (
|
||||||
<h3 className="plan-panel-title">永续行情</h3>
|
<div className="card">
|
||||||
<div className="kv">
|
<h3 className="plan-panel-title">指数 / 振幅</h3>
|
||||||
<span>买一</span>
|
<div className="kv">
|
||||||
<span className="mono">{fmtExPx("perp", snap?.perp?.bid)}</span>
|
<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>
|
||||||
<div className="kv">
|
) : (
|
||||||
<span>卖一</span>
|
<div className="card">
|
||||||
<span className="mono">{fmtExPx("perp", snap?.perp?.ask)}</span>
|
<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>
|
||||||
<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">
|
<div className="card">
|
||||||
<h3 className="plan-panel-title">
|
<h3 className="plan-panel-title">
|
||||||
{open ? "持仓期权" : "期权 ATM"}
|
{isOo
|
||||||
{snap?.pair
|
? open
|
||||||
? ` @ ${
|
? "持仓虚值"
|
||||||
snap.pair.strike != null
|
: "期期虚值"
|
||||||
? Math.round(Number(snap.pair.strike))
|
: 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>
|
</h3>
|
||||||
{(() => {
|
{(() => {
|
||||||
const under =
|
const under =
|
||||||
@@ -921,17 +986,29 @@ export default function PlanPage() {
|
|||||||
: open && heldSide === "put"
|
: open && heldSide === "put"
|
||||||
? putLev
|
? putLev
|
||||||
: null;
|
: null;
|
||||||
|
const callK = snap?.pair?.call_strike ?? snap?.pair?.strike;
|
||||||
|
const putK = snap?.pair?.put_strike ?? snap?.pair?.strike;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>Call {sideLabel}</span>
|
<span>
|
||||||
|
Call {sideLabel}
|
||||||
|
{isOo && callK != null
|
||||||
|
? ` @${Math.round(Number(callK))}`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
<span className="mono">
|
<span className="mono">
|
||||||
{fmtTopEx("option", callPx, callSz)}
|
{fmtTopEx("option", callPx, callSz)}
|
||||||
{!open ? ` · 杠杆 ${callLev}` : ""}
|
{!open ? ` · 杠杆 ${callLev}` : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>Put {sideLabel}</span>
|
<span>
|
||||||
|
Put {sideLabel}
|
||||||
|
{isOo && putK != null
|
||||||
|
? ` @${Math.round(Number(putK))}`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
<span className="mono">
|
<span className="mono">
|
||||||
{fmtTopEx("option", putPx, putSz)}
|
{fmtTopEx("option", putPx, putSz)}
|
||||||
{!open ? ` · 杠杆 ${putLev}` : ""}
|
{!open ? ` · 杠杆 ${putLev}` : ""}
|
||||||
@@ -940,7 +1017,7 @@ export default function PlanPage() {
|
|||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>实际杠杆</span>
|
<span>实际杠杆</span>
|
||||||
<span className="mono">
|
<span className="mono">
|
||||||
{open
|
{open && !isOo
|
||||||
? heldLev ||
|
? heldLev ||
|
||||||
(heldSide
|
(heldSide
|
||||||
? "—"
|
? "—"
|
||||||
@@ -951,29 +1028,51 @@ export default function PlanPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<div className="kv">
|
{isOo ? (
|
||||||
<span>距现价</span>
|
<div className="kv">
|
||||||
<span className="mono">
|
<span>相对现价</span>
|
||||||
{(() => {
|
<span className="mono">
|
||||||
const strike = snap?.pair?.strike;
|
{(() => {
|
||||||
const px =
|
const px = snap?.index_px;
|
||||||
snap?.index_px ??
|
const ck = snap?.pair?.call_strike ?? snap?.pair?.strike;
|
||||||
(snap?.perp?.bid != null && snap?.perp?.ask != null
|
const pk = snap?.pair?.put_strike;
|
||||||
? (snap.perp.bid + snap.perp.ask) / 2
|
if (px == null || ck == null) return "—";
|
||||||
: null);
|
const cOff = Number(ck) - Number(px);
|
||||||
if (strike == null || px == null) return "—";
|
const pOff =
|
||||||
const abs = Math.abs(strike - px);
|
pk != null ? Number(pk) - Number(px) : null;
|
||||||
const sign = strike - px > 0 ? "+" : "";
|
const fmtOff = (n: number) =>
|
||||||
const delta = `${sign}${(strike - px).toFixed(0)}`;
|
`${n > 0 ? "+" : ""}${n.toFixed(0)}`;
|
||||||
if (!plan?.atm_open_offset_enabled) {
|
return pOff != null
|
||||||
return `${delta} · 偏差限制关`;
|
? `C ${fmtOff(cOff)} · P ${fmtOff(pOff)}`
|
||||||
}
|
: `C ${fmtOff(cOff)}`;
|
||||||
const lim = plan?.max_atm_open_offset ?? 3;
|
})()}
|
||||||
const ok = abs <= lim + 1e-9;
|
</span>
|
||||||
return `${delta} · 开仓${ok ? "可" : "不可"}(|Δ|≤${lim})`;
|
</div>
|
||||||
})()}
|
) : (
|
||||||
</span>
|
<div className="kv">
|
||||||
</div>
|
<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">
|
<div className="kv">
|
||||||
<span>到期</span>
|
<span>到期</span>
|
||||||
<span className="mono">{snap?.pair?.expiry_ymd || "—"}</span>
|
<span className="mono">{snap?.pair?.expiry_ymd || "—"}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user