hyperliquid_account_analyzer.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  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. # Analyze specific addresses
  12. python utils/hyperliquid_account_analyzer.py [address1] [address2] ...
  13. # Use curated high-performance accounts (default)
  14. python utils/hyperliquid_account_analyzer.py
  15. python utils/hyperliquid_account_analyzer.py --limit 15
  16. # Use hardcoded top 10 addresses
  17. python utils/hyperliquid_account_analyzer.py --top10
  18. Options:
  19. --leaderboard Use curated high-performance accounts (recommended)
  20. --window Time window preference: 1d, 7d, 30d, allTime (default: 7d)
  21. --limit Number of accounts to analyze (default: 10)
  22. --top10 Use original hardcoded list of top 10 accounts
  23. Note: Hyperliquid's leaderboard API is not publicly accessible, so the script uses
  24. a manually curated list of high-performing accounts identified through analysis.
  25. """
  26. import asyncio
  27. import aiohttp
  28. import json
  29. import sys
  30. import os
  31. from datetime import datetime, timedelta
  32. from typing import Dict, List, Optional, Any, Tuple
  33. from dataclasses import dataclass
  34. import statistics
  35. from collections import defaultdict
  36. import argparse
  37. # Add src to path to import our modules
  38. sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
  39. @dataclass
  40. class Trade:
  41. """Represents a single trade"""
  42. timestamp: int
  43. coin: str
  44. side: str # 'buy' or 'sell'
  45. size: float
  46. price: float
  47. fee: float
  48. is_maker: bool
  49. @dataclass
  50. class Position:
  51. """Represents a position"""
  52. coin: str
  53. size: float
  54. side: str # 'long' or 'short'
  55. entry_price: float
  56. mark_price: float
  57. unrealized_pnl: float
  58. leverage: float
  59. margin_used: float
  60. @dataclass
  61. class AccountStats:
  62. """Comprehensive account statistics"""
  63. address: str
  64. total_pnl: float
  65. win_rate: float
  66. total_trades: int
  67. avg_trade_duration_hours: float
  68. max_drawdown: float
  69. sharpe_ratio: float
  70. avg_position_size: float
  71. max_leverage_used: float
  72. avg_leverage_used: float
  73. trading_frequency_per_day: float
  74. risk_reward_ratio: float
  75. consecutive_losses_max: int
  76. profit_factor: float
  77. largest_win: float
  78. largest_loss: float
  79. active_positions: int
  80. current_drawdown: float
  81. last_trade_timestamp: int
  82. analysis_period_days: int
  83. is_copyable: bool # Whether this account is suitable for copy trading
  84. copyability_reason: str # Why it is/isn't copyable
  85. class HyperliquidAccountAnalyzer:
  86. """Analyzes Hyperliquid trading accounts"""
  87. def __init__(self):
  88. self.info_url = "https://api.hyperliquid.xyz/info"
  89. self.session = None
  90. async def __aenter__(self):
  91. self.session = aiohttp.ClientSession()
  92. return self
  93. async def __aexit__(self, exc_type, exc_val, exc_tb):
  94. if self.session:
  95. await self.session.close()
  96. async def get_account_state(self, address: str) -> Optional[Dict]:
  97. """Get current account state including positions and balance"""
  98. try:
  99. payload = {
  100. "type": "clearinghouseState",
  101. "user": address
  102. }
  103. async with self.session.post(self.info_url, json=payload) as response:
  104. if response.status == 200:
  105. return await response.json()
  106. else:
  107. print(f"❌ Error fetching account state for {address}: HTTP {response.status}")
  108. return None
  109. except Exception as e:
  110. print(f"❌ Exception fetching account state for {address}: {e}")
  111. return None
  112. async def get_user_fills(self, address: str, limit: int = 1000) -> Optional[List[Dict]]:
  113. """Get recent fills/trades for a user"""
  114. try:
  115. payload = {
  116. "type": "userFills",
  117. "user": address
  118. }
  119. async with self.session.post(self.info_url, json=payload) as response:
  120. if response.status == 200:
  121. data = await response.json()
  122. # Return only the most recent fills up to limit
  123. fills = data if isinstance(data, list) else []
  124. return fills[:limit]
  125. else:
  126. print(f"❌ Error fetching fills for {address}: HTTP {response.status}")
  127. return None
  128. except Exception as e:
  129. print(f"❌ Exception fetching fills for {address}: {e}")
  130. return None
  131. async def get_funding_history(self, address: str) -> Optional[List[Dict]]:
  132. """Get funding payments history"""
  133. try:
  134. payload = {
  135. "type": "userFunding",
  136. "user": address
  137. }
  138. async with self.session.post(self.info_url, json=payload) as response:
  139. if response.status == 200:
  140. return await response.json()
  141. else:
  142. return []
  143. except Exception as e:
  144. print(f"⚠️ Could not fetch funding history for {address}: {e}")
  145. return []
  146. def parse_trades(self, fills: List[Dict]) -> List[Trade]:
  147. """Parse fills into Trade objects"""
  148. trades = []
  149. for fill in fills:
  150. try:
  151. # Parse timestamp
  152. timestamp = int(fill.get('time', 0))
  153. if timestamp == 0:
  154. continue
  155. # Parse trade data
  156. coin = fill.get('coin', 'UNKNOWN')
  157. side = fill.get('side', 'buy').lower()
  158. size = float(fill.get('sz', '0'))
  159. price = float(fill.get('px', '0'))
  160. fee = float(fill.get('fee', '0'))
  161. is_maker = fill.get('liquidation', False) == False # Simplified maker detection
  162. if size > 0 and price > 0:
  163. trades.append(Trade(
  164. timestamp=timestamp,
  165. coin=coin,
  166. side=side,
  167. size=size,
  168. price=price,
  169. fee=fee,
  170. is_maker=is_maker
  171. ))
  172. except (ValueError, KeyError) as e:
  173. print(f"⚠️ Warning: Could not parse fill: {fill} - {e}")
  174. continue
  175. return trades
  176. def parse_positions(self, account_state: Dict) -> List[Position]:
  177. """Parse account state into Position objects"""
  178. positions = []
  179. if not account_state or 'assetPositions' not in account_state:
  180. return positions
  181. for asset_pos in account_state['assetPositions']:
  182. try:
  183. position_data = asset_pos.get('position', {})
  184. coin = position_data.get('coin', 'UNKNOWN')
  185. size_str = position_data.get('szi', '0')
  186. size = float(size_str)
  187. if abs(size) < 1e-6: # Skip dust positions
  188. continue
  189. side = 'long' if size > 0 else 'short'
  190. entry_price = float(position_data.get('entryPx', '0'))
  191. mark_price = float(position_data.get('positionValue', '0')) / abs(size) if size != 0 else 0
  192. unrealized_pnl = float(position_data.get('unrealizedPnl', '0'))
  193. leverage = float(position_data.get('leverage', {}).get('value', '1'))
  194. margin_used = float(position_data.get('marginUsed', '0'))
  195. positions.append(Position(
  196. coin=coin,
  197. size=abs(size),
  198. side=side,
  199. entry_price=entry_price,
  200. mark_price=mark_price,
  201. unrealized_pnl=unrealized_pnl,
  202. leverage=leverage,
  203. margin_used=margin_used
  204. ))
  205. except (ValueError, KeyError) as e:
  206. print(f"⚠️ Warning: Could not parse position: {asset_pos} - {e}")
  207. continue
  208. return positions
  209. def calculate_trade_performance(self, trades: List[Trade]) -> Tuple[float, float, int, int]:
  210. """Calculate more accurate trade performance metrics"""
  211. if len(trades) < 2:
  212. return 0.0, 0.0, 0, 0
  213. # Group trades by coin and track P&L per completed round trip
  214. trades_by_coin = defaultdict(list)
  215. for trade in sorted(trades, key=lambda x: x.timestamp):
  216. trades_by_coin[trade.coin].append(trade)
  217. total_realized_pnl = 0.0
  218. winning_trades = 0
  219. losing_trades = 0
  220. total_fees = 0.0
  221. for coin, coin_trades in trades_by_coin.items():
  222. position = 0.0
  223. entry_price = 0.0
  224. entry_cost = 0.0
  225. for trade in coin_trades:
  226. total_fees += trade.fee
  227. if trade.side == 'buy':
  228. if position <= 0: # Opening long or closing short
  229. if position < 0: # Closing short position
  230. pnl = (entry_price - trade.price) * abs(position) - trade.fee
  231. total_realized_pnl += pnl
  232. if pnl > 0:
  233. winning_trades += 1
  234. else:
  235. losing_trades += 1
  236. # Start new long position
  237. new_size = trade.size - max(0, -position)
  238. if new_size > 0:
  239. entry_price = trade.price
  240. entry_cost = new_size * trade.price
  241. position = new_size
  242. else: # Adding to long position
  243. entry_cost += trade.size * trade.price
  244. position += trade.size
  245. entry_price = entry_cost / position
  246. elif trade.side == 'sell':
  247. if position >= 0: # Closing long or opening short
  248. if position > 0: # Closing long position
  249. pnl = (trade.price - entry_price) * min(position, trade.size) - trade.fee
  250. total_realized_pnl += pnl
  251. if pnl > 0:
  252. winning_trades += 1
  253. else:
  254. losing_trades += 1
  255. # Start new short position
  256. new_size = trade.size - max(0, position)
  257. if new_size > 0:
  258. entry_price = trade.price
  259. position = -new_size
  260. else: # Adding to short position
  261. position -= trade.size
  262. entry_price = trade.price # Simplified for shorts
  263. win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
  264. return total_realized_pnl, win_rate, winning_trades, losing_trades
  265. def analyze_hft_patterns(self, trades: List[Trade]) -> Dict[str, Any]:
  266. """
  267. Analyze high-frequency trading patterns that don't follow traditional open/close cycles
  268. """
  269. if not trades:
  270. return {
  271. 'avg_time_between_trades_minutes': 0,
  272. 'max_time_between_trades_hours': 0,
  273. 'min_time_between_trades_seconds': 0,
  274. 'trading_clusters': 0,
  275. 'trades_per_cluster': 0,
  276. 'is_hft_pattern': False
  277. }
  278. trades_sorted = sorted(trades, key=lambda x: x.timestamp)
  279. time_gaps = []
  280. # Calculate time gaps between consecutive trades
  281. for i in range(1, len(trades_sorted)):
  282. gap_ms = trades_sorted[i].timestamp - trades_sorted[i-1].timestamp
  283. gap_minutes = gap_ms / (1000 * 60)
  284. time_gaps.append(gap_minutes)
  285. if not time_gaps:
  286. return {
  287. 'avg_time_between_trades_minutes': 0,
  288. 'max_time_between_trades_hours': 0,
  289. 'min_time_between_trades_seconds': 0,
  290. 'trading_clusters': 0,
  291. 'trades_per_cluster': 0,
  292. 'is_hft_pattern': False
  293. }
  294. avg_gap_minutes = statistics.mean(time_gaps)
  295. max_gap_hours = max(time_gaps) / 60
  296. min_gap_seconds = min(time_gaps) * 60
  297. # Identify trading clusters (periods of intense activity)
  298. clusters = []
  299. current_cluster = [trades_sorted[0]]
  300. for i in range(1, len(trades_sorted)):
  301. gap_minutes = time_gaps[i-1]
  302. if gap_minutes <= 5: # Trades within 5 minutes = same cluster
  303. current_cluster.append(trades_sorted[i])
  304. else:
  305. if len(current_cluster) >= 3: # Minimum 3 trades to be a cluster
  306. clusters.append(current_cluster)
  307. current_cluster = [trades_sorted[i]]
  308. # Don't forget the last cluster
  309. if len(current_cluster) >= 3:
  310. clusters.append(current_cluster)
  311. avg_trades_per_cluster = statistics.mean([len(cluster) for cluster in clusters]) if clusters else 0
  312. # Determine if this is HFT pattern
  313. is_hft = (
  314. avg_gap_minutes < 30 and # Average < 30 minutes between trades
  315. len([gap for gap in time_gaps if gap < 1]) > len(time_gaps) * 0.3 # 30%+ trades within 1 minute
  316. )
  317. return {
  318. 'avg_time_between_trades_minutes': avg_gap_minutes,
  319. 'max_time_between_trades_hours': max_gap_hours,
  320. 'min_time_between_trades_seconds': min_gap_seconds,
  321. 'trading_clusters': len(clusters),
  322. 'trades_per_cluster': avg_trades_per_cluster,
  323. 'is_hft_pattern': is_hft
  324. }
  325. def calculate_rolling_pnl(self, trades: List[Trade]) -> Tuple[float, List[float], int, int]:
  326. """
  327. Calculate P&L using rolling window approach for HFT patterns
  328. """
  329. if not trades:
  330. return 0.0, [], 0, 0
  331. trades_sorted = sorted(trades, key=lambda x: x.timestamp)
  332. # Track net position and P&L over time
  333. cumulative_pnl = 0.0
  334. pnl_series = []
  335. winning_periods = 0
  336. losing_periods = 0
  337. # Use 1-hour windows for P&L calculation
  338. window_size_ms = 60 * 60 * 1000 # 1 hour
  339. if not trades_sorted:
  340. return 0.0, [], 0, 0
  341. start_time = trades_sorted[0].timestamp
  342. end_time = trades_sorted[-1].timestamp
  343. current_time = start_time
  344. window_trades = []
  345. while current_time <= end_time:
  346. window_end = current_time + window_size_ms
  347. # Get trades in this window
  348. window_trades = [
  349. t for t in trades_sorted
  350. if current_time <= t.timestamp < window_end
  351. ]
  352. if window_trades:
  353. # Calculate net flow and fees for this window
  354. net_usd_flow = 0.0
  355. window_fees = 0.0
  356. for trade in window_trades:
  357. trade_value = trade.size * trade.price
  358. if trade.side == 'buy':
  359. net_usd_flow -= trade_value # Cash out
  360. else: # sell
  361. net_usd_flow += trade_value # Cash in
  362. window_fees += trade.fee
  363. # Window P&L = net cash flow - fees
  364. window_pnl = net_usd_flow - window_fees
  365. cumulative_pnl += window_pnl
  366. pnl_series.append(cumulative_pnl)
  367. if window_pnl > 0:
  368. winning_periods += 1
  369. elif window_pnl < 0:
  370. losing_periods += 1
  371. current_time = window_end
  372. win_rate = winning_periods / (winning_periods + losing_periods) if (winning_periods + losing_periods) > 0 else 0
  373. return cumulative_pnl, pnl_series, winning_periods, losing_periods
  374. async def analyze_account(self, address: str) -> Optional[AccountStats]:
  375. """Analyze a single account and return comprehensive statistics"""
  376. print(f"\n🔍 Analyzing account: {address}")
  377. # Get account data
  378. account_state = await self.get_account_state(address)
  379. fills = await self.get_user_fills(address, limit=500) # Reduced limit for better analysis
  380. if not fills:
  381. print(f"❌ No trading data found for {address}")
  382. return None
  383. # Parse data
  384. trades = self.parse_trades(fills)
  385. positions = self.parse_positions(account_state) if account_state else []
  386. if not trades:
  387. print(f"❌ No valid trades found for {address}")
  388. return None
  389. print(f"📊 Found {len(trades)} trades, {len(positions)} active positions")
  390. # Calculate time period
  391. trades_sorted = sorted(trades, key=lambda x: x.timestamp)
  392. oldest_trade = trades_sorted[0].timestamp
  393. newest_trade = trades_sorted[-1].timestamp
  394. analysis_period_ms = newest_trade - oldest_trade
  395. analysis_period_days = max(1, analysis_period_ms / (1000 * 60 * 60 * 24))
  396. # Calculate improved metrics
  397. total_trades = len(trades)
  398. total_fees = sum(trade.fee for trade in trades)
  399. # Analyze HFT patterns first
  400. hft_patterns = self.analyze_hft_patterns(trades)
  401. # Check if this is a manageable trading frequency for copy trading
  402. trading_freq = total_trades / analysis_period_days if analysis_period_days > 0 else 0
  403. is_copyable_frequency = 1 <= trading_freq <= 20 # 1-20 trades per day is manageable
  404. if hft_patterns['is_hft_pattern'] or trading_freq > 50:
  405. print(f"🤖 ❌ UNSUITABLE: High-frequency algorithmic trading detected")
  406. print(f"⚡ Trading frequency: {trading_freq:.1f} trades/day (TOO HIGH for copy trading)")
  407. print(f"🕒 Avg time between trades: {hft_patterns['avg_time_between_trades_minutes']:.1f} minutes")
  408. print(f"❌ This account cannot be safely copied - would result in overtrading and high fees")
  409. # Still calculate metrics for completeness but mark as unsuitable
  410. rolling_pnl, pnl_series, winning_periods, losing_periods = self.calculate_rolling_pnl(trades)
  411. realized_pnl = rolling_pnl
  412. win_rate = winning_periods / (winning_periods + losing_periods) if (winning_periods + losing_periods) > 0 else 0
  413. avg_duration = hft_patterns['avg_time_between_trades_minutes'] / 60 # Convert to hours
  414. print(f"💰 Rolling P&L: ${realized_pnl:.2f}, Periods: {winning_periods}W/{losing_periods}L")
  415. elif is_copyable_frequency:
  416. print(f"✅ SUITABLE: Human-manageable trading pattern detected")
  417. print(f"📊 Trading frequency: {trading_freq:.1f} trades/day (GOOD for copy trading)")
  418. # Use traditional P&L calculation for human traders
  419. realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
  420. print(f"💰 Realized PnL: ${realized_pnl:.2f}, Wins: {winning_trades}, Losses: {losing_trades}")
  421. print(f"📈 Trade Win Rate: {win_rate:.1%}")
  422. # Calculate traditional trade durations
  423. durations = []
  424. position_tracker = defaultdict(lambda: {'size': 0, 'start_time': 0})
  425. for trade in trades_sorted:
  426. coin = trade.coin
  427. pos = position_tracker[coin]
  428. if trade.side == 'buy':
  429. if pos['size'] <= 0 and trade.size > abs(pos['size']): # Opening new long
  430. pos['start_time'] = trade.timestamp
  431. pos['size'] += trade.size
  432. else: # sell
  433. if pos['size'] > 0: # Closing long position
  434. if trade.size >= pos['size'] and pos['start_time'] > 0: # Fully closing
  435. duration_hours = (trade.timestamp - pos['start_time']) / (1000 * 3600)
  436. if duration_hours > 0:
  437. durations.append(duration_hours)
  438. pos['start_time'] = 0
  439. pos['size'] -= trade.size
  440. elif pos['size'] <= 0: # Opening short
  441. pos['start_time'] = trade.timestamp
  442. pos['size'] -= trade.size
  443. avg_duration = statistics.mean(durations) if durations else 0
  444. print(f"🕒 Found {len(durations)} completed trades, avg duration: {avg_duration:.1f} hours")
  445. else:
  446. print(f"⚠️ QUESTIONABLE: Low trading frequency detected")
  447. print(f"📊 Trading frequency: {trading_freq:.1f} trades/day (might be inactive)")
  448. # Use traditional analysis for low-frequency traders
  449. realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
  450. print(f"💰 Realized PnL: ${realized_pnl:.2f}, Wins: {winning_trades}, Losses: {losing_trades}")
  451. print(f"📈 Trade Win Rate: {win_rate:.1%}")
  452. avg_duration = 24.0 # Assume longer holds for infrequent traders
  453. print(f"🕒 Infrequent trading pattern - assuming longer hold times")
  454. # Common calculations
  455. unrealized_pnl = sum(pos.unrealized_pnl for pos in positions)
  456. total_pnl = realized_pnl + unrealized_pnl
  457. print(f"💰 Total PnL: ${total_pnl:.2f} (Realized: ${realized_pnl:.2f} + Unrealized: ${unrealized_pnl:.2f})")
  458. print(f"💸 Total Fees: ${total_fees:.2f}")
  459. # Calculate position size statistics
  460. position_sizes = [trade.size * trade.price for trade in trades]
  461. avg_position_size = statistics.mean(position_sizes) if position_sizes else 0
  462. # Calculate leverage statistics from current positions
  463. leverages = [pos.leverage for pos in positions if pos.leverage > 0]
  464. max_leverage = max(leverages) if leverages else 0
  465. avg_leverage = statistics.mean(leverages) if leverages else 1
  466. # Calculate trading frequency
  467. trading_freq = total_trades / analysis_period_days if analysis_period_days > 0 else 0
  468. # Simplified drawdown calculation
  469. max_drawdown = 0.0
  470. current_drawdown = 0.0
  471. if total_pnl < 0:
  472. max_drawdown = abs(total_pnl) / (avg_position_size * 10) if avg_position_size > 0 else 0
  473. current_drawdown = max_drawdown
  474. # Risk metrics
  475. profit_factor = abs(realized_pnl) / total_fees if total_fees > 0 else 0
  476. # Analyze HFT patterns
  477. hft_patterns = self.analyze_hft_patterns(trades)
  478. # Determine copyability
  479. is_hft = trading_freq > 50
  480. is_inactive = trading_freq < 1
  481. is_copyable_freq = 1 <= trading_freq <= 20
  482. if is_hft:
  483. is_copyable = False
  484. copyability_reason = f"HFT Bot ({trading_freq:.1f} trades/day - too fast to copy)"
  485. elif is_inactive:
  486. is_copyable = False
  487. copyability_reason = f"Inactive ({trading_freq:.1f} trades/day - insufficient activity)"
  488. elif is_copyable_freq:
  489. is_copyable = True
  490. copyability_reason = f"Human trader ({trading_freq:.1f} trades/day - manageable frequency)"
  491. else:
  492. is_copyable = False
  493. copyability_reason = f"Questionable frequency ({trading_freq:.1f} trades/day)"
  494. # Calculate risk reward ratio safely
  495. if hft_patterns['is_hft_pattern']:
  496. # For HFT, use win rate as proxy for risk/reward
  497. risk_reward_ratio = win_rate / (1 - win_rate) if win_rate < 1 else 1.0
  498. else:
  499. # For traditional trading, try to use winning/losing trade counts
  500. try:
  501. # These variables should exist from traditional analysis
  502. risk_reward_ratio = winning_trades / max(1, losing_trades)
  503. except NameError:
  504. # Fallback if variables don't exist
  505. risk_reward_ratio = win_rate / (1 - win_rate) if win_rate < 1 else 1.0
  506. return AccountStats(
  507. address=address,
  508. total_pnl=total_pnl,
  509. win_rate=win_rate,
  510. total_trades=total_trades,
  511. avg_trade_duration_hours=avg_duration,
  512. max_drawdown=max_drawdown,
  513. sharpe_ratio=0, # Would need returns data
  514. avg_position_size=avg_position_size,
  515. max_leverage_used=max_leverage,
  516. avg_leverage_used=avg_leverage,
  517. trading_frequency_per_day=trading_freq,
  518. risk_reward_ratio=risk_reward_ratio,
  519. consecutive_losses_max=0, # Would need sequence analysis
  520. profit_factor=profit_factor,
  521. largest_win=0, # Would need individual trade P&L
  522. largest_loss=0, # Would need individual trade P&L
  523. active_positions=len(positions),
  524. current_drawdown=current_drawdown,
  525. last_trade_timestamp=newest_trade,
  526. analysis_period_days=int(analysis_period_days),
  527. is_copyable=is_copyable,
  528. copyability_reason=copyability_reason
  529. )
  530. async def analyze_multiple_accounts(self, addresses: List[str]) -> List[AccountStats]:
  531. """Analyze multiple accounts concurrently"""
  532. print(f"🚀 Starting analysis of {len(addresses)} accounts...\n")
  533. tasks = [self.analyze_account(addr) for addr in addresses]
  534. results = await asyncio.gather(*tasks, return_exceptions=True)
  535. # Filter out None results and exceptions
  536. valid_results = []
  537. for i, result in enumerate(results):
  538. if isinstance(result, Exception):
  539. print(f"❌ Error analyzing {addresses[i]}: {result}")
  540. elif result is not None:
  541. valid_results.append(result)
  542. return valid_results
  543. def print_analysis_results(self, stats_list: List[AccountStats]):
  544. """Print comprehensive analysis results"""
  545. if not stats_list:
  546. print("❌ No valid analysis results to display")
  547. return
  548. print("\n" + "="*100)
  549. print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
  550. print("="*100)
  551. # Sort by a composite score optimized for COPYABLE accounts
  552. def calculate_score(stats: AccountStats) -> float:
  553. score = 0
  554. # FIRST: Check if account is suitable for copy trading
  555. is_hft = stats.trading_frequency_per_day > 50
  556. is_too_slow = stats.trading_frequency_per_day < 1
  557. is_copyable = 1 <= stats.trading_frequency_per_day <= 20
  558. # HFT and inactive accounts get heavily penalized
  559. if is_hft:
  560. score -= 50 # Major penalty for HFT
  561. print(f" ❌ HFT Account Penalty: -50 points")
  562. elif is_too_slow:
  563. score -= 20 # Penalty for inactive accounts
  564. print(f" ⚠️ Inactive Account Penalty: -20 points")
  565. elif is_copyable:
  566. score += 20 # Bonus for manageable frequency
  567. print(f" ✅ Copyable Frequency Bonus: +20 points")
  568. # Profitability (30% weight)
  569. if stats.total_pnl > 0:
  570. pnl_score = min(30, stats.total_pnl / 1000) # $1000 = 30 points
  571. score += pnl_score
  572. print(f" 💰 Profitability Score: +{pnl_score:.1f} points")
  573. else:
  574. score -= 10
  575. print(f" 💰 Unprofitable Penalty: -10 points")
  576. # Win rate (25% weight) - prefer consistent traders
  577. win_score = stats.win_rate * 25
  578. score += win_score
  579. print(f" 📈 Win Rate Score: +{win_score:.1f} points")
  580. # Trade duration preference (15% weight) - prefer 2-48 hour holds
  581. if 2 <= stats.avg_trade_duration_hours <= 48:
  582. duration_score = 15 # Perfect range
  583. elif 1 <= stats.avg_trade_duration_hours < 2:
  584. duration_score = 10 # Too fast but acceptable
  585. elif 48 < stats.avg_trade_duration_hours <= 168: # 1 week
  586. duration_score = 12 # Slower but still good
  587. else:
  588. duration_score = 5 # Too fast (<1hr) or too slow (>1week)
  589. score += duration_score
  590. print(f" 🕒 Duration Score: +{duration_score} points ({stats.avg_trade_duration_hours:.1f}h)")
  591. # Risk management (10% weight)
  592. if stats.max_drawdown < 0.1:
  593. risk_score = 10
  594. elif stats.max_drawdown < 0.2:
  595. risk_score = 7
  596. elif stats.max_drawdown < 0.3:
  597. risk_score = 4
  598. else:
  599. risk_score = 0
  600. score += risk_score
  601. print(f" 📉 Risk Score: +{risk_score} points ({stats.max_drawdown:.1%} drawdown)")
  602. print(f" 🏆 TOTAL SCORE: {score:.1f}/100")
  603. return score
  604. sorted_stats = sorted(stats_list, key=calculate_score, reverse=True)
  605. for i, stats in enumerate(sorted_stats, 1):
  606. score = calculate_score(stats)
  607. print(f"\n{i}. 📋 ACCOUNT: {stats.address}")
  608. print(f" 🏆 SCORE: {score:.1f}/100")
  609. print(f" 💰 Total PnL: ${stats.total_pnl:.2f}")
  610. print(f" 📈 Win Rate: {stats.win_rate:.1%}")
  611. print(f" 🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
  612. print(f" 📉 Max Drawdown: {stats.max_drawdown:.1%}")
  613. print(f" 🔄 Trading Frequency: {stats.trading_frequency_per_day:.1f} trades/day")
  614. print(f" 💵 Avg Position Size: ${stats.avg_position_size:.2f}")
  615. print(f" ⚡ Max Leverage: {stats.max_leverage_used:.1f}x")
  616. print(f" 📊 Total Trades: {stats.total_trades}")
  617. print(f" 📍 Active Positions: {stats.active_positions}")
  618. print(f" 📅 Analysis Period: {stats.analysis_period_days} days")
  619. # Copy Trading Suitability Evaluation
  620. evaluation = []
  621. is_hft_pattern = stats.trading_frequency_per_day > 50
  622. is_copyable = 1 <= stats.trading_frequency_per_day <= 20
  623. # First determine if account is copyable
  624. if is_hft_pattern:
  625. evaluation.append("❌ NOT COPYABLE - HFT/Bot")
  626. elif stats.trading_frequency_per_day < 1:
  627. evaluation.append("❌ NOT COPYABLE - Inactive")
  628. elif is_copyable:
  629. evaluation.append("✅ COPYABLE - Human trader")
  630. else:
  631. evaluation.append("⚠️ QUESTIONABLE - Check frequency")
  632. # Profitability check
  633. if stats.total_pnl > 0:
  634. evaluation.append("✅ Profitable")
  635. else:
  636. evaluation.append("❌ Not profitable")
  637. # Trade duration evaluation for copyable accounts
  638. if is_copyable:
  639. if 2 <= stats.avg_trade_duration_hours <= 48:
  640. evaluation.append("✅ Good trade duration")
  641. elif stats.avg_trade_duration_hours < 2:
  642. evaluation.append("⚠️ Very short trades")
  643. else:
  644. evaluation.append("⚠️ Long hold times")
  645. # Win rate for human traders
  646. if stats.win_rate > 0.6:
  647. evaluation.append("✅ Excellent win rate")
  648. elif stats.win_rate > 0.4:
  649. evaluation.append("✅ Good win rate")
  650. else:
  651. evaluation.append("⚠️ Low win rate")
  652. else:
  653. # For non-copyable accounts, just note the pattern
  654. if is_hft_pattern:
  655. evaluation.append("🤖 Algorithmic trading")
  656. else:
  657. evaluation.append("💤 Low activity")
  658. # Risk management (universal)
  659. if stats.max_drawdown < 0.15:
  660. evaluation.append("✅ Good risk management")
  661. elif stats.max_drawdown < 0.25:
  662. evaluation.append("⚠️ Moderate risk")
  663. else:
  664. evaluation.append("❌ High drawdown risk")
  665. print(f" 🎯 Evaluation: {' | '.join(evaluation)}")
  666. # Recommendation - Filter for copyable accounts only
  667. print("\n" + "="*100)
  668. print("🎯 COPY TRADING RECOMMENDATIONS")
  669. print("="*100)
  670. # Separate copyable from non-copyable accounts
  671. copyable_accounts = [stats for stats in sorted_stats if stats.is_copyable]
  672. non_copyable_accounts = [stats for stats in sorted_stats if not stats.is_copyable]
  673. if copyable_accounts:
  674. print(f"\n✅ FOUND {len(copyable_accounts)} COPYABLE ACCOUNTS:")
  675. best_copyable = copyable_accounts[0]
  676. best_score = calculate_score(best_copyable)
  677. print(f"\n🏆 TOP COPYABLE RECOMMENDATION: {best_copyable.address}")
  678. print(f" 📊 Score: {best_score:.1f}/100")
  679. print(f" 🎯 Status: {best_copyable.copyability_reason}")
  680. if best_score >= 60:
  681. recommendation = "🟢 HIGHLY RECOMMENDED"
  682. elif best_score >= 40:
  683. recommendation = "🟡 MODERATELY RECOMMENDED"
  684. elif best_score >= 20:
  685. recommendation = "🟠 PROCEED WITH CAUTION"
  686. else:
  687. recommendation = "🔴 NOT RECOMMENDED"
  688. print(f" {recommendation}")
  689. print(f"\n📋 Why this account is suitable:")
  690. print(f" ✅ Trading frequency: {best_copyable.trading_frequency_per_day:.1f} trades/day (manageable)")
  691. if best_copyable.total_pnl > 0:
  692. print(f" ✅ Profitable: ${best_copyable.total_pnl:.2f} total PnL")
  693. if 2 <= best_copyable.avg_trade_duration_hours <= 48:
  694. print(f" ✅ Good duration: {best_copyable.avg_trade_duration_hours:.1f} hour average")
  695. if best_copyable.win_rate > 0.4:
  696. print(f" ✅ Good performance: {best_copyable.win_rate:.1%} win rate")
  697. if best_copyable.max_drawdown < 0.2:
  698. print(f" ✅ Risk management: {best_copyable.max_drawdown:.1%} max drawdown")
  699. print(f"\n⚙️ Suggested copy trading settings:")
  700. print(f" 📊 Portfolio allocation: 5-15% (start conservative)")
  701. print(f" ⚡ Max leverage limit: 3-5x")
  702. print(f" 💰 Min position size: $25-50")
  703. print(f" 🔄 Expected trades: {best_copyable.trading_frequency_per_day:.1f} per day")
  704. else:
  705. print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
  706. print(f" All analyzed accounts are unsuitable for copy trading")
  707. if non_copyable_accounts:
  708. print(f"\n❌ {len(non_copyable_accounts)} UNSUITABLE ACCOUNTS (DO NOT COPY):")
  709. for i, account in enumerate(non_copyable_accounts[:3], 1): # Show top 3 unsuitable
  710. print(f" {i}. {account.address[:10]}... - {account.copyability_reason}")
  711. if len(non_copyable_accounts) > 3:
  712. print(f" ... and {len(non_copyable_accounts) - 3} more unsuitable accounts")
  713. print(f"\n⚠️ IMPORTANT COPY TRADING GUIDELINES:")
  714. print(f" • Only copy accounts with 1-20 trades per day")
  715. print(f" • Avoid HFT bots (50+ trades/day) - impossible to follow")
  716. print(f" • Start with small allocation (5%) and increase gradually")
  717. print(f" • Monitor performance and adjust leverage accordingly")
  718. async def get_leaderboard(self, window: str = "7d", limit: int = 20) -> Optional[List[str]]:
  719. """
  720. Get top accounts from Hyperliquid leaderboard
  721. Note: Hyperliquid's public API doesn't expose leaderboard data directly.
  722. This function serves as a template for when/if the API becomes available.
  723. Args:
  724. window: Time window for leaderboard ("1d", "7d", "30d", "allTime")
  725. limit: Number of top accounts to return
  726. Returns:
  727. List of account addresses from leaderboard (currently returns None)
  728. """
  729. print(f"⚠️ Hyperliquid leaderboard API not publicly accessible")
  730. print(f"💡 To analyze current top performers:")
  731. print(f" 1. Visit: https://app.hyperliquid.xyz/leaderboard")
  732. print(f" 2. Copy top performer addresses manually")
  733. print(f" 3. Run: python utils/hyperliquid_account_analyzer.py [address1] [address2] ...")
  734. print(f" 4. Or use --top10 for a curated list of known good traders")
  735. # Note: If Hyperliquid ever makes their leaderboard API public,
  736. # we can implement the actual fetching logic here
  737. return None
  738. async def _try_alternative_leaderboard(self, window: str, limit: int) -> Optional[List[str]]:
  739. """Try alternative methods to get leaderboard data"""
  740. try:
  741. # Try different payload formats
  742. alternative_payloads = [
  743. {
  744. "type": "leaderBoard",
  745. "timeWindow": window
  746. },
  747. {
  748. "type": "userLeaderboard",
  749. "window": window
  750. },
  751. {
  752. "type": "spotLeaderboard",
  753. "req": {"timeWindow": window}
  754. }
  755. ]
  756. for payload in alternative_payloads:
  757. try:
  758. async with self.session.post(self.info_url, json=payload) as response:
  759. if response.status == 200:
  760. data = await response.json()
  761. # Try to extract addresses from any structure
  762. addresses = self._extract_addresses_from_data(data, limit)
  763. if addresses:
  764. print(f"📊 Successfully fetched {len(addresses)} addresses using alternative method")
  765. return addresses
  766. except Exception as e:
  767. continue
  768. print("⚠️ Could not fetch leaderboard data, using fallback top accounts")
  769. return None
  770. except Exception as e:
  771. print(f"⚠️ Alternative leaderboard fetch failed: {e}")
  772. return None
  773. def _extract_addresses_from_data(self, data: Any, limit: int) -> List[str]:
  774. """Extract addresses from any nested data structure"""
  775. addresses = []
  776. def recursive_search(obj, depth=0):
  777. if depth > 5: # Prevent infinite recursion
  778. return
  779. if isinstance(obj, list):
  780. for item in obj:
  781. recursive_search(item, depth + 1)
  782. elif isinstance(obj, dict):
  783. # Check if this dict has an address field
  784. for addr_field in ['user', 'address', 'account', 'trader', 'wallet']:
  785. if addr_field in obj:
  786. addr = obj[addr_field]
  787. if isinstance(addr, str) and addr.startswith('0x') and len(addr) == 42:
  788. if addr not in addresses: # Avoid duplicates
  789. addresses.append(addr)
  790. # Recurse into nested objects
  791. for value in obj.values():
  792. recursive_search(value, depth + 1)
  793. recursive_search(data)
  794. return addresses[:limit]
  795. async def get_top_accounts_from_leaderboard(self, window: str = "7d", limit: int = 10) -> List[str]:
  796. """
  797. Get top performing accounts from Hyperliquid leaderboard
  798. Currently uses a curated list of high-performing accounts since
  799. the Hyperliquid leaderboard API is not publicly accessible.
  800. Args:
  801. window: Time window ("1d", "7d", "30d", "allTime")
  802. limit: Number of accounts to return
  803. Returns:
  804. List of top account addresses
  805. """
  806. print(f"🔍 Attempting to fetch top {limit} accounts from {window} leaderboard...")
  807. addresses = await self.get_leaderboard(window, limit)
  808. if not addresses:
  809. print("\n📋 Using curated list of high-performing accounts")
  810. print("💡 These accounts have been manually verified for good performance")
  811. # Curated list of known high-performing accounts
  812. # Updated based on our previous analysis
  813. curated_addresses = [
  814. "0x59a15c79a007cd6e9965b949fcf04125c2212524", # Best performer from previous analysis
  815. "0xa10ec245b3483f83e350a9165a52ae23dbab01bc",
  816. "0x0487b5e806ac781508cb3272ebd83ad603ddcc0f",
  817. "0x72fad4e75748b65566a3ebb555b6f6ee18ce08d1",
  818. "0xa70434af5778038245d53da1b4d360a30307a827",
  819. "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8",
  820. "0x3104b7668f9e46fb13ec0b141d2902e144d67efe",
  821. "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
  822. "0xc62df97dcf96324adf4edd30a4a7bffd5402f4da",
  823. "0xd11f5de0189d52b3abe6b0960b8377c20988e17e"
  824. ]
  825. selected_addresses = curated_addresses[:limit]
  826. print(f"📊 Selected {len(selected_addresses)} accounts for analysis:")
  827. for i, addr in enumerate(selected_addresses, 1):
  828. print(f" {i}. {addr}")
  829. return selected_addresses
  830. print(f"✅ Successfully fetched {len(addresses)} top accounts from leaderboard")
  831. for i, addr in enumerate(addresses, 1):
  832. print(f" {i}. {addr}")
  833. return addresses
  834. async def main():
  835. """Main function"""
  836. parser = argparse.ArgumentParser(description='Analyze Hyperliquid trading accounts')
  837. parser.add_argument('addresses', nargs='*', help='Account addresses to analyze')
  838. parser.add_argument('--top10', action='store_true', help='Analyze the provided top 10 accounts (hardcoded list)')
  839. parser.add_argument('--leaderboard', action='store_true', help='Fetch and analyze top accounts from Hyperliquid leaderboard')
  840. parser.add_argument('--window', default='7d', choices=['1d', '7d', '30d', 'allTime'],
  841. help='Time window for leaderboard (default: 7d)')
  842. parser.add_argument('--limit', type=int, default=10, help='Number of top accounts to analyze (default: 10)')
  843. args = parser.parse_args()
  844. # Top 10 accounts from the user (fallback)
  845. top10_addresses = [
  846. "0xa10ec245b3483f83e350a9165a52ae23dbab01bc",
  847. "0x2aab3badd6a5daa388da47de4c72a6fa618a6265",
  848. "0xd11f5de0189d52b3abe6b0960b8377c20988e17e",
  849. "0xc62df97dcf96324adf4edd30a4a7bffd5402f4da",
  850. "0xa70434af5778038245d53da1b4d360a30307a827",
  851. "0x72fad4e75748b65566a3ebb555b6f6ee18ce08d1",
  852. "0x0487b5e806ac781508cb3272ebd83ad603ddcc0f",
  853. "0x59a15c79a007cd6e9965b949fcf04125c2212524",
  854. "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8",
  855. "0x3104b7668f9e46fb13ec0b141d2902e144d67efe",
  856. "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
  857. "0x101a2d2afc2f9b0b217637f53e3a3e859104a33d",
  858. "0x836f01e63bd0fcbe673dcd905f882a5a808dd36e",
  859. "0xae42743b5d6a3594b7f95b5cebce64cfedc69318",
  860. "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
  861. "0x944fdea9d4956ce673c7545862cefccad6ee1b04",
  862. "0x2a93e999816c9826ade0b51aaa2d83240d8f4596",
  863. "0x7d3ca5fa94383b22ee49fc14e89aa417f65b4d92",
  864. "0xfacb7404c1fad06444bda161d1304e4b7aa14e77",
  865. "0x654d8c01f308d670d6bed13d892ee7ee285028a6",
  866. "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8"
  867. ]
  868. async with HyperliquidAccountAnalyzer() as analyzer:
  869. if args.leaderboard:
  870. # Fetch top accounts from leaderboard
  871. addresses = await analyzer.get_top_accounts_from_leaderboard(args.window, args.limit)
  872. elif args.top10:
  873. # Use hardcoded top 10 list
  874. addresses = top10_addresses
  875. print("ℹ️ Using hardcoded top 10 accounts")
  876. elif args.addresses:
  877. # Use provided addresses
  878. addresses = args.addresses
  879. print(f"ℹ️ Analyzing {len(addresses)} provided addresses")
  880. else:
  881. # Default: use curated list (since leaderboard API isn't available)
  882. print("ℹ️ No addresses specified, using curated high-performance accounts...")
  883. addresses = await analyzer.get_top_accounts_from_leaderboard(args.window, args.limit)
  884. if not addresses:
  885. print("❌ No addresses to analyze")
  886. return
  887. results = await analyzer.analyze_multiple_accounts(addresses)
  888. analyzer.print_analysis_results(results)
  889. if __name__ == "__main__":
  890. asyncio.run(main())