From 2ce67da8e8da376503f36674dbc3d8151fcea0fd Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 28 Jul 2026 14:31:17 +0800 Subject: [PATCH] Use daily open as perp-hedge entry and toggle buy-straddle vs perp overlays. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amp-stats now prices premium from each day's open, and the form switches mutually between straddle and perpetual-options对照. Co-authored-by: Cursor --- docs/振幅统计说明.md | 30 +++++-- lib/hub/amp_stats_lib.py | 114 +++++++++++++++---------- manual_trading_hub/amp_stats_routes.py | 12 ++- manual_trading_hub/static/amp_stats.js | 69 ++++++++++----- manual_trading_hub/static/index.html | 41 +++++---- tests/test_amp_stats_lib.py | 22 +++-- 6 files changed, 191 insertions(+), 97 deletions(-) diff --git a/docs/振幅统计说明.md b/docs/振幅统计说明.md index 2c17796..ced8f52 100644 --- a/docs/振幅统计说明.md +++ b/docs/振幅统计说明.md @@ -58,6 +58,19 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / --- +## 对照模式(买跨 / 永期二选一) + +表单 **对照模式** 切换: + +| 模式 | 表单 | 汇总块 | 日表末列 | +|------|------|--------|----------| +| 买跨双边 | 双边权利金、止盈点 | 买跨对照 | 收益 | +| 永期对冲 | 目标盈利、杠杆、比例 | 永期对冲对照 | 永期盈亏 | + +同一时刻只计算 / 展示当前模式;切换后在已有日表上本地重算。 + +--- + ## 买跨对照(赌波动) 表单可填 **双边权利金(点)**,例如 `30`;旁边可填 **止盈点**(可空): @@ -86,14 +99,16 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / 与中控 [永期对冲计算器](./永期对冲计算器.md) 同口径:**永续做多 1 币 + 买期权**(默认比例 **1:2**),在历史振幅日表上做对照。 -表单填:现价、目标盈利、永续杠杆、期权杠杆、比例(可改)。未填齐现价/目标/杠杆时不计算该块。 +表单填:目标盈利、永续杠杆、期权杠杆、比例(可改)。**入场价 = 当日开盘**,不再填现价。未填齐目标/杠杆时不计算该块。 ```text -单币权利金 = 现价 / 期权杠杆 -期权币数 = 1 × (期权比例 / 永续比例) -权利金总额 = 期权币数 × 单币权利金 +单币权利金(日) = 当日开盘 / 期权杠杆 +期权币数 = 1 × (期权比例 / 永续比例) +权利金总额(日) = 期权币数 × 单币权利金(日) ``` +推「所需点数」时,用样本 **开盘中位数** 作入场参照(汇总里展示的权利金中位同口径)。 + ### ① 所需点数达标 复用计算器「由比例推点数」: @@ -109,11 +124,11 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / | 日向 | 组合净利 | |------|----------| -| 上涨 `涨跌≥0` | `涨跌 − 权利金 − 永续开平手续费(开→收)` | -| 下跌 `涨跌<0` | `\|涨跌\|×(期权币数−1) − 权利金` | +| 上涨 `涨跌≥0` | `涨跌 − 当日权利金 − 永续开平手续费(开→收)` | +| 下跌 `涨跌<0` | `\|涨跌\|×(期权币数−1) − 当日权利金` | 汇总:合计、日均、胜率、上涨日/下跌日盈亏小计、单日最大赚亏;日表 **永期盈亏** 列。 -改永期参数 / 周末筛选会**本地重算**(不重拉 K 线)。权利金按表单现价固定,不按日开盘重估。 +改永期参数 / 周末筛选会**本地重算**(不重拉 K 线)。 --- @@ -146,3 +161,4 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / | 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 | | 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 | | 2026-07-28 | 永期对冲对照:所需点数达标 + 按日组合盈亏 | +| 2026-07-28 | 永期入场改按日开盘;买跨/永期对照模式二选一 | diff --git a/lib/hub/amp_stats_lib.py b/lib/hub/amp_stats_lib.py index ed27dc0..03e97fe 100644 --- a/lib/hub/amp_stats_lib.py +++ b/lib/hub/amp_stats_lib.py @@ -279,7 +279,7 @@ def enrich_rows_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"]), + option_leverage=float(hedge["option_leverage"]), opt_coins=float(hedge["opt_coins"]), ) else: @@ -291,22 +291,21 @@ def enrich_rows_pnl( def normalize_perp_hedge_params(raw: Any) -> Optional[dict[str, float]]: """永期对冲对照参数.缺必填则返回 None(不做对照). - 接受 dict 或带 spot/target_profit_u/perp_leverage/option_leverage 的对象字段. + 入场价按日开盘;表单只需目标盈利/杠杆/比例.旧字段 spot 可忽略. """ 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: + if 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: + if target < 0 or p_lev <= 0 or o_lev <= 0: return None if rp is None or rp <= 0: rp = 1.0 @@ -314,33 +313,38 @@ def normalize_perp_hedge_params(raw: Any) -> Optional[dict[str, float]]: 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_premium(*, open_px: float, option_leverage: float, opt_coins: float) -> float: + """单日权利金总额 = 开盘 / 期权杠杆 × 期权币数.""" + o = float(open_px or 0) + lev = float(option_leverage or 0) + coins = float(opt_coins or 0) + if o <= 0 or lev <= 0 or coins < 0: + return 0.0 + return coins * (o / lev) + + def perp_hedge_day_pnl( *, change: float, open_px: float, close_px: float, - premium_total: float, + option_leverage: float, opt_coins: float, ) -> float: - """单日组合净利(永续多1币 + 买期权). + """单日组合净利(永续多1币 + 买期权);入场/权利金按当日开盘. 上涨: change − 权利金 − 永续开平手续费 下跌: |change|×(opt_coins−1) − 权利金 @@ -348,7 +352,9 @@ def perp_hedge_day_pnl( from lib.trade.trade_fee_lib import estimate_roundtrip_fee_usdt chg = float(change or 0) - prem = float(premium_total or 0) + prem = perp_hedge_day_premium( + open_px=open_px, option_leverage=option_leverage, opt_coins=opt_coins + ) coins = float(opt_coins or 0) if chg >= 0: fee = 0.0 @@ -363,54 +369,74 @@ 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"], + opens = [float(r.get("open") or 0) for r in (rows or []) if float(r.get("open") or 0) > 0] + spot_ref = statistics.median(opens) if opens else None + prem_ref = ( + perp_hedge_day_premium( + open_px=spot_ref, + option_leverage=hedge["option_leverage"], + opt_coins=hedge["opt_coins"], + ) + if spot_ref is not None + else None ) + prem_per_coin_ref = (spot_ref / hedge["option_leverage"]) if spot_ref is not None else None + 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 + points_err = None + if spot_ref is not None and spot_ref > 0: + points_data, points_err = calc_perp_options_points( + base="ETH", + spot=spot_ref, + capital_usdt=max(spot_ref / 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"], + ) + 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 + else: + points_err = "样本无有效开盘价,无法推所需点数" - # 使用已 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"]), - ) + 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), + option_leverage=float(hedge["option_leverage"]), + opt_coins=float(hedge["opt_coins"]), + ) work.append(item) n = len(work) empty = { "enabled": True, - "spot": round(hedge["spot"], 4), + "entry": "open", + "spot": None if spot_ref is None else round(spot_ref, 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), + "prem_per_coin": None if prem_per_coin_ref is None else round(prem_per_coin_ref, 4), "opt_coins": round(hedge["opt_coins"], 4), "opt_sheets": round(hedge["opt_sheets"], 4), - "premium_total": round(hedge["premium_total"], 4), + "premium_total": None if prem_ref is None else round(prem_ref, 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, @@ -995,7 +1021,9 @@ def build_export_csv(payload: dict[str, Any]) -> str: "【永期对冲对照】", "比例", ph.get("ratio_label"), - "现价", + "入场", + "按日开盘", + "推点数开盘中位", ph.get("spot"), "目标", ph.get("target_profit_u"), @@ -1003,9 +1031,9 @@ def build_export_csv(payload: dict[str, Any]) -> str: ) w.writerow( [ - "单币权利金", + "单币权利金(开盘中位)", ph.get("prem_per_coin"), - "权利金总额", + "权利金总额(开盘中位)", ph.get("premium_total"), "期权币数", ph.get("opt_coins"), diff --git a/manual_trading_hub/amp_stats_routes.py b/manual_trading_hub/amp_stats_routes.py index 04007ed..6f23b00 100644 --- a/manual_trading_hub/amp_stats_routes.py +++ b/manual_trading_hub/amp_stats_routes.py @@ -103,7 +103,11 @@ def create_amp_stats_router() -> APIRouter: "timeframe": "1H", "metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)", "straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金", - "perp_hedge_note": "永期对冲:永续多1币+买期权;比例默认1:2;达标与组合盈亏见文档", + "perp_hedge_note": "永期对冲:永续多1币+买期权;入场按日开盘;比例默认1:2;与买跨二选一对照", + "overlay_modes": [ + {"key": "straddle", "label": "买跨双边"}, + {"key": "perp", "label": "永期对冲"}, + ], } @router.post("/compute") @@ -198,7 +202,6 @@ def create_amp_stats_router() -> APIRouter: hedge_ct_mult: float = Query(default=0.01), ): hedge_q = { - "spot": hedge_spot, "target_profit_u": hedge_target, "perp_leverage": hedge_perp_lev, "option_leverage": hedge_opt_lev, @@ -206,13 +209,14 @@ def create_amp_stats_router() -> APIRouter: "ratio_opt": hedge_ratio_opt, "ct_mult": hedge_ct_mult, } + hedge_q_ready = hedge_target is not None and hedge_perp_lev is not None and hedge_opt_lev is not None if (history_id or "").strip(): item = get_history(history_id.strip()) if not item: raise HTTPException(status_code=404, detail="历史不存在") rows_all = item.get("rows_all") or item.get("rows") or [] item_hedge = item.get("perp_hedge") if isinstance(item.get("perp_hedge"), dict) else None - use_hedge = hedge_q if hedge_spot is not None else item_hedge + use_hedge = hedge_q if hedge_q_ready else item_hedge try: payload = reframe_amp_stats( rows_all=rows_all, @@ -246,7 +250,7 @@ def create_amp_stats_router() -> APIRouter: straddle_premium=straddle_premium, take_profit=take_profit, weekend_filter=weekend_filter, - perp_hedge=hedge_q, + perp_hedge=hedge_q if hedge_q_ready else None, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc diff --git a/manual_trading_hub/static/amp_stats.js b/manual_trading_hub/static/amp_stats.js index 4ba6f79..9a7fbf4 100644 --- a/manual_trading_hub/static/amp_stats.js +++ b/manual_trading_hub/static/amp_stats.js @@ -39,7 +39,22 @@ return (n * 100).toFixed(1) + "%"; } + function overlayMode() { + return el("amp-overlay-mode")?.value || "straddle"; + } + + function syncOverlayMode() { + const isPerp = overlayMode() === "perp"; + page.querySelectorAll(".amp-overlay-straddle").forEach((n) => n.classList.toggle("hidden", isPerp)); + page.querySelectorAll(".amp-overlay-perp").forEach((n) => n.classList.toggle("hidden", !isPerp)); + el("amp-overlay-straddle-block")?.classList.toggle("hidden", isPerp); + el("amp-overlay-perp-block")?.classList.toggle("hidden", !isPerp); + const col = el("amp-col-pnl"); + if (col) col.textContent = isPerp ? "永期盈亏" : "收益"; + } + function readPremium() { + if (overlayMode() !== "straddle") return null; const raw = (el("amp-straddle-premium")?.value || "").trim(); if (!raw) return null; const n = Number(raw); @@ -48,6 +63,7 @@ } function readTakeProfit() { + if (overlayMode() !== "straddle") return null; const raw = (el("amp-take-profit")?.value || "").trim(); if (!raw) return null; const n = Number(raw); @@ -67,14 +83,13 @@ } function readPerpHedge() { - const spot = readNum("amp-hedge-spot"); + if (overlayMode() !== "perp") return null; const target = readNum("amp-hedge-target"); const perpLev = readNum("amp-hedge-perp-lev"); const optLev = readNum("amp-hedge-opt-lev"); - if (spot == null || target == null || perpLev == null || optLev == null) return null; - if (spot <= 0 || target < 0 || perpLev <= 0 || optLev <= 0) return null; + if (target == null || perpLev == null || optLev == null) return null; + if (target < 0 || perpLev <= 0 || optLev <= 0) return null; return { - spot, target_profit_u: target, perp_leverage: perpLev, option_leverage: optLev, @@ -130,6 +145,7 @@ const box = el("amp-summary"); if (!box) return; const s = summary || {}; + syncOverlayMode(); if (!s.sample_count) { box.innerHTML = '

暂无汇总

'; renderStraddle(null); @@ -147,8 +163,13 @@ `
涨/跌窗占比${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}
` + `
价源${esc(result && result.price_source)}
` + ``; - renderStraddle(s.straddle); - renderPerpHedge(s.perp_hedge); + if (overlayMode() === "perp") { + renderStraddle(null); + renderPerpHedge(s.perp_hedge); + } else { + renderPerpHedge(null); + renderStraddle(s.straddle); + } } function renderStraddle(st) { @@ -189,7 +210,7 @@ if (!box) return; if (!ph) { box.innerHTML = - '

填写「永期·现价 / 目标 / 杠杆」后计算;对照所需点数达标天数与组合盈亏(永续多1币+买期权)

'; + '

填写「目标 / 杠杆」后计算;入场按日开盘;对照所需点数达标与组合盈亏(永续多1币+买期权)

'; return; } const err = @@ -198,8 +219,9 @@ : ""; box.innerHTML = `
` + + `
入场按日开盘 · 推点数中位 ${esc(ph.spot)}
` + `
比例 / 期权仓${esc(ph.ratio_label)} · ${esc(ph.opt_coins)} 币
` + - `
单币/总权利金${esc(ph.prem_per_coin)} / ${esc(ph.premium_total)}
` + + `
单币/总权利金(中位)${esc(ph.prem_per_coin)} / ${esc(ph.premium_total)}
` + `
A所需点数(永续对)${esc(ph.move_a)}
` + `
A达标${esc(ph.hit_a_days)} 天 · ${esc(pct(ph.hit_a_ratio))}
` + `
B所需点数(组合)${esc(ph.move_b)}
` + @@ -225,20 +247,19 @@ const body = el("amp-table-body"); const pager = el("amp-pager"); if (!body) return; + const isPerp = overlayMode() === "perp"; + syncOverlayMode(); const rows = (pagePayload && pagePayload.rows) || []; if (!rows.length) { - body.innerHTML = '暂无数据'; + body.innerHTML = '暂无数据'; } else { body.innerHTML = rows .map((r) => { - const profit = - r.profit == null || r.profit === "" + const pnlVal = isPerp ? r.perp_hedge_pnl : r.profit; + const pnlCell = + pnlVal == null || pnlVal === "" ? "—" - : `${esc(r.profit)}`; - const hedgePnl = - r.perp_hedge_pnl == null || r.perp_hedge_pnl === "" - ? "—" - : `${esc(r.perp_hedge_pnl)}`; + : `${esc(pnlVal)}`; const trClass = r.is_weekend ? ' class="amp-row-weekend"' : ""; return ( `` + @@ -252,8 +273,7 @@ `${esc(r.down_points)}` + `${esc(r.amplitude)}` + `${esc(r.change)}` + - `${profit}` + - `${hedgePnl}` + + `${pnlCell}` + `` ); }) @@ -389,7 +409,6 @@ function appendHedgeQuery(q) { const h = readPerpHedge(); if (!h) return; - q.set("hedge_spot", String(h.spot)); q.set("hedge_target", String(h.target_profit_u)); q.set("hedge_perp_lev", String(h.perp_leverage)); q.set("hedge_opt_lev", String(h.option_leverage)); @@ -493,13 +512,16 @@ } const h = lastResult.perp_hedge; if (h && typeof h === "object") { - if (h.spot != null && el("amp-hedge-spot")) el("amp-hedge-spot").value = String(h.spot); + if (el("amp-overlay-mode")) el("amp-overlay-mode").value = "perp"; if (h.target_profit_u != null && el("amp-hedge-target")) el("amp-hedge-target").value = String(h.target_profit_u); if (h.perp_leverage != null && el("amp-hedge-perp-lev")) el("amp-hedge-perp-lev").value = String(h.perp_leverage); if (h.option_leverage != null && el("amp-hedge-opt-lev")) el("amp-hedge-opt-lev").value = String(h.option_leverage); if (h.ratio_perp != null && el("amp-hedge-ratio-perp")) el("amp-hedge-ratio-perp").value = String(h.ratio_perp); if (h.ratio_opt != null && el("amp-hedge-ratio-opt")) el("amp-hedge-ratio-opt").value = String(h.ratio_opt); + } else if (lastResult.straddle_premium != null && el("amp-overlay-mode")) { + el("amp-overlay-mode").value = "straddle"; } + syncOverlayMode(); pageNo = 1; setStatus("已载入历史 " + id); await reframe(true); @@ -520,10 +542,13 @@ el("amp-btn-compute")?.addEventListener("click", () => void compute(true)); el("amp-btn-save")?.addEventListener("click", () => void saveHistory()); el("amp-btn-download")?.addEventListener("click", downloadCurrent); + el("amp-overlay-mode")?.addEventListener("change", () => { + syncOverlayMode(); + void reframe(true); + }); el("amp-straddle-premium")?.addEventListener("input", scheduleReframe); el("amp-take-profit")?.addEventListener("input", scheduleReframe); [ - "amp-hedge-spot", "amp-hedge-target", "amp-hedge-perp-lev", "amp-hedge-opt-lev", @@ -532,6 +557,7 @@ ].forEach((id) => el(id)?.addEventListener("input", scheduleReframe)); el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true)); syncCustomDays(); + syncOverlayMode(); } window.hubAmpStatsPage = { @@ -539,6 +565,7 @@ bind(); setView("stats"); setStatus(""); + syncOverlayMode(); renderStraddle(null); renderPerpHedge(null); }, diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index f898847..995b7cf 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1265,34 +1265,37 @@ + -

-

口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.买跨收益=有效波动−权利金.永期对冲=永续多1币+买期权(默认1:2),对照所需点数达标与组合盈亏.周末按结算日标注/筛选.

+

口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.对照模式二选一:买跨收益=有效波动−权利金;永期对冲=永续多1币+买期权(默认1:2),入场/权利金按日开盘.周末按结算日标注/筛选.

汇总

-

买跨对照

-
-

永期对冲对照

-
+
+

买跨对照

+
+
+

日表明细

- + - +
结算日窗起点开→高开→低振幅涨跌收益永期盈亏开→高开→低振幅涨跌收益
点击「计算」加载
点击「计算」加载
diff --git a/tests/test_amp_stats_lib.py b/tests/test_amp_stats_lib.py index 68aeeb2..5c2e604 100644 --- a/tests/test_amp_stats_lib.py +++ b/tests/test_amp_stats_lib.py @@ -95,7 +95,7 @@ class AmpStatsLibTests(unittest.TestCase): def test_perp_hedge_hit_and_pnl(self): from lib.hub.amp_stats_lib import perp_hedge_day_pnl - # spot=1800 optLev=100 → prem/coin=18; 1:2 → premium=36 + # 开盘=1800 optLev=100 → prem/coin=18; 1:2 → premium=36 # A move ≈ 52.83; B portfolio move = 51 rows = [ {"open": 1800, "close": 1860, "change": 60, "up_points": 60, "down_points": 0, "amplitude": 60, "settlement_day": "2026-07-01"}, @@ -103,7 +103,6 @@ class AmpStatsLibTests(unittest.TestCase): {"open": 1800, "close": 1820, "change": 20, "up_points": 20, "down_points": 0, "amplitude": 20, "settlement_day": "2026-07-03"}, ] hedge = { - "spot": 1800, "target_profit_u": 15, "perp_leverage": 10, "option_leverage": 100, @@ -114,19 +113,31 @@ class AmpStatsLibTests(unittest.TestCase): s = summarize_rows(rows, perp_hedge=hedge) ph = s["perp_hedge"] self.assertIsNotNone(ph) + self.assertEqual(ph["entry"], "open") + self.assertEqual(ph["spot"], 1800.0) self.assertEqual(ph["opt_coins"], 2.0) self.assertEqual(ph["premium_total"], 36.0) self.assertAlmostEqual(ph["move_b"], 51.0, places=4) self.assertEqual(ph["hit_a_days"], 1) # only +60 self.assertEqual(ph["hit_b_days"], 1) # only -60 - # up day pnl = 60 - 36 - fee - up_pnl = perp_hedge_day_pnl(change=60, open_px=1800, close_px=1860, premium_total=36, opt_coins=2) - down_pnl = perp_hedge_day_pnl(change=-60, open_px=1800, close_px=1740, premium_total=36, opt_coins=2) + up_pnl = perp_hedge_day_pnl( + change=60, open_px=1800, close_px=1860, option_leverage=100, opt_coins=2 + ) + down_pnl = perp_hedge_day_pnl( + change=-60, open_px=1800, close_px=1740, option_leverage=100, opt_coins=2 + ) self.assertAlmostEqual(down_pnl, 60 * (2 - 1) - 36, places=4) # 24 self.assertAlmostEqual(ph["down_pnl_total"], down_pnl, places=4) self.assertGreater(up_pnl, 0) self.assertEqual(ph["up_days"], 2) self.assertEqual(ph["down_days"], 1) + + # 不同开盘 → 不同权利金 + hi_open_pnl = perp_hedge_day_pnl( + change=-60, open_px=2000, close_px=1940, option_leverage=100, opt_coins=2 + ) + self.assertAlmostEqual(hi_open_pnl, 60 - 40, places=4) # prem=40 + csv_text = build_export_csv( { "exchange": "okx", @@ -139,6 +150,7 @@ class AmpStatsLibTests(unittest.TestCase): ) self.assertIn("永期对冲对照", csv_text) self.assertIn("永期盈亏", csv_text) + self.assertIn("按日开盘", csv_text) def test_long_straddle_stats(self): rows = [