diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py index ca44c44..b44cc53 100644 --- a/lib/hedge_plan/hedge_plan_calc_lib.py +++ b/lib/hedge_plan/hedge_plan_calc_lib.py @@ -58,6 +58,42 @@ def option_expiry_pnl( 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, @@ -484,6 +520,23 @@ def build_options_options_preview( 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 @@ -502,29 +555,29 @@ def build_options_options_preview( { "id": "rr_leg_a_full", "label": f"腿A达盈亏比{rr:g}(亏腿全损)", - "spot": None, + "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": "盈利=总权利金×盈亏比;亏损腿本合约全亏", + "note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏", }, { "id": "rr_leg_b_full", "label": f"腿B达盈亏比{rr:g}(亏腿全损)", - "spot": None, + "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": "盈利=总权利金×盈亏比;亏损腿本合约全亏", + "note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏", }, { "id": "rr_leg_a_residual", "label": f"腿A达盈亏比{rr:g}(亏腿残值20%)", - "spot": None, + "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": "亏损腿买一回收约本合约权利金20%", + "note": "现货同腿A达标反推;亏腿买一回收约本合约权利金20%", }, { "id": "expiry_flat", @@ -547,6 +600,8 @@ def build_options_options_preview( ], "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), diff --git a/tests/test_hedge_plan_calc.py b/tests/test_hedge_plan_calc.py index 76fe54c..60f4d63 100644 --- a/tests/test_hedge_plan_calc.py +++ b/tests/test_hedge_plan_calc.py @@ -114,6 +114,11 @@ class TestHedgePlanCalc(unittest.TestCase): self.assertEqual(len(p["scenarios"]), 5) self.assertEqual(p["scenarios"][0]["id"], "rr_leg_a_full") self.assertEqual(p["scenarios"][1]["id"], "rr_leg_b_full") + # 到期实值反推:Call 盈利20 → 价值25 → 每币2500 → spot=3300+2500 + self.assertEqual(p["scenarios"][0]["spot"], 5800.0) + # Put 盈利20 → spot=3100-2500 + self.assertEqual(p["scenarios"][1]["spot"], 600.0) + self.assertEqual(p["scenarios"][2]["spot"], 5800.0) # 残值情景同腿A反推 def test_oo_legacy_single_target_still_works(self): a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}