Forráskód Böngészése

Enhance Hyperliquid Account Analyzer with account balance and PnL percentage calculations

- Added methods to extract account balance and calculate total PnL as a percentage of the account balance, improving profitability assessments.
- Updated scoring metrics to utilize percentage returns for fair comparison across accounts, enhancing analysis accuracy.
- Refined output messages to provide clearer insights into account performance, including detailed return and balance information.
Carles Sentis 4 napja
szülő
commit
09a45afe5d
2 módosított fájl, 96 hozzáadás és 36 törlés
  1. 1 1
      trading_bot.py
  2. 95 35
      utils/hyperliquid_account_analyzer.py

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "3.0.343"
+BOT_VERSION = "3.0.344"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))

+ 95 - 35
utils/hyperliquid_account_analyzer.py

@@ -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]]: