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 @@ `填写「永期·现价 / 目标 / 杠杆」后计算;对照所需点数达标天数与组合盈亏(永续多1币+买期权)
'; + '填写「目标 / 杠杆」后计算;入场按日开盘;对照所需点数达标与组合盈亏(永续多1币+买期权)
'; return; } const err = @@ -198,8 +219,9 @@ : ""; box.innerHTML = `口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.买跨收益=有效波动−权利金.永期对冲=永续多1币+买期权(默认1:2),对照所需点数达标与组合盈亏.周末按结算日标注/筛选.
+口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.对照模式二选一:买跨收益=有效波动−权利金;永期对冲=永续多1币+买期权(默认1:2),入场/权利金按日开盘.周末按结算日标注/筛选.
| 结算日 | 窗起点 | 开 | 高 | 低 | 收 | -开→高 | 开→低 | 振幅 | 涨跌 | 收益 | 永期盈亏 | +开→高 | 开→低 | 振幅 | 涨跌 | 收益 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 点击「计算」加载 | ||||||||||||||||
| 点击「计算」加载 | ||||||||||||||||