Files
crypto_monitor/lib/hub/hub_compare_lib.py
T
2026-07-23 14:39:59 +08:00

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": [
"期权止盈按标的到价的内在价值近似,非盘口卖出价",
"到期小盈/小亏未纳入主表与推荐",
"仅本地测算,不下单",
],
}