"""中控策略对比:同风险额下 合约 / 单期权 / 期期7:3 情景测算(纯函数).""" from __future__ import annotations import math 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 default_contract_size(base: str) -> float: """OKX 线性永续常用面值(币/张);与计算器缺省一致.""" b = (base or "ETH").strip().upper() return 0.01 def default_ct_mult(base: str) -> float: return 0.01 def floor_sheets(n: float, step: float = 1.0) -> float: if n is None or not math.isfinite(n) or n <= 0: return 0.0 s = float(step) if step and step > 0 else 1.0 return math.floor(n / s + 1e-12) * s def option_unit_cost(*, ask: float, ct_mult: float) -> float: return float(ask) * float(ct_mult or 0.01) def option_intrinsic_value( *, opt_type: str, strike: float, spot: float, sheets: float, ct_mult: float, ) -> float: o = (opt_type or "").strip().upper() k = float(strike) s = float(spot) if o == "C": intrinsic = max(0.0, s - k) elif o == "P": intrinsic = max(0.0, k - s) else: intrinsic = 0.0 return intrinsic * float(sheets) * float(ct_mult or 0.01) def option_pnl_at_spot( *, opt_type: str, strike: float, spot: float, sheets: float, ct_mult: float, premium_paid: float, ) -> float: return option_intrinsic_value( opt_type=opt_type, strike=strike, spot=spot, sheets=sheets, ct_mult=ct_mult, ) - float(premium_paid) def perp_pnl( *, direction: str, entry: float, exit_px: float, contracts: float, contract_size: float, ) -> float: coins = float(contracts) * float(contract_size or 0.01) d = (direction or "long").strip().lower() if d == "short": return (float(entry) - float(exit_px)) * coins return (float(exit_px) - float(entry)) * coins def _validate_common(inp: dict[str, Any]) -> Optional[str]: base = str(inp.get("base") or "ETH").strip().upper() if base not in ("ETH", "BTC"): return "标的仅支持 ETH / BTC" direction = str(inp.get("direction") or "long").strip().lower() if direction not in ("long", "short"): return "方向须为 long / short" s0 = _f(inp.get("entry")) sl = _f(inp.get("sl")) tp = _f(inp.get("tp")) risk = _f(inp.get("risk_u")) if s0 is None or s0 <= 0: return "请填写有效入场价" if sl is None or sl <= 0: return "请填写有效止损价" if tp is None or tp <= 0: return "请填写有效止盈价" if risk is None or risk <= 0: return "请填写有效风险额 R" if direction == "long" and not (sl < s0 < tp): return "做多须满足 止损 < 入场 < 止盈" if direction == "short" and not (tp < s0 < sl): return "做空须满足 止盈 < 入场 < 止损" return None def _calc_perp(inp: dict[str, Any], *, contract_size: float) -> dict[str, Any]: direction = str(inp.get("direction") or "long").strip().lower() s0 = float(inp["entry"]) sl = float(inp["sl"]) tp = float(inp["tp"]) risk = float(inp["risk_u"]) per_sheet_sl = abs(s0 - sl) * contract_size sheets = floor_sheets(risk / per_sheet_sl) if per_sheet_sl > 0 else 0.0 actual_sl_loss = abs(perp_pnl( direction=direction, entry=s0, exit_px=sl, contracts=sheets, contract_size=contract_size )) tp_pnl = perp_pnl( direction=direction, entry=s0, exit_px=tp, contracts=sheets, contract_size=contract_size ) # 路径 C:本单已止损 −actual;踏空未拿到 = 原止盈盈利 path_a = round(tp_pnl, 4) path_b = round(-actual_sl_loss if sheets > 0 else -risk, 4) path_c_realized = path_b path_c_missed = path_a return { "kind": "perp", "sheets": sheets, "contract_size": contract_size, "per_sheet_sl_u": round(per_sheet_sl, 6), "risk_used_u": round(actual_sl_loss, 4), "path_a_tp": path_a, "path_b_sl": path_b, "path_c_realized": path_c_realized, "path_c_missed": path_c_missed, "path_c_note": "本单已止损;踏空未拿到原止盈空间", "worst_u": path_b, } def _calc_single_option(inp: dict[str, Any], *, ct_mult: float) -> dict[str, Any]: direction = str(inp.get("direction") or "long").strip().lower() risk = float(inp["risk_u"]) tp = float(inp.get("tp_opt") if inp.get("tp_opt") not in (None, "") else inp["tp"]) sl = float(inp["sl"]) opt = inp.get("option") if isinstance(inp.get("option"), dict) else {} default_type = "C" if direction == "long" else "P" opt_type = str(opt.get("opt_type") or default_type).strip().upper() if opt_type not in ("C", "P"): opt_type = default_type strike = _f(opt.get("strike")) ask = _f(opt.get("ask")) if strike is None or strike <= 0: return {"ok": False, "msg": "请填写单期权行权价"} if ask is None or ask <= 0: return {"ok": False, "msg": "请填写单期权卖一价"} unit = option_unit_cost(ask=ask, ct_mult=ct_mult) sheets = floor_sheets(risk / unit) if unit > 0 else 0.0 premium = option_unit_cost(ask=ask, ct_mult=ct_mult) * sheets if sheets else 0.0 # 若张数为 0 path_a = option_pnl_at_spot( opt_type=opt_type, strike=strike, spot=tp, sheets=sheets, ct_mult=ct_mult, premium_paid=premium ) path_b_at_sl = option_pnl_at_spot( opt_type=opt_type, strike=strike, spot=sl, sheets=sheets, ct_mult=ct_mult, premium_paid=premium ) path_b_worst = -premium # 踏空路径:合约被洗后标的仍到 TP,期权仍持有 → 同止盈 path_c = path_a return { "ok": True, "kind": "option", "opt_type": opt_type, "strike": strike, "ask": ask, "ct_mult": ct_mult, "sheets": sheets, "unit_cost_u": round(unit, 6), "premium_u": round(premium, 4), "path_a_tp": round(path_a, 4), "path_b_sl": round(path_b_at_sl, 4), "path_b_worst": round(path_b_worst, 4), "path_c_hold_to_tp": round(path_c, 4), "path_c_note": "合约踏空路径下期权仍持有至目标价(内在近似)", "worst_u": round(path_b_worst, 4), } def _calc_hedge(inp: dict[str, Any], *, ct_mult: float) -> dict[str, Any]: direction = str(inp.get("direction") or "long").strip().lower() risk = float(inp["risk_u"]) tp = float(inp.get("tp_hedge") if inp.get("tp_hedge") not in (None, "") else inp["tp"]) sl = float(inp["sl"]) hedge = inp.get("hedge") if isinstance(inp.get("hedge"), dict) else {} main_default = "C" if direction == "long" else "P" side_default = "P" if direction == "long" else "C" main = hedge.get("main") if isinstance(hedge.get("main"), dict) else {} side = hedge.get("side") if isinstance(hedge.get("side"), dict) else {} main_type = str(main.get("opt_type") or main_default).strip().upper() side_type = str(side.get("opt_type") or side_default).strip().upper() if main_type not in ("C", "P"): main_type = main_default if side_type not in ("C", "P"): side_type = side_default main_k = _f(main.get("strike")) main_ask = _f(main.get("ask")) side_k = _f(side.get("strike")) side_ask = _f(side.get("ask")) if None in (main_k, main_ask, side_k, side_ask) or min( main_k or 0, main_ask or 0, side_k or 0, side_ask or 0 ) <= 0: return {"ok": False, "msg": "请填写期期对冲两腿的行权价与卖一"} main_budget = 0.7 * risk side_budget = 0.3 * risk main_unit = option_unit_cost(ask=float(main_ask), ct_mult=ct_mult) side_unit = option_unit_cost(ask=float(side_ask), ct_mult=ct_mult) main_sheets = floor_sheets(main_budget / main_unit) if main_unit > 0 else 0.0 side_sheets = floor_sheets(side_budget / side_unit) if side_unit > 0 else 0.0 main_prem = main_unit * main_sheets side_prem = side_unit * side_sheets premium = main_prem + side_prem def combo_at(spot: float) -> float: a = option_pnl_at_spot( opt_type=main_type, strike=float(main_k), spot=spot, sheets=main_sheets, ct_mult=ct_mult, premium_paid=main_prem, ) b = option_pnl_at_spot( opt_type=side_type, strike=float(side_k), spot=spot, sheets=side_sheets, ct_mult=ct_mult, premium_paid=side_prem, ) return a + b path_a = combo_at(tp) path_b_at_sl = combo_at(sl) path_b_worst = -premium path_c = path_a return { "ok": True, "kind": "hedge", "ratio": "7:3", "ct_mult": ct_mult, "main": { "opt_type": main_type, "strike": main_k, "ask": main_ask, "sheets": main_sheets, "premium_u": round(main_prem, 4), "budget_u": round(main_budget, 4), }, "side": { "opt_type": side_type, "strike": side_k, "ask": side_ask, "sheets": side_sheets, "premium_u": round(side_prem, 4), "budget_u": round(side_budget, 4), }, "premium_u": round(premium, 4), "path_a_tp": round(path_a, 4), "path_b_sl": round(path_b_at_sl, 4), "path_b_worst": round(path_b_worst, 4), "path_c_hold_to_tp": round(path_c, 4), "path_c_note": "合约踏空路径下对冲组合仍持有至目标价(内在近似)", "worst_u": round(path_b_worst, 4), } def recommend(perp: dict[str, Any], opt: dict[str, Any], hedge: dict[str, Any], risk: float) -> dict[str, Any]: """可解释规则推荐.""" candidates: list[tuple[str, float, dict[str, Any]]] = [] if perp and perp.get("sheets", 0) > 0: candidates.append(("合约", float(perp.get("path_a_tp") or 0), perp)) if opt and opt.get("ok") and opt.get("sheets", 0) > 0: candidates.append(("单期权", float(opt.get("path_a_tp") or 0), opt)) if hedge and hedge.get("ok") and (hedge.get("premium_u") or 0) > 0: candidates.append(("期期对冲", float(hedge.get("path_a_tp") or 0), hedge)) if not candidates: return { "choice": "—", "reason": "输入不足,无法推荐", "bullets": ["请检查风险额与卖一/止损距是否过小导致张数为 0"], } best_name, best_a, _ = max(candidates, key=lambda x: x[1]) perp_a = float(perp.get("path_a_tp") or 0) if perp else 0.0 opt_a = float(opt.get("path_a_tp") or 0) if opt and opt.get("ok") else 0.0 hedge_a = float(hedge.get("path_a_tp") or 0) if hedge and hedge.get("ok") else 0.0 # 踏空:合约 C 实现为亏损,期权/对冲 C 仍接近 A perp_miss = float(perp.get("path_c_missed") or 0) if perp else 0.0 opt_c = float(opt.get("path_c_hold_to_tp") or 0) if opt and opt.get("ok") else None hedge_c = float(hedge.get("path_c_hold_to_tp") or 0) if hedge and hedge.get("ok") else None anti_whipsaw = False if perp_miss > 0 and ( (opt_c is not None and opt_c > 0) or (hedge_c is not None and hedge_c > 0) ): anti_whipsaw = True # 合约止盈明显更高(>= 另两者 1.15 倍)且用户能接受踏空 → 推合约 others_max = max(opt_a, hedge_a, 0.0) choice = best_name if perp_a > 0 and perp_a >= others_max * 1.15 and perp_a >= best_a * 0.99: choice = "合约" if anti_whipsaw: reason = "合约止盈赔付更高,但震荡易洗时存在踏空;能接受洗盘再走可选合约" else: reason = "同风险下合约干净止盈赔付最高" elif anti_whipsaw and (opt_a > 0 or hedge_a > 0): # 抗踏空优先期权类;期期与单腿接近时推期期 if hedge_a > 0 and (opt_a <= 0 or hedge_a >= opt_a * 0.85): choice = "期期对冲" reason = "震荡易洗时期权类更抗踏空;期期 7:3 兼顾方向与保护" else: choice = "单期权" reason = "震荡易洗时单期权仍可持有到目标,抗踏空优于合约" else: reason = f"同风险下「{best_name}」干净止盈赔付最高" bullets = [ f"止盈对比:合约 {perp_a:.2f}U / 单期权 {opt_a:.2f}U / 期期 {hedge_a:.2f}U(风险 R={risk:.2f}U)", ( "止损与踏空:合约打止损即结束并可能踏空;" "期权/对冲最坏约亏满权利金,踏空路径下常仍持有至目标" if anti_whipsaw else "止损与踏空:三者最坏接近 −R;关注合约是否易被洗后错过止盈" ), f"选用建议:{reason}", ] return {"choice": choice, "reason": reason, "bullets": bullets} def run_compare(inp: dict[str, Any]) -> dict[str, Any]: err = _validate_common(inp) if err: return {"ok": False, "msg": err} base = str(inp.get("base") or "ETH").strip().upper() risk = float(inp["risk_u"]) cs = _f(inp.get("contract_size")) or default_contract_size(base) ct = _f(inp.get("ct_mult")) or default_ct_mult(base) perp = _calc_perp(inp, contract_size=float(cs)) opt = _calc_single_option(inp, ct_mult=float(ct)) hedge = _calc_hedge(inp, ct_mult=float(ct)) rec = recommend( perp, opt if opt.get("ok") else {"ok": False}, hedge if hedge.get("ok") else {"ok": False}, risk, ) warnings: list[str] = [] if perp.get("sheets", 0) <= 0: warnings.append("合约张数为 0:止损距过大或 R 过小") if isinstance(opt, dict) and opt.get("ok") and opt.get("sheets", 0) <= 0: warnings.append("单期权张数为 0:卖一过高或 R 过小") if isinstance(hedge, dict) and hedge.get("ok") and hedge.get("premium_u", 0) <= 0: warnings.append("期期对冲未开出张数:卖一过高或 R 过小") if isinstance(opt, dict) and not opt.get("ok"): warnings.append(str(opt.get("msg") or "单期权输入不完整")) if isinstance(hedge, dict) and not hedge.get("ok"): warnings.append(str(hedge.get("msg") or "期期对冲输入不完整")) return { "ok": True, "base": base, "direction": str(inp.get("direction") or "long").strip().lower(), "entry": float(inp["entry"]), "sl": float(inp["sl"]), "tp": float(inp["tp"]), "risk_u": risk, "contract_size": float(cs), "ct_mult": float(ct), "perp": perp, "option": opt, "hedge": hedge, "recommend": rec, "warnings": warnings, "notes": [ "期权止盈按标的到价的内在价值近似,非盘口卖出价", "到期小盈/小亏未纳入主表与推荐", "仅本地测算,不下单", ], }