5fb1d52026
Big-picture changes since b941223:
KOL pipeline (new) — Substack/podcast/blog RSS → AI ticker extraction →
on-chain wallet diff → talks-vs-trades divergence detection. Daily polls,
19 feeds, divergence emits Post + Telegram fan-out.
Telegram push (new) — walletless free tier + wallet-linked Pro upgrade,
in-bot preference commands (/trump /btc /funding /kol /conf /quiet),
signed-envelope API for dashboard. Disconnect-wallet keeps free
subscription.
BTC funding-rate reversal scanner (new) — hourly cron, 30d cumulative
funding threshold + mean-revert + 7d price confirm, emits via
/api/signals/ingest. BTC bottom-reversal scanner promoted to System 2.
WS broadcast rewrite — per-client send timeout + parallel fan-out
(asyncio.gather). Fixes "Binance WS no close frame" reconnect storms +
APScheduler 11-min job misses, both caused by one slow client stalling
the kline loop.
Error visibility — three silent-error sites (trumpstruth/truth_social
fetchers, funding_reversal scanner) now include exception type name so
httpx ConnectError-style empty-message errors stop logging blank lines.
Telegram bot loop now classifies ReadTimeout vs network vs unknown +
logger.exception for the unknown bucket.
Security hygiene — trumpsignal.db untracked from git (held subscriber
wallets + encrypted HL keys + 22 bot trades); .gitignore now blocks
*.db/.next/backups. CORS only allows FRONTEND_URL in production.
New ops scripts —
- scripts/preflight.py: env/DB/Telegram/AI auth verification gate
- scripts/backup_db.sh: cron-friendly daily DB backup (SQLite + Postgres)
- scripts/seed_kol_wallets.py: idempotent KOL on-chain wallet seeder
15 new Alembic migrations (007-021) covering convex strategy fields,
phase-1 safety, two-system frozen exits, invalidation prices, dynamic
SYS2 leverage, staged de-risk + pyramiding, peak gain tracking, risk
mode, auto-trade + grow flags, KOL module, KOL on-chain, KOL divergence,
Telegram bindings + walletless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
8.9 KiB
Python
197 lines
8.9 KiB
Python
"""
|
||
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))
|