|
@@ -99,6 +99,8 @@ class AccountStats:
|
|
|
short_percentage: float # Percentage of trades that are likely shorts
|
|
|
trading_style: str # Directional trading style description
|
|
|
buy_sell_ratio: float # Ratio of buys to sells
|
|
|
+ account_balance: float # Current account balance (accountValue)
|
|
|
+ pnl_percentage: float # Total PnL as percentage of account balance
|
|
|
|
|
|
class HyperliquidAccountAnalyzer:
|
|
|
"""Analyzes Hyperliquid trading accounts"""
|
|
@@ -210,6 +212,34 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
return trades
|
|
|
|
|
|
+ def get_account_balance(self, account_state: Dict) -> float:
|
|
|
+ """Extract account balance from account state"""
|
|
|
+ if not account_state:
|
|
|
+ return 0.0
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Try to get account value from marginSummary
|
|
|
+ margin_summary = account_state.get('marginSummary', {})
|
|
|
+ if margin_summary:
|
|
|
+ account_value = float(margin_summary.get('accountValue', '0'))
|
|
|
+ if account_value > 0:
|
|
|
+ return account_value
|
|
|
+
|
|
|
+ # Fallback: try crossMarginSummary
|
|
|
+ cross_margin = account_state.get('crossMarginSummary', {})
|
|
|
+ if cross_margin:
|
|
|
+ account_value = float(cross_margin.get('accountValue', '0'))
|
|
|
+ if account_value > 0:
|
|
|
+ return account_value
|
|
|
+
|
|
|
+ # Last resort: estimate from withdrawable balance
|
|
|
+ withdrawable = float(account_state.get('withdrawable', '0'))
|
|
|
+ return withdrawable
|
|
|
+
|
|
|
+ except (ValueError, KeyError) as e:
|
|
|
+ print(f"⚠️ Warning: Could not extract account balance: {e}")
|
|
|
+ return 0.0
|
|
|
+
|
|
|
def parse_positions(self, account_state: Dict) -> List[Position]:
|
|
|
"""Parse account state into Position objects"""
|
|
|
positions = []
|
|
@@ -719,8 +749,22 @@ class HyperliquidAccountAnalyzer:
|
|
|
unrealized_pnl = sum(pos.unrealized_pnl for pos in positions)
|
|
|
total_pnl = realized_pnl + unrealized_pnl
|
|
|
|
|
|
+ # Extract account balance for percentage calculations
|
|
|
+ account_balance = self.get_account_balance(account_state)
|
|
|
+
|
|
|
+ # Calculate percentage return
|
|
|
+ if account_balance > 0:
|
|
|
+ # PnL percentage = total_pnl / (account_balance - total_pnl) * 100
|
|
|
+ # This gives us the return on the original balance before PnL
|
|
|
+ original_balance = account_balance - total_pnl
|
|
|
+ pnl_percentage = (total_pnl / original_balance * 100) if original_balance > 0 else 0.0
|
|
|
+ else:
|
|
|
+ pnl_percentage = 0.0
|
|
|
+
|
|
|
print(f"💰 Total PnL: ${total_pnl:.2f} (Realized: ${realized_pnl:.2f} + Unrealized: ${unrealized_pnl:.2f})")
|
|
|
print(f"💸 Total Fees: ${total_fees:.2f}")
|
|
|
+ print(f"🏦 Account Balance: ${account_balance:.2f}")
|
|
|
+ print(f"📊 PnL Percentage: {pnl_percentage:.2f}%")
|
|
|
|
|
|
# Calculate position size statistics
|
|
|
position_sizes = [trade.size * trade.price for trade in trades]
|
|
@@ -812,7 +856,9 @@ class HyperliquidAccountAnalyzer:
|
|
|
top_tokens=top_tokens,
|
|
|
short_percentage=short_long_analysis['short_percentage'],
|
|
|
trading_style=short_long_analysis['trading_style'],
|
|
|
- buy_sell_ratio=short_long_analysis['buy_sell_ratio']
|
|
|
+ buy_sell_ratio=short_long_analysis['buy_sell_ratio'],
|
|
|
+ account_balance=account_balance,
|
|
|
+ pnl_percentage=pnl_percentage
|
|
|
)
|
|
|
|
|
|
async def analyze_multiple_accounts(self, addresses: List[str]) -> List[AccountStats]:
|
|
@@ -854,19 +900,26 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
ranges = {}
|
|
|
|
|
|
- # Profitability range (use all accounts)
|
|
|
+ # Profitability range (use percentage returns for fair comparison)
|
|
|
+ pnl_percentages = [s.pnl_percentage for s in all_accounts]
|
|
|
+ ranges['pnl_pct_min'] = min(pnl_percentages)
|
|
|
+ ranges['pnl_pct_max'] = max(pnl_percentages)
|
|
|
+ ranges['pnl_pct_range'] = ranges['pnl_pct_max'] - ranges['pnl_pct_min']
|
|
|
+
|
|
|
+ # Separate positive and negative percentage returns for different scoring
|
|
|
+ positive_pnl_pcts = [p for p in pnl_percentages if p > 0]
|
|
|
+ negative_pnl_pcts = [p for p in pnl_percentages if p < 0]
|
|
|
+ ranges['has_profitable'] = len(positive_pnl_pcts) > 0
|
|
|
+ ranges['has_unprofitable'] = len(negative_pnl_pcts) > 0
|
|
|
+ ranges['most_profitable_pct'] = max(positive_pnl_pcts) if positive_pnl_pcts else 0
|
|
|
+ ranges['most_unprofitable_pct'] = min(negative_pnl_pcts) if negative_pnl_pcts else 0
|
|
|
+
|
|
|
+ # Keep absolute PnL for display purposes
|
|
|
pnls = [s.total_pnl for s in all_accounts]
|
|
|
ranges['pnl_min'] = min(pnls)
|
|
|
ranges['pnl_max'] = max(pnls)
|
|
|
- ranges['pnl_range'] = ranges['pnl_max'] - ranges['pnl_min']
|
|
|
-
|
|
|
- # Separate positive and negative PnL for different scoring
|
|
|
- positive_pnls = [p for p in pnls if p > 0]
|
|
|
- negative_pnls = [p for p in pnls if p < 0]
|
|
|
- ranges['has_profitable'] = len(positive_pnls) > 0
|
|
|
- ranges['has_unprofitable'] = len(negative_pnls) > 0
|
|
|
- ranges['most_profitable'] = max(positive_pnls) if positive_pnls else 0
|
|
|
- ranges['most_unprofitable'] = min(negative_pnls) if negative_pnls else 0
|
|
|
+ ranges['most_profitable'] = max([p for p in pnls if p > 0]) if any(p > 0 for p in pnls) else 0
|
|
|
+ ranges['most_unprofitable'] = min([p for p in pnls if p < 0]) if any(p < 0 for p in pnls) else 0
|
|
|
|
|
|
# Win rate range (use all accounts)
|
|
|
win_rates = [s.win_rate for s in all_accounts]
|
|
@@ -936,27 +989,27 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
score += copyability_score
|
|
|
|
|
|
- # 2. PROFITABILITY (30% weight) - HARSH PUNISHMENT for losses
|
|
|
- if stats.total_pnl < 0:
|
|
|
- # Severe punishment for unprofitable accounts
|
|
|
- if ranges['has_unprofitable'] and ranges['most_unprofitable'] < stats.total_pnl:
|
|
|
+ # 2. PROFITABILITY (30% weight) - FAIR COMPARISON using percentage returns
|
|
|
+ if stats.pnl_percentage < 0:
|
|
|
+ # Severe punishment for unprofitable accounts (based on percentage loss)
|
|
|
+ if ranges['has_unprofitable'] and ranges['most_unprofitable_pct'] < stats.pnl_percentage:
|
|
|
# Scale from -15 (worst) to 0 (break-even)
|
|
|
- loss_severity = abs(stats.total_pnl) / abs(ranges['most_unprofitable'])
|
|
|
+ loss_severity = abs(stats.pnl_percentage) / abs(ranges['most_unprofitable_pct'])
|
|
|
profitability_score = -15 * loss_severity # Negative score for losses!
|
|
|
else:
|
|
|
profitability_score = -15 # Maximum penalty
|
|
|
- score_breakdown['profitability'] = f"❌ LOSING ({profitability_score:.1f} points - ${stats.total_pnl:.0f} LOSS)"
|
|
|
- elif stats.total_pnl == 0:
|
|
|
+ score_breakdown['profitability'] = f"❌ LOSING ({profitability_score:.1f} points - {stats.pnl_percentage:.1f}% LOSS, ${stats.total_pnl:.0f})"
|
|
|
+ elif stats.pnl_percentage == 0:
|
|
|
profitability_score = 0 # Breakeven gets no points
|
|
|
- score_breakdown['profitability'] = f"⚖️ Breakeven (0 points - ${stats.total_pnl:.0f})"
|
|
|
+ score_breakdown['profitability'] = f"⚖️ Breakeven (0 points - {stats.pnl_percentage:.1f}%, ${stats.total_pnl:.0f})"
|
|
|
else:
|
|
|
- # Positive PnL gets full scoring
|
|
|
- if ranges['has_profitable'] and ranges['most_profitable'] > 0:
|
|
|
- profit_ratio = stats.total_pnl / ranges['most_profitable']
|
|
|
+ # Positive percentage returns get full scoring - FAIR comparison regardless of account size
|
|
|
+ if ranges['has_profitable'] and ranges['most_profitable_pct'] > 0:
|
|
|
+ profit_ratio = stats.pnl_percentage / ranges['most_profitable_pct']
|
|
|
profitability_score = profit_ratio * 30
|
|
|
else:
|
|
|
profitability_score = 15 # Average score if only one profitable account
|
|
|
- score_breakdown['profitability'] = f"✅ Profitable ({profitability_score:.1f} points - ${stats.total_pnl:.0f})"
|
|
|
+ score_breakdown['profitability'] = f"✅ Profitable ({profitability_score:.1f} points - {stats.pnl_percentage:.1f}% return, ${stats.total_pnl:.0f})"
|
|
|
|
|
|
score += profitability_score
|
|
|
|
|
@@ -1024,26 +1077,33 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
# Print data ranges for context
|
|
|
print(f"\n📊 COHORT ANALYSIS (for relative scoring):")
|
|
|
- print(f" 💰 PnL Range: ${ranges['pnl_min']:.0f} to ${ranges['pnl_max']:.0f}")
|
|
|
+ print(f" 📊 Return Range: {ranges['pnl_pct_min']:.1f}% to {ranges['pnl_pct_max']:.1f}% (FAIR COMPARISON)")
|
|
|
if ranges['has_unprofitable']:
|
|
|
- print(f" ❌ Worst Loss: ${ranges['most_unprofitable']:.0f}")
|
|
|
+ print(f" ❌ Worst Loss: {ranges['most_unprofitable_pct']:.1f}% (${ranges['most_unprofitable']:.0f})")
|
|
|
if ranges['has_profitable']:
|
|
|
- print(f" ✅ Best Profit: ${ranges['most_profitable']:.0f}")
|
|
|
+ print(f" ✅ Best Return: {ranges['most_profitable_pct']:.1f}% (${ranges['most_profitable']:.0f})")
|
|
|
+ print(f" 💰 Absolute PnL Range: ${ranges['pnl_min']:.0f} to ${ranges['pnl_max']:.0f}")
|
|
|
print(f" 📈 Win Rate Range: {ranges['winrate_min']:.1%} to {ranges['winrate_max']:.1%}")
|
|
|
print(f" 🔄 Frequency Range: {ranges['freq_min']:.1f} to {ranges['freq_max']:.1f} trades/day")
|
|
|
print(f" 📉 Drawdown Range: {ranges['drawdown_min']:.1%} to {ranges['drawdown_max']:.1%}")
|
|
|
print(f" 📅 Account Age Range: {ranges['age_min']} to {ranges['age_max']} days")
|
|
|
- print(f"\n⚠️ WARNING: Accounts with losses or high drawdown receive NEGATIVE scores!")
|
|
|
+ print(f"\n⚠️ NOTE: Scoring now uses PERCENTAGE RETURNS for fair comparison across account sizes!")
|
|
|
+ print(f"⚠️ WARNING: Accounts with losses or high drawdown receive NEGATIVE scores!")
|
|
|
+
|
|
|
+ # Print results (top 10 only)
|
|
|
+ total_accounts = len(scored_accounts)
|
|
|
+ showing_count = min(10, total_accounts)
|
|
|
+ print(f"\n📋 DETAILED ANALYSIS - Showing top {showing_count} of {total_accounts} accounts:")
|
|
|
|
|
|
- # Print results
|
|
|
- for i, (stats, score, breakdown) in enumerate(scored_accounts, 1):
|
|
|
+ for i, (stats, score, breakdown) in enumerate(scored_accounts[:10], 1):
|
|
|
print(f"\n{i}. 📋 ACCOUNT: {stats.address}")
|
|
|
print(f" 🏆 RELATIVE SCORE: {score:.1f}/100")
|
|
|
print(f" 📊 Score Breakdown:")
|
|
|
for metric, description in breakdown.items():
|
|
|
print(f" {description}")
|
|
|
|
|
|
- print(f" 💰 Total PnL: ${stats.total_pnl:.2f}")
|
|
|
+ print(f" 📊 Return: {stats.pnl_percentage:.2f}% (${stats.total_pnl:.2f})")
|
|
|
+ print(f" 🏦 Account Balance: ${stats.account_balance:.2f}")
|
|
|
print(f" 📈 Win Rate: {stats.win_rate:.1%}")
|
|
|
print(f" 🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
|
|
|
print(f" 📉 Max Drawdown: {stats.max_drawdown:.1%}")
|
|
@@ -1101,11 +1161,11 @@ class HyperliquidAccountAnalyzer:
|
|
|
else:
|
|
|
evaluation.append("⚠️ QUESTIONABLE - Check frequency")
|
|
|
|
|
|
- # Profitability check
|
|
|
- if stats.total_pnl > 0:
|
|
|
- evaluation.append("✅ Profitable")
|
|
|
+ # Profitability check (using percentage returns for fairness)
|
|
|
+ if stats.pnl_percentage > 0:
|
|
|
+ evaluation.append(f"✅ Profitable ({stats.pnl_percentage:.1f}% return)")
|
|
|
else:
|
|
|
- evaluation.append("❌ Not profitable")
|
|
|
+ evaluation.append(f"❌ Not profitable ({stats.pnl_percentage:.1f}% loss)")
|
|
|
|
|
|
# Trade duration evaluation for copyable accounts
|
|
|
if is_copyable:
|
|
@@ -1227,7 +1287,7 @@ class HyperliquidAccountAnalyzer:
|
|
|
risk_indicator = "⛔ DANGEROUS" if score < 0 else "🔴 Risky" if score < 20 else "⚠️ Caution" if score < 40 else "✅ Good"
|
|
|
print(f" {i}. {stats.address[:10]}... - {stats.short_percentage:.1f}% shorts ({short_capability} short capability)")
|
|
|
print(f" Score: {score:.1f}/100 ({risk_indicator}) | Style: {stats.trading_style}")
|
|
|
- print(f" Age: {stats.analysis_period_days} days | PnL: ${stats.total_pnl:.0f} | Drawdown: {stats.max_drawdown:.1%}")
|
|
|
+ print(f" Age: {stats.analysis_period_days} days | Return: {stats.pnl_percentage:.1f}% (${stats.total_pnl:.0f}) | Drawdown: {stats.max_drawdown:.1%}")
|
|
|
print(f" Advantage: Can profit when {', '.join(stats.top_tokens[:2])} prices move in EITHER direction")
|
|
|
|
|
|
async def get_leaderboard(self, window: str = "7d", limit: int = 20) -> Optional[List[str]]:
|