Keep OO amplitude refreshed while holding a position.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 17:30:51 +08:00
parent 0ac03cc726
commit 43a2a9634d
3 changed files with 88 additions and 23 deletions
+25 -2
View File
@@ -2011,9 +2011,19 @@ class Matcher:
if index_px is None and snap.perp: if index_px is None and snap.perp:
index_px = snap.perp.mark_px index_px = snap.perp.mark_px
qty = float(pos.get("option_qty_eth") or 0) qty = float(pos.get("option_qty_eth") or 0)
qty2 = float(pos.get("option2_qty_eth") or qty) raw_q2 = pos.get("option2_qty_eth")
try:
qty2 = float(raw_q2) if raw_q2 is not None else 0.0
except (TypeError, ValueError):
qty2 = 0.0
if qty2 <= 0:
qty2 = qty
contracts1 = float(pos.get("option_qty_contracts") or 0) contracts1 = float(pos.get("option_qty_contracts") or 0)
contracts2 = float(pos.get("option2_qty_contracts") or 0) raw_c2 = pos.get("option2_qty_contracts")
try:
contracts2 = float(raw_c2) if raw_c2 is not None else 0.0
except (TypeError, ValueError):
contracts2 = 0.0
prem1 = float(pos.get("initial_premium") or 0) prem1 = float(pos.get("initial_premium") or 0)
prem2 = float(pos.get("initial_premium2") or 0) prem2 = float(pos.get("initial_premium2") or 0)
call_id = str(pos.get("option_inst_id") or "") call_id = str(pos.get("option_inst_id") or "")
@@ -2022,6 +2032,19 @@ class Matcher:
entry2 = float(pos.get("option2_entry_px") or 0) entry2 = float(pos.get("option2_entry_px") or 0)
oq1 = self._quote_held_option(call_id) if call_id else None oq1 = self._quote_held_option(call_id) if call_id else None
oq2 = self._quote_held_option(put_id) if put_id else None oq2 = self._quote_held_option(put_id) if put_id else None
# 盘口缓存未订阅到 Put 时,回退 session 监控腿(与「持仓虚值」同数据源)
if (oq2 is None or (oq2.bid is None and oq2.mark_px is None)) and put_id:
try:
if snap.put and str(getattr(snap.put, "inst_id", "") or "") == put_id:
oq2 = snap.put
except Exception:
pass
if (oq1 is None or (oq1.bid is None and oq1.mark_px is None)) and call_id:
try:
if snap.call and str(getattr(snap.call, "inst_id", "") or "") == call_id:
oq1 = snap.call
except Exception:
pass
option_upl = 0.0 option_upl = 0.0
option2_upl = 0.0 option2_upl = 0.0
fees = 0.0 fees = 0.0
+48 -17
View File
@@ -332,6 +332,8 @@ class StrategySession:
def align_to_held_position(self) -> OptionPair | None: def align_to_held_position(self) -> OptionPair | None:
"""有活跃仓时:监控对锁定为持仓合约的到期/行权价。""" """有活跃仓时:监控对锁定为持仓合约的到期/行权价。"""
if _hedge_mode() == "option_option":
self.refresh_oo_amplitude()
call_id, put_id = _held_option_legs() call_id, put_id = _held_option_legs()
held = call_id or _held_option_inst_id() held = call_id or _held_option_inst_id()
if not held: if not held:
@@ -441,24 +443,17 @@ 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: def refresh_oo_amplitude(self) -> dict[str, Any] | None:
"""期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅超限仍对齐候选)。""" """刷新振幅高低(有仓/无仓都要,否则持仓后 UI 指数/振幅会空)。"""
if _hedge_mode() != "option_option":
return None
try:
from ..exchange.candles import fetch_amplitude_hl_for_runtime from ..exchange.candles import fetch_amplitude_hl_for_runtime
from .oo_selection import select_oo_pair
if _has_open_position(): amp_pct, amp_hours, _, _ = _oo_settings()
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) amp = fetch_amplitude_hl_for_runtime(amp_hours)
if amp is None: if amp is None:
self._oo_amp = None return self._oo_amp
raise RuntimeError("无法获取振幅 K 线高低点")
self._oo_amp = { self._oo_amp = {
"high": float(amp.high), "high": float(amp.high),
"low": float(amp.low), "low": float(amp.low),
@@ -468,13 +463,37 @@ class StrategySession:
"max_pct": float(amp_pct), "max_pct": float(amp_pct),
"ok": float(amp.range_pct) <= float(amp_pct) + 1e-12, "ok": float(amp.range_pct) <= float(amp_pct) + 1e-12,
} }
return self._oo_amp
except Exception:
logger.exception("refresh_oo_amplitude failed")
return self._oo_amp
def align_oo_instruments(self) -> OptionPair | None:
"""期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅超限仍对齐候选)。"""
from .oo_selection import select_oo_pair
# 有仓也刷新振幅(钉仓不再走选约,否则 _oo_amp 一直空)
self.refresh_oo_amplitude()
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)
if self._oo_amp is None:
raise RuntimeError("无法获取振幅 K 线高低点")
amp_high = float(self._oo_amp["high"])
amp_low = float(self._oo_amp["low"])
contracts = self.ex.list_option_contracts(s.option_inst_family) contracts = self.ex.list_option_contracts(s.option_inst_family)
skip = _skip_expiry_ymds_for_next() skip = _skip_expiry_ymds_for_next()
picked = select_oo_pair( picked = select_oo_pair(
contracts, contracts,
spot=underlying, spot=underlying,
high=float(amp.high), high=amp_high,
low=float(amp.low), low=amp_low,
min_hours=float(min_hours), min_hours=float(min_hours),
skip_expiry_ymds=skip, skip_expiry_ymds=skip,
) )
@@ -518,6 +537,16 @@ class StrategySession:
if amp is None: if amp is None:
logger.info("oo: amplitude candles unavailable") logger.info("oo: amplitude candles unavailable")
return None return None
# 无论是否超限都写入,供「指数/振幅」面板展示
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),
"max_pct": float(amp_pct),
"ok": float(amp.range_pct) <= float(amp_pct) + 1e-12,
}
if float(amp.range_pct) > float(amp_pct) + 1e-12: if float(amp.range_pct) > float(amp_pct) + 1e-12:
logger.info( logger.info(
"oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)", "oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)",
@@ -862,7 +891,9 @@ class StrategySession:
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/虚值);期期仍刷新振幅供 UI
if _hedge_mode() == "option_option":
await asyncio.to_thread(self.refresh_oo_amplitude)
call_id, put_id = _held_option_legs() call_id, put_id = _held_option_legs()
held = call_id or _held_option_inst_id() held = call_id or _held_option_inst_id()
if held and ( if held and (
+16 -5
View File
@@ -766,14 +766,25 @@ export default function PlanPage() {
: "—"} : "—"}
</span> </span>
<span className="pos-meta-item mono"> <span className="pos-meta-item mono">
{fmt(pos?.option2_qty_eth, 2)} ETH ·{" "} {fmt(
{fmt(pos?.option2_qty_contracts, 0)} pos?.option2_qty_eth ??
plan?.oo_put_qty_eth ??
plan?.risk_sizing_preview?.put_qty_eth,
2,
)}{" "}
ETH · {fmt(pos?.option2_qty_contracts, 0)}
</span> </span>
<span <span
className="pos-meta-item mono" className="pos-meta-item mono"
title="开仓指数÷开仓均价" title="开仓指数÷开仓均价"
> >
{fmt(pos?.option2_leverage, 0)}x {" "}
{pos?.option2_leverage != null
? `${fmt(pos.option2_leverage, 0)}x`
: optionLevLabel(
pos?.entry_index_px ?? snap?.index_px,
pos?.option2_entry_px,
)}
</span> </span>
</div> </div>
<div className="pos-grid"> <div className="pos-grid">
@@ -788,8 +799,8 @@ export default function PlanPage() {
<span className="pos-value mono"> <span className="pos-value mono">
{fmtBidLiqEx( {fmtBidLiqEx(
"option", "option",
pos?.option2_mark_px, pos?.option2_mark_px ?? snap?.put?.bid,
pos?.option2_bid_sz, pos?.option2_bid_sz ?? snap?.put?.bid_sz,
)} )}
</span> </span>
</div> </div>