hyperliquid_account_analyzer.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. #!/usr/bin/env python3
  2. """
  3. Hyperliquid Account Analyzer
  4. Analyzes Hyperliquid trading accounts to evaluate:
  5. - Profitability and performance metrics
  6. - Average trade duration and trading patterns
  7. - Risk management quality
  8. - Win rates and consistency
  9. - Position sizing and leverage usage
  10. Usage:
  11. python utils/hyperliquid_account_analyzer.py [address1] [address2] ...
  12. Or run with the provided top 10 addresses:
  13. python utils/hyperliquid_account_analyzer.py --top10
  14. """
  15. import asyncio
  16. import aiohttp
  17. import json
  18. import sys
  19. import os
  20. from datetime import datetime, timedelta
  21. from typing import Dict, List, Optional, Any, Tuple
  22. from dataclasses import dataclass
  23. import statistics
  24. from collections import defaultdict
  25. import argparse
  26. # Add src to path to import our modules
  27. sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
  28. @dataclass
  29. class Trade:
  30. """Represents a single trade"""
  31. timestamp: int
  32. coin: str
  33. side: str # 'buy' or 'sell'
  34. size: float
  35. price: float
  36. fee: float
  37. is_maker: bool
  38. @dataclass
  39. class Position:
  40. """Represents a position"""
  41. coin: str
  42. size: float
  43. side: str # 'long' or 'short'
  44. entry_price: float
  45. mark_price: float
  46. unrealized_pnl: float
  47. leverage: float
  48. margin_used: float
  49. @dataclass
  50. class AccountStats:
  51. """Comprehensive account statistics"""
  52. address: str
  53. total_pnl: float
  54. win_rate: float
  55. total_trades: int
  56. avg_trade_duration_hours: float
  57. max_drawdown: float
  58. sharpe_ratio: float
  59. avg_position_size: float
  60. max_leverage_used: float
  61. avg_leverage_used: float
  62. trading_frequency_per_day: float
  63. risk_reward_ratio: float
  64. consecutive_losses_max: int
  65. profit_factor: float
  66. largest_win: float
  67. largest_loss: float
  68. active_positions: int
  69. current_drawdown: float
  70. last_trade_timestamp: int
  71. analysis_period_days: int
  72. class HyperliquidAccountAnalyzer:
  73. """Analyzes Hyperliquid trading accounts"""
  74. def __init__(self):
  75. self.info_url = "https://api.hyperliquid.xyz/info"
  76. self.session = None
  77. async def __aenter__(self):
  78. self.session = aiohttp.ClientSession()
  79. return self
  80. async def __aexit__(self, exc_type, exc_val, exc_tb):
  81. if self.session:
  82. await self.session.close()
  83. async def get_account_state(self, address: str) -> Optional[Dict]:
  84. """Get current account state including positions and balance"""
  85. try:
  86. payload = {
  87. "type": "clearinghouseState",
  88. "user": address
  89. }
  90. async with self.session.post(self.info_url, json=payload) as response:
  91. if response.status == 200:
  92. return await response.json()
  93. else:
  94. print(f"❌ Error fetching account state for {address}: HTTP {response.status}")
  95. return None
  96. except Exception as e:
  97. print(f"❌ Exception fetching account state for {address}: {e}")
  98. return None
  99. async def get_user_fills(self, address: str, limit: int = 1000) -> Optional[List[Dict]]:
  100. """Get recent fills/trades for a user"""
  101. try:
  102. payload = {
  103. "type": "userFills",
  104. "user": address
  105. }
  106. async with self.session.post(self.info_url, json=payload) as response:
  107. if response.status == 200:
  108. data = await response.json()
  109. # Return only the most recent fills up to limit
  110. fills = data if isinstance(data, list) else []
  111. return fills[:limit]
  112. else:
  113. print(f"❌ Error fetching fills for {address}: HTTP {response.status}")
  114. return None
  115. except Exception as e:
  116. print(f"❌ Exception fetching fills for {address}: {e}")
  117. return None
  118. async def get_funding_history(self, address: str) -> Optional[List[Dict]]:
  119. """Get funding payments history"""
  120. try:
  121. payload = {
  122. "type": "userFunding",
  123. "user": address
  124. }
  125. async with self.session.post(self.info_url, json=payload) as response:
  126. if response.status == 200:
  127. return await response.json()
  128. else:
  129. return []
  130. except Exception as e:
  131. print(f"⚠️ Could not fetch funding history for {address}: {e}")
  132. return []
  133. def parse_trades(self, fills: List[Dict]) -> List[Trade]:
  134. """Parse fills into Trade objects"""
  135. trades = []
  136. for fill in fills:
  137. try:
  138. # Parse timestamp
  139. timestamp = int(fill.get('time', 0))
  140. if timestamp == 0:
  141. continue
  142. # Parse trade data
  143. coin = fill.get('coin', 'UNKNOWN')
  144. side = fill.get('side', 'buy').lower()
  145. size = float(fill.get('sz', '0'))
  146. price = float(fill.get('px', '0'))
  147. fee = float(fill.get('fee', '0'))
  148. is_maker = fill.get('liquidation', False) == False # Simplified maker detection
  149. if size > 0 and price > 0:
  150. trades.append(Trade(
  151. timestamp=timestamp,
  152. coin=coin,
  153. side=side,
  154. size=size,
  155. price=price,
  156. fee=fee,
  157. is_maker=is_maker
  158. ))
  159. except (ValueError, KeyError) as e:
  160. print(f"⚠️ Warning: Could not parse fill: {fill} - {e}")
  161. continue
  162. return trades
  163. def parse_positions(self, account_state: Dict) -> List[Position]:
  164. """Parse account state into Position objects"""
  165. positions = []
  166. if not account_state or 'assetPositions' not in account_state:
  167. return positions
  168. for asset_pos in account_state['assetPositions']:
  169. try:
  170. position_data = asset_pos.get('position', {})
  171. coin = position_data.get('coin', 'UNKNOWN')
  172. size_str = position_data.get('szi', '0')
  173. size = float(size_str)
  174. if abs(size) < 1e-6: # Skip dust positions
  175. continue
  176. side = 'long' if size > 0 else 'short'
  177. entry_price = float(position_data.get('entryPx', '0'))
  178. mark_price = float(position_data.get('positionValue', '0')) / abs(size) if size != 0 else 0
  179. unrealized_pnl = float(position_data.get('unrealizedPnl', '0'))
  180. leverage = float(position_data.get('leverage', {}).get('value', '1'))
  181. margin_used = float(position_data.get('marginUsed', '0'))
  182. positions.append(Position(
  183. coin=coin,
  184. size=abs(size),
  185. side=side,
  186. entry_price=entry_price,
  187. mark_price=mark_price,
  188. unrealized_pnl=unrealized_pnl,
  189. leverage=leverage,
  190. margin_used=margin_used
  191. ))
  192. except (ValueError, KeyError) as e:
  193. print(f"⚠️ Warning: Could not parse position: {asset_pos} - {e}")
  194. continue
  195. return positions
  196. def calculate_trade_performance(self, trades: List[Trade]) -> Tuple[float, float, int, int]:
  197. """Calculate more accurate trade performance metrics"""
  198. if len(trades) < 2:
  199. return 0.0, 0.0, 0, 0
  200. # Group trades by coin and track P&L per completed round trip
  201. trades_by_coin = defaultdict(list)
  202. for trade in sorted(trades, key=lambda x: x.timestamp):
  203. trades_by_coin[trade.coin].append(trade)
  204. total_realized_pnl = 0.0
  205. winning_trades = 0
  206. losing_trades = 0
  207. total_fees = 0.0
  208. for coin, coin_trades in trades_by_coin.items():
  209. position = 0.0
  210. entry_price = 0.0
  211. entry_cost = 0.0
  212. for trade in coin_trades:
  213. total_fees += trade.fee
  214. if trade.side == 'buy':
  215. if position <= 0: # Opening long or closing short
  216. if position < 0: # Closing short position
  217. pnl = (entry_price - trade.price) * abs(position) - trade.fee
  218. total_realized_pnl += pnl
  219. if pnl > 0:
  220. winning_trades += 1
  221. else:
  222. losing_trades += 1
  223. # Start new long position
  224. new_size = trade.size - max(0, -position)
  225. if new_size > 0:
  226. entry_price = trade.price
  227. entry_cost = new_size * trade.price
  228. position = new_size
  229. else: # Adding to long position
  230. entry_cost += trade.size * trade.price
  231. position += trade.size
  232. entry_price = entry_cost / position
  233. elif trade.side == 'sell':
  234. if position >= 0: # Closing long or opening short
  235. if position > 0: # Closing long position
  236. pnl = (trade.price - entry_price) * min(position, trade.size) - trade.fee
  237. total_realized_pnl += pnl
  238. if pnl > 0:
  239. winning_trades += 1
  240. else:
  241. losing_trades += 1
  242. # Start new short position
  243. new_size = trade.size - max(0, position)
  244. if new_size > 0:
  245. entry_price = trade.price
  246. position = -new_size
  247. else: # Adding to short position
  248. position -= trade.size
  249. entry_price = trade.price # Simplified for shorts
  250. win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
  251. return total_realized_pnl, win_rate, winning_trades, losing_trades
  252. async def analyze_account(self, address: str) -> Optional[AccountStats]:
  253. """Analyze a single account and return comprehensive statistics"""
  254. print(f"\n🔍 Analyzing account: {address}")
  255. # Get account data
  256. account_state = await self.get_account_state(address)
  257. fills = await self.get_user_fills(address, limit=500) # Reduced limit for better analysis
  258. if not fills:
  259. print(f"❌ No trading data found for {address}")
  260. return None
  261. # Parse data
  262. trades = self.parse_trades(fills)
  263. positions = self.parse_positions(account_state) if account_state else []
  264. if not trades:
  265. print(f"❌ No valid trades found for {address}")
  266. return None
  267. print(f"📊 Found {len(trades)} trades, {len(positions)} active positions")
  268. # Calculate time period
  269. trades_sorted = sorted(trades, key=lambda x: x.timestamp)
  270. oldest_trade = trades_sorted[0].timestamp
  271. newest_trade = trades_sorted[-1].timestamp
  272. analysis_period_ms = newest_trade - oldest_trade
  273. analysis_period_days = max(1, analysis_period_ms / (1000 * 60 * 60 * 24))
  274. # Calculate improved metrics
  275. total_trades = len(trades)
  276. total_fees = sum(trade.fee for trade in trades)
  277. # Get better PnL calculation
  278. realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
  279. unrealized_pnl = sum(pos.unrealized_pnl for pos in positions)
  280. total_pnl = realized_pnl + unrealized_pnl
  281. print(f"💰 Realized PnL: ${realized_pnl:.2f}, Unrealized: ${unrealized_pnl:.2f}, Fees: ${total_fees:.2f}")
  282. print(f"📈 Wins: {winning_trades}, Losses: {losing_trades}, Win Rate: {win_rate:.1%}")
  283. # Calculate trade durations (improved)
  284. durations = []
  285. position_tracker = defaultdict(lambda: {'size': 0, 'start_time': 0})
  286. for trade in trades_sorted:
  287. coin = trade.coin
  288. pos = position_tracker[coin]
  289. if trade.side == 'buy':
  290. if pos['size'] <= 0 and trade.size > abs(pos['size']): # Opening new long
  291. pos['start_time'] = trade.timestamp
  292. pos['size'] += trade.size
  293. else: # sell
  294. if pos['size'] > 0: # Closing long position
  295. if trade.size >= pos['size'] and pos['start_time'] > 0: # Fully closing
  296. duration_hours = (trade.timestamp - pos['start_time']) / (1000 * 3600)
  297. if duration_hours > 0:
  298. durations.append(duration_hours)
  299. pos['start_time'] = 0
  300. pos['size'] -= trade.size
  301. elif pos['size'] <= 0: # Opening short
  302. pos['start_time'] = trade.timestamp
  303. pos['size'] -= trade.size
  304. avg_duration = statistics.mean(durations) if durations else 0
  305. print(f"🕒 Found {len(durations)} completed trades, avg duration: {avg_duration:.1f} hours")
  306. # Calculate position size statistics
  307. position_sizes = [trade.size * trade.price for trade in trades]
  308. avg_position_size = statistics.mean(position_sizes) if position_sizes else 0
  309. # Calculate leverage statistics from current positions
  310. leverages = [pos.leverage for pos in positions if pos.leverage > 0]
  311. max_leverage = max(leverages) if leverages else 0
  312. avg_leverage = statistics.mean(leverages) if leverages else 1
  313. # Calculate trading frequency
  314. trading_freq = total_trades / analysis_period_days if analysis_period_days > 0 else 0
  315. # Simplified drawdown calculation
  316. max_drawdown = 0.0
  317. current_drawdown = 0.0
  318. if total_pnl < 0:
  319. max_drawdown = abs(total_pnl) / (avg_position_size * 10) if avg_position_size > 0 else 0
  320. current_drawdown = max_drawdown
  321. # Risk metrics
  322. profit_factor = abs(realized_pnl) / total_fees if total_fees > 0 else 0
  323. return AccountStats(
  324. address=address,
  325. total_pnl=total_pnl,
  326. win_rate=win_rate,
  327. total_trades=total_trades,
  328. avg_trade_duration_hours=avg_duration,
  329. max_drawdown=max_drawdown,
  330. sharpe_ratio=0, # Would need returns data
  331. avg_position_size=avg_position_size,
  332. max_leverage_used=max_leverage,
  333. avg_leverage_used=avg_leverage,
  334. trading_frequency_per_day=trading_freq,
  335. risk_reward_ratio=winning_trades / max(1, losing_trades),
  336. consecutive_losses_max=0, # Would need sequence analysis
  337. profit_factor=profit_factor,
  338. largest_win=0, # Would need individual trade P&L
  339. largest_loss=0, # Would need individual trade P&L
  340. active_positions=len(positions),
  341. current_drawdown=current_drawdown,
  342. last_trade_timestamp=newest_trade,
  343. analysis_period_days=int(analysis_period_days)
  344. )
  345. async def analyze_multiple_accounts(self, addresses: List[str]) -> List[AccountStats]:
  346. """Analyze multiple accounts concurrently"""
  347. print(f"🚀 Starting analysis of {len(addresses)} accounts...\n")
  348. tasks = [self.analyze_account(addr) for addr in addresses]
  349. results = await asyncio.gather(*tasks, return_exceptions=True)
  350. # Filter out None results and exceptions
  351. valid_results = []
  352. for i, result in enumerate(results):
  353. if isinstance(result, Exception):
  354. print(f"❌ Error analyzing {addresses[i]}: {result}")
  355. elif result is not None:
  356. valid_results.append(result)
  357. return valid_results
  358. def print_analysis_results(self, stats_list: List[AccountStats]):
  359. """Print comprehensive analysis results"""
  360. if not stats_list:
  361. print("❌ No valid analysis results to display")
  362. return
  363. print("\n" + "="*100)
  364. print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
  365. print("="*100)
  366. # Sort by a composite score (you can adjust this ranking)
  367. def calculate_score(stats: AccountStats) -> float:
  368. score = 0
  369. # Profitability (40% weight)
  370. if stats.total_pnl > 0:
  371. score += 40
  372. # Win rate (20% weight)
  373. score += stats.win_rate * 20
  374. # Short duration trades (20% weight) - prefer < 24 hours
  375. if stats.avg_trade_duration_hours > 0:
  376. duration_score = max(0, 20 - (stats.avg_trade_duration_hours / 24) * 20)
  377. score += duration_score
  378. # Trading frequency (10% weight) - prefer active traders
  379. freq_score = min(10, stats.trading_frequency_per_day * 2)
  380. score += freq_score
  381. # Low drawdown (10% weight)
  382. drawdown_score = max(0, 10 - stats.max_drawdown * 100)
  383. score += drawdown_score
  384. return score
  385. sorted_stats = sorted(stats_list, key=calculate_score, reverse=True)
  386. for i, stats in enumerate(sorted_stats, 1):
  387. score = calculate_score(stats)
  388. print(f"\n{i}. 📋 ACCOUNT: {stats.address}")
  389. print(f" 🏆 SCORE: {score:.1f}/100")
  390. print(f" 💰 Total PnL: ${stats.total_pnl:.2f}")
  391. print(f" 📈 Win Rate: {stats.win_rate:.1%}")
  392. print(f" 🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
  393. print(f" 📉 Max Drawdown: {stats.max_drawdown:.1%}")
  394. print(f" 🔄 Trading Frequency: {stats.trading_frequency_per_day:.1f} trades/day")
  395. print(f" 💵 Avg Position Size: ${stats.avg_position_size:.2f}")
  396. print(f" ⚡ Max Leverage: {stats.max_leverage_used:.1f}x")
  397. print(f" 📊 Total Trades: {stats.total_trades}")
  398. print(f" 📍 Active Positions: {stats.active_positions}")
  399. print(f" 📅 Analysis Period: {stats.analysis_period_days} days")
  400. # Evaluation
  401. evaluation = []
  402. if stats.total_pnl > 0:
  403. evaluation.append("✅ Profitable")
  404. else:
  405. evaluation.append("❌ Not profitable")
  406. if stats.avg_trade_duration_hours < 24:
  407. evaluation.append("✅ Short-term trades")
  408. else:
  409. evaluation.append("⚠️ Longer-term trades")
  410. if stats.win_rate > 0.5:
  411. evaluation.append("✅ Good win rate")
  412. else:
  413. evaluation.append("⚠️ Low win rate")
  414. if stats.max_drawdown < 0.2:
  415. evaluation.append("✅ Good risk management")
  416. else:
  417. evaluation.append("⚠️ High drawdown risk")
  418. print(f" 🎯 Evaluation: {' | '.join(evaluation)}")
  419. # Recommendation
  420. print("\n" + "="*100)
  421. print("🎯 RECOMMENDATION FOR COPY TRADING")
  422. print("="*100)
  423. if sorted_stats:
  424. best_account = sorted_stats[0]
  425. best_score = calculate_score(best_account)
  426. print(f"\n🏆 TOP RECOMMENDATION: {best_account.address}")
  427. print(f" 📊 Overall Score: {best_score:.1f}/100")
  428. if best_score >= 70:
  429. recommendation = "🟢 HIGHLY RECOMMENDED"
  430. elif best_score >= 50:
  431. recommendation = "🟡 MODERATELY RECOMMENDED"
  432. else:
  433. recommendation = "🔴 NOT RECOMMENDED"
  434. print(f" {recommendation}")
  435. print(f"\n📋 Why this account:")
  436. if best_account.total_pnl > 0:
  437. print(f" ✅ Profitable: ${best_account.total_pnl:.2f} total PnL")
  438. if best_account.avg_trade_duration_hours < 24:
  439. print(f" ✅ Short trades: {best_account.avg_trade_duration_hours:.1f} hour average")
  440. if best_account.win_rate > 0.5:
  441. print(f" ✅ Good performance: {best_account.win_rate:.1%} win rate")
  442. if best_account.max_drawdown < 0.2:
  443. print(f" ✅ Risk management: {best_account.max_drawdown:.1%} max drawdown")
  444. print(f"\n⚙️ Suggested copy trading settings:")
  445. print(f" 📊 Portfolio allocation: 5-10% (conservative start)")
  446. print(f" ⚡ Max leverage limit: {min(5, best_account.avg_leverage_used):.0f}x")
  447. print(f" 💰 Min position size: $25")
  448. async def main():
  449. """Main function"""
  450. parser = argparse.ArgumentParser(description='Analyze Hyperliquid trading accounts')
  451. parser.add_argument('addresses', nargs='*', help='Account addresses to analyze')
  452. parser.add_argument('--top10', action='store_true', help='Analyze the provided top 10 accounts')
  453. args = parser.parse_args()
  454. # Top 10 accounts from the user
  455. top10_addresses = [
  456. "0xa10ec245b3483f83e350a9165a52ae23dbab01bc",
  457. "0xd11f5de0189d52b3abe6b0960b8377c20988e17e",
  458. "0xc62df97dcf96324adf4edd30a4a7bffd5402f4da",
  459. "0xa70434af5778038245d53da1b4d360a30307a827",
  460. "0x72fad4e75748b65566a3ebb555b6f6ee18ce08d1",
  461. "0x0487b5e806ac781508cb3272ebd83ad603ddcc0f",
  462. "0x59a15c79a007cd6e9965b949fcf04125c2212524",
  463. "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8",
  464. "0x3104b7668f9e46fb13ec0b141d2902e144d67efe",
  465. "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4"
  466. ]
  467. if args.top10:
  468. addresses = top10_addresses
  469. elif args.addresses:
  470. addresses = args.addresses
  471. else:
  472. addresses = top10_addresses
  473. print("ℹ️ No addresses specified, analyzing top 10 accounts")
  474. async with HyperliquidAccountAnalyzer() as analyzer:
  475. results = await analyzer.analyze_multiple_accounts(addresses)
  476. analyzer.print_analysis_results(results)
  477. if __name__ == "__main__":
  478. asyncio.run(main())