""" System-2 生命周期·小额真单端到端验证 验证我们新写的三个动钱路径在真实 Hyperliquid 上的行为,并和账面记账对账: 开仓 → 加仓(pyramid) → 部分减仓(de-risk) → 全平 每一步都把"预期"和"HL 实际"并排打印,并用和 bot_engine 完全一致的 公式做 PnL 自洽校验。 用法: source venv/bin/activate HL_API_PRIVATE_KEY="0x..." HL_ACCOUNT_ADDRESS="0x..." \ python scripts/verify_sys2_lifecycle.py # 干跑(不下单, 只打印计划) ... python scripts/verify_sys2_lifecycle.py --live # 真下单(小额, 需确认) 安全: - 默认 DRY-RUN, 不下任何单 - --live 才真下单, 且会要求手动输入 YES 确认 - 名义金额默认 $20, 上限 $40 (超过需 --force), 杠杆默认 2x - 任何异常 / 结束都会尝试把仓位平掉 (best-effort flatten) - mainnet/testnet 跟随 settings.hl_mainnet """ import argparse import asyncio import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.config import settings from app.services.hyperliquid import HyperliquidTrader from app.services.bot_engine import HL_TAKER_FEE_RATE ASSET = "BTC" SIDE = "long" HARD_CAP_USD = 40.0 def _slice_pnl(notional: float, entry: float, exit_px: float, side: str) -> float: """Identical to bot_engine: notional × signed move − round-trip taker.""" pct = (exit_px - entry) / entry if entry else 0.0 signed = pct if side == "long" else -pct return notional * signed - notional * HL_TAKER_FEE_RATE * 2 def _pos(positions: list): return next((p for p in positions if p.get("coin") == ASSET), None) def _row(label, expected, actual): print(f" {label:<28} expected={expected!s:<22} actual={actual!s}") async def _flatten(trader, why: str): try: pos = _pos(await trader.get_open_positions()) if pos: print(f"\n🧹 安全平仓 ({why}) — 当前 szi={pos['szi']}") r = await trader.close_position(ASSET) print(f" 平仓结果: {r}") else: print(f"\n🧹 无残留仓位 ({why})") except Exception as exc: print(f"\n⚠️ 安全平仓失败 ({why}): {exc} — 请手动检查 HL!") async def main(live: bool, size_usd: float, leverage: int, force: bool): api_key = os.getenv("HL_API_PRIVATE_KEY") or os.getenv("HL_API_KEY", "") account = os.getenv("HL_ACCOUNT_ADDRESS", "") if not api_key or not account: print("❌ 需要 HL_API_PRIVATE_KEY 和 HL_ACCOUNT_ADDRESS 环境变量") sys.exit(1) if size_usd > HARD_CAP_USD and not force: print(f"❌ size_usd ${size_usd} 超过安全上限 ${HARD_CAP_USD}(加 --force 才允许)") sys.exit(1) net = "MAINNET 真钱" if settings.hl_mainnet else "TESTNET" add_usd = round(size_usd * 0.30, 2) # 模拟 pyramid 第1档 (+30% base) derisk_frac_of_cur = 1.0 / 3.0 # 模拟 de-risk 第1档 (减当前 1/3) print("=" * 64) print(f"System-2 生命周期验证 · {net} · {ASSET} {SIDE} {leverage}x") print(f"计划: 开 ${size_usd} → 加 ${add_usd} → 减当前 1/3 → 全平") print(f"模式: {'🔴 LIVE 真下单' if live else '🟢 DRY-RUN 仅打印'}") print("=" * 64) if not live: print("\n干跑结束。确认计划无误后加 --live 真跑(小额)。") return confirm = input(f"\n⚠️ 将在 {net} 下真单(约 ${size_usd})。输入大写 YES 继续: ") if confirm.strip() != "YES": print("已取消。") return trader = HyperliquidTrader( api_private_key=api_key, account_address=account, leverage=leverage, mainnet=settings.hl_mainnet, ) bal = await trader.get_balance() print(f"\n账户可用 USDC: ${bal:.2f}") if bal < size_usd: print("❌ 余额不足,放弃。") return if _pos(await trader.get_open_positions()): print(f"❌ 已存在 {ASSET} 持仓 — 为避免干扰,请先手动清空后再跑。") return try: # ── 1. 开仓 ────────────────────────────────────────────────────── print("\n[1] 开仓 open_position") o = await trader.open_position(ASSET, SIDE, size_usd) entry = float(o["fill_price"]) base_coins = float(o["size_coins"]) base_notional = base_coins * entry pos = _pos(await trader.get_open_positions()) _row("entry fill", "~mkt", entry) _row("size_coins", f"~{size_usd/entry:.6f}", base_coins) _row("HL szi", f"~{base_coins:.6f}", pos and pos["szi"]) assert pos and abs(abs(pos["szi"]) - base_coins) / base_coins < 0.05, "开仓后 HL 仓位不符" # ── 2. 加仓 (pyramid 第1档) ────────────────────────────────────── print("\n[2] 加仓 open_position(模拟 pyramid +30%)") a = await trader.open_position(ASSET, SIDE, add_usd) add_fill = float(a["fill_price"]) add_coins = float(a["size_coins"]) actual_add_notional = add_coins * add_fill # ← 和修过的 pyramid_add 一致 # 混合均价:按名义加权(与 bot_engine.pyramid_add 完全相同的公式) old_notional = base_notional new_notional = old_notional + actual_add_notional blended = (old_notional * entry + actual_add_notional * add_fill) / new_notional pos = _pos(await trader.get_open_positions()) exp_coins = base_coins + add_coins _row("add fill", "~mkt", add_fill) _row("add size_coins", f"~{add_usd/add_fill:.6f}", add_coins) _row("blended entry", f"{blended:.2f}", "(账面)") _row("HL szi", f"~{exp_coins:.6f}", pos and pos["szi"]) assert pos and abs(abs(pos["szi"]) - exp_coins) / exp_coins < 0.05, "加仓后 HL 仓位不符" # ── 3. 部分减仓 (de-risk 第1档:减当前 1/3) ────────────────────── print("\n[3] 部分减仓 reduce_position(1/3)") pre_coins = abs(pos["szi"]) r = await trader.reduce_position(ASSET, derisk_frac_of_cur) cut_fill = float(r["fill_price"]) closed_frac = float(r["closed_fraction"]) pos = _pos(await trader.get_open_positions()) post_coins = abs(pos["szi"]) if pos else 0.0 actually_cut = pre_coins - post_coins slice_notional = actually_cut * cut_fill slice_pnl = _slice_pnl(slice_notional, blended, cut_fill, SIDE) _row("reduce fill", "~mkt", cut_fill) _row("closed_fraction", f"~{derisk_frac_of_cur:.3f}", round(closed_frac, 4)) _row("coins cut", f"~{pre_coins/3:.6f}", round(actually_cut, 6)) _row("剩余 szi", f"~{pre_coins*2/3:.6f}", pos and pos["szi"]) _row("该片已实现PnL($)", "—", round(slice_pnl, 4)) assert 0.25 < closed_frac < 0.42, "减仓比例偏离 1/3 过大" # ── 4. 全平 ────────────────────────────────────────────────────── print("\n[4] 全平 close_position") c = await trader.close_position(ASSET) close_fill = float(c["fill_price"]) remaining_notional = post_coins * close_fill remaining_pnl = _slice_pnl(remaining_notional, blended, close_fill, SIDE) pos = _pos(await trader.get_open_positions()) _row("close fill", "~mkt", close_fill) _row("收尾后持仓", "None", pos) assert pos is None, "全平后仍有残留仓位!" total_pnl = slice_pnl + remaining_pnl print("\n" + "=" * 64) print("对账小结(账面 vs 行为)") print(f" 混合均价 : {blended:.2f}") print(f" 分片PnL(减1/3) : {slice_pnl:+.4f} USD") print(f" 收尾PnL(剩2/3) : {remaining_pnl:+.4f} USD") print(f" 合计PnL : {total_pnl:+.4f} USD(应≈HL账户实际变动,含滑点/费)") print(" ✅ 开/加/减/平 四步与 HL 实际持仓全部一致") print("=" * 64) print("\n说明: 这里用市价瞬时往返,PnL≈ -往返taker费(~0.09%)±滑点,属正常。") except AssertionError as e: print(f"\n❌ 校验失败: {e}") except Exception as e: print(f"\n❌ 异常: {e}") finally: await _flatten(trader, "收尾") if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--live", action="store_true", help="真下单(默认干跑)") ap.add_argument("--size", type=float, default=20.0, help="开仓名义USD(默认20)") ap.add_argument("--leverage", type=int, default=2, help="杠杆(默认2x)") ap.add_argument("--force", action="store_true", help="允许 size 超过安全上限") a = ap.parse_args() asyncio.run(main(a.live, a.size, a.leverage, a.force))