diff --git a/docs/振幅统计说明.md b/docs/振幅统计说明.md index ced8f52..9c6c6cf 100644 --- a/docs/振幅统计说明.md +++ b/docs/振幅统计说明.md @@ -111,23 +111,26 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / ### ① 所需点数达标 -复用计算器「由比例推点数」: +复用计算器「由比例推点数」(按**当日开盘**推 A/B;汇总展示用样本开盘中位): | 指标 | 规则 | |------|------| | A 所需点数 | 永续方向对、净利=目标 | -| A 达标 | 日 `涨跌 ≥ A点数` 的天数与占比 | +| A 达标 | 日 `开→高 ≥ A点数` 的天数与占比 | | B 所需点数 | 期权方向对、**组合净利**=目标 | -| B 达标 | 日 `涨跌 ≤ −B点数` 的天数与占比 | +| B 达标 | 日 `开→低 ≥ B点数` 的天数与占比 | -### ② 按日组合盈亏 +### ② 按日组合盈亏(目标出场) -| 日向 | 组合净利 | -|------|----------| -| 上涨 `涨跌≥0` | `涨跌 − 当日权利金 − 永续开平手续费(开→收)` | -| 下跌 `涨跌<0` | `\|涨跌\|×(期权币数−1) − 当日权利金` | +日盈利目标(如 **15U**)用于出场: -汇总:合计、日均、胜率、上涨日/下跌日盈亏小计、单日最大赚亏;日表 **永期盈亏** 列。 +| 情形 | 日盈亏 | +|------|--------| +| 开→高触达 A,或 开→低触达 B | **= 目标盈利**(出场) | +| 两边都触达 | 仍按目标盈利(OHLC 未知先后) | +| 均未触达 | 收盘结算:上涨 `涨跌−权利金−手续费`;下跌 `\|涨跌\|×(期权币数−1)−权利金` | + +汇总:目标出场天数 / 收盘结算天数、合计、日均、胜率、上涨日/下跌日盈亏小计、单日最大赚亏;日表 **永期盈亏** 列。 改永期参数 / 周末筛选会**本地重算**(不重拉 K 线)。 --- @@ -162,3 +165,4 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / | 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 | | 2026-07-28 | 永期对冲对照:所需点数达标 + 按日组合盈亏 | | 2026-07-28 | 永期入场改按日开盘;买跨/永期对照模式二选一 | +| 2026-07-28 | 永期日盈亏按目标盈利出场(开→高/低触达),未触达才收盘结算 | diff --git a/lib/hub/amp_stats_lib.py b/lib/hub/amp_stats_lib.py index 03e97fe..5ce49a9 100644 --- a/lib/hub/amp_stats_lib.py +++ b/lib/hub/amp_stats_lib.py @@ -275,15 +275,16 @@ def enrich_rows_pnl( 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), - option_leverage=float(hedge["option_leverage"]), - opt_coins=float(hedge["opt_coins"]), - ) + day = perp_hedge_day_result(item, hedge) + item["perp_hedge_pnl"] = day["pnl"] + item["perp_hedge_exit"] = day["exit"] + item["perp_hedge_hit_a"] = day["hit_a"] + item["perp_hedge_hit_b"] = day["hit_b"] else: item["perp_hedge_pnl"] = None + item["perp_hedge_exit"] = None + item["perp_hedge_hit_a"] = False + item["perp_hedge_hit_b"] = False out.append(item) return out @@ -336,7 +337,37 @@ def perp_hedge_day_premium(*, open_px: float, option_leverage: float, opt_coins: return coins * (o / lev) -def perp_hedge_day_pnl( +def perp_hedge_required_moves( + *, + open_px: float, + hedge: dict[str, float], +) -> tuple[Optional[float], Optional[float], Optional[str]]: + """按当日开盘推 A/B 达目标盈利所需点数.""" + from lib.hub.hub_perp_options_calc_lib import calc_perp_options_points + + spot = float(open_px or 0) + if spot <= 0: + return None, None, "开盘价无效" + points_data, points_err = calc_perp_options_points( + base="ETH", + spot=spot, + capital_usdt=max(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"], + ) + if not points_data: + return None, None, points_err + 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 + return move_a, move_b, points_err + + +def perp_hedge_day_pnl_eod( *, change: float, open_px: float, @@ -344,7 +375,7 @@ def perp_hedge_day_pnl( option_leverage: float, opt_coins: float, ) -> float: - """单日组合净利(永续多1币 + 买期权);入场/权利金按当日开盘. + """未触达目标时按收盘结算的组合净利. 上涨: change − 权利金 − 永续开平手续费 下跌: |change|×(opt_coins−1) − 权利金 @@ -361,20 +392,114 @@ def perp_hedge_day_pnl( 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_day_result(row: dict[str, Any], hedge: dict[str, float]) -> dict[str, Any]: + """单日永期结果:触达目标点数则按目标盈利出场,否则收盘结算. + + A: 开→高 ≥ move_a → 出场净利 = 目标盈利 + B: 开→低 ≥ move_b → 出场净利 = 目标盈利 + 两边都触达时仍按目标盈利(路径未知,任一边出场均约为目标). + """ + open_px = float(row.get("open") or 0) + close_px = float(row.get("close") or 0) + change = float(row.get("change") or 0) + up_pts = float(row.get("up_points") or 0) + down_pts = float(row.get("down_points") or 0) + target = float(hedge["target_profit_u"]) + move_a, move_b, _err = perp_hedge_required_moves(open_px=open_px, hedge=hedge) + hit_a = bool(move_a is not None and move_a > 0 and up_pts >= move_a) + hit_b = bool(move_b is not None and move_b > 0 and down_pts >= move_b) + if hit_a or hit_b: + if hit_a and hit_b: + exit_tag = "target_both" + elif hit_a: + exit_tag = "target_a" + else: + exit_tag = "target_b" + return { + "pnl": round(target, 4), + "exit": exit_tag, + "hit_a": hit_a, + "hit_b": hit_b, + "move_a": move_a, + "move_b": move_b, + } + return { + "pnl": perp_hedge_day_pnl_eod( + change=change, + open_px=open_px, + close_px=close_px, + option_leverage=float(hedge["option_leverage"]), + opt_coins=float(hedge["opt_coins"]), + ), + "exit": "eod", + "hit_a": False, + "hit_b": False, + "move_a": move_a, + "move_b": move_b, + } + + +# 兼容旧名:默认按「目标出场」完整日结果取 pnl +def perp_hedge_day_pnl( + *, + change: float, + open_px: float, + close_px: float, + option_leverage: float, + opt_coins: float, + up_points: Optional[float] = None, + down_points: Optional[float] = None, + target_profit_u: Optional[float] = None, + perp_leverage: float = 10.0, + ratio_perp: float = 1.0, + ratio_opt: float = 2.0, + ct_mult: float = 0.01, +) -> float: + """单日盈亏.若给了目标与开→高/低,触达则按目标出场;否则收盘结算.""" + if target_profit_u is None or up_points is None or down_points is None: + return perp_hedge_day_pnl_eod( + change=change, + open_px=open_px, + close_px=close_px, + option_leverage=option_leverage, + opt_coins=opt_coins, + ) + hedge = { + "target_profit_u": float(target_profit_u), + "perp_leverage": float(perp_leverage), + "option_leverage": float(option_leverage), + "ratio_perp": float(ratio_perp), + "ratio_opt": float(ratio_opt), + "ct_mult": float(ct_mult), + "opt_coins": float(opt_coins), + "opt_sheets": float(opt_coins) / float(ct_mult), + } + return float( + perp_hedge_day_result( + { + "open": open_px, + "close": close_px, + "change": change, + "up_points": up_points, + "down_points": down_points, + }, + hedge, + )["pnl"] + ) + + def perp_hedge_stats( rows: list[dict[str, Any]], hedge: dict[str, float], ) -> dict[str, Any]: """永期对冲:所需点数达标 + 按日组合盈亏汇总. - 日盈亏权利金按当日开盘;推所需点数用样本开盘中位数作入场参照. + 达标看开→高/开→低是否触达当日入场推得的 A/B 点数; + 触达则日盈亏=目标盈利,否则收盘结算.汇总展示点数用样本开盘中位. """ - from lib.hub.hub_perp_options_calc_lib import calc_perp_options_points - 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 = ( @@ -392,40 +517,24 @@ def perp_hedge_stats( move_b = 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 + move_a, move_b, points_err = perp_hedge_required_moves(open_px=spot_ref, hedge=hedge) else: points_err = "样本无有效开盘价,无法推所需点数" - # 按日开盘重算盈亏(不沿用固定权利金) work: list[dict[str, Any]] = [] for r in rows or []: item = dict(r) - 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"]), - ) + day = perp_hedge_day_result(item, hedge) + item["perp_hedge_pnl"] = day["pnl"] + item["perp_hedge_exit"] = day["exit"] + item["perp_hedge_hit_a"] = day["hit_a"] + item["perp_hedge_hit_b"] = day["hit_b"] work.append(item) n = len(work) empty = { "enabled": True, "entry": "open", + "exit": "target_or_eod", "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), @@ -445,6 +554,9 @@ def perp_hedge_stats( "hit_a_ratio": None, "hit_b_days": 0, "hit_b_ratio": None, + "target_exit_days": 0, + "target_exit_ratio": None, + "eod_days": 0, "pnl_total": None, "pnl_avg": None, "win_days": 0, @@ -459,12 +571,10 @@ def perp_hedge_stats( 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) + hit_a = sum(1 for r in work if r.get("perp_hedge_hit_a")) + hit_b = sum(1 for r in work if r.get("perp_hedge_hit_b")) + target_exits = sum(1 for r in work if str(r.get("perp_hedge_exit") or "").startswith("target")) + eod_days = sum(1 for r in work if r.get("perp_hedge_exit") == "eod") 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) @@ -476,9 +586,12 @@ def perp_hedge_stats( empty.update( { "hit_a_days": hit_a, - "hit_a_ratio": round(hit_a / n, 4) if move_a else None, + "hit_a_ratio": round(hit_a / n, 4), "hit_b_days": hit_b, - "hit_b_ratio": round(hit_b / n, 4) if move_b else None, + "hit_b_ratio": round(hit_b / n, 4), + "target_exit_days": target_exits, + "target_exit_ratio": round(target_exits / n, 4), + "eod_days": eod_days, "pnl_total": round(sum(pnls), 4) if pnls else None, "pnl_avg": round(statistics.fmean(pnls), 4) if pnls else None, "win_days": win, @@ -1043,7 +1156,7 @@ def build_export_csv(payload: dict[str, Any]) -> str: [ "A所需点数", ph.get("move_a"), - "A达标天", + "A达标天(开→高)", ph.get("hit_a_days"), "占比", ph.get("hit_a_ratio"), @@ -1053,12 +1166,22 @@ def build_export_csv(payload: dict[str, Any]) -> str: [ "B所需点数(组合)", ph.get("move_b"), - "B达标天", + "B达标天(开→低)", ph.get("hit_b_days"), "占比", ph.get("hit_b_ratio"), ] ) + w.writerow( + [ + "目标出场天", + ph.get("target_exit_days"), + "收盘结算天", + ph.get("eod_days"), + "目标盈利", + ph.get("target_profit_u"), + ] + ) w.writerow( [ "组合盈亏合计", diff --git a/manual_trading_hub/static/amp_stats.js b/manual_trading_hub/static/amp_stats.js index 9a7fbf4..da90cab 100644 --- a/manual_trading_hub/static/amp_stats.js +++ b/manual_trading_hub/static/amp_stats.js @@ -210,7 +210,7 @@ if (!box) return; if (!ph) { box.innerHTML = - '

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

'; + '

填写「目标 / 杠杆」后计算;入场按日开盘;触达目标点数按目标盈利出场,否则收盘结算

'; return; } const err = @@ -219,13 +219,14 @@ : ""; box.innerHTML = `
` + - `
入场按日开盘 · 推点数中位 ${esc(ph.spot)}
` + + `
入场 / 出场开盘 · 目标 ${esc(ph.target_profit_u)}U 或收盘
` + `
比例 / 期权仓${esc(ph.ratio_label)} · ${esc(ph.opt_coins)} 币
` + `
单币/总权利金(中位)${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))}
` + + `
A达标(开→高)${esc(ph.hit_a_days)} 天 · ${esc(pct(ph.hit_a_ratio))}
` + `
B所需点数(组合)${esc(ph.move_b)}
` + - `
B达标${esc(ph.hit_b_days)} 天 · ${esc(pct(ph.hit_b_ratio))}
` + + `
B达标(开→低)${esc(ph.hit_b_days)} 天 · ${esc(pct(ph.hit_b_ratio))}
` + + `
目标出场 / 收盘结算${esc(ph.target_exit_days)} / ${esc(ph.eod_days)} 天
` + `
组合盈亏合计${esc(ph.pnl_total)}
` + `
日均 / 胜率${esc(ph.pnl_avg)} · ${esc(pct(ph.win_ratio))}
` + `
上涨日盈亏${esc(ph.up_pnl_total)} (${esc(ph.up_days)}天)
` + diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 995b7cf..53e3abf 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1306,7 +1306,7 @@

-

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

+

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

汇总

diff --git a/tests/test_amp_stats_lib.py b/tests/test_amp_stats_lib.py index 5c2e604..aab2bc6 100644 --- a/tests/test_amp_stats_lib.py +++ b/tests/test_amp_stats_lib.py @@ -93,10 +93,10 @@ class AmpStatsLibTests(unittest.TestCase): self.assertIsNone(s["perp_hedge"]) def test_perp_hedge_hit_and_pnl(self): - from lib.hub.amp_stats_lib import perp_hedge_day_pnl + from lib.hub.amp_stats_lib import perp_hedge_day_pnl, perp_hedge_day_pnl_eod - # 开盘=1800 optLev=100 → prem/coin=18; 1:2 → premium=36 - # A move ≈ 52.83; B portfolio move = 51 + # 开盘=1800 optLev=100 → prem=36; A≈52.83; B=51 + # 触达目标出场 → 日盈亏=15, 不再按收盘涨跌算满仓 rows = [ {"open": 1800, "close": 1860, "change": 60, "up_points": 60, "down_points": 0, "amplitude": 60, "settlement_day": "2026-07-01"}, {"open": 1800, "close": 1740, "change": -60, "up_points": 0, "down_points": 60, "amplitude": 60, "settlement_day": "2026-07-02"}, @@ -114,30 +114,48 @@ class AmpStatsLibTests(unittest.TestCase): ph = s["perp_hedge"] self.assertIsNotNone(ph) self.assertEqual(ph["entry"], "open") + self.assertEqual(ph["exit"], "target_or_eod") 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 + self.assertEqual(ph["hit_a_days"], 1) # up 60 >= A + self.assertEqual(ph["hit_b_days"], 1) # down 60 >= B + self.assertEqual(ph["target_exit_days"], 2) + self.assertEqual(ph["eod_days"], 1) + # 触达目标 → 15U + self.assertAlmostEqual(ph["pnl_max"], 15.0, places=4) up_pnl = perp_hedge_day_pnl( - change=60, open_px=1800, close_px=1860, option_leverage=100, opt_coins=2 + change=60, + open_px=1800, + close_px=1860, + option_leverage=100, + opt_coins=2, + up_points=60, + down_points=0, + target_profit_u=15, ) down_pnl = perp_hedge_day_pnl( - change=-60, open_px=1800, close_px=1740, option_leverage=100, opt_coins=2 + change=-60, + open_px=1800, + close_px=1740, + option_leverage=100, + opt_coins=2, + up_points=0, + down_points=60, + target_profit_u=15, ) - 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.assertAlmostEqual(up_pnl, 15.0, places=4) + self.assertAlmostEqual(down_pnl, 15.0, places=4) + self.assertAlmostEqual(ph["down_pnl_total"], 15.0, places=4) + # 未触达:收盘结算 + eod = perp_hedge_day_pnl_eod( + change=20, open_px=1800, close_px=1820, option_leverage=100, opt_coins=2 + ) + self.assertLess(eod, 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",