8ecc70a61c
Replace single S* with up/down targets across UI, preview, persist, monitor, and alerts so ranging breakouts can close the winner either way. Co-authored-by: Cursor <cursoragent@cursor.com>
386 lines
12 KiB
Python
386 lines
12 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 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 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,
|
||
index_px: float,
|
||
leg_a: dict[str, Any],
|
||
leg_b: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗."""
|
||
|
||
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),
|
||
)
|
||
|
||
# 兼容旧单目标:若未传上下目标则用 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)
|
||
|
||
prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
|
||
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(-float(leg_a.get("premium_paid") or 0), 4),
|
||
"leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 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,
|
||
},
|
||
}
|
||
|
||
|
||
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,
|
||
) -> 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 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 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,
|
||
"can_preview": can_preview,
|
||
"can_start": can_start,
|
||
"reasons": reasons,
|
||
}
|