38e3e00fe9
达标情景现货价按权利金价值与行权价反推,便于对照到期后效果。 Co-authored-by: Cursor <cursoragent@cursor.com>
783 lines
27 KiB
Python
783 lines
27 KiB
Python
"""对冲计划:情景测算与全仓建议仓(纯函数,无 IO)."""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Optional
|
||
|
||
|
||
def _f(v: Any) -> Optional[float]:
|
||
if v is None or v == "":
|
||
return None
|
||
try:
|
||
return float(v)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
def perp_coin_amount(*, contracts: float, contract_size: float) -> float:
|
||
return float(contracts) * float(contract_size or 1.0)
|
||
|
||
|
||
def perp_pnl(
|
||
*,
|
||
direction: str,
|
||
entry: float,
|
||
exit_px: float,
|
||
contracts: float,
|
||
contract_size: float,
|
||
) -> float:
|
||
coins = perp_coin_amount(contracts=contracts, contract_size=contract_size)
|
||
d = (direction or "long").strip().lower()
|
||
if d == "short":
|
||
return (float(entry) - float(exit_px)) * coins
|
||
return (float(exit_px) - float(entry)) * coins
|
||
|
||
|
||
def option_premium_total(*, ask: float, sheets: float, ct_mult: float) -> float:
|
||
"""卖一报价为每 1 币;权利金 = ask × 张数 × ct_mult."""
|
||
return float(ask) * float(sheets) * float(ct_mult or 0.01)
|
||
|
||
|
||
def option_expiry_pnl(
|
||
*,
|
||
opt_type: str,
|
||
strike: float,
|
||
spot: float,
|
||
sheets: float,
|
||
ct_mult: float,
|
||
premium_paid: float,
|
||
) -> float:
|
||
o = (opt_type or "").strip().upper()
|
||
intrinsic_per_coin = 0.0
|
||
if o in ("C", "CALL"):
|
||
intrinsic_per_coin = max(0.0, float(spot) - float(strike))
|
||
elif o in ("P", "PUT"):
|
||
intrinsic_per_coin = max(0.0, float(strike) - float(spot))
|
||
else:
|
||
return -float(premium_paid)
|
||
value = intrinsic_per_coin * float(sheets) * float(ct_mult or 0.01)
|
||
return value - float(premium_paid)
|
||
|
||
|
||
def spot_from_expiry_intrinsic_profit(
|
||
*,
|
||
opt_type: str,
|
||
strike: float,
|
||
sheets: float,
|
||
ct_mult: float,
|
||
premium_paid: float,
|
||
profit: float,
|
||
) -> float | None:
|
||
"""按到期实值反推现货价:使该腿到期盈亏 ≈ profit.
|
||
|
||
到期价值=实值×张数×乘数;盈亏=价值−权利金 → 实值/币=(profit+权利金)/(张数×乘数).
|
||
Call: spot=K+实值/币; Put: spot=K−实值/币.
|
||
"""
|
||
try:
|
||
k = float(strike)
|
||
n = float(sheets or 0)
|
||
ct = float(ct_mult or 0.01)
|
||
prem = float(premium_paid or 0)
|
||
pnl = float(profit)
|
||
except (TypeError, ValueError):
|
||
return None
|
||
denom = n * ct
|
||
if denom <= 0:
|
||
return None
|
||
need = (pnl + prem) / denom
|
||
if need < 0:
|
||
need = 0.0
|
||
o = (opt_type or "").strip().upper()
|
||
if o in ("C", "CALL"):
|
||
return round(k + need, 2)
|
||
if o in ("P", "PUT"):
|
||
return round(k - need, 2)
|
||
return None
|
||
|
||
|
||
def suggest_contracts_from_notional(
|
||
*,
|
||
notional: float,
|
||
entry: float,
|
||
contract_size: float,
|
||
) -> float:
|
||
if entry <= 0 or contract_size <= 0 or notional <= 0:
|
||
return 0.0
|
||
return float(notional) / (float(entry) * float(contract_size))
|
||
|
||
|
||
def floor_contracts_to_precision(contracts: float, decimals: int) -> float:
|
||
"""按交易所张数精度向下取整,避免建议张数超过可用保证金."""
|
||
import math
|
||
|
||
raw = float(contracts or 0.0)
|
||
if raw <= 0:
|
||
return 0.0
|
||
try:
|
||
d = int(decimals)
|
||
except (TypeError, ValueError):
|
||
d = 0
|
||
if d <= 0:
|
||
return float(math.floor(raw + 1e-12))
|
||
scale = 10**d
|
||
return math.floor(raw * scale + 1e-12) / scale
|
||
|
||
|
||
def option_unit_cost_usdc(*, ask: float, ct_mult: float) -> float:
|
||
"""单张权利金(USDC) = 卖一价 × ct_mult."""
|
||
a = _f(ask)
|
||
if a is None or a <= 0:
|
||
return 0.0
|
||
return float(a) * float(ct_mult or 0.01)
|
||
|
||
|
||
def resolve_oo_budget_usdc(
|
||
*,
|
||
trading_usdc: Any,
|
||
trade_budget_usdc: Any,
|
||
buffer_ratio: Any = 0.95,
|
||
) -> dict[str, Any]:
|
||
"""期期可用预算 = min(交易户×buffer, 单笔预算)."""
|
||
import math
|
||
|
||
trading = _f(trading_usdc)
|
||
cap = _f(trade_budget_usdc)
|
||
buf = _f(buffer_ratio)
|
||
if buf is None or buf <= 0:
|
||
buf = 0.95
|
||
if buf > 1:
|
||
buf = 1.0
|
||
trading_cap = None if trading is None else max(0.0, float(trading) * float(buf))
|
||
trade_cap = None if cap is None else max(0.0, float(cap))
|
||
if trading_cap is None and trade_cap is None:
|
||
return {
|
||
"ok": False,
|
||
"budget_usdc": 0.0,
|
||
"trading_cap": None,
|
||
"trade_budget_cap": None,
|
||
"buffer_ratio": float(buf),
|
||
"msg": "缺少交易户余额与单笔预算",
|
||
}
|
||
if trading_cap is None:
|
||
budget = float(trade_cap or 0.0)
|
||
elif trade_cap is None:
|
||
budget = float(trading_cap)
|
||
else:
|
||
budget = min(float(trading_cap), float(trade_cap))
|
||
budget = float(math.floor(budget * 1e6 + 1e-12) / 1e6)
|
||
return {
|
||
"ok": budget > 0,
|
||
"budget_usdc": budget,
|
||
"trading_cap": None if trading_cap is None else round(float(trading_cap), 6),
|
||
"trade_budget_cap": None if trade_cap is None else round(float(trade_cap), 6),
|
||
"buffer_ratio": float(buf),
|
||
"msg": "" if budget > 0 else "可用预算为 0",
|
||
}
|
||
|
||
|
||
def _cap_sheets_by_ask_depth(sheets: int, ask_sz: Any) -> int:
|
||
import math
|
||
|
||
n = max(0, int(sheets))
|
||
depth = _f(ask_sz)
|
||
if depth is None:
|
||
return n
|
||
if depth <= 0:
|
||
return 0
|
||
return min(n, int(math.floor(float(depth) + 1e-12)))
|
||
|
||
|
||
def _normalize_oo_sheets_mode(mode: str) -> str:
|
||
m = (mode or "same_sheets").strip().lower()
|
||
if m in ("long_bias", "bias_long", "long", "做多"):
|
||
return "long_bias"
|
||
if m in ("short_bias", "bias_short", "short", "做空"):
|
||
return "short_bias"
|
||
# 旧「均分」兼容:按预算 50/50(页面已移除)
|
||
if m in ("split", "equal_budget", "split_budget", "均分"):
|
||
return "split_budget"
|
||
return "same_sheets"
|
||
|
||
|
||
def _normalize_oo_bias_split_by(raw: Any) -> str:
|
||
v = str(raw or "budget").strip().lower()
|
||
if v in ("sheets", "qty", "quantity", "张数"):
|
||
return "sheets"
|
||
return "budget"
|
||
|
||
|
||
def _clamp_oo_bias_ratio(raw: Any, default: float = 0.7) -> float:
|
||
try:
|
||
r = float(raw)
|
||
except (TypeError, ValueError):
|
||
r = float(default)
|
||
if r <= 0 or r >= 1:
|
||
r = float(default)
|
||
return r
|
||
|
||
|
||
def _oo_call_put_leg_index(opt_type_a: str, opt_type_b: str) -> tuple[Optional[str], Optional[str], str]:
|
||
"""返回 (call_side, put_side, err);side 为 'a'/'b'."""
|
||
a = (opt_type_a or "").strip().upper()
|
||
b = (opt_type_b or "").strip().upper()
|
||
if a.startswith("C"):
|
||
a = "C"
|
||
elif a.startswith("P"):
|
||
a = "P"
|
||
if b.startswith("C"):
|
||
b = "C"
|
||
elif b.startswith("P"):
|
||
b = "P"
|
||
if {a, b} != {"C", "P"}:
|
||
return None, None, "做多/做空需一腿 Call、一腿 Put"
|
||
call_side = "a" if a == "C" else "b"
|
||
put_side = "b" if call_side == "a" else "a"
|
||
return call_side, put_side, ""
|
||
|
||
|
||
def suggest_oo_sheets(
|
||
*,
|
||
mode: str,
|
||
budget_usdc: float,
|
||
ask_a: float,
|
||
ct_mult_a: float = 0.01,
|
||
ask_sz_a: Any = None,
|
||
opt_type_a: str = "",
|
||
ask_b: float,
|
||
ct_mult_b: float = 0.01,
|
||
ask_sz_b: Any = None,
|
||
opt_type_b: str = "",
|
||
bias_split_by: str = "budget",
|
||
bias_ratio: float = 0.7,
|
||
) -> dict[str, Any]:
|
||
"""期期建议张数:same_sheets / long_bias / short_bias(及旧 split_budget)."""
|
||
import math
|
||
|
||
m = _normalize_oo_sheets_mode(mode)
|
||
split_by = _normalize_oo_bias_split_by(bias_split_by)
|
||
ratio = _clamp_oo_bias_ratio(bias_ratio)
|
||
budget = max(0.0, float(budget_usdc or 0.0))
|
||
cost_a = option_unit_cost_usdc(ask=ask_a, ct_mult=ct_mult_a)
|
||
cost_b = option_unit_cost_usdc(ask=ask_b, ct_mult=ct_mult_b)
|
||
|
||
def _fail(msg: str, n_a: int = 0, n_b: int = 0) -> dict[str, Any]:
|
||
return {
|
||
"mode": m,
|
||
"sheets_a": n_a,
|
||
"sheets_b": n_b,
|
||
"cost_a": round(cost_a, 8),
|
||
"cost_b": round(cost_b, 8),
|
||
"premium_est": round(cost_a * n_a + cost_b * n_b, 6),
|
||
"ok": False,
|
||
"msg": msg,
|
||
"bias_split_by": split_by,
|
||
"bias_ratio": ratio,
|
||
}
|
||
|
||
if budget <= 0:
|
||
return _fail("可用预算为 0")
|
||
if cost_a <= 0 or cost_b <= 0:
|
||
return _fail("缺少有效卖一价,无法建议张数")
|
||
|
||
pair = cost_a + cost_b
|
||
n_pair = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
|
||
# 与同张数一致:先按预算得 n,再各自深度封顶后取 min
|
||
n_same = min(
|
||
_cap_sheets_by_ask_depth(n_pair, ask_sz_a),
|
||
_cap_sheets_by_ask_depth(n_pair, ask_sz_b),
|
||
)
|
||
|
||
if m == "same_sheets":
|
||
n_a = n_same
|
||
n_b = n_same
|
||
elif m == "split_budget":
|
||
half = budget / 2.0
|
||
n_a = int(math.floor(half / cost_a + 1e-12))
|
||
n_b = int(math.floor(half / cost_b + 1e-12))
|
||
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
|
||
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
|
||
else:
|
||
call_side, put_side, err = _oo_call_put_leg_index(opt_type_a, opt_type_b)
|
||
if err:
|
||
return _fail(err)
|
||
major_is_call = m == "long_bias"
|
||
if split_by == "sheets":
|
||
# 总张数 = 同张数两侧合计(每腿 n → 共 2n),再按比例拆到 Call/Put
|
||
total = int(n_same) * 2
|
||
if total < 2:
|
||
return _fail("同张数总规模不足 2,无法按比例拆分")
|
||
major_n = int(round(total * ratio))
|
||
major_n = max(1, min(major_n, total - 1))
|
||
minor_n = total - major_n
|
||
n_call = major_n if major_is_call else minor_n
|
||
n_put = minor_n if major_is_call else major_n
|
||
else:
|
||
maj_budget = budget * ratio
|
||
min_budget = budget * (1.0 - ratio)
|
||
cost_call = cost_a if call_side == "a" else cost_b
|
||
cost_put = cost_b if call_side == "a" else cost_a
|
||
if major_is_call:
|
||
n_call = int(math.floor(maj_budget / cost_call + 1e-12)) if cost_call > 0 else 0
|
||
n_put = int(math.floor(min_budget / cost_put + 1e-12)) if cost_put > 0 else 0
|
||
else:
|
||
n_put = int(math.floor(maj_budget / cost_put + 1e-12)) if cost_put > 0 else 0
|
||
n_call = int(math.floor(min_budget / cost_call + 1e-12)) if cost_call > 0 else 0
|
||
n_a = n_call if call_side == "a" else n_put
|
||
n_b = n_put if call_side == "a" else n_call
|
||
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
|
||
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
|
||
|
||
prem = cost_a * n_a + cost_b * n_b
|
||
ok = n_a >= 1 and n_b >= 1
|
||
msg = "" if ok else "预算不够开 1+1(或卖一深度不足)"
|
||
return {
|
||
"mode": m,
|
||
"sheets_a": n_a,
|
||
"sheets_b": n_b,
|
||
"cost_a": round(cost_a, 8),
|
||
"cost_b": round(cost_b, 8),
|
||
"premium_est": round(prem, 6),
|
||
"ok": ok,
|
||
"msg": msg,
|
||
"bias_split_by": split_by,
|
||
"bias_ratio": ratio,
|
||
}
|
||
|
||
|
||
def build_perp_options_preview(
|
||
*,
|
||
direction: str,
|
||
entry: float,
|
||
tp: float,
|
||
sl: float,
|
||
contracts: float,
|
||
contract_size: float,
|
||
opt_type: str,
|
||
strike: float,
|
||
sheets: float,
|
||
ct_mult: float,
|
||
premium_paid: float,
|
||
index_px: Optional[float] = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
永期情景.
|
||
止盈账:永续止盈盈利 - 权利金.
|
||
止损账:期权到期内在(按 SL 价) - 永续止损亏损额.
|
||
"""
|
||
d = (direction or "long").strip().lower()
|
||
pnl_tp_perp = perp_pnl(
|
||
direction=d, entry=entry, exit_px=tp, contracts=contracts, contract_size=contract_size
|
||
)
|
||
pnl_sl_perp = perp_pnl(
|
||
direction=d, entry=entry, exit_px=sl, contracts=contracts, contract_size=contract_size
|
||
)
|
||
# 止盈统计口径
|
||
tp_total = float(pnl_tp_perp) - float(premium_paid)
|
||
# 止损:期权按 SL 价结算内在 - |永续亏损|
|
||
opt_at_sl = option_expiry_pnl(
|
||
opt_type=opt_type,
|
||
strike=strike,
|
||
spot=sl,
|
||
sheets=sheets,
|
||
ct_mult=ct_mult,
|
||
premium_paid=premium_paid,
|
||
)
|
||
sl_total = float(opt_at_sl) - abs(float(pnl_sl_perp)) if pnl_sl_perp < 0 else float(opt_at_sl) + float(
|
||
pnl_sl_perp
|
||
)
|
||
# 有符号相加更稳:期权盈亏 + 永续盈亏
|
||
sl_total_signed = float(opt_at_sl) + float(pnl_sl_perp)
|
||
|
||
spot = float(index_px) if index_px is not None else float(entry)
|
||
opt_flat = option_expiry_pnl(
|
||
opt_type=opt_type,
|
||
strike=strike,
|
||
spot=spot,
|
||
sheets=sheets,
|
||
ct_mult=ct_mult,
|
||
premium_paid=premium_paid,
|
||
)
|
||
flat_total = 0.0 + float(opt_flat)
|
||
|
||
opt_at_tp = option_expiry_pnl(
|
||
opt_type=opt_type,
|
||
strike=strike,
|
||
spot=tp,
|
||
sheets=sheets,
|
||
ct_mult=ct_mult,
|
||
premium_paid=premium_paid,
|
||
)
|
||
|
||
return {
|
||
"plan_type": "perp_options",
|
||
"direction": d,
|
||
"contracts": contracts,
|
||
"coin_amount": perp_coin_amount(contracts=contracts, contract_size=contract_size),
|
||
"premium_paid": round(float(premium_paid), 6),
|
||
"scenarios": [
|
||
{
|
||
"id": "tp",
|
||
"label": "止盈(计划结束口径)",
|
||
"spot": tp,
|
||
"perp_pnl": round(pnl_tp_perp, 4),
|
||
"options_pnl": round(-float(premium_paid), 4),
|
||
"total": round(tp_total, 4),
|
||
"note": "止盈盈利 − 权利金;期权可不强平",
|
||
},
|
||
{
|
||
"id": "sl",
|
||
"label": "止损(计划结束口径)",
|
||
"spot": sl,
|
||
"perp_pnl": round(pnl_sl_perp, 4),
|
||
"options_pnl": round(opt_at_sl, 4),
|
||
"total": round(sl_total_signed, 4),
|
||
"note": "期权盈利 − 永续亏损(有符号相加);期权须强平",
|
||
},
|
||
{
|
||
"id": "flat",
|
||
"label": "到期·现价附近",
|
||
"spot": spot,
|
||
"perp_pnl": 0.0,
|
||
"options_pnl": round(opt_flat, 4),
|
||
"total": round(flat_total, 4),
|
||
"note": "示意:永续未动,期权按到期内在",
|
||
},
|
||
{
|
||
"id": "expiry_tp",
|
||
"label": "到期·止盈价",
|
||
"spot": tp,
|
||
"perp_pnl": round(pnl_tp_perp, 4),
|
||
"options_pnl": round(opt_at_tp, 4),
|
||
"total": round(pnl_tp_perp + opt_at_tp, 4),
|
||
"note": "若期权拿到 TP 价到期(参考)",
|
||
},
|
||
{
|
||
"id": "expiry_sl",
|
||
"label": "到期·止损价",
|
||
"spot": sl,
|
||
"perp_pnl": round(pnl_sl_perp, 4),
|
||
"options_pnl": round(opt_at_sl, 4),
|
||
"total": round(pnl_sl_perp + opt_at_sl, 4),
|
||
"note": "与止损口径相近(期权用内在)",
|
||
},
|
||
],
|
||
"summary": {
|
||
"tp_total": round(tp_total, 4),
|
||
"sl_total": round(sl_total_signed, 4),
|
||
"premium_paid": round(float(premium_paid), 4),
|
||
"hedge_ratio_at_sl": _hedge_ratio(opt_at_sl, pnl_sl_perp),
|
||
},
|
||
}
|
||
|
||
|
||
def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
|
||
loss = abs(float(perp_pnl)) if float(perp_pnl) < 0 else 0.0
|
||
if loss <= 1e-12:
|
||
return None
|
||
if float(opt_pnl) <= 0:
|
||
return 0.0
|
||
return round(float(opt_pnl) / loss * 100.0, 2)
|
||
|
||
|
||
def build_options_options_preview(
|
||
*,
|
||
target_price: float | None = None,
|
||
target_price_up: float | None = None,
|
||
target_price_down: float | None = None,
|
||
profit_rr: float | None = None,
|
||
index_px: float,
|
||
leg_a: dict[str, Any],
|
||
leg_b: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""期期情景:盈亏比达标 / 到期现价 / 最大保费损耗.
|
||
|
||
新口径优先 profit_rr(盈利金额/总权利金);若未传则兼容旧上/下破目标价.
|
||
残值按亏损腿本合约权利金的 20% 计.
|
||
"""
|
||
|
||
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
|
||
return option_expiry_pnl(
|
||
opt_type=str(leg.get("opt_type") or ""),
|
||
strike=float(leg["strike"]),
|
||
spot=spot,
|
||
sheets=float(leg.get("sheets") or 0),
|
||
ct_mult=float(leg.get("ct_mult") or 0.01),
|
||
premium_paid=float(leg.get("premium_paid") or 0),
|
||
)
|
||
|
||
prem_a = float(leg_a.get("premium_paid") or 0)
|
||
prem_b = float(leg_b.get("premium_paid") or 0)
|
||
prem = prem_a + prem_b
|
||
rr = float(profit_rr) if profit_rr is not None else None
|
||
|
||
# 新:盈亏比情景(不依赖指数上下破价)
|
||
if rr is not None and rr > 0:
|
||
# 盈利腿达 RR:盈利金额 = rr × 总权利金;亏损腿按全亏 / 本合约残值20%回收
|
||
win_profit = rr * prem
|
||
a_at_a = win_profit
|
||
b_at_a_full = -prem_b
|
||
b_at_a_res = -prem_b * 0.8 # 本合约回收 20%
|
||
b_at_b = win_profit
|
||
a_at_b_full = -prem_a
|
||
a_at_b_res = -prem_a * 0.8
|
||
|
||
spot_a = spot_from_expiry_intrinsic_profit(
|
||
opt_type=str(leg_a.get("opt_type") or ""),
|
||
strike=float(leg_a["strike"]),
|
||
sheets=float(leg_a.get("sheets") or 0),
|
||
ct_mult=float(leg_a.get("ct_mult") or 0.01),
|
||
premium_paid=prem_a,
|
||
profit=win_profit,
|
||
)
|
||
spot_b = spot_from_expiry_intrinsic_profit(
|
||
opt_type=str(leg_b.get("opt_type") or ""),
|
||
strike=float(leg_b["strike"]),
|
||
sheets=float(leg_b.get("sheets") or 0),
|
||
ct_mult=float(leg_b.get("ct_mult") or 0.01),
|
||
premium_paid=prem_b,
|
||
profit=win_profit,
|
||
)
|
||
|
||
a_flat = _leg_pnl(leg_a, index_px)
|
||
b_flat = _leg_pnl(leg_b, index_px)
|
||
flat_total = a_flat + b_flat
|
||
|
||
return {
|
||
"plan_type": "options_options",
|
||
"premium_paid": round(prem, 6),
|
||
"profit_rr": rr,
|
||
"target_price": None,
|
||
"target_price_up": None,
|
||
"target_price_down": None,
|
||
"winner_at_up": "a",
|
||
"winner_at_down": "b",
|
||
"winner_at_target": "a",
|
||
"scenarios": [
|
||
{
|
||
"id": "rr_leg_a_full",
|
||
"label": f"腿A达盈亏比{rr:g}(亏腿全损)",
|
||
"spot": spot_a,
|
||
"leg_a_pnl": round(a_at_a, 4),
|
||
"leg_b_pnl": round(b_at_a_full, 4),
|
||
"total": round(a_at_a + b_at_a_full, 4),
|
||
"note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏",
|
||
},
|
||
{
|
||
"id": "rr_leg_b_full",
|
||
"label": f"腿B达盈亏比{rr:g}(亏腿全损)",
|
||
"spot": spot_b,
|
||
"leg_a_pnl": round(a_at_b_full, 4),
|
||
"leg_b_pnl": round(b_at_b, 4),
|
||
"total": round(a_at_b_full + b_at_b, 4),
|
||
"note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏",
|
||
},
|
||
{
|
||
"id": "rr_leg_a_residual",
|
||
"label": f"腿A达盈亏比{rr:g}(亏腿残值20%)",
|
||
"spot": spot_a,
|
||
"leg_a_pnl": round(a_at_a, 4),
|
||
"leg_b_pnl": round(b_at_a_res, 4),
|
||
"total": round(a_at_a + b_at_a_res, 4),
|
||
"note": "现货同腿A达标反推;亏腿买一回收约本合约权利金20%",
|
||
},
|
||
{
|
||
"id": "expiry_flat",
|
||
"label": "到期·现价",
|
||
"spot": index_px,
|
||
"leg_a_pnl": round(a_flat, 4),
|
||
"leg_b_pnl": round(b_flat, 4),
|
||
"total": round(flat_total, 4),
|
||
"note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
|
||
},
|
||
{
|
||
"id": "max_premium_loss",
|
||
"label": "最大保费损耗",
|
||
"spot": None,
|
||
"leg_a_pnl": round(-prem_a, 4),
|
||
"leg_b_pnl": round(-prem_b, 4),
|
||
"total": round(-prem, 4),
|
||
"note": "双腿权利金全部损失",
|
||
},
|
||
],
|
||
"summary": {
|
||
"profit_rr": rr,
|
||
"spot_at_rr_a": spot_a,
|
||
"spot_at_rr_b": spot_b,
|
||
"at_rr_a_full_total": round(a_at_a + b_at_a_full, 4),
|
||
"at_rr_b_full_total": round(a_at_b_full + b_at_b, 4),
|
||
"at_rr_a_residual_total": round(a_at_a + b_at_a_res, 4),
|
||
"at_target_up_total": round(a_at_a + b_at_a_full, 4),
|
||
"at_target_down_total": round(a_at_b_full + b_at_b, 4),
|
||
"at_target_total": round(a_at_a + b_at_a_full, 4),
|
||
"expiry_flat_total": round(flat_total, 4),
|
||
"premium_paid": round(prem, 6),
|
||
"expiry_is_loss": flat_total <= 0,
|
||
"rr_risk_premium": round(prem, 6),
|
||
"rr_at_up": round((a_at_a + b_at_a_full) / prem, 4) if prem > 0 else None,
|
||
"rr_at_down": round((a_at_b_full + b_at_b) / prem, 4) if prem > 0 else None,
|
||
},
|
||
}
|
||
|
||
# 兼容旧单目标:若未传上下目标则用 target_price 填两边
|
||
up = target_price_up if target_price_up is not None else target_price
|
||
down = target_price_down if target_price_down is not None else target_price
|
||
if up is None or down is None:
|
||
raise ValueError("缺少盈亏比或上破/下破目标价")
|
||
up_f = float(up)
|
||
down_f = float(down)
|
||
|
||
a_up = _leg_pnl(leg_a, up_f)
|
||
b_up = _leg_pnl(leg_b, up_f)
|
||
at_up = a_up + b_up
|
||
win_up = "a" if a_up >= b_up else "b"
|
||
|
||
a_dn = _leg_pnl(leg_a, down_f)
|
||
b_dn = _leg_pnl(leg_b, down_f)
|
||
at_dn = a_dn + b_dn
|
||
win_dn = "a" if a_dn >= b_dn else "b"
|
||
|
||
a_flat = _leg_pnl(leg_a, index_px)
|
||
b_flat = _leg_pnl(leg_b, index_px)
|
||
flat_total = a_flat + b_flat
|
||
expiry_loss = flat_total if flat_total <= 0 else flat_total
|
||
|
||
return {
|
||
"plan_type": "options_options",
|
||
"premium_paid": round(prem, 6),
|
||
"target_price": up_f, # 兼容旧字段,取上破
|
||
"target_price_up": up_f,
|
||
"target_price_down": down_f,
|
||
"winner_at_up": win_up,
|
||
"winner_at_down": win_dn,
|
||
"winner_at_target": win_up,
|
||
"scenarios": [
|
||
{
|
||
"id": "target_up",
|
||
"label": "上破目标",
|
||
"spot": up_f,
|
||
"leg_a_pnl": round(a_up, 4),
|
||
"leg_b_pnl": round(b_up, 4),
|
||
"total": round(at_up, 4),
|
||
"note": f"盈利方≈腿{win_up.upper()}(可平);亏损方默认到期",
|
||
},
|
||
{
|
||
"id": "target_down",
|
||
"label": "下破目标",
|
||
"spot": down_f,
|
||
"leg_a_pnl": round(a_dn, 4),
|
||
"leg_b_pnl": round(b_dn, 4),
|
||
"total": round(at_dn, 4),
|
||
"note": f"盈利方≈腿{win_dn.upper()}(可平);亏损方默认到期",
|
||
},
|
||
{
|
||
"id": "expiry_flat",
|
||
"label": "到期·现价(无突破)",
|
||
"spot": index_px,
|
||
"leg_a_pnl": round(a_flat, 4),
|
||
"leg_b_pnl": round(b_flat, 4),
|
||
"total": round(flat_total, 4),
|
||
"note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
|
||
},
|
||
{
|
||
"id": "max_premium_loss",
|
||
"label": "最大保费损耗",
|
||
"spot": None,
|
||
"leg_a_pnl": round(-prem_a, 4),
|
||
"leg_b_pnl": round(-prem_b, 4),
|
||
"total": round(-prem, 4),
|
||
"note": "双腿权利金全部损失",
|
||
},
|
||
],
|
||
"summary": {
|
||
"at_target_up_total": round(at_up, 4),
|
||
"at_target_down_total": round(at_dn, 4),
|
||
"at_target_total": round(at_up, 4),
|
||
"expiry_flat_total": round(expiry_loss, 4),
|
||
"premium_paid": round(prem, 6),
|
||
"expiry_is_loss": flat_total <= 0,
|
||
# 盈亏比:盈利/全亏保费(风险=权利金全损)
|
||
"rr_risk_premium": round(prem, 6),
|
||
"rr_at_up": round(at_up / prem, 4) if prem > 0 else None,
|
||
"rr_at_down": round(at_dn / prem, 4) if prem > 0 else None,
|
||
},
|
||
}
|
||
|
||
|
||
def gate_status(
|
||
*,
|
||
hedge_enabled: bool,
|
||
sizing_mode: str,
|
||
plan_type: str,
|
||
options_enabled: bool,
|
||
live_order: bool = False,
|
||
live_trading: bool = False,
|
||
active_count: int = 0,
|
||
max_active: int = 1,
|
||
show_perp_options: bool = True,
|
||
show_options_options: bool = True,
|
||
mutual_exclusive: bool = True,
|
||
has_standalone_option: bool = False,
|
||
) -> dict[str, Any]:
|
||
from lib.trade.position_sizing_lib import is_full_margin_mode
|
||
|
||
full = is_full_margin_mode(sizing_mode)
|
||
pt = (plan_type or "").strip().lower()
|
||
can_preview = True
|
||
can_start = True
|
||
reasons: list[str] = []
|
||
if not hedge_enabled:
|
||
can_start = False
|
||
reasons.append("对冲计划未启用(HEDGE_PLAN_ENABLED)")
|
||
if not options_enabled:
|
||
can_preview = False
|
||
can_start = False
|
||
reasons.append("期权模块未启用")
|
||
if pt == "perp_options" and not show_perp_options:
|
||
can_preview = False
|
||
can_start = False
|
||
reasons.append("永期对冲已隐藏(HEDGE_PLAN_SHOW_PERP_OPTIONS)")
|
||
if pt == "options_options" and not show_options_options:
|
||
can_preview = False
|
||
can_start = False
|
||
reasons.append("期期对冲已隐藏(HEDGE_PLAN_SHOW_OPTIONS_OPTIONS)")
|
||
if not live_order:
|
||
can_start = False
|
||
reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)")
|
||
if active_count >= max(1, int(max_active or 1)):
|
||
can_start = False
|
||
reasons.append(f"活跃计划已达上限({max_active})")
|
||
if mutual_exclusive and has_standalone_option:
|
||
can_start = False
|
||
reasons.append("存在单独期权持仓,禁止启动对冲计划(互斥门控)")
|
||
if pt == "perp_options":
|
||
if not full:
|
||
can_start = False
|
||
reasons.append("永期开仓仅全仓模式可用(当前可测算)")
|
||
if not live_trading:
|
||
can_start = False
|
||
reasons.append("未开启实盘(LIVE_TRADING_ENABLED)")
|
||
elif pt == "options_options":
|
||
pass
|
||
else:
|
||
can_start = False
|
||
reasons.append("未知计划类型")
|
||
if can_start:
|
||
reasons = []
|
||
return {
|
||
"hedge_enabled": hedge_enabled,
|
||
"options_enabled": options_enabled,
|
||
"sizing_mode": sizing_mode,
|
||
"is_full_margin": full,
|
||
"plan_type": pt,
|
||
"live_order": live_order,
|
||
"live_trading": live_trading,
|
||
"active_count": active_count,
|
||
"max_active": max_active,
|
||
"show_perp_options": bool(show_perp_options),
|
||
"show_options_options": bool(show_options_options),
|
||
"mutual_exclusive": bool(mutual_exclusive),
|
||
"has_standalone_option": bool(has_standalone_option),
|
||
"can_preview": can_preview,
|
||
"can_start": can_start,
|
||
"reasons": reasons,
|
||
}
|