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:
+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:
|
||||
|
||||
Reference in New Issue
Block a user