|
@@ -10,10 +10,24 @@ Analyzes Hyperliquid trading accounts to evaluate:
|
|
|
- Position sizing and leverage usage
|
|
|
|
|
|
Usage:
|
|
|
+ # Analyze specific addresses
|
|
|
python utils/hyperliquid_account_analyzer.py [address1] [address2] ...
|
|
|
|
|
|
-Or run with the provided top 10 addresses:
|
|
|
+ # Use curated high-performance accounts (default)
|
|
|
+ python utils/hyperliquid_account_analyzer.py
|
|
|
+ python utils/hyperliquid_account_analyzer.py --limit 15
|
|
|
+
|
|
|
+ # Use hardcoded top 10 addresses
|
|
|
python utils/hyperliquid_account_analyzer.py --top10
|
|
|
+
|
|
|
+Options:
|
|
|
+ --leaderboard Use curated high-performance accounts (recommended)
|
|
|
+ --window Time window preference: 1d, 7d, 30d, allTime (default: 7d)
|
|
|
+ --limit Number of accounts to analyze (default: 10)
|
|
|
+ --top10 Use original hardcoded list of top 10 accounts
|
|
|
+
|
|
|
+Note: Hyperliquid's leaderboard API is not publicly accessible, so the script uses
|
|
|
+ a manually curated list of high-performing accounts identified through analysis.
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
@@ -77,6 +91,8 @@ class AccountStats:
|
|
|
current_drawdown: float
|
|
|
last_trade_timestamp: int
|
|
|
analysis_period_days: int
|
|
|
+ is_copyable: bool # Whether this account is suitable for copy trading
|
|
|
+ copyability_reason: str # Why it is/isn't copyable
|
|
|
|
|
|
class HyperliquidAccountAnalyzer:
|
|
|
"""Analyzes Hyperliquid trading accounts"""
|
|
@@ -296,6 +312,144 @@ class HyperliquidAccountAnalyzer:
|
|
|
win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
|
|
|
return total_realized_pnl, win_rate, winning_trades, losing_trades
|
|
|
|
|
|
+ def analyze_hft_patterns(self, trades: List[Trade]) -> Dict[str, Any]:
|
|
|
+ """
|
|
|
+ Analyze high-frequency trading patterns that don't follow traditional open/close cycles
|
|
|
+ """
|
|
|
+ if not trades:
|
|
|
+ return {
|
|
|
+ 'avg_time_between_trades_minutes': 0,
|
|
|
+ 'max_time_between_trades_hours': 0,
|
|
|
+ 'min_time_between_trades_seconds': 0,
|
|
|
+ 'trading_clusters': 0,
|
|
|
+ 'trades_per_cluster': 0,
|
|
|
+ 'is_hft_pattern': False
|
|
|
+ }
|
|
|
+
|
|
|
+ trades_sorted = sorted(trades, key=lambda x: x.timestamp)
|
|
|
+ time_gaps = []
|
|
|
+
|
|
|
+ # Calculate time gaps between consecutive trades
|
|
|
+ for i in range(1, len(trades_sorted)):
|
|
|
+ gap_ms = trades_sorted[i].timestamp - trades_sorted[i-1].timestamp
|
|
|
+ gap_minutes = gap_ms / (1000 * 60)
|
|
|
+ time_gaps.append(gap_minutes)
|
|
|
+
|
|
|
+ if not time_gaps:
|
|
|
+ return {
|
|
|
+ 'avg_time_between_trades_minutes': 0,
|
|
|
+ 'max_time_between_trades_hours': 0,
|
|
|
+ 'min_time_between_trades_seconds': 0,
|
|
|
+ 'trading_clusters': 0,
|
|
|
+ 'trades_per_cluster': 0,
|
|
|
+ 'is_hft_pattern': False
|
|
|
+ }
|
|
|
+
|
|
|
+ avg_gap_minutes = statistics.mean(time_gaps)
|
|
|
+ max_gap_hours = max(time_gaps) / 60
|
|
|
+ min_gap_seconds = min(time_gaps) * 60
|
|
|
+
|
|
|
+ # Identify trading clusters (periods of intense activity)
|
|
|
+ clusters = []
|
|
|
+ current_cluster = [trades_sorted[0]]
|
|
|
+
|
|
|
+ for i in range(1, len(trades_sorted)):
|
|
|
+ gap_minutes = time_gaps[i-1]
|
|
|
+
|
|
|
+ if gap_minutes <= 5: # Trades within 5 minutes = same cluster
|
|
|
+ current_cluster.append(trades_sorted[i])
|
|
|
+ else:
|
|
|
+ if len(current_cluster) >= 3: # Minimum 3 trades to be a cluster
|
|
|
+ clusters.append(current_cluster)
|
|
|
+ current_cluster = [trades_sorted[i]]
|
|
|
+
|
|
|
+ # Don't forget the last cluster
|
|
|
+ if len(current_cluster) >= 3:
|
|
|
+ clusters.append(current_cluster)
|
|
|
+
|
|
|
+ avg_trades_per_cluster = statistics.mean([len(cluster) for cluster in clusters]) if clusters else 0
|
|
|
+
|
|
|
+ # Determine if this is HFT pattern
|
|
|
+ is_hft = (
|
|
|
+ avg_gap_minutes < 30 and # Average < 30 minutes between trades
|
|
|
+ len([gap for gap in time_gaps if gap < 1]) > len(time_gaps) * 0.3 # 30%+ trades within 1 minute
|
|
|
+ )
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'avg_time_between_trades_minutes': avg_gap_minutes,
|
|
|
+ 'max_time_between_trades_hours': max_gap_hours,
|
|
|
+ 'min_time_between_trades_seconds': min_gap_seconds,
|
|
|
+ 'trading_clusters': len(clusters),
|
|
|
+ 'trades_per_cluster': avg_trades_per_cluster,
|
|
|
+ 'is_hft_pattern': is_hft
|
|
|
+ }
|
|
|
+
|
|
|
+ def calculate_rolling_pnl(self, trades: List[Trade]) -> Tuple[float, List[float], int, int]:
|
|
|
+ """
|
|
|
+ Calculate P&L using rolling window approach for HFT patterns
|
|
|
+ """
|
|
|
+ if not trades:
|
|
|
+ return 0.0, [], 0, 0
|
|
|
+
|
|
|
+ trades_sorted = sorted(trades, key=lambda x: x.timestamp)
|
|
|
+
|
|
|
+ # Track net position and P&L over time
|
|
|
+ cumulative_pnl = 0.0
|
|
|
+ pnl_series = []
|
|
|
+ winning_periods = 0
|
|
|
+ losing_periods = 0
|
|
|
+
|
|
|
+ # Use 1-hour windows for P&L calculation
|
|
|
+ window_size_ms = 60 * 60 * 1000 # 1 hour
|
|
|
+
|
|
|
+ if not trades_sorted:
|
|
|
+ return 0.0, [], 0, 0
|
|
|
+
|
|
|
+ start_time = trades_sorted[0].timestamp
|
|
|
+ end_time = trades_sorted[-1].timestamp
|
|
|
+
|
|
|
+ current_time = start_time
|
|
|
+ window_trades = []
|
|
|
+
|
|
|
+ while current_time <= end_time:
|
|
|
+ window_end = current_time + window_size_ms
|
|
|
+
|
|
|
+ # Get trades in this window
|
|
|
+ window_trades = [
|
|
|
+ t for t in trades_sorted
|
|
|
+ if current_time <= t.timestamp < window_end
|
|
|
+ ]
|
|
|
+
|
|
|
+ if window_trades:
|
|
|
+ # Calculate net flow and fees for this window
|
|
|
+ net_usd_flow = 0.0
|
|
|
+ window_fees = 0.0
|
|
|
+
|
|
|
+ for trade in window_trades:
|
|
|
+ trade_value = trade.size * trade.price
|
|
|
+ if trade.side == 'buy':
|
|
|
+ net_usd_flow -= trade_value # Cash out
|
|
|
+ else: # sell
|
|
|
+ net_usd_flow += trade_value # Cash in
|
|
|
+
|
|
|
+ window_fees += trade.fee
|
|
|
+
|
|
|
+ # Window P&L = net cash flow - fees
|
|
|
+ window_pnl = net_usd_flow - window_fees
|
|
|
+ cumulative_pnl += window_pnl
|
|
|
+ pnl_series.append(cumulative_pnl)
|
|
|
+
|
|
|
+ if window_pnl > 0:
|
|
|
+ winning_periods += 1
|
|
|
+ elif window_pnl < 0:
|
|
|
+ losing_periods += 1
|
|
|
+
|
|
|
+ current_time = window_end
|
|
|
+
|
|
|
+ win_rate = winning_periods / (winning_periods + losing_periods) if (winning_periods + losing_periods) > 0 else 0
|
|
|
+
|
|
|
+ return cumulative_pnl, pnl_series, winning_periods, losing_periods
|
|
|
+
|
|
|
async def analyze_account(self, address: str) -> Optional[AccountStats]:
|
|
|
"""Analyze a single account and return comprehensive statistics"""
|
|
|
print(f"\n🔍 Analyzing account: {address}")
|
|
@@ -329,40 +483,83 @@ class HyperliquidAccountAnalyzer:
|
|
|
total_trades = len(trades)
|
|
|
total_fees = sum(trade.fee for trade in trades)
|
|
|
|
|
|
- # Get better PnL calculation
|
|
|
- realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
|
|
|
+ # Analyze HFT patterns first
|
|
|
+ hft_patterns = self.analyze_hft_patterns(trades)
|
|
|
+
|
|
|
+ # Check if this is a manageable trading frequency for copy trading
|
|
|
+ trading_freq = total_trades / analysis_period_days if analysis_period_days > 0 else 0
|
|
|
+ is_copyable_frequency = 1 <= trading_freq <= 20 # 1-20 trades per day is manageable
|
|
|
+
|
|
|
+ if hft_patterns['is_hft_pattern'] or trading_freq > 50:
|
|
|
+ print(f"🤖 ❌ UNSUITABLE: High-frequency algorithmic trading detected")
|
|
|
+ print(f"⚡ Trading frequency: {trading_freq:.1f} trades/day (TOO HIGH for copy trading)")
|
|
|
+ print(f"🕒 Avg time between trades: {hft_patterns['avg_time_between_trades_minutes']:.1f} minutes")
|
|
|
+ print(f"❌ This account cannot be safely copied - would result in overtrading and high fees")
|
|
|
+
|
|
|
+ # Still calculate metrics for completeness but mark as unsuitable
|
|
|
+ rolling_pnl, pnl_series, winning_periods, losing_periods = self.calculate_rolling_pnl(trades)
|
|
|
+ realized_pnl = rolling_pnl
|
|
|
+ win_rate = winning_periods / (winning_periods + losing_periods) if (winning_periods + losing_periods) > 0 else 0
|
|
|
+ avg_duration = hft_patterns['avg_time_between_trades_minutes'] / 60 # Convert to hours
|
|
|
+
|
|
|
+ print(f"💰 Rolling P&L: ${realized_pnl:.2f}, Periods: {winning_periods}W/{losing_periods}L")
|
|
|
+
|
|
|
+ elif is_copyable_frequency:
|
|
|
+ print(f"✅ SUITABLE: Human-manageable trading pattern detected")
|
|
|
+ print(f"📊 Trading frequency: {trading_freq:.1f} trades/day (GOOD for copy trading)")
|
|
|
+
|
|
|
+ # Use traditional P&L calculation for human traders
|
|
|
+ realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
|
|
|
+
|
|
|
+ print(f"💰 Realized PnL: ${realized_pnl:.2f}, Wins: {winning_trades}, Losses: {losing_trades}")
|
|
|
+ print(f"📈 Trade Win Rate: {win_rate:.1%}")
|
|
|
+
|
|
|
+ # Calculate traditional trade durations
|
|
|
+ durations = []
|
|
|
+ position_tracker = defaultdict(lambda: {'size': 0, 'start_time': 0})
|
|
|
+
|
|
|
+ for trade in trades_sorted:
|
|
|
+ coin = trade.coin
|
|
|
+ pos = position_tracker[coin]
|
|
|
+
|
|
|
+ if trade.side == 'buy':
|
|
|
+ if pos['size'] <= 0 and trade.size > abs(pos['size']): # Opening new long
|
|
|
+ pos['start_time'] = trade.timestamp
|
|
|
+ pos['size'] += trade.size
|
|
|
+ else: # sell
|
|
|
+ if pos['size'] > 0: # Closing long position
|
|
|
+ if trade.size >= pos['size'] and pos['start_time'] > 0: # Fully closing
|
|
|
+ duration_hours = (trade.timestamp - pos['start_time']) / (1000 * 3600)
|
|
|
+ if duration_hours > 0:
|
|
|
+ durations.append(duration_hours)
|
|
|
+ pos['start_time'] = 0
|
|
|
+ pos['size'] -= trade.size
|
|
|
+ elif pos['size'] <= 0: # Opening short
|
|
|
+ pos['start_time'] = trade.timestamp
|
|
|
+ pos['size'] -= trade.size
|
|
|
+
|
|
|
+ avg_duration = statistics.mean(durations) if durations else 0
|
|
|
+ print(f"🕒 Found {len(durations)} completed trades, avg duration: {avg_duration:.1f} hours")
|
|
|
+
|
|
|
+ else:
|
|
|
+ print(f"⚠️ QUESTIONABLE: Low trading frequency detected")
|
|
|
+ print(f"📊 Trading frequency: {trading_freq:.1f} trades/day (might be inactive)")
|
|
|
+
|
|
|
+ # Use traditional analysis for low-frequency traders
|
|
|
+ realized_pnl, win_rate, winning_trades, losing_trades = self.calculate_trade_performance(trades)
|
|
|
+
|
|
|
+ print(f"💰 Realized PnL: ${realized_pnl:.2f}, Wins: {winning_trades}, Losses: {losing_trades}")
|
|
|
+ print(f"📈 Trade Win Rate: {win_rate:.1%}")
|
|
|
+
|
|
|
+ avg_duration = 24.0 # Assume longer holds for infrequent traders
|
|
|
+ print(f"🕒 Infrequent trading pattern - assuming longer hold times")
|
|
|
+
|
|
|
+ # Common calculations
|
|
|
unrealized_pnl = sum(pos.unrealized_pnl for pos in positions)
|
|
|
total_pnl = realized_pnl + unrealized_pnl
|
|
|
|
|
|
- print(f"💰 Realized PnL: ${realized_pnl:.2f}, Unrealized: ${unrealized_pnl:.2f}, Fees: ${total_fees:.2f}")
|
|
|
- print(f"📈 Wins: {winning_trades}, Losses: {losing_trades}, Win Rate: {win_rate:.1%}")
|
|
|
-
|
|
|
- # Calculate trade durations (improved)
|
|
|
- durations = []
|
|
|
- position_tracker = defaultdict(lambda: {'size': 0, 'start_time': 0})
|
|
|
-
|
|
|
- for trade in trades_sorted:
|
|
|
- coin = trade.coin
|
|
|
- pos = position_tracker[coin]
|
|
|
-
|
|
|
- if trade.side == 'buy':
|
|
|
- if pos['size'] <= 0 and trade.size > abs(pos['size']): # Opening new long
|
|
|
- pos['start_time'] = trade.timestamp
|
|
|
- pos['size'] += trade.size
|
|
|
- else: # sell
|
|
|
- if pos['size'] > 0: # Closing long position
|
|
|
- if trade.size >= pos['size'] and pos['start_time'] > 0: # Fully closing
|
|
|
- duration_hours = (trade.timestamp - pos['start_time']) / (1000 * 3600)
|
|
|
- if duration_hours > 0:
|
|
|
- durations.append(duration_hours)
|
|
|
- pos['start_time'] = 0
|
|
|
- pos['size'] -= trade.size
|
|
|
- elif pos['size'] <= 0: # Opening short
|
|
|
- pos['start_time'] = trade.timestamp
|
|
|
- pos['size'] -= trade.size
|
|
|
-
|
|
|
- avg_duration = statistics.mean(durations) if durations else 0
|
|
|
- print(f"🕒 Found {len(durations)} completed trades, avg duration: {avg_duration:.1f} hours")
|
|
|
+ print(f"💰 Total PnL: ${total_pnl:.2f} (Realized: ${realized_pnl:.2f} + Unrealized: ${unrealized_pnl:.2f})")
|
|
|
+ print(f"💸 Total Fees: ${total_fees:.2f}")
|
|
|
|
|
|
# Calculate position size statistics
|
|
|
position_sizes = [trade.size * trade.price for trade in trades]
|
|
@@ -386,6 +583,40 @@ class HyperliquidAccountAnalyzer:
|
|
|
# Risk metrics
|
|
|
profit_factor = abs(realized_pnl) / total_fees if total_fees > 0 else 0
|
|
|
|
|
|
+ # Analyze HFT patterns
|
|
|
+ hft_patterns = self.analyze_hft_patterns(trades)
|
|
|
+
|
|
|
+ # Determine copyability
|
|
|
+ is_hft = trading_freq > 50
|
|
|
+ is_inactive = trading_freq < 1
|
|
|
+ is_copyable_freq = 1 <= trading_freq <= 20
|
|
|
+
|
|
|
+ if is_hft:
|
|
|
+ is_copyable = False
|
|
|
+ copyability_reason = f"HFT Bot ({trading_freq:.1f} trades/day - too fast to copy)"
|
|
|
+ elif is_inactive:
|
|
|
+ is_copyable = False
|
|
|
+ copyability_reason = f"Inactive ({trading_freq:.1f} trades/day - insufficient activity)"
|
|
|
+ elif is_copyable_freq:
|
|
|
+ is_copyable = True
|
|
|
+ copyability_reason = f"Human trader ({trading_freq:.1f} trades/day - manageable frequency)"
|
|
|
+ else:
|
|
|
+ is_copyable = False
|
|
|
+ copyability_reason = f"Questionable frequency ({trading_freq:.1f} trades/day)"
|
|
|
+
|
|
|
+ # Calculate risk reward ratio safely
|
|
|
+ if hft_patterns['is_hft_pattern']:
|
|
|
+ # For HFT, use win rate as proxy for risk/reward
|
|
|
+ risk_reward_ratio = win_rate / (1 - win_rate) if win_rate < 1 else 1.0
|
|
|
+ else:
|
|
|
+ # For traditional trading, try to use winning/losing trade counts
|
|
|
+ try:
|
|
|
+ # These variables should exist from traditional analysis
|
|
|
+ risk_reward_ratio = winning_trades / max(1, losing_trades)
|
|
|
+ except NameError:
|
|
|
+ # Fallback if variables don't exist
|
|
|
+ risk_reward_ratio = win_rate / (1 - win_rate) if win_rate < 1 else 1.0
|
|
|
+
|
|
|
return AccountStats(
|
|
|
address=address,
|
|
|
total_pnl=total_pnl,
|
|
@@ -398,7 +629,7 @@ class HyperliquidAccountAnalyzer:
|
|
|
max_leverage_used=max_leverage,
|
|
|
avg_leverage_used=avg_leverage,
|
|
|
trading_frequency_per_day=trading_freq,
|
|
|
- risk_reward_ratio=winning_trades / max(1, losing_trades),
|
|
|
+ risk_reward_ratio=risk_reward_ratio,
|
|
|
consecutive_losses_max=0, # Would need sequence analysis
|
|
|
profit_factor=profit_factor,
|
|
|
largest_win=0, # Would need individual trade P&L
|
|
@@ -406,7 +637,9 @@ class HyperliquidAccountAnalyzer:
|
|
|
active_positions=len(positions),
|
|
|
current_drawdown=current_drawdown,
|
|
|
last_trade_timestamp=newest_trade,
|
|
|
- analysis_period_days=int(analysis_period_days)
|
|
|
+ analysis_period_days=int(analysis_period_days),
|
|
|
+ is_copyable=is_copyable,
|
|
|
+ copyability_reason=copyability_reason
|
|
|
)
|
|
|
|
|
|
async def analyze_multiple_accounts(self, addresses: List[str]) -> List[AccountStats]:
|
|
@@ -436,30 +669,67 @@ class HyperliquidAccountAnalyzer:
|
|
|
print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
|
|
|
print("="*100)
|
|
|
|
|
|
- # Sort by a composite score (you can adjust this ranking)
|
|
|
+ # Sort by a composite score optimized for COPYABLE accounts
|
|
|
def calculate_score(stats: AccountStats) -> float:
|
|
|
score = 0
|
|
|
|
|
|
- # Profitability (40% weight)
|
|
|
+ # FIRST: Check if account is suitable for copy trading
|
|
|
+ is_hft = stats.trading_frequency_per_day > 50
|
|
|
+ is_too_slow = stats.trading_frequency_per_day < 1
|
|
|
+ is_copyable = 1 <= stats.trading_frequency_per_day <= 20
|
|
|
+
|
|
|
+ # HFT and inactive accounts get heavily penalized
|
|
|
+ if is_hft:
|
|
|
+ score -= 50 # Major penalty for HFT
|
|
|
+ print(f" ❌ HFT Account Penalty: -50 points")
|
|
|
+ elif is_too_slow:
|
|
|
+ score -= 20 # Penalty for inactive accounts
|
|
|
+ print(f" ⚠️ Inactive Account Penalty: -20 points")
|
|
|
+ elif is_copyable:
|
|
|
+ score += 20 # Bonus for manageable frequency
|
|
|
+ print(f" ✅ Copyable Frequency Bonus: +20 points")
|
|
|
+
|
|
|
+ # Profitability (30% weight)
|
|
|
if stats.total_pnl > 0:
|
|
|
- score += 40
|
|
|
+ pnl_score = min(30, stats.total_pnl / 1000) # $1000 = 30 points
|
|
|
+ score += pnl_score
|
|
|
+ print(f" 💰 Profitability Score: +{pnl_score:.1f} points")
|
|
|
+ else:
|
|
|
+ score -= 10
|
|
|
+ print(f" 💰 Unprofitable Penalty: -10 points")
|
|
|
+
|
|
|
+ # Win rate (25% weight) - prefer consistent traders
|
|
|
+ win_score = stats.win_rate * 25
|
|
|
+ score += win_score
|
|
|
+ print(f" 📈 Win Rate Score: +{win_score:.1f} points")
|
|
|
|
|
|
- # Win rate (20% weight)
|
|
|
- score += stats.win_rate * 20
|
|
|
+ # Trade duration preference (15% weight) - prefer 2-48 hour holds
|
|
|
+ if 2 <= stats.avg_trade_duration_hours <= 48:
|
|
|
+ duration_score = 15 # Perfect range
|
|
|
+ elif 1 <= stats.avg_trade_duration_hours < 2:
|
|
|
+ duration_score = 10 # Too fast but acceptable
|
|
|
+ elif 48 < stats.avg_trade_duration_hours <= 168: # 1 week
|
|
|
+ duration_score = 12 # Slower but still good
|
|
|
+ else:
|
|
|
+ duration_score = 5 # Too fast (<1hr) or too slow (>1week)
|
|
|
|
|
|
- # Short duration trades (20% weight) - prefer < 24 hours
|
|
|
- if stats.avg_trade_duration_hours > 0:
|
|
|
- duration_score = max(0, 20 - (stats.avg_trade_duration_hours / 24) * 20)
|
|
|
- score += duration_score
|
|
|
+ score += duration_score
|
|
|
+ print(f" 🕒 Duration Score: +{duration_score} points ({stats.avg_trade_duration_hours:.1f}h)")
|
|
|
|
|
|
- # Trading frequency (10% weight) - prefer active traders
|
|
|
- freq_score = min(10, stats.trading_frequency_per_day * 2)
|
|
|
- score += freq_score
|
|
|
+ # Risk management (10% weight)
|
|
|
+ if stats.max_drawdown < 0.1:
|
|
|
+ risk_score = 10
|
|
|
+ elif stats.max_drawdown < 0.2:
|
|
|
+ risk_score = 7
|
|
|
+ elif stats.max_drawdown < 0.3:
|
|
|
+ risk_score = 4
|
|
|
+ else:
|
|
|
+ risk_score = 0
|
|
|
|
|
|
- # Low drawdown (10% weight)
|
|
|
- drawdown_score = max(0, 10 - stats.max_drawdown * 100)
|
|
|
- score += drawdown_score
|
|
|
+ score += risk_score
|
|
|
+ print(f" 📉 Risk Score: +{risk_score} points ({stats.max_drawdown:.1%} drawdown)")
|
|
|
|
|
|
+ print(f" 🏆 TOTAL SCORE: {score:.1f}/100")
|
|
|
return score
|
|
|
|
|
|
sorted_stats = sorted(stats_list, key=calculate_score, reverse=True)
|
|
@@ -480,77 +750,284 @@ class HyperliquidAccountAnalyzer:
|
|
|
print(f" 📍 Active Positions: {stats.active_positions}")
|
|
|
print(f" 📅 Analysis Period: {stats.analysis_period_days} days")
|
|
|
|
|
|
- # Evaluation
|
|
|
+ # Copy Trading Suitability Evaluation
|
|
|
evaluation = []
|
|
|
+ is_hft_pattern = stats.trading_frequency_per_day > 50
|
|
|
+ is_copyable = 1 <= stats.trading_frequency_per_day <= 20
|
|
|
+
|
|
|
+ # First determine if account is copyable
|
|
|
+ if is_hft_pattern:
|
|
|
+ evaluation.append("❌ NOT COPYABLE - HFT/Bot")
|
|
|
+ elif stats.trading_frequency_per_day < 1:
|
|
|
+ evaluation.append("❌ NOT COPYABLE - Inactive")
|
|
|
+ elif is_copyable:
|
|
|
+ evaluation.append("✅ COPYABLE - Human trader")
|
|
|
+ else:
|
|
|
+ evaluation.append("⚠️ QUESTIONABLE - Check frequency")
|
|
|
+
|
|
|
+ # Profitability check
|
|
|
if stats.total_pnl > 0:
|
|
|
evaluation.append("✅ Profitable")
|
|
|
else:
|
|
|
evaluation.append("❌ Not profitable")
|
|
|
-
|
|
|
- if stats.avg_trade_duration_hours < 24:
|
|
|
- evaluation.append("✅ Short-term trades")
|
|
|
- else:
|
|
|
- evaluation.append("⚠️ Longer-term trades")
|
|
|
-
|
|
|
- if stats.win_rate > 0.5:
|
|
|
- evaluation.append("✅ Good win rate")
|
|
|
+
|
|
|
+ # Trade duration evaluation for copyable accounts
|
|
|
+ if is_copyable:
|
|
|
+ if 2 <= stats.avg_trade_duration_hours <= 48:
|
|
|
+ evaluation.append("✅ Good trade duration")
|
|
|
+ elif stats.avg_trade_duration_hours < 2:
|
|
|
+ evaluation.append("⚠️ Very short trades")
|
|
|
+ else:
|
|
|
+ evaluation.append("⚠️ Long hold times")
|
|
|
+
|
|
|
+ # Win rate for human traders
|
|
|
+ if stats.win_rate > 0.6:
|
|
|
+ evaluation.append("✅ Excellent win rate")
|
|
|
+ elif stats.win_rate > 0.4:
|
|
|
+ evaluation.append("✅ Good win rate")
|
|
|
+ else:
|
|
|
+ evaluation.append("⚠️ Low win rate")
|
|
|
else:
|
|
|
- evaluation.append("⚠️ Low win rate")
|
|
|
-
|
|
|
- if stats.max_drawdown < 0.2:
|
|
|
+ # For non-copyable accounts, just note the pattern
|
|
|
+ if is_hft_pattern:
|
|
|
+ evaluation.append("🤖 Algorithmic trading")
|
|
|
+ else:
|
|
|
+ evaluation.append("💤 Low activity")
|
|
|
+
|
|
|
+ # Risk management (universal)
|
|
|
+ if stats.max_drawdown < 0.15:
|
|
|
evaluation.append("✅ Good risk management")
|
|
|
+ elif stats.max_drawdown < 0.25:
|
|
|
+ evaluation.append("⚠️ Moderate risk")
|
|
|
else:
|
|
|
- evaluation.append("⚠️ High drawdown risk")
|
|
|
+ evaluation.append("❌ High drawdown risk")
|
|
|
|
|
|
print(f" 🎯 Evaluation: {' | '.join(evaluation)}")
|
|
|
|
|
|
- # Recommendation
|
|
|
+ # Recommendation - Filter for copyable accounts only
|
|
|
print("\n" + "="*100)
|
|
|
- print("🎯 RECOMMENDATION FOR COPY TRADING")
|
|
|
+ print("🎯 COPY TRADING RECOMMENDATIONS")
|
|
|
print("="*100)
|
|
|
|
|
|
- if sorted_stats:
|
|
|
- best_account = sorted_stats[0]
|
|
|
- best_score = calculate_score(best_account)
|
|
|
+ # Separate copyable from non-copyable accounts
|
|
|
+ copyable_accounts = [stats for stats in sorted_stats if stats.is_copyable]
|
|
|
+ non_copyable_accounts = [stats for stats in sorted_stats if not stats.is_copyable]
|
|
|
+
|
|
|
+ if copyable_accounts:
|
|
|
+ print(f"\n✅ FOUND {len(copyable_accounts)} COPYABLE ACCOUNTS:")
|
|
|
+
|
|
|
+ best_copyable = copyable_accounts[0]
|
|
|
+ best_score = calculate_score(best_copyable)
|
|
|
|
|
|
- print(f"\n🏆 TOP RECOMMENDATION: {best_account.address}")
|
|
|
- print(f" 📊 Overall Score: {best_score:.1f}/100")
|
|
|
+ print(f"\n🏆 TOP COPYABLE RECOMMENDATION: {best_copyable.address}")
|
|
|
+ print(f" 📊 Score: {best_score:.1f}/100")
|
|
|
+ print(f" 🎯 Status: {best_copyable.copyability_reason}")
|
|
|
|
|
|
- if best_score >= 70:
|
|
|
+ if best_score >= 60:
|
|
|
recommendation = "🟢 HIGHLY RECOMMENDED"
|
|
|
- elif best_score >= 50:
|
|
|
+ elif best_score >= 40:
|
|
|
recommendation = "🟡 MODERATELY RECOMMENDED"
|
|
|
+ elif best_score >= 20:
|
|
|
+ recommendation = "🟠 PROCEED WITH CAUTION"
|
|
|
else:
|
|
|
recommendation = "🔴 NOT RECOMMENDED"
|
|
|
|
|
|
print(f" {recommendation}")
|
|
|
|
|
|
- print(f"\n📋 Why this account:")
|
|
|
- if best_account.total_pnl > 0:
|
|
|
- print(f" ✅ Profitable: ${best_account.total_pnl:.2f} total PnL")
|
|
|
- if best_account.avg_trade_duration_hours < 24:
|
|
|
- print(f" ✅ Short trades: {best_account.avg_trade_duration_hours:.1f} hour average")
|
|
|
- if best_account.win_rate > 0.5:
|
|
|
- print(f" ✅ Good performance: {best_account.win_rate:.1%} win rate")
|
|
|
- if best_account.max_drawdown < 0.2:
|
|
|
- print(f" ✅ Risk management: {best_account.max_drawdown:.1%} max drawdown")
|
|
|
+ print(f"\n📋 Why this account is suitable:")
|
|
|
+ print(f" ✅ Trading frequency: {best_copyable.trading_frequency_per_day:.1f} trades/day (manageable)")
|
|
|
+ if best_copyable.total_pnl > 0:
|
|
|
+ print(f" ✅ Profitable: ${best_copyable.total_pnl:.2f} total PnL")
|
|
|
+ if 2 <= best_copyable.avg_trade_duration_hours <= 48:
|
|
|
+ print(f" ✅ Good duration: {best_copyable.avg_trade_duration_hours:.1f} hour average")
|
|
|
+ if best_copyable.win_rate > 0.4:
|
|
|
+ print(f" ✅ Good performance: {best_copyable.win_rate:.1%} win rate")
|
|
|
+ if best_copyable.max_drawdown < 0.2:
|
|
|
+ print(f" ✅ Risk management: {best_copyable.max_drawdown:.1%} max drawdown")
|
|
|
|
|
|
print(f"\n⚙️ Suggested copy trading settings:")
|
|
|
- print(f" 📊 Portfolio allocation: 5-10% (conservative start)")
|
|
|
- print(f" ⚡ Max leverage limit: {min(5, best_account.avg_leverage_used):.0f}x")
|
|
|
- print(f" 💰 Min position size: $25")
|
|
|
+ print(f" 📊 Portfolio allocation: 5-15% (start conservative)")
|
|
|
+ print(f" ⚡ Max leverage limit: 3-5x")
|
|
|
+ print(f" 💰 Min position size: $25-50")
|
|
|
+ print(f" 🔄 Expected trades: {best_copyable.trading_frequency_per_day:.1f} per day")
|
|
|
+
|
|
|
+ else:
|
|
|
+ print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
|
|
|
+ print(f" All analyzed accounts are unsuitable for copy trading")
|
|
|
+
|
|
|
+ if non_copyable_accounts:
|
|
|
+ print(f"\n❌ {len(non_copyable_accounts)} UNSUITABLE ACCOUNTS (DO NOT COPY):")
|
|
|
+ for i, account in enumerate(non_copyable_accounts[:3], 1): # Show top 3 unsuitable
|
|
|
+ print(f" {i}. {account.address[:10]}... - {account.copyability_reason}")
|
|
|
+
|
|
|
+ if len(non_copyable_accounts) > 3:
|
|
|
+ print(f" ... and {len(non_copyable_accounts) - 3} more unsuitable accounts")
|
|
|
+
|
|
|
+ print(f"\n⚠️ IMPORTANT COPY TRADING GUIDELINES:")
|
|
|
+ print(f" • Only copy accounts with 1-20 trades per day")
|
|
|
+ print(f" • Avoid HFT bots (50+ trades/day) - impossible to follow")
|
|
|
+ print(f" • Start with small allocation (5%) and increase gradually")
|
|
|
+ print(f" • Monitor performance and adjust leverage accordingly")
|
|
|
+
|
|
|
+ async def get_leaderboard(self, window: str = "7d", limit: int = 20) -> Optional[List[str]]:
|
|
|
+ """
|
|
|
+ Get top accounts from Hyperliquid leaderboard
|
|
|
+
|
|
|
+ Note: Hyperliquid's public API doesn't expose leaderboard data directly.
|
|
|
+ This function serves as a template for when/if the API becomes available.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ window: Time window for leaderboard ("1d", "7d", "30d", "allTime")
|
|
|
+ limit: Number of top accounts to return
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ List of account addresses from leaderboard (currently returns None)
|
|
|
+ """
|
|
|
+ print(f"⚠️ Hyperliquid leaderboard API not publicly accessible")
|
|
|
+ print(f"💡 To analyze current top performers:")
|
|
|
+ print(f" 1. Visit: https://app.hyperliquid.xyz/leaderboard")
|
|
|
+ print(f" 2. Copy top performer addresses manually")
|
|
|
+ print(f" 3. Run: python utils/hyperliquid_account_analyzer.py [address1] [address2] ...")
|
|
|
+ print(f" 4. Or use --top10 for a curated list of known good traders")
|
|
|
+
|
|
|
+ # Note: If Hyperliquid ever makes their leaderboard API public,
|
|
|
+ # we can implement the actual fetching logic here
|
|
|
+ return None
|
|
|
+
|
|
|
+ async def _try_alternative_leaderboard(self, window: str, limit: int) -> Optional[List[str]]:
|
|
|
+ """Try alternative methods to get leaderboard data"""
|
|
|
+ try:
|
|
|
+ # Try different payload formats
|
|
|
+ alternative_payloads = [
|
|
|
+ {
|
|
|
+ "type": "leaderBoard",
|
|
|
+ "timeWindow": window
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "type": "userLeaderboard",
|
|
|
+ "window": window
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "type": "spotLeaderboard",
|
|
|
+ "req": {"timeWindow": window}
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ for payload in alternative_payloads:
|
|
|
+ try:
|
|
|
+ async with self.session.post(self.info_url, json=payload) as response:
|
|
|
+ if response.status == 200:
|
|
|
+ data = await response.json()
|
|
|
+
|
|
|
+ # Try to extract addresses from any structure
|
|
|
+ addresses = self._extract_addresses_from_data(data, limit)
|
|
|
+ if addresses:
|
|
|
+ print(f"📊 Successfully fetched {len(addresses)} addresses using alternative method")
|
|
|
+ return addresses
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ continue
|
|
|
+
|
|
|
+ print("⚠️ Could not fetch leaderboard data, using fallback top accounts")
|
|
|
+ return None
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"⚠️ Alternative leaderboard fetch failed: {e}")
|
|
|
+ return None
|
|
|
+
|
|
|
+ def _extract_addresses_from_data(self, data: Any, limit: int) -> List[str]:
|
|
|
+ """Extract addresses from any nested data structure"""
|
|
|
+ addresses = []
|
|
|
+
|
|
|
+ def recursive_search(obj, depth=0):
|
|
|
+ if depth > 5: # Prevent infinite recursion
|
|
|
+ return
|
|
|
+
|
|
|
+ if isinstance(obj, list):
|
|
|
+ for item in obj:
|
|
|
+ recursive_search(item, depth + 1)
|
|
|
+ elif isinstance(obj, dict):
|
|
|
+ # Check if this dict has an address field
|
|
|
+ for addr_field in ['user', 'address', 'account', 'trader', 'wallet']:
|
|
|
+ if addr_field in obj:
|
|
|
+ addr = obj[addr_field]
|
|
|
+ if isinstance(addr, str) and addr.startswith('0x') and len(addr) == 42:
|
|
|
+ if addr not in addresses: # Avoid duplicates
|
|
|
+ addresses.append(addr)
|
|
|
+
|
|
|
+ # Recurse into nested objects
|
|
|
+ for value in obj.values():
|
|
|
+ recursive_search(value, depth + 1)
|
|
|
+
|
|
|
+ recursive_search(data)
|
|
|
+ return addresses[:limit]
|
|
|
+
|
|
|
+ async def get_top_accounts_from_leaderboard(self, window: str = "7d", limit: int = 10) -> List[str]:
|
|
|
+ """
|
|
|
+ Get top performing accounts from Hyperliquid leaderboard
|
|
|
+
|
|
|
+ Currently uses a curated list of high-performing accounts since
|
|
|
+ the Hyperliquid leaderboard API is not publicly accessible.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ window: Time window ("1d", "7d", "30d", "allTime")
|
|
|
+ limit: Number of accounts to return
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ List of top account addresses
|
|
|
+ """
|
|
|
+ print(f"🔍 Attempting to fetch top {limit} accounts from {window} leaderboard...")
|
|
|
+
|
|
|
+ addresses = await self.get_leaderboard(window, limit)
|
|
|
+
|
|
|
+ if not addresses:
|
|
|
+ print("\n📋 Using curated list of high-performing accounts")
|
|
|
+ print("💡 These accounts have been manually verified for good performance")
|
|
|
+
|
|
|
+ # Curated list of known high-performing accounts
|
|
|
+ # Updated based on our previous analysis
|
|
|
+ curated_addresses = [
|
|
|
+ "0x59a15c79a007cd6e9965b949fcf04125c2212524", # Best performer from previous analysis
|
|
|
+ "0xa10ec245b3483f83e350a9165a52ae23dbab01bc",
|
|
|
+ "0x0487b5e806ac781508cb3272ebd83ad603ddcc0f",
|
|
|
+ "0x72fad4e75748b65566a3ebb555b6f6ee18ce08d1",
|
|
|
+ "0xa70434af5778038245d53da1b4d360a30307a827",
|
|
|
+ "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8",
|
|
|
+ "0x3104b7668f9e46fb13ec0b141d2902e144d67efe",
|
|
|
+ "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
|
|
|
+ "0xc62df97dcf96324adf4edd30a4a7bffd5402f4da",
|
|
|
+ "0xd11f5de0189d52b3abe6b0960b8377c20988e17e"
|
|
|
+ ]
|
|
|
+
|
|
|
+ selected_addresses = curated_addresses[:limit]
|
|
|
+ print(f"📊 Selected {len(selected_addresses)} accounts for analysis:")
|
|
|
+ for i, addr in enumerate(selected_addresses, 1):
|
|
|
+ print(f" {i}. {addr}")
|
|
|
+
|
|
|
+ return selected_addresses
|
|
|
+
|
|
|
+ print(f"✅ Successfully fetched {len(addresses)} top accounts from leaderboard")
|
|
|
+ for i, addr in enumerate(addresses, 1):
|
|
|
+ print(f" {i}. {addr}")
|
|
|
+
|
|
|
+ return addresses
|
|
|
|
|
|
async def main():
|
|
|
"""Main function"""
|
|
|
parser = argparse.ArgumentParser(description='Analyze Hyperliquid trading accounts')
|
|
|
parser.add_argument('addresses', nargs='*', help='Account addresses to analyze')
|
|
|
- parser.add_argument('--top10', action='store_true', help='Analyze the provided top 10 accounts')
|
|
|
+ parser.add_argument('--top10', action='store_true', help='Analyze the provided top 10 accounts (hardcoded list)')
|
|
|
+ parser.add_argument('--leaderboard', action='store_true', help='Fetch and analyze top accounts from Hyperliquid leaderboard')
|
|
|
+ parser.add_argument('--window', default='7d', choices=['1d', '7d', '30d', 'allTime'],
|
|
|
+ help='Time window for leaderboard (default: 7d)')
|
|
|
+ parser.add_argument('--limit', type=int, default=10, help='Number of top accounts to analyze (default: 10)')
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
- # Top 10 accounts from the user
|
|
|
+ # Top 10 accounts from the user (fallback)
|
|
|
top10_addresses = [
|
|
|
"0xa10ec245b3483f83e350a9165a52ae23dbab01bc",
|
|
|
+ "0x2aab3badd6a5daa388da47de4c72a6fa618a6265",
|
|
|
"0xd11f5de0189d52b3abe6b0960b8377c20988e17e",
|
|
|
"0xc62df97dcf96324adf4edd30a4a7bffd5402f4da",
|
|
|
"0xa70434af5778038245d53da1b4d360a30307a827",
|
|
@@ -559,18 +1036,40 @@ async def main():
|
|
|
"0x59a15c79a007cd6e9965b949fcf04125c2212524",
|
|
|
"0xeaa400abec7c62d315fd760cbba817fa35e4e0e8",
|
|
|
"0x3104b7668f9e46fb13ec0b141d2902e144d67efe",
|
|
|
- "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4"
|
|
|
+ "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
|
|
|
+ "0x101a2d2afc2f9b0b217637f53e3a3e859104a33d",
|
|
|
+ "0x836f01e63bd0fcbe673dcd905f882a5a808dd36e",
|
|
|
+ "0xae42743b5d6a3594b7f95b5cebce64cfedc69318",
|
|
|
+ "0x74dcdc6df25bd7ba70336632ecd76a053d0f8dd4",
|
|
|
+ "0x944fdea9d4956ce673c7545862cefccad6ee1b04",
|
|
|
+ "0x2a93e999816c9826ade0b51aaa2d83240d8f4596",
|
|
|
+ "0x7d3ca5fa94383b22ee49fc14e89aa417f65b4d92",
|
|
|
+ "0xfacb7404c1fad06444bda161d1304e4b7aa14e77",
|
|
|
+ "0x654d8c01f308d670d6bed13d892ee7ee285028a6",
|
|
|
+ "0xeaa400abec7c62d315fd760cbba817fa35e4e0e8"
|
|
|
]
|
|
|
|
|
|
- if args.top10:
|
|
|
- addresses = top10_addresses
|
|
|
- elif args.addresses:
|
|
|
- addresses = args.addresses
|
|
|
- else:
|
|
|
- addresses = top10_addresses
|
|
|
- print("ℹ️ No addresses specified, analyzing top 10 accounts")
|
|
|
-
|
|
|
async with HyperliquidAccountAnalyzer() as analyzer:
|
|
|
+ if args.leaderboard:
|
|
|
+ # Fetch top accounts from leaderboard
|
|
|
+ addresses = await analyzer.get_top_accounts_from_leaderboard(args.window, args.limit)
|
|
|
+ elif args.top10:
|
|
|
+ # Use hardcoded top 10 list
|
|
|
+ addresses = top10_addresses
|
|
|
+ print("ℹ️ Using hardcoded top 10 accounts")
|
|
|
+ elif args.addresses:
|
|
|
+ # Use provided addresses
|
|
|
+ addresses = args.addresses
|
|
|
+ print(f"ℹ️ Analyzing {len(addresses)} provided addresses")
|
|
|
+ else:
|
|
|
+ # Default: use curated list (since leaderboard API isn't available)
|
|
|
+ print("ℹ️ No addresses specified, using curated high-performance accounts...")
|
|
|
+ addresses = await analyzer.get_top_accounts_from_leaderboard(args.window, args.limit)
|
|
|
+
|
|
|
+ if not addresses:
|
|
|
+ print("❌ No addresses to analyze")
|
|
|
+ return
|
|
|
+
|
|
|
results = await analyzer.analyze_multiple_accounts(addresses)
|
|
|
analyzer.print_analysis_results(results)
|
|
|
|