Add perpetual-options hedge overlay to amp-stats with hit rates and daily PnL.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+278
-4
@@ -249,10 +249,12 @@ def enrich_rows_pnl(
|
||||
*,
|
||||
straddle_premium: Optional[float] = None,
|
||||
take_profit: Optional[float] = None,
|
||||
perp_hedge: Any = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""为日表附加有效波动 / 是否触达止盈 / 收益(有权利金时)."""
|
||||
"""为日表附加有效波动 / 是否触达止盈 / 收益(有权利金时) / 永期盈亏."""
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
||||
out: list[dict[str, Any]] = []
|
||||
for r in rows or []:
|
||||
item = dict(r)
|
||||
@@ -272,10 +274,200 @@ def enrich_rows_pnl(
|
||||
item["effective_move"] = round(move, 4)
|
||||
item["take_profit_hit"] = hit
|
||||
item["profit"] = round(move - prem, 4) if prem is not None else None
|
||||
if hedge is not None:
|
||||
item["perp_hedge_pnl"] = perp_hedge_day_pnl(
|
||||
change=float(item.get("change") or 0),
|
||||
open_px=float(item.get("open") or 0),
|
||||
close_px=float(item.get("close") or 0),
|
||||
premium_total=float(hedge["premium_total"]),
|
||||
opt_coins=float(hedge["opt_coins"]),
|
||||
)
|
||||
else:
|
||||
item["perp_hedge_pnl"] = None
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_perp_hedge_params(raw: Any) -> Optional[dict[str, float]]:
|
||||
"""永期对冲对照参数.缺必填则返回 None(不做对照).
|
||||
|
||||
接受 dict 或带 spot/target_profit_u/perp_leverage/option_leverage 的对象字段.
|
||||
"""
|
||||
if raw is None or raw == "":
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
spot = _safe_float(raw.get("spot"))
|
||||
target = _safe_float(raw.get("target_profit_u") if "target_profit_u" in raw else raw.get("target"))
|
||||
p_lev = _safe_float(raw.get("perp_leverage"))
|
||||
o_lev = _safe_float(raw.get("option_leverage"))
|
||||
rp = _safe_float(raw.get("ratio_perp"))
|
||||
ro = _safe_float(raw.get("ratio_opt"))
|
||||
ct = _safe_float(raw.get("ct_mult"))
|
||||
if spot is None or target is None or p_lev is None or o_lev is None:
|
||||
return None
|
||||
if spot <= 0 or target < 0 or p_lev <= 0 or o_lev <= 0:
|
||||
return None
|
||||
if rp is None or rp <= 0:
|
||||
rp = 1.0
|
||||
if ro is None or ro <= 0:
|
||||
ro = 2.0
|
||||
if ct is None or ct <= 0:
|
||||
ct = 0.01
|
||||
prem_per_coin = spot / o_lev
|
||||
opt_coins = 1.0 * (ro / rp)
|
||||
premium_total = opt_coins * prem_per_coin
|
||||
return {
|
||||
"spot": spot,
|
||||
"target_profit_u": target,
|
||||
"perp_leverage": p_lev,
|
||||
"option_leverage": o_lev,
|
||||
"ratio_perp": rp,
|
||||
"ratio_opt": ro,
|
||||
"ct_mult": ct,
|
||||
"prem_per_coin": prem_per_coin,
|
||||
"opt_coins": opt_coins,
|
||||
"opt_sheets": opt_coins / ct,
|
||||
"premium_total": premium_total,
|
||||
}
|
||||
|
||||
|
||||
def perp_hedge_day_pnl(
|
||||
*,
|
||||
change: float,
|
||||
open_px: float,
|
||||
close_px: float,
|
||||
premium_total: float,
|
||||
opt_coins: float,
|
||||
) -> float:
|
||||
"""单日组合净利(永续多1币 + 买期权).
|
||||
|
||||
上涨: change − 权利金 − 永续开平手续费
|
||||
下跌: |change|×(opt_coins−1) − 权利金
|
||||
"""
|
||||
from lib.trade.trade_fee_lib import estimate_roundtrip_fee_usdt
|
||||
|
||||
chg = float(change or 0)
|
||||
prem = float(premium_total or 0)
|
||||
coins = float(opt_coins or 0)
|
||||
if chg >= 0:
|
||||
fee = 0.0
|
||||
if open_px and close_px and open_px > 0 and close_px > 0:
|
||||
fee = estimate_roundtrip_fee_usdt(open_px, close_px, qty=1.0, contract_size=1.0)
|
||||
return round(chg - prem - fee, 4)
|
||||
# 下跌: 永续亏 chg(负), 期权内在 |chg|*coins
|
||||
return round(abs(chg) * (coins - 1.0) - prem, 4)
|
||||
|
||||
|
||||
def perp_hedge_stats(
|
||||
rows: list[dict[str, Any]],
|
||||
hedge: dict[str, float],
|
||||
) -> dict[str, Any]:
|
||||
"""永期对冲:所需点数达标 + 按日组合盈亏汇总."""
|
||||
from lib.hub.hub_perp_options_calc_lib import calc_perp_options_points
|
||||
|
||||
points_data, points_err = calc_perp_options_points(
|
||||
base="ETH",
|
||||
spot=hedge["spot"],
|
||||
capital_usdt=max(hedge["spot"] / hedge["perp_leverage"] * 2, 1000.0),
|
||||
target_profit_u=hedge["target_profit_u"],
|
||||
perp_leverage=hedge["perp_leverage"],
|
||||
option_leverage=hedge["option_leverage"],
|
||||
ratio_perp=hedge["ratio_perp"],
|
||||
ratio_opt=hedge["ratio_opt"],
|
||||
ct_mult=hedge["ct_mult"],
|
||||
)
|
||||
move_a = None
|
||||
move_b = None
|
||||
if points_data:
|
||||
move_a = float((points_data.get("case_a") or {}).get("move_points") or 0) or None
|
||||
mb = (points_data.get("case_b") or {}).get("move_points_portfolio")
|
||||
move_b = float(mb) if mb is not None else None
|
||||
|
||||
# 使用已 enrich 的 perp_hedge_pnl;若无则当场补算
|
||||
work: list[dict[str, Any]] = []
|
||||
for r in rows or []:
|
||||
item = dict(r)
|
||||
if item.get("perp_hedge_pnl") is None:
|
||||
item["perp_hedge_pnl"] = perp_hedge_day_pnl(
|
||||
change=float(item.get("change") or 0),
|
||||
open_px=float(item.get("open") or 0),
|
||||
close_px=float(item.get("close") or 0),
|
||||
premium_total=float(hedge["premium_total"]),
|
||||
opt_coins=float(hedge["opt_coins"]),
|
||||
)
|
||||
work.append(item)
|
||||
n = len(work)
|
||||
empty = {
|
||||
"enabled": True,
|
||||
"spot": round(hedge["spot"], 4),
|
||||
"target_profit_u": round(hedge["target_profit_u"], 4),
|
||||
"perp_leverage": round(hedge["perp_leverage"], 4),
|
||||
"option_leverage": round(hedge["option_leverage"], 4),
|
||||
"ratio_perp": round(hedge["ratio_perp"], 4),
|
||||
"ratio_opt": round(hedge["ratio_opt"], 4),
|
||||
"ratio_label": f"{hedge['ratio_perp']:g}:{hedge['ratio_opt']:g}",
|
||||
"prem_per_coin": round(hedge["prem_per_coin"], 4),
|
||||
"opt_coins": round(hedge["opt_coins"], 4),
|
||||
"opt_sheets": round(hedge["opt_sheets"], 4),
|
||||
"premium_total": round(hedge["premium_total"], 4),
|
||||
"move_a": None if move_a is None else round(move_a, 4),
|
||||
"move_b": None if move_b is None else round(move_b, 4),
|
||||
"points_error": points_err,
|
||||
"sample_count": n,
|
||||
"hit_a_days": 0,
|
||||
"hit_a_ratio": None,
|
||||
"hit_b_days": 0,
|
||||
"hit_b_ratio": None,
|
||||
"pnl_total": None,
|
||||
"pnl_avg": None,
|
||||
"win_days": 0,
|
||||
"win_ratio": None,
|
||||
"pnl_max": None,
|
||||
"pnl_min": None,
|
||||
"up_days": 0,
|
||||
"down_days": 0,
|
||||
"up_pnl_total": None,
|
||||
"down_pnl_total": None,
|
||||
}
|
||||
if n <= 0:
|
||||
return empty
|
||||
|
||||
hit_a = 0
|
||||
hit_b = 0
|
||||
if move_a is not None and move_a > 0:
|
||||
hit_a = sum(1 for r in work if float(r.get("change") or 0) >= move_a)
|
||||
if move_b is not None and move_b > 0:
|
||||
hit_b = sum(1 for r in work if float(r.get("change") or 0) <= -move_b)
|
||||
|
||||
pnls = [float(r["perp_hedge_pnl"]) for r in work if r.get("perp_hedge_pnl") is not None]
|
||||
win = sum(1 for p in pnls if p > 0)
|
||||
up_rows = [r for r in work if float(r.get("change") or 0) >= 0]
|
||||
down_rows = [r for r in work if float(r.get("change") or 0) < 0]
|
||||
up_pnls = [float(r["perp_hedge_pnl"]) for r in up_rows if r.get("perp_hedge_pnl") is not None]
|
||||
down_pnls = [float(r["perp_hedge_pnl"]) for r in down_rows if r.get("perp_hedge_pnl") is not None]
|
||||
|
||||
empty.update(
|
||||
{
|
||||
"hit_a_days": hit_a,
|
||||
"hit_a_ratio": round(hit_a / n, 4) if move_a else None,
|
||||
"hit_b_days": hit_b,
|
||||
"hit_b_ratio": round(hit_b / n, 4) if move_b else None,
|
||||
"pnl_total": round(sum(pnls), 4) if pnls else None,
|
||||
"pnl_avg": round(statistics.fmean(pnls), 4) if pnls else None,
|
||||
"win_days": win,
|
||||
"win_ratio": round(win / n, 4),
|
||||
"pnl_max": round(max(pnls), 4) if pnls else None,
|
||||
"pnl_min": round(min(pnls), 4) if pnls else None,
|
||||
"up_days": len(up_rows),
|
||||
"down_days": len(down_rows),
|
||||
"up_pnl_total": round(sum(up_pnls), 4) if up_pnls else None,
|
||||
"down_pnl_total": round(sum(down_pnls), 4) if down_pnls else None,
|
||||
}
|
||||
)
|
||||
return empty
|
||||
|
||||
|
||||
def straddle_long_stats(
|
||||
rows: list[dict[str, Any]],
|
||||
premium: float,
|
||||
@@ -343,7 +535,9 @@ def summarize_rows(
|
||||
*,
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
perp_hedge: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
||||
if not rows:
|
||||
out = {
|
||||
"sample_count": 0,
|
||||
@@ -358,10 +552,13 @@ def summarize_rows(
|
||||
"up_day_ratio": None,
|
||||
"down_day_ratio": None,
|
||||
"straddle": None,
|
||||
"perp_hedge": None,
|
||||
}
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
if prem is not None:
|
||||
out["straddle"] = straddle_long_stats([], prem, take_profit=take_profit)
|
||||
if hedge is not None:
|
||||
out["perp_hedge"] = perp_hedge_stats([], hedge)
|
||||
return out
|
||||
amps = [float(r["amplitude"]) for r in rows]
|
||||
ups = [float(r["up_points"]) for r in rows]
|
||||
@@ -384,10 +581,13 @@ def summarize_rows(
|
||||
"up_day_ratio": round(up_days / n, 4),
|
||||
"down_day_ratio": round(down_days / n, 4),
|
||||
"straddle": None,
|
||||
"perp_hedge": None,
|
||||
}
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
if prem is not None:
|
||||
out["straddle"] = straddle_long_stats(rows, prem, take_profit=take_profit)
|
||||
if hedge is not None:
|
||||
out["perp_hedge"] = perp_hedge_stats(rows, hedge)
|
||||
return out
|
||||
|
||||
|
||||
@@ -594,6 +794,7 @@ def compute_amp_stats(
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
perp_hedge: Any = None,
|
||||
now: Optional[datetime] = None,
|
||||
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -604,6 +805,7 @@ def compute_amp_stats(
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
we_mode = normalize_weekend_filter(weekend_filter)
|
||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
||||
sample_days = resolve_sample_days(period, custom_days)
|
||||
settlements = list_settlement_dates(sample_days=sample_days, now=now)
|
||||
if not settlements:
|
||||
@@ -636,6 +838,7 @@ def compute_amp_stats(
|
||||
straddle_premium=prem,
|
||||
take_profit=tp,
|
||||
weekend_filter=we_mode,
|
||||
perp_hedge=hedge,
|
||||
price_source=price_source,
|
||||
inst_id=inst_id,
|
||||
missing=missing,
|
||||
@@ -652,6 +855,7 @@ def build_amp_result(
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
perp_hedge: Any = None,
|
||||
price_source: str = "",
|
||||
inst_id: str = "",
|
||||
missing: Optional[list[str]] = None,
|
||||
@@ -659,9 +863,10 @@ def build_amp_result(
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
we_mode = normalize_weekend_filter(weekend_filter)
|
||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
||||
filtered = filter_weekend_rows(rows_all, we_mode)
|
||||
rows = enrich_rows_pnl(filtered, straddle_premium=prem, take_profit=tp)
|
||||
summary = summarize_rows(rows, straddle_premium=prem, take_profit=tp)
|
||||
rows = enrich_rows_pnl(filtered, straddle_premium=prem, take_profit=tp, perp_hedge=hedge)
|
||||
summary = summarize_rows(rows, straddle_premium=prem, take_profit=tp, perp_hedge=hedge)
|
||||
if period == "custom" or str(period).startswith("custom:"):
|
||||
period_label = period if str(period).startswith("custom:") else f"custom:{sample_days}"
|
||||
else:
|
||||
@@ -679,6 +884,7 @@ def build_amp_result(
|
||||
"straddle_premium": prem,
|
||||
"take_profit": tp,
|
||||
"weekend_filter": we_mode,
|
||||
"perp_hedge": hedge,
|
||||
"timeframe": TIMEFRAME,
|
||||
"price_source": price_source,
|
||||
"inst_id": inst_id,
|
||||
@@ -701,11 +907,12 @@ def reframe_amp_stats(
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
perp_hedge: Any = None,
|
||||
price_source: str = "",
|
||||
inst_id: str = "",
|
||||
missing: Optional[list[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""已有日表上改周末/权利金/止盈,不拉 K 线."""
|
||||
"""已有日表上改周末/权利金/止盈/永期参数,不拉 K 线."""
|
||||
key = normalize_symbol(symbol)
|
||||
return build_amp_result(
|
||||
rows_all=list(rows_all or []),
|
||||
@@ -716,6 +923,7 @@ def reframe_amp_stats(
|
||||
straddle_premium=straddle_premium,
|
||||
take_profit=take_profit,
|
||||
weekend_filter=weekend_filter,
|
||||
perp_hedge=perp_hedge,
|
||||
price_source=price_source,
|
||||
inst_id=inst_id,
|
||||
missing=missing,
|
||||
@@ -779,6 +987,70 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
]
|
||||
)
|
||||
w.writerow(["单日最大赚", st.get("pnl_max"), "单日最大亏", st.get("pnl_min")])
|
||||
ph = s.get("perp_hedge") or {}
|
||||
if ph:
|
||||
w.writerow([])
|
||||
w.writerow(
|
||||
[
|
||||
"【永期对冲对照】",
|
||||
"比例",
|
||||
ph.get("ratio_label"),
|
||||
"现价",
|
||||
ph.get("spot"),
|
||||
"目标",
|
||||
ph.get("target_profit_u"),
|
||||
]
|
||||
)
|
||||
w.writerow(
|
||||
[
|
||||
"单币权利金",
|
||||
ph.get("prem_per_coin"),
|
||||
"权利金总额",
|
||||
ph.get("premium_total"),
|
||||
"期权币数",
|
||||
ph.get("opt_coins"),
|
||||
]
|
||||
)
|
||||
w.writerow(
|
||||
[
|
||||
"A所需点数",
|
||||
ph.get("move_a"),
|
||||
"A达标天",
|
||||
ph.get("hit_a_days"),
|
||||
"占比",
|
||||
ph.get("hit_a_ratio"),
|
||||
]
|
||||
)
|
||||
w.writerow(
|
||||
[
|
||||
"B所需点数(组合)",
|
||||
ph.get("move_b"),
|
||||
"B达标天",
|
||||
ph.get("hit_b_days"),
|
||||
"占比",
|
||||
ph.get("hit_b_ratio"),
|
||||
]
|
||||
)
|
||||
w.writerow(
|
||||
[
|
||||
"组合盈亏合计",
|
||||
ph.get("pnl_total"),
|
||||
"日均",
|
||||
ph.get("pnl_avg"),
|
||||
"胜率",
|
||||
ph.get("win_ratio"),
|
||||
]
|
||||
)
|
||||
w.writerow(
|
||||
[
|
||||
"上涨日盈亏",
|
||||
ph.get("up_pnl_total"),
|
||||
"下跌日盈亏",
|
||||
ph.get("down_pnl_total"),
|
||||
"最大赚/亏",
|
||||
f"{ph.get('pnl_max')} / {ph.get('pnl_min')}",
|
||||
]
|
||||
)
|
||||
w.writerow([])
|
||||
w.writerow(["【日表明细】"])
|
||||
w.writerow(
|
||||
@@ -799,6 +1071,7 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
"有效波动",
|
||||
"触达止盈",
|
||||
"收益",
|
||||
"永期盈亏",
|
||||
]
|
||||
)
|
||||
for r in payload.get("rows") or []:
|
||||
@@ -820,6 +1093,7 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
r.get("effective_move"),
|
||||
"是" if r.get("take_profit_hit") else "否",
|
||||
r.get("profit"),
|
||||
r.get("perp_hedge_pnl"),
|
||||
]
|
||||
)
|
||||
return buf.getvalue()
|
||||
|
||||
Reference in New Issue
Block a user