فهرست منبع

Enhance Hyperliquid Account Analyzer with relative scoring for account evaluation

- Updated the analysis results printing to include relative scoring metrics for better performance comparison.
- Introduced a new scoring system that evaluates accounts based on copyability, profitability, win rate, trade duration, and risk management.
- Added functionality to calculate data ranges for cohort analysis, improving the context of scores.
- Refactored recommendation logic to highlight top copyable accounts based on relative scores, enhancing user guidance.
Carles Sentis 4 روز پیش
والد
کامیت
5052a3e859
2فایلهای تغییر یافته به همراه157 افزوده شده و 78 حذف شده
  1. 1 1
      trading_bot.py
  2. 156 77
      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.335"
+BOT_VERSION = "3.0.336"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))

+ 156 - 77
utils/hyperliquid_account_analyzer.py

@@ -660,7 +660,7 @@ class HyperliquidAccountAnalyzer:
         return valid_results
 
     def print_analysis_results(self, stats_list: List[AccountStats]):
-        """Print comprehensive analysis results"""
+        """Print comprehensive analysis results with relative scoring"""
         if not stats_list:
             print("❌ No valid analysis results to display")
             return
@@ -669,76 +669,162 @@ class HyperliquidAccountAnalyzer:
         print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
         print("="*100)
         
-        # Sort by a composite score optimized for COPYABLE accounts
-        def calculate_score(stats: AccountStats) -> float:
-            score = 0
+        # Calculate data ranges for relative scoring
+        def get_data_ranges(stats_list):
+            """Calculate min/max values for relative scoring"""
+            if not stats_list:
+                return {}
             
-            # FIRST: Check if account is suitable for copy trading
+            # Separate copyable from non-copyable for different scoring
+            copyable_accounts = [s for s in stats_list if s.is_copyable]
+            all_accounts = stats_list
+            
+            ranges = {}
+            
+            # Profitability range (use all accounts)
+            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']
+            
+            # Win rate range (use all accounts)
+            win_rates = [s.win_rate for s in all_accounts]
+            ranges['winrate_min'] = min(win_rates)
+            ranges['winrate_max'] = max(win_rates)
+            ranges['winrate_range'] = ranges['winrate_max'] - ranges['winrate_min']
+            
+            # Trading frequency range (use all accounts)
+            frequencies = [s.trading_frequency_per_day for s in all_accounts]
+            ranges['freq_min'] = min(frequencies)
+            ranges['freq_max'] = max(frequencies)
+            ranges['freq_range'] = ranges['freq_max'] - ranges['freq_min']
+            
+            # Trade duration range (use all accounts)
+            durations = [s.avg_trade_duration_hours for s in all_accounts if s.avg_trade_duration_hours > 0]
+            if durations:
+                ranges['duration_min'] = min(durations)
+                ranges['duration_max'] = max(durations)
+                ranges['duration_range'] = ranges['duration_max'] - ranges['duration_min']
+            else:
+                ranges['duration_min'] = 0
+                ranges['duration_max'] = 24
+                ranges['duration_range'] = 24
+            
+            # Drawdown range (use all accounts)
+            drawdowns = [s.max_drawdown for s in all_accounts]
+            ranges['drawdown_min'] = min(drawdowns)
+            ranges['drawdown_max'] = max(drawdowns)
+            ranges['drawdown_range'] = ranges['drawdown_max'] - ranges['drawdown_min']
+            
+            return ranges
+        
+        ranges = get_data_ranges(stats_list)
+        
+        # Relative scoring function
+        def calculate_relative_score(stats: AccountStats, ranges: dict) -> float:
+            score = 0.0
+            score_breakdown = {}
+            
+            # 1. COPYABILITY FILTER (40% weight - most important)
             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")
+                copyability_score = 0  # HFT bots get 0
+                score_breakdown['copyability'] = f"❌ HFT Bot (0 points)"
             elif is_too_slow:
-                score -= 20  # Penalty for inactive accounts
-                print(f"   ⚠️ Inactive Account Penalty: -20 points")
+                copyability_score = 10  # Inactive accounts get minimal points
+                score_breakdown['copyability'] = f"⚠️ Inactive (10 points)"
             elif is_copyable:
-                score += 20  # Bonus for manageable frequency
-                print(f"   ✅ Copyable Frequency Bonus: +20 points")
+                # For copyable accounts, score based on how close to ideal frequency (20 trades/day)
+                ideal_freq = 20
+                freq_distance = abs(stats.trading_frequency_per_day - ideal_freq)
+                # Max score when exactly at ideal, decreases as distance increases
+                copyability_score = max(0, 40 - (freq_distance * 2))  # Lose 2 points per trade away from ideal
+                score_breakdown['copyability'] = f"✅ Copyable ({copyability_score:.1f} points - {stats.trading_frequency_per_day:.1f} trades/day)"
+            else:
+                copyability_score = 20  # Questionable frequency
+                score_breakdown['copyability'] = f"⚠️ Questionable ({copyability_score} points)"
             
-            # Profitability (30% weight)
-            if stats.total_pnl > 0:
-                pnl_score = min(30, stats.total_pnl / 1000)  # $1000 = 30 points
-                score += pnl_score
-                print(f"   💰 Profitability Score: +{pnl_score:.1f} points")
+            score += copyability_score
+            
+            # 2. PROFITABILITY (25% weight) - Relative to cohort
+            if ranges['pnl_range'] > 0:
+                # Linear interpolation: worst performer gets 0, best gets 25
+                pnl_normalized = (stats.total_pnl - ranges['pnl_min']) / ranges['pnl_range']
+                profitability_score = pnl_normalized * 25
             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")
-            
-            # 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
+                profitability_score = 12.5  # If all same PnL, give average score
+            
+            score += profitability_score
+            score_breakdown['profitability'] = f"💰 Profitability ({profitability_score:.1f} points - ${stats.total_pnl:.0f})"
+            
+            # 3. WIN RATE (15% weight) - Relative to cohort
+            if ranges['winrate_range'] > 0:
+                winrate_normalized = (stats.win_rate - ranges['winrate_min']) / ranges['winrate_range']
+                winrate_score = winrate_normalized * 15
             else:
-                duration_score = 5  # Too fast (<1hr) or too slow (>1week)
+                winrate_score = 7.5  # If all same win rate, give average score
+            
+            score += winrate_score
+            score_breakdown['winrate'] = f"📈 Win Rate ({winrate_score:.1f} points - {stats.win_rate:.1%})"
+            
+            # 4. TRADE DURATION (10% weight) - Preference for 2-48 hour range
+            if stats.avg_trade_duration_hours == 0:
+                duration_score = 2  # Minimal score for 0 duration
+            else:
+                # Ideal range: 2-48 hours gets full score
+                if 2 <= stats.avg_trade_duration_hours <= 48:
+                    duration_score = 10  # Perfect range
+                elif 1 <= stats.avg_trade_duration_hours < 2:
+                    duration_score = 7  # Slightly too fast
+                elif 48 < stats.avg_trade_duration_hours <= 168:  # 1 week
+                    duration_score = 8  # Slightly too slow but still good
+                else:
+                    duration_score = 3  # Too extreme
             
             score += duration_score
-            print(f"   🕒 Duration Score: +{duration_score} points ({stats.avg_trade_duration_hours:.1f}h)")
-            
-            # 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
+            score_breakdown['duration'] = f"🕒 Duration ({duration_score:.1f} points - {stats.avg_trade_duration_hours:.1f}h)"
+            
+            # 5. RISK MANAGEMENT (10% weight) - Lower drawdown is better
+            if ranges['drawdown_range'] > 0:
+                # Invert: lowest drawdown gets full score
+                drawdown_normalized = 1 - ((stats.max_drawdown - ranges['drawdown_min']) / ranges['drawdown_range'])
+                risk_score = drawdown_normalized * 10
             else:
-                risk_score = 0
+                risk_score = 5  # If all same drawdown, give average 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)
-        
-        for i, stats in enumerate(sorted_stats, 1):
-            score = calculate_score(stats)
+            score_breakdown['risk'] = f"📉 Risk Mgmt ({risk_score:.1f} points - {stats.max_drawdown:.1%} drawdown)"
             
+            return score, score_breakdown
+        
+        # Calculate scores for all accounts
+        scored_accounts = []
+        for stats in stats_list:
+            score, breakdown = calculate_relative_score(stats, ranges)
+            scored_accounts.append((stats, score, breakdown))
+        
+        # Sort by score
+        scored_accounts.sort(key=lambda x: x[1], reverse=True)
+        
+        # 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"   📈 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"   🕒 Duration Range: {ranges['duration_min']:.1f}h to {ranges['duration_max']:.1f}h")
+        print(f"   📉 Drawdown Range: {ranges['drawdown_min']:.1%} to {ranges['drawdown_max']:.1%}")
+        
+        # Print results
+        for i, (stats, score, breakdown) in enumerate(scored_accounts, 1):
             print(f"\n{i}. 📋 ACCOUNT: {stats.address}")
-            print(f"   🏆 SCORE: {score:.1f}/100")
+            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"   📈 Win Rate: {stats.win_rate:.1%}")
             print(f"   🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
@@ -804,52 +890,44 @@ class HyperliquidAccountAnalyzer:
                 
             print(f"   🎯 Evaluation: {' | '.join(evaluation)}")
         
-        # Recommendation - Filter for copyable accounts only
+        # Recommendation section (rest remains the same)
         print("\n" + "="*100)
         print("🎯 COPY TRADING RECOMMENDATIONS")
         print("="*100)
         
         # 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]
+        copyable_accounts = [(stats, score, breakdown) for stats, score, breakdown in scored_accounts if stats.is_copyable]
+        non_copyable_accounts = [(stats, score, breakdown) for stats, score, breakdown in scored_accounts 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)
+            best_stats, best_score, best_breakdown = copyable_accounts[0]
             
-            print(f"\n🏆 TOP COPYABLE RECOMMENDATION: {best_copyable.address}")
-            print(f"   📊 Score: {best_score:.1f}/100")
-            print(f"   🎯 Status: {best_copyable.copyability_reason}")
+            print(f"\n🏆 TOP COPYABLE RECOMMENDATION: {best_stats.address}")
+            print(f"   📊 Relative Score: {best_score:.1f}/100")
+            print(f"   🎯 Status: {best_stats.copyability_reason}")
             
-            if best_score >= 60:
+            if best_score >= 70:
                 recommendation = "🟢 HIGHLY RECOMMENDED"
-            elif best_score >= 40:
+            elif best_score >= 50:
                 recommendation = "🟡 MODERATELY RECOMMENDED"
-            elif best_score >= 20:
+            elif best_score >= 30:
                 recommendation = "🟠 PROCEED WITH CAUTION"
             else:
                 recommendation = "🔴 NOT RECOMMENDED"
             
             print(f"   {recommendation}")
             
-            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📋 Why this account scored highest:")
+            for metric, description in best_breakdown.items():
+                print(f"   {description}")
             
             print(f"\n⚙️ Suggested copy trading settings:")
             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")
+            print(f"   🔄 Expected trades: {best_stats.trading_frequency_per_day:.1f} per day")
             
         else:
             print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
@@ -857,8 +935,8 @@ class HyperliquidAccountAnalyzer:
         
         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}")
+            for i, (account, score, breakdown) in enumerate(non_copyable_accounts[:3], 1):  # Show top 3 unsuitable
+                print(f"   {i}. {account.address[:10]}... - {account.copyability_reason} (Score: {score:.1f})")
             
             if len(non_copyable_accounts) > 3:
                 print(f"   ... and {len(non_copyable_accounts) - 3} more unsuitable accounts")
@@ -868,6 +946,7 @@ class HyperliquidAccountAnalyzer:
         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")
+        print(f"   • Higher relative scores indicate better performance within this cohort")
 
     async def get_leaderboard(self, window: str = "7d", limit: int = 20) -> Optional[List[str]]:
         """