ed3033d793
Co-authored-by: Cursor <cursoragent@cursor.com>
401 lines
15 KiB
Python
401 lines
15 KiB
Python
"""中控策略对比:同风险额下 合约 / 单期权 / 期期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": [
|
|
"期权止盈按标的到价的内在价值近似,非盘口卖出价",
|
|
"到期小盈/小亏未纳入主表与推荐",
|
|
"仅本地测算,不下单",
|
|
],
|
|
}
|