ソースを参照

Enhance Hyperliquid Account Analyzer with improved scoring metrics and account analysis features

- Introduced separate scoring for positive and negative PnL, allowing for more nuanced profitability assessments.
- Added account age analysis to evaluate maturity, impacting overall scoring.
- Enhanced risk management scoring with harsher penalties for high drawdowns.
- Adjusted weights for scoring categories to reflect updated importance, including reduced emphasis on win rate.
- Updated output messages to provide clearer guidelines for copy trading and account evaluation.
Carles Sentis 4 日 前
コミット
f390966f26
2 ファイル変更126 行追加61 行削除
  1. 1 1
      trading_bot.py
  2. 125 60
      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.338"
+BOT_VERSION = "3.0.339"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))

+ 125 - 60
utils/hyperliquid_account_analyzer.py

@@ -860,6 +860,14 @@ class HyperliquidAccountAnalyzer:
             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
+            
             # Win rate range (use all accounts)
             win_rates = [s.win_rate for s in all_accounts]
             ranges['winrate_min'] = min(win_rates)
@@ -883,12 +891,18 @@ class HyperliquidAccountAnalyzer:
                 ranges['duration_max'] = 24
                 ranges['duration_range'] = 24
             
-            # Drawdown range (use all accounts)
+            # Drawdown range (use all accounts) - ENHANCED
             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']
             
+            # Account age range (NEW)
+            ages = [s.analysis_period_days for s in all_accounts]
+            ranges['age_min'] = min(ages)
+            ranges['age_max'] = max(ages)
+            ranges['age_range'] = ranges['age_max'] - ranges['age_min']
+            
             return ranges
         
         ranges = get_data_ranges(stats_list)
@@ -898,7 +912,7 @@ class HyperliquidAccountAnalyzer:
             score = 0.0
             score_breakdown = {}
             
-            # 1. COPYABILITY FILTER (40% weight - most important)
+            # 1. COPYABILITY FILTER (35% 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
@@ -907,69 +921,95 @@ class HyperliquidAccountAnalyzer:
                 copyability_score = 0  # HFT bots get 0
                 score_breakdown['copyability'] = f"❌ HFT Bot (0 points)"
             elif is_too_slow:
-                copyability_score = 10  # Inactive accounts get minimal points
-                score_breakdown['copyability'] = f"⚠️ Inactive (10 points)"
+                copyability_score = 5  # Inactive accounts get very low points
+                score_breakdown['copyability'] = f"⚠️ Inactive (5 points)"
             elif is_copyable:
-                # For copyable accounts, score based on how close to ideal frequency (20 trades/day)
-                ideal_freq = 20
+                # For copyable accounts, score based on how close to ideal frequency (15 trades/day)
+                ideal_freq = 15
                 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
+                copyability_score = max(0, 35 - (freq_distance * 1.5))  # Lose 1.5 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
+                copyability_score = 15  # Questionable frequency
                 score_breakdown['copyability'] = f"⚠️ Questionable ({copyability_score} 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
+            # 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:
+                    # Scale from -15 (worst) to 0 (break-even)
+                    loss_severity = abs(stats.total_pnl) / abs(ranges['most_unprofitable'])
+                    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:
+                profitability_score = 0  # Breakeven gets no points
+                score_breakdown['profitability'] = f"⚖️ Breakeven (0 points - ${stats.total_pnl:.0f})"
             else:
-                profitability_score = 12.5  # If all same PnL, give average score
+                # Positive PnL gets full scoring
+                if ranges['has_profitable'] and ranges['most_profitable'] > 0:
+                    profit_ratio = stats.total_pnl / ranges['most_profitable']
+                    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 += 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:
-                winrate_score = 7.5  # If all same win rate, give average score
+            # 3. RISK MANAGEMENT (20% weight) - HARSH PUNISHMENT for high drawdown
+            if stats.max_drawdown > 0.5:  # 50%+ drawdown is disqualifying
+                risk_score = -10  # Negative score for extreme risk!
+                score_breakdown['risk'] = f"❌ EXTREME RISK ({risk_score} points - {stats.max_drawdown:.1%} drawdown)"
+            elif stats.max_drawdown > 0.25:  # 25%+ drawdown is very bad
+                risk_score = -5  # Negative score for high risk
+                score_breakdown['risk'] = f"❌ HIGH RISK ({risk_score} points - {stats.max_drawdown:.1%} drawdown)"
+            elif stats.max_drawdown > 0.15:  # 15%+ drawdown is concerning
+                risk_score = 5  # Low positive score
+                score_breakdown['risk'] = f"⚠️ Moderate Risk ({risk_score} points - {stats.max_drawdown:.1%} drawdown)"
+            elif stats.max_drawdown > 0.05:  # 5-15% drawdown is acceptable
+                risk_score = 15  # Good score
+                score_breakdown['risk'] = f"✅ Good Risk Mgmt ({risk_score} points - {stats.max_drawdown:.1%} drawdown)"
+            else:  # <5% drawdown is excellent
+                risk_score = 20  # Full points
+                score_breakdown['risk'] = f"✅ Excellent Risk ({risk_score} points - {stats.max_drawdown:.1%} drawdown)"
             
-            score += winrate_score
-            score_breakdown['winrate'] = f"📈 Win Rate ({winrate_score:.1f} points - {stats.win_rate:.1%})"
+            score += risk_score
             
-            # 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
+            # 4. ACCOUNT MATURITY (10% weight) - NEW FACTOR
+            min_good_age = 30  # At least 30 days of history is preferred
+            if stats.analysis_period_days < 7:
+                age_score = 0  # Too new, no confidence
+                score_breakdown['maturity'] = f"❌ Too New ({age_score} points - {stats.analysis_period_days} days)"
+            elif stats.analysis_period_days < 14:
+                age_score = 2  # Very new
+                score_breakdown['maturity'] = f"⚠️ Very New ({age_score} points - {stats.analysis_period_days} days)"
+            elif stats.analysis_period_days < min_good_age:
+                age_score = 5  # Somewhat new
+                score_breakdown['maturity'] = f"⚠️ Somewhat New ({age_score} points - {stats.analysis_period_days} days)"
             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
+                # Scale from 30 days (7 points) to max age (10 points)
+                if ranges['age_range'] > 0:
+                    age_ratio = min(1.0, (stats.analysis_period_days - min_good_age) / max(1, ranges['age_max'] - min_good_age))
+                    age_score = 7 + (age_ratio * 3)  # 7-10 points
                 else:
-                    duration_score = 3  # Too extreme
+                    age_score = 8  # Average if all same age
+                score_breakdown['maturity'] = f"✅ Mature ({age_score:.1f} points - {stats.analysis_period_days} days)"
             
-            score += duration_score
-            score_breakdown['duration'] = f"🕒 Duration ({duration_score:.1f} points - {stats.avg_trade_duration_hours:.1f}h)"
+            score += age_score
             
-            # 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
+            # 5. WIN RATE (5% weight) - Reduced importance
+            if ranges['winrate_range'] > 0:
+                winrate_normalized = (stats.win_rate - ranges['winrate_min']) / ranges['winrate_range']
+                winrate_score = winrate_normalized * 5
             else:
-                risk_score = 5  # If all same drawdown, give average score
+                winrate_score = 2.5  # If all same win rate, give average score
             
-            score += risk_score
-            score_breakdown['risk'] = f"📉 Risk Mgmt ({risk_score:.1f} points - {stats.max_drawdown:.1%} drawdown)"
+            score += winrate_score
+            score_breakdown['winrate'] = f"📈 Win Rate ({winrate_score:.1f} points - {stats.win_rate:.1%})"
             
             return score, score_breakdown
         
@@ -985,10 +1025,15 @@ 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}")
+        if ranges['has_unprofitable']:
+            print(f"   ❌ Worst Loss: ${ranges['most_unprofitable']:.0f}")
+        if ranges['has_profitable']:
+            print(f"   ✅ Best Profit: ${ranges['most_profitable']:.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(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 results
         for i, (stats, score, breakdown) in enumerate(scored_accounts, 1):
@@ -1113,14 +1158,16 @@ class HyperliquidAccountAnalyzer:
             print(f"   📊 Relative Score: {best_score:.1f}/100")
             print(f"   🎯 Status: {best_stats.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 >= 30:
-                recommendation = "🟠 PROCEED WITH CAUTION"
+            elif best_score >= 20:
+                recommendation = "🟠 PROCEED WITH EXTREME CAUTION"
+            elif best_score >= 0:
+                recommendation = "🔴 NOT RECOMMENDED (Risky)"
             else:
-                recommendation = "🔴 NOT RECOMMENDED"
+                recommendation = "⛔ DANGEROUS (Negative Score)"
             
             print(f"   {recommendation}")
             
@@ -1129,10 +1176,22 @@ class HyperliquidAccountAnalyzer:
                 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")
+            if best_score >= 60:
+                print(f"   📊 Portfolio allocation: 10-25% (confident allocation)")
+                print(f"   ⚡ Max leverage limit: 5-10x")
+            elif best_score >= 40:
+                print(f"   📊 Portfolio allocation: 5-15% (moderate allocation)")
+                print(f"   ⚡ Max leverage limit: 3-5x")
+            elif best_score >= 20:
+                print(f"   📊 Portfolio allocation: 2-5% (very small allocation)")
+                print(f"   ⚡ Max leverage limit: 2-3x")
+            else:
+                print(f"   📊 Portfolio allocation: DO NOT COPY")
+                print(f"   ⚡ ACCOUNT IS TOO RISKY FOR COPY TRADING")
+            
             print(f"   💰 Min position size: $25-50")
             print(f"   🔄 Expected trades: {best_stats.trading_frequency_per_day:.1f} per day")
+            print(f"   📅 Account age: {best_stats.analysis_period_days} days")
             
         else:
             print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
@@ -1141,17 +1200,21 @@ class HyperliquidAccountAnalyzer:
         if non_copyable_accounts:
             print(f"\n❌ {len(non_copyable_accounts)} UNSUITABLE ACCOUNTS (DO NOT COPY):")
             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})")
+                score_indicator = "⛔ DANGEROUS" if score < 0 else "🔴 Risky" if score < 20 else "⚠️ Poor"
+                print(f"   {i}. {account.address[:10]}... - {account.copyability_reason} ({score_indicator}: {score:.1f})")
             
             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")
-        print(f"   • Higher relative scores indicate better performance within this cohort")
+        print(f"\n⚠️ ENHANCED COPY TRADING GUIDELINES:")
+        print(f"   • ✅ ONLY copy accounts with 30+ days of history")
+        print(f"   • ✅ ONLY copy PROFITABLE accounts (positive PnL)")
+        print(f"   • ✅ AVOID accounts with >15% max drawdown")
+        print(f"   • ✅ Ideal frequency: 5-15 trades per day")
+        print(f"   • ❌ NEVER copy accounts with negative scores")
+        print(f"   • ❌ NEVER copy accounts losing money")
+        print(f"   • ⚠️ Start with 2-5% allocation even for good accounts")
+        print(f"   • 📊 Higher scores = more reliable performance")
         print(f"   • 🔄 ADVANTAGE: Perpetual traders can profit in BOTH bull & bear markets!")
         print(f"   • 📈📉 They go long (profit when price rises) AND short (profit when price falls)")
         print(f"   • 💡 This means potential profits in any market condition")
@@ -1161,8 +1224,10 @@ class HyperliquidAccountAnalyzer:
             print(f"\n🎯 DIRECTIONAL TRADING ANALYSIS OF COPYABLE ACCOUNTS:")
             for i, (stats, score, breakdown) in enumerate(copyable_accounts, 1):
                 short_capability = "✅ Excellent" if stats.short_percentage > 30 else "⚠️ Limited" if stats.short_percentage > 10 else "❌ Minimal"
+                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"      Style: {stats.trading_style}")
+                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"      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]]: