Files
crypto_monitor/lib/options/options_pricing_lib.py
T
dekun e15eca76f3 Show option net P/L from bid recycle minus premium.
Rename floating P/L to net P/L, align ROI, and display bid depth as price/liquidity for only the levels needed to close.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 20:54:15 +08:00

413 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OKX USDⓈ 期权:张数与权利金计算."""
from __future__ import annotations
import math
from typing import Any
def ct_mult_from_meta(meta: dict[str, Any] | None) -> float:
if not meta:
return 0.01
try:
return float(meta.get("ctMult") or 0.01)
except (TypeError, ValueError):
return 0.01
def min_sz_from_meta(meta: dict[str, Any] | None) -> int:
if not meta:
return 1
try:
return max(1, int(float(meta.get("minSz") or 1)))
except (TypeError, ValueError):
return 1
def premium_per_sheet(quote_per_unit: float, ct_mult: float = 0.01) -> float:
"""报价为每 1 ETH/BTC;每张权利金 = 报价 × ctMult."""
return float(quote_per_unit) * float(ct_mult)
def format_quote_liquidity(px: float | None, sz: float | None, *, px_decimals: int = 4) -> str | None:
"""盘口展示:价格/张数,如 17.2/150."""
if px is None:
return None
try:
price = f"{float(px):.{px_decimals}f}".rstrip("0").rstrip(".")
except (TypeError, ValueError):
return None
if sz is None:
return price
try:
s = float(sz)
size = str(int(s)) if abs(s - int(s)) < 1e-9 else str(s).rstrip("0").rstrip(".")
except (TypeError, ValueError):
return price
return f"{price}/{size}"
def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.01) -> float:
return float(quote_per_unit) * float(eth_amount)
def estimate_close_by_bids(
bids: list[dict[str, Any]] | None,
sheets: int | float,
*,
ct_mult: float = 0.01,
premium_paid: float | None = None,
) -> dict[str, Any]:
"""按买一到买N逐档估算限价卖出可收回金额."""
target = max(0, int(float(sheets or 0)))
remaining = target
total_received = 0.0
levels: list[dict[str, Any]] = []
if target <= 0 or ct_mult <= 0:
return {
"levels": [],
"covered_sheets": 0,
"uncovered_sheets": target,
"total_received": 0.0,
"avg_px": None,
"estimated_pnl": None,
"estimated_pnl_ratio_pct": None,
}
for i, level in enumerate(bids or [], start=1):
if remaining <= 0:
break
try:
px = float(level.get("px"))
sz = int(float(level.get("sz")))
except (AttributeError, TypeError, ValueError):
continue
if px <= 0 or sz <= 0:
continue
take = min(remaining, sz)
eth_amount = eth_amount_from_sheets(take, ct_mult)
received = total_premium(px, eth_amount)
levels.append(
{
"level": i,
"px": px,
"available_sheets": sz,
"sheets": take,
"eth_amount": eth_amount,
"received": round(received, 4),
}
)
total_received += received
remaining -= take
covered = target - remaining
avg_px = (total_received / eth_amount_from_sheets(covered, ct_mult)) if covered > 0 else None
# 净盈亏 = 按买盘可回收 − 全部权利金(与「可落袋」口径一致;买一不够会展开更多档)
estimated_pnl = None
estimated_pnl_ratio_pct = None
if premium_paid is not None and covered > 0:
paid = float(premium_paid)
estimated_pnl = round(total_received - paid, 4)
if paid > 0:
estimated_pnl_ratio_pct = round(estimated_pnl / paid * 100.0, 2)
return {
"levels": levels,
"covered_sheets": covered,
"uncovered_sheets": remaining,
"total_received": round(total_received, 4),
"avg_px": round(avg_px, 4) if avg_px is not None else None,
"estimated_pnl": estimated_pnl,
"estimated_pnl_ratio_pct": estimated_pnl_ratio_pct,
}
def sheets_from_eth_amount(eth_amount: float, ct_mult: float = 0.01) -> int:
if eth_amount <= 0 or ct_mult <= 0:
return 0
return int(math.floor(eth_amount / ct_mult + 1e-12))
def eth_amount_from_sheets(sheets: int, ct_mult: float = 0.01) -> float:
return round(int(sheets) * float(ct_mult), 8)
def calc_order_size(
*,
quote_per_unit: float,
ct_mult: float,
min_sz: int,
budget_usdc: float | None = None,
budget_buffer: float = 0.95,
eth_amount: float | None = None,
sheets: int | None = None,
budget_cap: float | None = None,
) -> dict[str, Any]:
"""
返回 sheets, eth_amount, total_premium.
mode: budget_full / eth_amount / sheets.
"""
if quote_per_unit <= 0:
return {"ok": False, "msg": "卖一价无效", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
if sheets is not None and int(sheets) > 0:
sheets = int(sheets)
elif eth_amount is not None and eth_amount > 0:
sheets = sheets_from_eth_amount(eth_amount, ct_mult)
elif budget_usdc is not None and budget_usdc > 0:
eff = float(budget_usdc) * float(budget_buffer)
per_sheet = premium_per_sheet(quote_per_unit, ct_mult)
if per_sheet <= 0:
return {"ok": False, "msg": "无法计算单张权利金", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
sheets = int(math.floor(eff / per_sheet))
else:
return {"ok": False, "msg": "请指定预算,币数量或张数", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
if sheets < min_sz:
per = premium_per_sheet(quote_per_unit, ct_mult)
return {
"ok": False,
"msg": f"预算不足,无法买入 {min_sz} 张(单张约 {per:.4f} USDC)",
"sheets": sheets,
"eth_amount": eth_amount_from_sheets(sheets, ct_mult),
"total_premium": total_premium(quote_per_unit, eth_amount_from_sheets(sheets, ct_mult)),
}
eth = eth_amount_from_sheets(sheets, ct_mult)
prem = total_premium(quote_per_unit, eth)
if budget_cap is not None and prem > float(budget_cap) + 1e-9:
return {
"ok": False,
"msg": f"权利金 {prem:.4f} 超过单笔上限 {budget_cap} USDC",
"sheets": sheets,
"eth_amount": eth,
"total_premium": prem,
}
return {"ok": True, "msg": "", "sheets": sheets, "eth_amount": eth, "total_premium": prem}
def is_shallow_itm(
*,
opt_type: str,
strike: float,
index_px: float,
max_dist_usd: float,
) -> bool:
o = (opt_type or "").upper()
if o == "C":
if strike >= index_px:
return False
return (index_px - strike) <= max_dist_usd
if o == "P":
if strike <= index_px:
return False
return (strike - index_px) <= max_dist_usd
return False
def option_moneyness(*, opt_type: str, strike: float, index_px: float) -> str:
"""返回 itm / otm / atm."""
o = (opt_type or "").upper()
if strike is None or index_px is None or index_px <= 0:
return "unknown"
atm_band = max(index_px * 0.002, 2.0)
if abs(strike - index_px) <= atm_band:
return "atm"
if o == "C":
return "itm" if strike < index_px else "otm"
if o == "P":
return "itm" if strike > index_px else "otm"
return "unknown"
def option_moneyness_label(moneyness: str) -> str:
return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "")
def expiry_breakeven_from_ask(
*,
opt_type: str,
strike: float | None,
ask_px: float | None,
mark_px: float | None = None,
) -> float | None:
"""买入前预估到期平衡:权利金按卖一;无卖一时回退标记价."""
prem = ask_px if ask_px is not None and ask_px > 0 else mark_px
return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem)
def expiry_breakeven_px(
*,
opt_type: str,
strike: float | None,
avg_px: float | None,
be_px_api: float | None = None,
) -> float | None:
"""到期平衡点:持有至到期时标的指数盈亏为 0 的价格.优先 OKX bePx."""
if be_px_api is not None and be_px_api > 0:
return round(float(be_px_api), 2)
if strike is None or avg_px is None:
return None
o = (opt_type or "").upper()
if o == "C":
return round(strike + avg_px, 2)
if o == "P":
return round(strike - avg_px, 2)
return None
def close_breakeven_idx(
*,
opt_type: str,
idx_px: float | None,
mark_px: float | None,
avg_px: float | None,
delta_pa: float | None = None,
pos: float = 0,
ct_mult: float = 0.01,
) -> float | None:
"""
平掉回本:标的指数达到该价位时,按标记价平仓近似盈亏为 0.
优先用 deltaPA 线性外推,否则用时间价值近似(适合短期轻度实值).
"""
if idx_px is None or mark_px is None or avg_px is None:
return None
eth_amt = abs(float(pos)) * float(ct_mult)
if eth_amt > 1e-12 and delta_pa is not None and abs(float(delta_pa)) > 1e-12:
slope = float(delta_pa) / eth_amt
return round(float(idx_px) + (float(avg_px) - float(mark_px)) / slope, 2)
o = (opt_type or "").upper()
if o == "C":
return round(float(idx_px) + float(avg_px) - float(mark_px), 2)
if o == "P":
return round(float(idx_px) + float(mark_px) - float(avg_px), 2)
return None
def idx_distance_to_be(idx_px: float | None, be_px: float | None) -> float | None:
"""指数距平衡点(正=指数需上涨才到平衡点)."""
if idx_px is None or be_px is None:
return None
return round(float(be_px) - float(idx_px), 2)
def format_options_breakeven_line(
*,
expiry_be_px: float | None,
close_be_px: float | None,
idx_px: float | None = None,
) -> str:
"""持仓摘要行:到期平衡 / 平掉回本."""
parts: list[str] = []
if expiry_be_px is not None:
parts.append(f"到期平衡{expiry_be_px:.0f}")
if close_be_px is not None:
parts.append(f"平掉回本{close_be_px:.0f}")
if idx_px is not None and parts:
return " ".join(parts) + f"(指数{idx_px:.0f})"
return " ".join(parts)
def estimate_expiry_value_at_index(
*,
opt_type: str,
strike: float | None,
target_idx: float | None,
eth_amount: float | None,
) -> float | None:
"""到期测算:目标指数价下期权内在价值总额(不含已付权利金)."""
if strike is None or target_idx is None or eth_amount is None:
return None
if eth_amount <= 0:
return None
o = (opt_type or "").upper()
if o == "C":
intrinsic = max(0.0, float(target_idx) - float(strike))
elif o == "P":
intrinsic = max(0.0, float(strike) - float(target_idx))
else:
return None
return round(intrinsic * float(eth_amount), 2)
def estimate_expiry_profit_at_index(
*,
opt_type: str,
strike: float | None,
target_idx: float | None,
entry_px: float | None,
eth_amount: float | None,
total_premium: float | None = None,
) -> float | None:
"""到期测算:目标指数价下净盈利 = 预计价值 − 权利金."""
value = estimate_expiry_value_at_index(
opt_type=opt_type,
strike=strike,
target_idx=target_idx,
eth_amount=eth_amount,
)
if value is None:
return None
prem = total_premium
if prem is None and entry_px is not None and eth_amount is not None:
prem = float(entry_px) * float(eth_amount)
if prem is None:
return None
return round(float(value) - float(prem), 2)
def equivalent_contract_leverage(
*,
index_px: float | None,
eth_amount: float | None,
total_premium: float | None,
) -> float | None:
"""名义价值 / 权利金,近似相当于永续合约杠杆倍数(测算用)."""
if index_px is None or eth_amount is None or total_premium is None:
return None
if eth_amount <= 0 or total_premium <= 0:
return None
return round(float(index_px) * float(eth_amount) / float(total_premium), 1)
def straddle_ask_per_unit(
call_ask: float | None,
put_ask: float | None,
) -> float | None:
"""跨式双买:每 1 标的币的卖一报价之和."""
if call_ask is None or put_ask is None:
return None
if float(call_ask) <= 0 or float(put_ask) <= 0:
return None
return round(float(call_ask) + float(put_ask), 4)
def straddle_premium_total(
call_ask: float | None,
put_ask: float | None,
eth_amount: float | None,
) -> float | None:
"""跨式双买权利金总额(USDC)."""
per = straddle_ask_per_unit(call_ask, put_ask)
if per is None or eth_amount is None or float(eth_amount) <= 0:
return None
return round(per * float(eth_amount), 2)
def straddle_breakeven_band(
strike: float | None,
combined_ask_per_unit: float | None,
) -> tuple[float | None, float | None]:
"""跨式到期平衡带:下平衡 ~ 上平衡(按双卖一报价和)."""
if strike is None or combined_ask_per_unit is None:
return None, None
k = float(strike)
d = float(combined_ask_per_unit)
return round(k - d, 2), round(k + d, 2)
def format_straddle_band(
strike: float | None,
combined_ask_per_unit: float | None,
) -> str:
lo, hi = straddle_breakeven_band(strike, combined_ask_per_unit)
if lo is None or hi is None:
return ""
return f"{lo:.0f} ~ {hi:.0f}"