|
@@ -660,7 +660,7 @@ class HyperliquidAccountAnalyzer:
|
|
return valid_results
|
|
return valid_results
|
|
|
|
|
|
def print_analysis_results(self, stats_list: List[AccountStats]):
|
|
def print_analysis_results(self, stats_list: List[AccountStats]):
|
|
- """Print comprehensive analysis results"""
|
|
|
|
|
|
+ """Print comprehensive analysis results with relative scoring"""
|
|
if not stats_list:
|
|
if not stats_list:
|
|
print("❌ No valid analysis results to display")
|
|
print("❌ No valid analysis results to display")
|
|
return
|
|
return
|
|
@@ -669,76 +669,162 @@ class HyperliquidAccountAnalyzer:
|
|
print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
|
|
print("📊 HYPERLIQUID ACCOUNT ANALYSIS RESULTS")
|
|
print("="*100)
|
|
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_hft = stats.trading_frequency_per_day > 50
|
|
is_too_slow = stats.trading_frequency_per_day < 1
|
|
is_too_slow = stats.trading_frequency_per_day < 1
|
|
is_copyable = 1 <= stats.trading_frequency_per_day <= 20
|
|
is_copyable = 1 <= stats.trading_frequency_per_day <= 20
|
|
|
|
|
|
- # HFT and inactive accounts get heavily penalized
|
|
|
|
if is_hft:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
else:
|
|
- risk_score = 0
|
|
|
|
|
|
+ risk_score = 5 # If all same drawdown, give average score
|
|
|
|
|
|
score += risk_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"\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" 💰 Total PnL: ${stats.total_pnl:.2f}")
|
|
print(f" 📈 Win Rate: {stats.win_rate:.1%}")
|
|
print(f" 📈 Win Rate: {stats.win_rate:.1%}")
|
|
print(f" 🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
|
|
print(f" 🕒 Avg Trade Duration: {stats.avg_trade_duration_hours:.1f} hours")
|
|
@@ -804,52 +890,44 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
print(f" 🎯 Evaluation: {' | '.join(evaluation)}")
|
|
print(f" 🎯 Evaluation: {' | '.join(evaluation)}")
|
|
|
|
|
|
- # Recommendation - Filter for copyable accounts only
|
|
|
|
|
|
+ # Recommendation section (rest remains the same)
|
|
print("\n" + "="*100)
|
|
print("\n" + "="*100)
|
|
print("🎯 COPY TRADING RECOMMENDATIONS")
|
|
print("🎯 COPY TRADING RECOMMENDATIONS")
|
|
print("="*100)
|
|
print("="*100)
|
|
|
|
|
|
# Separate copyable from non-copyable accounts
|
|
# 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:
|
|
if copyable_accounts:
|
|
print(f"\n✅ FOUND {len(copyable_accounts)} 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"
|
|
recommendation = "🟢 HIGHLY RECOMMENDED"
|
|
- elif best_score >= 40:
|
|
|
|
|
|
+ elif best_score >= 50:
|
|
recommendation = "🟡 MODERATELY RECOMMENDED"
|
|
recommendation = "🟡 MODERATELY RECOMMENDED"
|
|
- elif best_score >= 20:
|
|
|
|
|
|
+ elif best_score >= 30:
|
|
recommendation = "🟠 PROCEED WITH CAUTION"
|
|
recommendation = "🟠 PROCEED WITH CAUTION"
|
|
else:
|
|
else:
|
|
recommendation = "🔴 NOT RECOMMENDED"
|
|
recommendation = "🔴 NOT RECOMMENDED"
|
|
|
|
|
|
print(f" {recommendation}")
|
|
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"\n⚙️ Suggested copy trading settings:")
|
|
print(f" 📊 Portfolio allocation: 5-15% (start conservative)")
|
|
print(f" 📊 Portfolio allocation: 5-15% (start conservative)")
|
|
print(f" ⚡ Max leverage limit: 3-5x")
|
|
print(f" ⚡ Max leverage limit: 3-5x")
|
|
print(f" 💰 Min position size: $25-50")
|
|
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:
|
|
else:
|
|
print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
|
|
print(f"\n❌ NO COPYABLE ACCOUNTS FOUND")
|
|
@@ -857,8 +935,8 @@ class HyperliquidAccountAnalyzer:
|
|
|
|
|
|
if non_copyable_accounts:
|
|
if non_copyable_accounts:
|
|
print(f"\n❌ {len(non_copyable_accounts)} UNSUITABLE ACCOUNTS (DO NOT COPY):")
|
|
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:
|
|
if len(non_copyable_accounts) > 3:
|
|
print(f" ... and {len(non_copyable_accounts) - 3} more unsuitable accounts")
|
|
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" • Avoid HFT bots (50+ trades/day) - impossible to follow")
|
|
print(f" • Start with small allocation (5%) and increase gradually")
|
|
print(f" • Start with small allocation (5%) and increase gradually")
|
|
print(f" • Monitor performance and adjust leverage accordingly")
|
|
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]]:
|
|
async def get_leaderboard(self, window: str = "7d", limit: int = 20) -> Optional[List[str]]:
|
|
"""
|
|
"""
|