
Wallet Profiling
- 205 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
wallet-profiling is a Claude Code skill that classifies Solana wallets by trading style and behavior and computes performance metrics to evaluate whether a wallet is worth following.
About
A Claude Code skill that profiles Solana wallets by behavior, performance, and trading style. It classifies wallets as snipers, scalpers, day traders, swing traders, or position holders, distinguishes bots and MEV from humans, and computes performance metrics like win rate and profit factor. Developers use it to evaluate wallets before copy-trading or following smart money.
- Classifies wallets by trading style, size, behavior type, and focus area
- Computes win rate, profit factor, ROI, drawdown, and a Sharpe-like ratio
- Separates skilled traders from lucky gamblers and wash-trading bots
Wallet Profiling by the numbers
- 205 all-time installs (skills.sh)
- Ranked #458 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
wallet-profiling capabilities & compatibility
Free skill; needs an on-chain data source (e.g. SolanaTracker or Birdeye) for wallet trade history
- Capabilities
- wallet profiling · token holder analysis · whale tracking · copy trade evaluation · bot detection
- Use cases
- data analysis · research · trading
- Pricing
- Bring your own API key
What wallet-profiling says it does
Behavioral classification, performance analysis, and trading style detection for Solana wallets.
Profiling separates genuinely skilled traders from lucky gamblers and wash-trading bots.
Classification is based on the **median hold time** across all closed trades
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill wallet-profilingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 205 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Profile a Solana wallet's trading style and performance to decide whether it is worth following or copy-trading.
Who is it for?
Evaluating a Solana wallet's genuine skill before copy-trading or following it as smart money.
Skip if: Non-Solana wallets or executing trades.
When should I use this skill?
You want to know how a wallet trades and whether its performance is genuine before mirroring it.
What you get
A behavioral and performance profile of a Solana wallet with a copy-trade verdict.
- A behavioral classification and performance profile of a Solana wallet
By the numbers
- 5 trading-style classes by median hold time
- Minimum 30 closed trades for statistical significance
Files
Wallet Profiling
Behavioral classification, performance analysis, and trading style detection for Solana wallets. Profile any wallet to understand how it trades, how well it performs, and whether it is worth following.
Why Wallet Profiling Matters
Copy-Trade Evaluation
Before mirroring another wallet's trades, you need evidence that its historical performance is genuine, consistent, and not the result of a single lucky hit. Profiling quantifies win rate, profit factor, hold time, and consistency so you can make informed decisions about which wallets merit attention.
Smart Money Identification
Wallets that consistently buy tokens early and exit profitably are signal sources. Profiling separates genuinely skilled traders from lucky gamblers and wash-trading bots. Key differentiators: sustained profit factor above 2.0, win rates above 45% across 100+ trades, and diversified token selection.
Counterparty Analysis
When a large wallet enters a position you hold, understanding its historical behavior (sniper vs. holder, bot vs. human) helps you anticipate what will happen next. A sniper wallet buying suggests a quick dump is coming; a swing trader buying suggests multi-day conviction.
Risk Assessment
Token holder analysis benefits from knowing whether top holders are bots, snipers, or genuine investors. A token where 60% of holders are classified as snipers has very different risk characteristics than one held primarily by swing traders.
Wallet Classification
By Trading Style
Classification is based on the median hold time across all closed trades:
| Style | Median Hold Time | Characteristics |
|---|---|---|
| Sniper | < 5 minutes | First-block buyers, MEV-adjacent, extremely fast exits |
| Scalper | 5 min – 1 hour | Quick momentum trades, high frequency |
| Day Trader | 1 – 24 hours | Intraday positions, moderate frequency |
| Swing Trader | 1 – 7 days | Multi-day conviction holds |
| Position Holder | > 7 days | Long-term accumulation, low frequency |
See references/classification_methods.md for the full classification algorithm.
By Trade Size
Based on median trade size in SOL:
| Tier | Median Trade Size | Typical Behavior |
|---|---|---|
| Whale | > 100 SOL | Market-moving entries, often front-run |
| Large | 10 – 100 SOL | Significant but not dominant |
| Medium | 1 – 10 SOL | Active retail traders |
| Small | < 1 SOL | Micro-cap gamblers, new wallets |
By Behavior Type
| Type | Detection Method |
|---|---|
| Bot | Low inter-trade timing variance (CV < 0.3), uniform sizing |
| Human | Variable timing, variable sizing, session-based activity |
| MEV | Sandwich patterns, consistent small profits, high frequency |
By Focus Area
| Focus | Detection Criteria |
|---|---|
| PumpFun Specialist | > 70% of trades on PumpFun-launched tokens |
| DEX Trader | Primarily swaps on Raydium/Orca/Meteora |
| DeFi Farmer | Frequent LP add/remove, staking operations |
| NFT Trader | Significant NFT marketplace interactions |
| Multi-Strategy | No single category exceeds 50% |
Performance Metrics
Core Metrics
Win Rate — Percentage of trades that are profitable.
win_rate = count(pnl > 0) / count(all_closed_trades)Minimum 30 trades for statistical significance. A 60% win rate across 200 trades is far more meaningful than 80% across 10 trades.
Average ROI Per Trade — Mean return across all closed positions.
avg_roi = mean((exit_value - entry_value) / entry_value)Include all fees: platform fees, priority fees, and estimated slippage.
Profit Factor — Ratio of gross profits to gross losses.
profit_factor = sum(winning_pnl) / abs(sum(losing_pnl))Interpretation: > 2.0 excellent, 1.5–2.0 good, 1.0–1.5 marginal, < 1.0 losing.
Total PnL — Cumulative profit/loss in SOL.
total_pnl = sum(all_trade_pnl)Maximum Drawdown — Largest peak-to-trough decline in cumulative PnL curve.
drawdown = (peak_equity - trough_equity) / peak_equitySharpe-Like Ratio — Risk-adjusted return metric.
sharpe = mean(trade_returns) / std(trade_returns) * sqrt(trades_per_year)See references/performance_metrics.md for detailed formulas, edge cases, and interpretation guidelines.
Activity Metrics
| Metric | Calculation | What It Reveals |
|---|---|---|
| Trades per day | total_trades / active_days | Activity level and capacity |
| Average hold time | mean(exit_time - entry_time) | Trading style confirmation |
| Token diversity | unique_tokens / total_trades | Specialization vs. diversification |
| Peak hours | mode(hour_of_trade) | Session patterns, timezone hints |
| Activity streaks | consecutive active days | Dedication and consistency |
Data Sources
SolanaTracker PnL API (Primary)
The SolanaTracker API provides pre-computed PnL data per wallet per token.
import httpx
url = f"https://data.solanatracker.io/pnl/{wallet_address}"
headers = {"x-api-key": os.getenv("ST_API_KEY")}
resp = httpx.get(url, headers=headers)
pnl_data = resp.json()Response includes per-token: realized, unrealized, total_invested, total_sold, num_buys, num_sells, last_trade_time.
Helius Parsed Transactions (Detailed)
For granular transaction-level analysis, use the helius-api skill to fetch parsed transaction history. This provides exact timestamps, amounts, and program interactions.
Birdeye Trader Data
Birdeye's trader endpoints provide wallet-level analytics. See the birdeye-api skill for endpoint details.
DexScreener (Free Fallback)
DexScreener does not provide wallet-level PnL but can be used to validate token prices at trade timestamps.
Copy-Trade Evaluation Framework
Before following a wallet's trades, verify these criteria:
Minimum Requirements
- Trade history: At least 50 closed trades (100+ preferred)
- Time span: Active for at least 30 days
- Consistent performance: Rolling 7-day win rate standard deviation < 15%
- Reasonable sizing: No single trade > 20% of observed portfolio
- Diverse tokens: At least 10 unique tokens traded
Green Flags
- Profit factor > 1.8 sustained over 60+ days
- Win rate 45–65% (unrealistically high rates suggest wash trading)
- Moderate trade frequency (2–20 trades/day)
- Mixed hold times indicating adaptive strategy
- Gradual equity curve growth (not step-function jumps)
Red Flags
- New wallet (< 14 days old): Possible sybil or one-hit-wonder
- Single big win: One trade accounts for > 50% of total PnL
- Declining performance: Last-30-day metrics significantly below all-time
- Bot-like patterns: Uniform timing/sizing without proportional edge
- Extreme win rate: > 80% often indicates small wins with catastrophic losses
- Concentration: > 50% of PnL from a single token
- Wash trading signals: Repeated buy/sell of same token with minimal price movement
Risk Score Calculation
risk_score = 0 # 0 = low risk, 100 = high risk
if wallet_age_days < 14:
risk_score += 25
if top_trade_pnl_pct > 0.5:
risk_score += 20
if recent_pf < historical_pf * 0.7:
risk_score += 15
if bot_probability > 0.7:
risk_score += 15
if win_rate > 0.8:
risk_score += 10
if unique_tokens < 5:
risk_score += 15Integration with Other Skills
- `whale-tracking`: Identify large wallets, then profile them here for behavioral context
- `token-holder-analysis`: Profile top holders of a token to assess holder quality
- `solana-onchain`: Fetch raw transaction data for deep-dive analysis
- `helius-api`: Parsed transaction history for granular trade reconstruction
- `birdeye-api`: Token price data for PnL validation
Quick Start
Profile a Single Wallet
# Set environment variables
# export WALLET_ADDRESS=YourTargetWallet...
# export ST_API_KEY=your_solanatracker_key (optional)
python scripts/profile_wallet.py
# Or use demo mode:
python scripts/profile_wallet.py --demoCompare Multiple Wallets
# export WALLET_ADDRESSES=Wallet1...,Wallet2...,Wallet3...
# export ST_API_KEY=your_solanatracker_key (optional)
python scripts/compare_wallets.py
# Or use demo mode:
python scripts/compare_wallets.py --demoFiles
| File | Description |
|---|---|
references/classification_methods.md | Hold time, size, bot detection, and focus classification algorithms |
references/performance_metrics.md | Detailed metric formulas, interpretation, edge cases, and decay detection |
scripts/profile_wallet.py | Profile a single wallet: fetch data, compute metrics, classify, report |
scripts/compare_wallets.py | Compare multiple wallets side-by-side with ranking |
Dependencies
uv pip install httpxEnvironment Variables
| Variable | Required | Description |
|---|---|---|
WALLET_ADDRESS | For profile_wallet.py | Solana wallet address to profile |
WALLET_ADDRESSES | For compare_wallets.py | Comma-separated wallet addresses |
ST_API_KEY | No | SolanaTracker API key for PnL data |
Wallet Classification Methods
Algorithms for categorizing Solana wallets by trading style, size, behavior type, and focus area.
Style Classification (Hold Time)
Classify based on the median hold time of all closed trades. Use median rather than mean to avoid skew from forgotten positions.
Algorithm
def classify_style(hold_times_minutes: list[float]) -> str:
"""Classify wallet trading style from hold time distribution.
Args:
hold_times_minutes: List of hold durations in minutes for closed trades.
Returns:
Style label: sniper, scalper, day_trader, swing, or holder.
"""
if not hold_times_minutes:
return "unknown"
import statistics
median = statistics.median(hold_times_minutes)
if median < 5:
return "sniper"
elif median < 60:
return "scalper"
elif median < 1440: # 24 hours
return "day_trader"
elif median < 10080: # 7 days
return "swing"
else:
return "holder"Hold Time Distribution Analysis
Beyond the median, examine the full distribution shape:
| Distribution Shape | Interpretation |
|---|---|
| Tight unimodal (low std) | Consistent strategy, single style |
| Bimodal | Two distinct strategies (e.g., scalp + swing) |
| Right-skewed | Primarily fast trades with occasional holds |
| Uniform/flat | No clear strategy, opportunistic |
def analyze_hold_distribution(hold_times: list[float]) -> dict:
"""Analyze the shape of hold time distribution."""
import statistics
if len(hold_times) < 5:
return {"shape": "insufficient_data"}
median = statistics.median(hold_times)
mean = statistics.mean(hold_times)
stdev = statistics.stdev(hold_times)
cv = stdev / mean if mean > 0 else 0
return {
"median_minutes": round(median, 1),
"mean_minutes": round(mean, 1),
"stdev_minutes": round(stdev, 1),
"cv": round(cv, 2),
"skew_direction": "right" if mean > median * 1.5 else "left" if mean < median * 0.67 else "symmetric",
"consistency": "high" if cv < 0.5 else "medium" if cv < 1.0 else "low",
}Size Classification (Trade Size)
Based on median trade size in SOL across all buy transactions.
def classify_size(trade_sizes_sol: list[float]) -> str:
"""Classify wallet by typical trade size.
Args:
trade_sizes_sol: List of trade sizes in SOL.
Returns:
Size tier: whale, large, medium, or small.
"""
if not trade_sizes_sol:
return "unknown"
import statistics
median = statistics.median(trade_sizes_sol)
if median > 100:
return "whale"
elif median > 10:
return "large"
elif median > 1:
return "medium"
else:
return "small"Size Consistency
Check whether the wallet uses consistent sizing or varies dramatically:
def sizing_consistency(trade_sizes: list[float]) -> str:
"""Assess how consistent trade sizing is."""
import statistics
if len(trade_sizes) < 5:
return "insufficient_data"
cv = statistics.stdev(trade_sizes) / statistics.mean(trade_sizes)
if cv < 0.3:
return "very_uniform" # Likely bot or fixed-size strategy
elif cv < 0.6:
return "moderate" # Adaptive but disciplined
elif cv < 1.0:
return "variable" # Opportunistic sizing
else:
return "highly_variable" # Erratic or conviction-basedBot vs. Human Detection
Inter-Trade Interval Analysis
The primary signal for bot detection is the coefficient of variation (CV) of time gaps between consecutive trades.
def bot_probability(trade_timestamps: list[float]) -> float:
"""Estimate probability that a wallet is operated by a bot.
Uses inter-trade interval regularity as the primary signal.
CV < 0.3 is highly suggestive of automated execution.
Args:
trade_timestamps: Unix timestamps of trades, sorted ascending.
Returns:
Probability from 0.0 (definitely human) to 1.0 (definitely bot).
"""
if len(trade_timestamps) < 10:
return 0.0 # Insufficient data
import statistics
intervals = [
trade_timestamps[i + 1] - trade_timestamps[i]
for i in range(len(trade_timestamps) - 1)
]
intervals = [i for i in intervals if i > 0] # Remove duplicates
if len(intervals) < 5:
return 0.0
mean_interval = statistics.mean(intervals)
std_interval = statistics.stdev(intervals)
cv = std_interval / mean_interval if mean_interval > 0 else 999
# Score components
timing_score = max(0, 1.0 - cv / 0.5) # Low CV = more bot-like
# Check for round-number intervals (e.g., exactly 60s, 300s)
round_count = sum(1 for i in intervals if i % 10 < 1 or i % 10 > 9)
round_pct = round_count / len(intervals)
round_score = round_pct # Higher = more bot-like
# 24/7 activity (humans sleep, bots don't)
from datetime import datetime, timezone
hours = [datetime.fromtimestamp(t, tz=timezone.utc).hour for t in trade_timestamps]
unique_hours = len(set(hours))
hour_coverage = unique_hours / 24
hour_score = hour_coverage # Trading across all hours = more bot-like
# Weighted combination
probability = (timing_score * 0.5 + round_score * 0.25 + hour_score * 0.25)
return round(min(1.0, max(0.0, probability)), 2)Additional Bot Indicators
| Indicator | Bot Signal | Human Signal |
|---|---|---|
| Trade timing CV | < 0.3 | > 0.8 |
| Active hours per day | > 20 | 8–16 |
| Weekend activity | Same as weekday | Reduced |
| Size variance | Very low (CV < 0.2) | Moderate (CV 0.4–1.0) |
| Response to price moves | Immediate (< 2 blocks) | Delayed (minutes) |
Focus Area Detection
Classify wallets by which protocols and token types they interact with most.
def classify_focus(
token_types: list[str],
programs_used: list[str],
) -> str:
"""Classify wallet focus area based on interaction patterns.
Args:
token_types: List like ["pumpfun", "raydium", "pumpfun", "orca", ...].
programs_used: List of program IDs or labels interacted with.
Returns:
Focus label: pumpfun_specialist, dex_trader, defi_farmer,
nft_trader, or multi_strategy.
"""
if not token_types:
return "unknown"
total = len(token_types)
from collections import Counter
type_counts = Counter(token_types)
pumpfun_pct = type_counts.get("pumpfun", 0) / total
program_counts = Counter(programs_used)
lp_programs = {"raydium_amm", "orca_whirlpool", "meteora_dlmm"}
lp_interactions = sum(program_counts.get(p, 0) for p in lp_programs)
lp_pct = lp_interactions / max(len(programs_used), 1)
nft_programs = {"metaplex", "tensor", "magic_eden"}
nft_interactions = sum(program_counts.get(p, 0) for p in nft_programs)
nft_pct = nft_interactions / max(len(programs_used), 1)
if pumpfun_pct > 0.7:
return "pumpfun_specialist"
elif nft_pct > 0.5:
return "nft_trader"
elif lp_pct > 0.4:
return "defi_farmer"
elif pumpfun_pct < 0.3:
return "dex_trader"
else:
return "multi_strategy"Composite Classification
Combine all classifiers into a single wallet profile:
def build_profile(
hold_times: list[float],
trade_sizes: list[float],
timestamps: list[float],
token_types: list[str],
programs: list[str],
) -> dict:
"""Build a complete wallet classification profile."""
return {
"style": classify_style(hold_times),
"size_tier": classify_size(trade_sizes),
"bot_probability": bot_probability(timestamps),
"focus": classify_focus(token_types, programs),
"hold_distribution": analyze_hold_distribution(hold_times),
"sizing_consistency": sizing_consistency(trade_sizes),
"trade_count": len(hold_times),
}Confidence Levels
| Trade Count | Confidence | Recommendation |
|---|---|---|
| < 10 | Very Low | Do not classify |
| 10–29 | Low | Tentative classification only |
| 30–99 | Moderate | Reasonable classification |
| 100–499 | High | Reliable classification |
| 500+ | Very High | Strong statistical basis |
Performance Metrics Reference
Detailed formulas, interpretation guidelines, and edge case handling for wallet performance analysis.
Win Rate
Formula:
win_rate = count(trades where pnl > 0) / count(all_closed_trades)Requirements:
- Minimum 30 closed trades for statistical significance
- Only count fully closed positions (all tokens sold)
- Include fees in PnL calculation (a trade that made 0.01 SOL but cost 0.005 SOL in fees is still a win at 0.005 SOL net)
Interpretation:
| Win Rate | Assessment | Context |
|---|---|---|
| > 80% | Suspicious | Possible wash trading or tiny wins with large losses |
| 60–80% | Excellent | Sustainable if profit factor also high |
| 45–60% | Good | Standard for profitable traders with good R:R |
| 30–45% | Acceptable | Can be profitable with high avg win / avg loss ratio |
| < 30% | Poor | Rarely sustainable regardless of win size |
By token type: Separate win rates for PumpFun tokens vs. established tokens. PumpFun win rates are typically lower (30–40%) but winners can be 10x+, while established token trading shows higher win rates (50–60%) with smaller moves.
ROI Per Trade
Formula:
roi = (exit_value - entry_value - total_fees) / entry_valueWhere:
entry_value= total SOL spent buying the token (sum of all buys)exit_value= total SOL received selling the token (sum of all sells)total_fees= platform fees + priority fees + estimated slippage
Handling Partial Exits (FIFO):
def compute_roi_fifo(buys: list[dict], sells: list[dict]) -> float:
"""Compute ROI using FIFO cost basis.
Args:
buys: [{"amount": token_qty, "cost_sol": sol_spent}, ...]
sells: [{"amount": token_qty, "proceeds_sol": sol_received}, ...]
Returns:
ROI as a decimal (0.5 = 50% gain).
"""
buy_queue = list(buys) # FIFO queue
total_cost = 0.0
total_proceeds = 0.0
for sell in sells:
remaining = sell["amount"]
total_proceeds += sell["proceeds_sol"]
while remaining > 0 and buy_queue:
buy = buy_queue[0]
if buy["amount"] <= remaining:
total_cost += buy["cost_sol"]
remaining -= buy["amount"]
buy_queue.pop(0)
else:
fraction = remaining / buy["amount"]
total_cost += buy["cost_sol"] * fraction
buy["amount"] -= remaining
buy["cost_sol"] *= (1 - fraction)
remaining = 0
if total_cost == 0:
return 0.0
return (total_proceeds - total_cost) / total_costFee Estimation:
- Solana base fee: 0.000005 SOL per transaction
- Priority fee: variable, typically 0.0001–0.01 SOL
- Jupiter platform fee: 0 (no fee) for basic swaps
- Slippage: estimate from trade size vs. pool liquidity
Profit Factor
Formula:
profit_factor = sum(pnl for trades where pnl > 0) / abs(sum(pnl for trades where pnl < 0))Edge Cases:
- No losing trades: profit_factor = infinity (use a cap of 99.9)
- No winning trades: profit_factor = 0
- No closed trades: undefined (return None)
Interpretation:
| Profit Factor | Rating | Meaning |
|---|---|---|
| > 3.0 | Exceptional | Very high edge, verify not cherry-picked |
| 2.0–3.0 | Excellent | Strong, sustainable edge |
| 1.5–2.0 | Good | Solid performance |
| 1.0–1.5 | Marginal | Small edge, vulnerable to costs |
| 0.7–1.0 | Poor | Losing after costs |
| < 0.7 | Very Poor | Significant negative edge |
Rolling Profit Factor: Calculate profit factor over rolling windows to detect performance trends:
def rolling_profit_factor(pnl_series: list[float], window: int = 20) -> list[float]:
"""Compute rolling profit factor over a sliding window."""
results = []
for i in range(window, len(pnl_series) + 1):
window_pnl = pnl_series[i - window:i]
wins = sum(p for p in window_pnl if p > 0)
losses = abs(sum(p for p in window_pnl if p < 0))
pf = wins / losses if losses > 0 else 99.9
results.append(round(pf, 2))
return resultsSharpe-Like Ratio
Formula:
sharpe = mean(trade_returns) / std(trade_returns) * sqrt(trades_per_year)Annualization:
- Estimate
trades_per_yearfrom observed frequency trades_per_year = trades_per_day * 365- If trading for < 30 days, annualization is unreliable
Interpretation:
| Annualized Sharpe | Rating |
|---|---|
| > 3.0 | Exceptional (verify data quality) |
| 2.0–3.0 | Excellent |
| 1.0–2.0 | Good |
| 0.5–1.0 | Mediocre |
| < 0.5 | Poor |
Note: This is an approximation. True Sharpe uses continuous returns and a risk-free rate. For crypto trading with discrete trade-level returns, this metric provides directional guidance rather than precise comparison with traditional finance Sharpe ratios.
Maximum Drawdown
Formula:
def max_drawdown(cumulative_pnl: list[float]) -> tuple[float, int, int]:
"""Compute maximum drawdown from cumulative PnL series.
Returns:
(max_dd_pct, peak_index, trough_index)
"""
peak = cumulative_pnl[0]
peak_idx = 0
max_dd = 0.0
max_dd_peak = 0
max_dd_trough = 0
for i, value in enumerate(cumulative_pnl):
if value > peak:
peak = value
peak_idx = i
if peak > 0:
dd = (peak - value) / peak
else:
dd = 0
if dd > max_dd:
max_dd = dd
max_dd_peak = peak_idx
max_dd_trough = i
return (round(max_dd, 4), max_dd_peak, max_dd_trough)Interpretation:
| Max Drawdown | Assessment |
|---|---|
| < 10% | Very conservative |
| 10–25% | Disciplined risk management |
| 25–50% | Moderate risk, common in crypto |
| 50–75% | High risk tolerance |
| > 75% | Extremely aggressive or poor risk management |
Performance Decay Detection
Compare recent performance to historical performance to detect declining edge.
def detect_decay(
trade_pnls: list[float],
recent_window: int = 30,
) -> dict:
"""Detect performance decay by comparing recent vs. historical metrics.
Args:
trade_pnls: Chronological list of trade PnLs.
recent_window: Number of recent trades to compare.
Returns:
Decay assessment with metrics comparison.
"""
if len(trade_pnls) < recent_window * 2:
return {"status": "insufficient_data"}
recent = trade_pnls[-recent_window:]
historical = trade_pnls[:-recent_window]
recent_wr = sum(1 for p in recent if p > 0) / len(recent)
hist_wr = sum(1 for p in historical if p > 0) / len(historical)
recent_avg = sum(recent) / len(recent)
hist_avg = sum(historical) / len(historical)
r_wins = sum(p for p in recent if p > 0)
r_losses = abs(sum(p for p in recent if p < 0))
recent_pf = r_wins / r_losses if r_losses > 0 else 99.9
h_wins = sum(p for p in historical if p > 0)
h_losses = abs(sum(p for p in historical if p < 0))
hist_pf = h_wins / h_losses if h_losses > 0 else 99.9
wr_change = recent_wr - hist_wr
pf_change = recent_pf - hist_pf
if wr_change < -0.1 and pf_change < -0.5:
status = "significant_decay"
elif wr_change < -0.05 or pf_change < -0.3:
status = "moderate_decay"
elif wr_change > 0.05 and pf_change > 0.3:
status = "improving"
else:
status = "stable"
return {
"status": status,
"recent_win_rate": round(recent_wr, 3),
"historical_win_rate": round(hist_wr, 3),
"win_rate_delta": round(wr_change, 3),
"recent_profit_factor": round(recent_pf, 2),
"historical_profit_factor": round(hist_pf, 2),
"profit_factor_delta": round(pf_change, 2),
"recent_avg_pnl": round(recent_avg, 4),
"historical_avg_pnl": round(hist_avg, 4),
}Consistency Metrics
Rolling Win Rate Stability
def win_rate_stability(trade_pnls: list[float], window: int = 20) -> float:
"""Compute standard deviation of rolling win rate.
Lower values indicate more consistent performance.
"""
import statistics
if len(trade_pnls) < window * 2:
return -1.0
rolling_wrs = []
for i in range(window, len(trade_pnls) + 1):
chunk = trade_pnls[i - window:i]
wr = sum(1 for p in chunk if p > 0) / len(chunk)
rolling_wrs.append(wr)
return round(statistics.stdev(rolling_wrs), 3)Consistency Score
Combine stability measures into a composite 0–100 score by summing contributions from win rate stability (lower std = better), drawdown control (lower = better), profit factor sustainability (higher = better), and sample size (more trades = more reliable). Start from a base of 50 and add/subtract up to 20 points per factor.
#!/usr/bin/env python3
"""Compare multiple Solana wallets side-by-side: fetch data, compute metrics
for each, rank by key performance indicators, and identify best performers.
Usage:
python scripts/compare_wallets.py
python scripts/compare_wallets.py --demo
Dependencies:
uv pip install httpx
Environment Variables:
WALLET_ADDRESSES: Comma-separated Solana wallet addresses
ST_API_KEY: SolanaTracker API key (optional, enables PnL data)
"""
import os
import sys
import statistics
import math
from datetime import datetime, timezone
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
WALLET_ADDRESSES = os.getenv("WALLET_ADDRESSES", "")
ST_API_KEY = os.getenv("ST_API_KEY", "")
DEMO_MODE = "--demo" in sys.argv
# ── Demo Data ───────────────────────────────────────────────────────
def generate_demo_wallets() -> dict[str, list[dict]]:
"""Generate demo trade data for three wallets with different profiles.
Returns:
Dict mapping wallet address to list of trade dicts.
"""
import random
wallets = {}
# Wallet A: Consistent scalper, moderate edge
random.seed(100)
base_time = 1735689600
trades_a = []
for i in range(200):
is_win = random.random() < 0.58
if is_win:
pnl = random.uniform(0.2, 3.0)
roi = random.uniform(0.05, 0.8)
else:
pnl = -random.uniform(0.1, 2.0)
roi = random.uniform(-0.6, -0.05)
trades_a.append({
"token_address": f"TokenA{i:04d}{'B' * 32}"[:44],
"token_symbol": f"TKA{i}",
"pnl_sol": round(pnl, 4),
"roi": round(roi, 4),
"entry_value_sol": round(random.uniform(2, 8), 4),
"hold_time_minutes": round(random.uniform(10, 120), 1),
"entry_timestamp": base_time + i * random.randint(3600, 14400),
"num_buys": 1,
"num_sells": 1,
})
wallets["ScalperAlpha111111111111111111111111111111"] = trades_a
# Wallet B: Aggressive sniper, high win rate but volatile
random.seed(200)
trades_b = []
for i in range(120):
is_win = random.random() < 0.45
if is_win:
pnl = random.uniform(1.0, 25.0)
roi = random.uniform(0.5, 10.0)
else:
pnl = -random.uniform(0.5, 5.0)
roi = random.uniform(-0.95, -0.2)
trades_b.append({
"token_address": f"TokenB{i:04d}{'C' * 32}"[:44],
"token_symbol": f"TKB{i}",
"pnl_sol": round(pnl, 4),
"roi": round(roi, 4),
"entry_value_sol": round(random.uniform(5, 30), 4),
"hold_time_minutes": round(random.uniform(0.5, 5), 1),
"entry_timestamp": base_time + i * random.randint(7200, 28800),
"num_buys": 1,
"num_sells": 1,
})
wallets["SniperBeta2222222222222222222222222222222222"] = trades_b
# Wallet C: Swing trader, fewer trades but high conviction
random.seed(300)
trades_c = []
for i in range(60):
is_win = random.random() < 0.52
if is_win:
pnl = random.uniform(2.0, 15.0)
roi = random.uniform(0.1, 1.5)
else:
pnl = -random.uniform(1.0, 8.0)
roi = random.uniform(-0.5, -0.1)
trades_c.append({
"token_address": f"TokenC{i:04d}{'D' * 32}"[:44],
"token_symbol": f"TKC{i}",
"pnl_sol": round(pnl, 4),
"roi": round(roi, 4),
"entry_value_sol": round(random.uniform(10, 50), 4),
"hold_time_minutes": round(random.uniform(1440, 14400), 1),
"entry_timestamp": base_time + i * random.randint(43200, 172800),
"num_buys": random.randint(1, 4),
"num_sells": random.randint(1, 3),
})
wallets["SwingGamma33333333333333333333333333333333333"] = trades_c
return wallets
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_wallet_trades(wallet: str, api_key: str) -> Optional[list[dict]]:
"""Fetch PnL data from SolanaTracker API for a single wallet.
Args:
wallet: Solana wallet address.
api_key: SolanaTracker API key.
Returns:
List of trade dicts, or None on failure.
"""
try:
import httpx
except ImportError:
print("httpx not installed. Run: uv pip install httpx")
return None
url = f"https://data.solanatracker.io/pnl/{wallet}"
headers = {"x-api-key": api_key}
try:
resp = httpx.get(url, headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
except httpx.HTTPStatusError as e:
print(f" API error for {wallet[:8]}...: {e.response.status_code}")
return None
except httpx.RequestError as e:
print(f" Network error for {wallet[:8]}...: {e}")
return None
if not isinstance(data, list):
data = data.get("tokens", data.get("data", []))
trades = []
for token_data in data:
if not isinstance(token_data, dict):
continue
realized = token_data.get("realized", 0) or 0
total_invested = token_data.get("total_invested", 0) or 0
total_sold = token_data.get("total_sold", 0) or 0
num_buys = token_data.get("num_buys", 0) or 0
num_sells = token_data.get("num_sells", 0) or 0
if num_buys == 0 and num_sells == 0:
continue
roi = (total_sold - total_invested) / total_invested if total_invested > 0 else 0
last_trade = token_data.get("last_trade_time", 0) or 0
first_trade = token_data.get("first_trade_time", last_trade) or last_trade
hold_minutes = (last_trade - first_trade) / 60 if last_trade > first_trade else 0
trades.append({
"token_address": token_data.get("token", "unknown"),
"token_symbol": token_data.get("symbol", "???"),
"pnl_sol": round(realized, 4),
"roi": round(roi, 4),
"entry_value_sol": round(total_invested, 4),
"hold_time_minutes": round(hold_minutes, 1),
"entry_timestamp": first_trade,
"num_buys": num_buys,
"num_sells": num_sells,
})
return trades
# ── Metrics ─────────────────────────────────────────────────────────
def compute_metrics(trades: list[dict]) -> dict:
"""Compute all performance metrics for a wallet's trades.
Args:
trades: List of trade dicts.
Returns:
Dict of metric name to value.
"""
if not trades:
return {
"trade_count": 0, "win_rate": 0, "avg_roi": 0,
"profit_factor": 0, "total_pnl": 0, "max_drawdown": 0,
"sharpe": 0, "style": "unknown", "median_hold_min": 0,
"median_size_sol": 0,
}
closed = [t for t in trades if t.get("num_sells", 0) > 0]
wins = sum(1 for t in closed if t["pnl_sol"] > 0) if closed else 0
win_rate = wins / len(closed) if closed else 0
rois = [t["roi"] for t in trades if t.get("roi") is not None]
avg_roi = statistics.mean(rois) if rois else 0
gross_win = sum(t["pnl_sol"] for t in trades if t["pnl_sol"] > 0)
gross_loss = abs(sum(t["pnl_sol"] for t in trades if t["pnl_sol"] < 0))
profit_factor = gross_win / gross_loss if gross_loss > 0 else (99.9 if gross_win > 0 else 0)
total_pnl = sum(t["pnl_sol"] for t in trades)
# Max drawdown
sorted_trades = sorted(trades, key=lambda t: t.get("entry_timestamp", 0))
cumulative = []
running = 0.0
for t in sorted_trades:
running += t["pnl_sol"]
cumulative.append(running)
peak = cumulative[0] if cumulative else 0
max_dd = 0.0
for val in cumulative:
if val > peak:
peak = val
if peak > 0:
dd = (peak - val) / peak
max_dd = max(max_dd, dd)
# Sharpe
sharpe = 0.0
if len(rois) >= 5:
mean_r = statistics.mean(rois)
std_r = statistics.stdev(rois)
if std_r > 0:
timestamps = sorted(t["entry_timestamp"] for t in trades if t.get("entry_timestamp"))
if len(timestamps) >= 2:
span = (timestamps[-1] - timestamps[0]) / 86400
tpy = len(trades) / max(span, 1) * 365
else:
tpy = 365
sharpe = mean_r / std_r * math.sqrt(tpy)
# Style
hold_times = [t["hold_time_minutes"] for t in trades if t.get("hold_time_minutes", 0) > 0]
median_hold = statistics.median(hold_times) if hold_times else 0
if median_hold < 5:
style = "sniper"
elif median_hold < 60:
style = "scalper"
elif median_hold < 1440:
style = "day_trader"
elif median_hold < 10080:
style = "swing"
else:
style = "holder"
sizes = [t["entry_value_sol"] for t in trades if t.get("entry_value_sol", 0) > 0]
median_size = statistics.median(sizes) if sizes else 0
# Consistency: rolling win rate std
consistency = -1.0
if len(trades) >= 40:
rolling_wrs = []
for i in range(20, len(trades) + 1):
chunk = trades[i - 20:i]
wr = sum(1 for t in chunk if t["pnl_sol"] > 0) / len(chunk)
rolling_wrs.append(wr)
if len(rolling_wrs) >= 2:
consistency = statistics.stdev(rolling_wrs)
return {
"trade_count": len(trades),
"win_rate": round(win_rate, 3),
"avg_roi": round(avg_roi, 3),
"profit_factor": round(profit_factor, 2),
"total_pnl": round(total_pnl, 2),
"max_drawdown": round(max_dd, 3),
"sharpe": round(sharpe, 2),
"style": style,
"median_hold_min": round(median_hold, 1),
"median_size_sol": round(median_size, 2),
"consistency": round(consistency, 3) if consistency >= 0 else None,
}
# ── Ranking ─────────────────────────────────────────────────────────
def rank_wallets(wallet_metrics: dict[str, dict]) -> dict[str, dict]:
"""Rank wallets across multiple dimensions.
Args:
wallet_metrics: Dict mapping wallet address to metrics dict.
Returns:
Dict mapping wallet to ranks dict.
"""
wallets = list(wallet_metrics.keys())
if not wallets:
return {}
categories = ["win_rate", "profit_factor", "total_pnl", "sharpe"]
ranks: dict[str, dict] = {w: {} for w in wallets}
for cat in categories:
sorted_by = sorted(wallets, key=lambda w: wallet_metrics[w].get(cat, 0), reverse=True)
for rank, w in enumerate(sorted_by, 1):
ranks[w][cat] = rank
# Composite rank: average of ranks (lower = better)
for w in wallets:
ranks[w]["composite"] = round(
sum(ranks[w][cat] for cat in categories) / len(categories), 1
)
return ranks
# ── Report ──────────────────────────────────────────────────────────
def print_comparison_report(
wallet_metrics: dict[str, dict],
wallet_ranks: dict[str, dict],
) -> None:
"""Print formatted comparison table.
Args:
wallet_metrics: Dict mapping wallet address to metrics.
wallet_ranks: Dict mapping wallet address to ranks.
"""
divider = "=" * 90
print(f"\n{divider}")
print(" WALLET COMPARISON REPORT")
print(f"{divider}")
print(f" Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print(f" Wallets: {len(wallet_metrics)}")
print(f"{divider}\n")
# Summary table
wallets = sorted(
wallet_metrics.keys(),
key=lambda w: wallet_ranks.get(w, {}).get("composite", 99),
)
# Header
print(f" {'Wallet':<14} {'Trades':>6} {'Win%':>6} {'PF':>6} {'PnL(SOL)':>10} "
f"{'Sharpe':>7} {'MaxDD':>7} {'Style':<12} {'Rank':>5}")
print(f" {'-' * 85}")
for w in wallets:
m = wallet_metrics[w]
r = wallet_ranks.get(w, {})
label = w[:12] + ".."
print(
f" {label:<14} {m['trade_count']:>6} {m['win_rate']:>5.0%} "
f"{m['profit_factor']:>6.2f} {m['total_pnl']:>10.2f} "
f"{m['sharpe']:>7.2f} {m['max_drawdown']:>6.0%} "
f"{m['style']:<12} {r.get('composite', 'N/A'):>5}"
)
print()
# Detailed rankings
print(" RANKINGS BY CATEGORY")
print(f" {'-' * 50}")
categories = [
("win_rate", "Win Rate"),
("profit_factor", "Profit Factor"),
("total_pnl", "Total PnL"),
("sharpe", "Sharpe Ratio"),
]
for key, label in categories:
ranked = sorted(wallets, key=lambda w: wallet_ranks.get(w, {}).get(key, 99))
winner = ranked[0] if ranked else "N/A"
val = wallet_metrics.get(winner, {}).get(key, 0)
if key == "win_rate":
val_str = f"{val:.0%}"
elif key == "total_pnl":
val_str = f"{val:.2f} SOL"
else:
val_str = f"{val:.2f}"
print(f" {label:<20} #1: {winner[:12]:<14} ({val_str})")
print()
# Overall winner
best = min(wallets, key=lambda w: wallet_ranks.get(w, {}).get("composite", 99))
print(f" OVERALL BEST PERFORMER: {best[:20]}...")
m = wallet_metrics[best]
print(f" Style: {m['style'].replace('_', ' ').title()}, "
f"Win Rate: {m['win_rate']:.0%}, "
f"PF: {m['profit_factor']:.2f}, "
f"PnL: {m['total_pnl']:.2f} SOL")
# Consistency comparison
consistent_wallets = {
w: m for w, m in wallet_metrics.items() if m.get("consistency") is not None
}
if consistent_wallets:
print()
print(" CONSISTENCY (Rolling Win Rate StdDev — lower is better)")
print(f" {'-' * 50}")
for w in sorted(consistent_wallets, key=lambda w: consistent_wallets[w]["consistency"]):
c = consistent_wallets[w]["consistency"]
label = "Very Consistent" if c < 0.05 else "Consistent" if c < 0.10 else "Variable" if c < 0.15 else "Erratic"
print(f" {w[:14]:<16} StdDev: {c:.3f} ({label})")
print(f"\n{divider}")
print(" NOTE: This analysis is for informational purposes only.")
print(" Past performance does not guarantee future results.")
print(f"{divider}\n")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run wallet comparison pipeline."""
if DEMO_MODE:
print("Running in demo mode with synthetic data for 3 wallets...\n")
wallet_data = generate_demo_wallets()
elif not WALLET_ADDRESSES:
print("Set WALLET_ADDRESSES environment variable or use --demo flag.")
print("Usage:")
print(" export WALLET_ADDRESSES=Wallet1...,Wallet2...,Wallet3...")
print(" python scripts/compare_wallets.py")
print(" python scripts/compare_wallets.py --demo")
sys.exit(1)
else:
addresses = [a.strip() for a in WALLET_ADDRESSES.split(",") if a.strip()]
if len(addresses) < 2:
print("Provide at least 2 wallet addresses separated by commas.")
sys.exit(1)
if not ST_API_KEY:
print("ST_API_KEY not set. Cannot fetch wallet data without API key.")
print("Use --demo to see example output.")
sys.exit(1)
wallet_data = {}
for addr in addresses:
print(f"Fetching data for {addr[:8]}...")
trades = fetch_wallet_trades(addr, ST_API_KEY)
if trades:
wallet_data[addr] = trades
else:
print(f" Skipping {addr[:8]}... (no data)")
if len(wallet_data) < 2:
print("Need at least 2 wallets with data for comparison.")
sys.exit(1)
# Compute metrics for each wallet
wallet_metrics = {}
for wallet, trades in wallet_data.items():
trades.sort(key=lambda t: t.get("entry_timestamp", 0))
wallet_metrics[wallet] = compute_metrics(trades)
# Rank wallets
wallet_ranks = rank_wallets(wallet_metrics)
# Print comparison
print_comparison_report(wallet_metrics, wallet_ranks)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Profile a Solana wallet: fetch trade data, compute performance metrics,
classify trading style, and generate a comprehensive report.
Usage:
python scripts/profile_wallet.py
python scripts/profile_wallet.py --demo
Dependencies:
uv pip install httpx
Environment Variables:
WALLET_ADDRESS: Solana wallet address to profile
ST_API_KEY: SolanaTracker API key (optional, enables PnL data)
"""
import os
import sys
import json
import statistics
import math
from datetime import datetime, timezone
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
WALLET_ADDRESS = os.getenv("WALLET_ADDRESS", "")
ST_API_KEY = os.getenv("ST_API_KEY", "")
DEMO_MODE = "--demo" in sys.argv
# ── Demo Data ───────────────────────────────────────────────────────
def generate_demo_data() -> list[dict]:
"""Generate realistic demo trade data for testing.
Returns:
List of trade dicts with pnl, timestamps, sizes, and hold times.
"""
import random
random.seed(42)
base_time = 1735689600 # 2025-01-01 00:00:00 UTC
trades = []
for i in range(150):
# Simulate a scalper/day-trader with ~55% win rate
is_win = random.random() < 0.55
if is_win:
pnl_sol = random.uniform(0.1, 8.0)
roi = random.uniform(0.05, 2.5)
else:
pnl_sol = -random.uniform(0.1, 4.0)
roi = random.uniform(-0.95, -0.05)
hold_minutes = random.lognormvariate(3.5, 1.2) # Median ~33 min
trade_size = random.lognormvariate(1.5, 0.8) # Median ~4.5 SOL
entry_time = base_time + i * random.randint(3600, 28800)
token_types = ["pumpfun"] * 4 + ["raydium"] * 3 + ["orca"] * 2 + ["meteora"]
token_type = random.choice(token_types)
trades.append({
"token_address": f"DemoToken{i:04d}{'A' * 32}"[:44],
"token_symbol": f"DEMO{i}",
"pnl_sol": round(pnl_sol, 4),
"roi": round(roi, 4),
"entry_value_sol": round(trade_size, 4),
"exit_value_sol": round(trade_size * (1 + roi), 4),
"hold_time_minutes": round(hold_minutes, 1),
"entry_timestamp": entry_time,
"exit_timestamp": entry_time + int(hold_minutes * 60),
"num_buys": random.randint(1, 3),
"num_sells": random.randint(1, 2),
"token_type": token_type,
})
return trades
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_solanatracker_pnl(wallet: str, api_key: str) -> Optional[list[dict]]:
"""Fetch PnL data from SolanaTracker API.
Args:
wallet: Solana wallet address.
api_key: SolanaTracker API key.
Returns:
List of trade dicts, or None on failure.
"""
try:
import httpx
except ImportError:
print("httpx not installed. Run: uv pip install httpx")
return None
url = f"https://data.solanatracker.io/pnl/{wallet}"
headers = {"x-api-key": api_key}
try:
resp = httpx.get(url, headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
except httpx.HTTPStatusError as e:
print(f"SolanaTracker API error: {e.response.status_code} - {e.response.text[:200]}")
return None
except httpx.RequestError as e:
print(f"Network error fetching SolanaTracker data: {e}")
return None
if not isinstance(data, list):
# API may return dict with tokens list
data = data.get("tokens", data.get("data", []))
trades = []
for token_data in data:
if not isinstance(token_data, dict):
continue
realized = token_data.get("realized", 0) or 0
total_invested = token_data.get("total_invested", 0) or 0
total_sold = token_data.get("total_sold", 0) or 0
num_buys = token_data.get("num_buys", 0) or 0
num_sells = token_data.get("num_sells", 0) or 0
if num_buys == 0 and num_sells == 0:
continue
roi = (total_sold - total_invested) / total_invested if total_invested > 0 else 0
last_trade = token_data.get("last_trade_time", 0) or 0
first_trade = token_data.get("first_trade_time", last_trade) or last_trade
hold_minutes = (last_trade - first_trade) / 60 if last_trade > first_trade else 0
trades.append({
"token_address": token_data.get("token", "unknown"),
"token_symbol": token_data.get("symbol", "???"),
"pnl_sol": round(realized, 4),
"roi": round(roi, 4),
"entry_value_sol": round(total_invested, 4),
"exit_value_sol": round(total_sold, 4),
"hold_time_minutes": round(hold_minutes, 1),
"entry_timestamp": first_trade,
"exit_timestamp": last_trade,
"num_buys": num_buys,
"num_sells": num_sells,
"token_type": "unknown",
})
return trades
# ── Metric Computation ──────────────────────────────────────────────
def compute_win_rate(trades: list[dict]) -> float:
"""Compute win rate from trade list.
Args:
trades: List of trade dicts with 'pnl_sol' key.
Returns:
Win rate as decimal (0.0 to 1.0).
"""
closed = [t for t in trades if t.get("num_sells", 0) > 0]
if not closed:
return 0.0
wins = sum(1 for t in closed if t["pnl_sol"] > 0)
return wins / len(closed)
def compute_profit_factor(trades: list[dict]) -> float:
"""Compute profit factor: gross_profit / gross_loss.
Args:
trades: List of trade dicts with 'pnl_sol' key.
Returns:
Profit factor. Returns 99.9 if no losses.
"""
wins = sum(t["pnl_sol"] for t in trades if t["pnl_sol"] > 0)
losses = abs(sum(t["pnl_sol"] for t in trades if t["pnl_sol"] < 0))
if losses == 0:
return 99.9 if wins > 0 else 0.0
return wins / losses
def compute_total_pnl(trades: list[dict]) -> float:
"""Compute total PnL in SOL.
Args:
trades: List of trade dicts with 'pnl_sol' key.
Returns:
Total PnL in SOL.
"""
return sum(t["pnl_sol"] for t in trades)
def compute_avg_roi(trades: list[dict]) -> float:
"""Compute average ROI per trade.
Args:
trades: List of trade dicts with 'roi' key.
Returns:
Mean ROI as decimal.
"""
rois = [t["roi"] for t in trades if t.get("roi") is not None]
if not rois:
return 0.0
return statistics.mean(rois)
def compute_max_drawdown(trades: list[dict]) -> float:
"""Compute maximum drawdown from cumulative PnL.
Args:
trades: List of trade dicts sorted by time with 'pnl_sol' key.
Returns:
Max drawdown as decimal (0.0 to 1.0).
"""
if not trades:
return 0.0
cumulative = []
running = 0.0
for t in trades:
running += t["pnl_sol"]
cumulative.append(running)
peak = cumulative[0]
max_dd = 0.0
for val in cumulative:
if val > peak:
peak = val
if peak > 0:
dd = (peak - val) / peak
max_dd = max(max_dd, dd)
return max_dd
def compute_sharpe_like(trades: list[dict]) -> float:
"""Compute Sharpe-like ratio from trade returns.
Args:
trades: List of trade dicts with 'roi' key.
Returns:
Annualized Sharpe-like ratio.
"""
rois = [t["roi"] for t in trades if t.get("roi") is not None]
if len(rois) < 5:
return 0.0
mean_ret = statistics.mean(rois)
std_ret = statistics.stdev(rois)
if std_ret == 0:
return 0.0
# Estimate trades per year from timestamps
timestamps = sorted(t["entry_timestamp"] for t in trades if t.get("entry_timestamp"))
if len(timestamps) >= 2:
span_days = (timestamps[-1] - timestamps[0]) / 86400
if span_days > 0:
trades_per_year = len(trades) / span_days * 365
else:
trades_per_year = 365
else:
trades_per_year = 365
return mean_ret / std_ret * math.sqrt(trades_per_year)
# ── Classification ──────────────────────────────────────────────────
def classify_style(trades: list[dict]) -> str:
"""Classify trading style from hold time distribution.
Args:
trades: List of trade dicts with 'hold_time_minutes' key.
Returns:
Style label.
"""
hold_times = [t["hold_time_minutes"] for t in trades if t.get("hold_time_minutes", 0) > 0]
if not hold_times:
return "unknown"
median = statistics.median(hold_times)
if median < 5:
return "sniper"
elif median < 60:
return "scalper"
elif median < 1440:
return "day_trader"
elif median < 10080:
return "swing"
else:
return "holder"
def classify_size(trades: list[dict]) -> str:
"""Classify wallet by typical trade size.
Args:
trades: List of trade dicts with 'entry_value_sol' key.
Returns:
Size tier label.
"""
sizes = [t["entry_value_sol"] for t in trades if t.get("entry_value_sol", 0) > 0]
if not sizes:
return "unknown"
median = statistics.median(sizes)
if median > 100:
return "whale"
elif median > 10:
return "large"
elif median > 1:
return "medium"
else:
return "small"
def estimate_bot_probability(trades: list[dict]) -> float:
"""Estimate probability wallet is a bot from timing patterns.
Args:
trades: List of trade dicts with 'entry_timestamp' key.
Returns:
Probability from 0.0 to 1.0.
"""
timestamps = sorted(t["entry_timestamp"] for t in trades if t.get("entry_timestamp"))
if len(timestamps) < 10:
return 0.0
intervals = [
timestamps[i + 1] - timestamps[i]
for i in range(len(timestamps) - 1)
]
intervals = [iv for iv in intervals if iv > 0]
if len(intervals) < 5:
return 0.0
mean_iv = statistics.mean(intervals)
std_iv = statistics.stdev(intervals)
cv = std_iv / mean_iv if mean_iv > 0 else 999
timing_score = max(0, 1.0 - cv / 0.5)
# Round-number interval check
round_count = sum(1 for iv in intervals if iv % 10 < 1 or iv % 10 > 9)
round_score = round_count / len(intervals)
# Hour coverage (bots trade 24/7)
hours = set()
for ts in timestamps:
hours.add(datetime.fromtimestamp(ts, tz=timezone.utc).hour)
hour_score = len(hours) / 24
probability = timing_score * 0.5 + round_score * 0.25 + hour_score * 0.25
return round(min(1.0, max(0.0, probability)), 2)
def classify_focus(trades: list[dict]) -> str:
"""Classify wallet focus area.
Args:
trades: List of trade dicts with 'token_type' key.
Returns:
Focus area label.
"""
types = [t.get("token_type", "unknown") for t in trades]
if not types:
return "unknown"
from collections import Counter
counts = Counter(types)
total = len(types)
pumpfun_pct = counts.get("pumpfun", 0) / total
if pumpfun_pct > 0.7:
return "pumpfun_specialist"
elif pumpfun_pct > 0.3:
return "mixed_memecoin"
else:
return "dex_trader"
# ── Activity Analysis ───────────────────────────────────────────────
def analyze_activity(trades: list[dict]) -> dict:
"""Analyze trading activity patterns.
Args:
trades: List of trade dicts with timestamps.
Returns:
Activity metrics dict.
"""
if not trades:
return {}
timestamps = sorted(t["entry_timestamp"] for t in trades if t.get("entry_timestamp"))
if len(timestamps) < 2:
return {"total_trades": len(trades)}
span_days = max((timestamps[-1] - timestamps[0]) / 86400, 1)
trades_per_day = len(trades) / span_days
hold_times = [t["hold_time_minutes"] for t in trades if t.get("hold_time_minutes", 0) > 0]
avg_hold = statistics.mean(hold_times) if hold_times else 0
median_hold = statistics.median(hold_times) if hold_times else 0
# Unique tokens
unique_tokens = len(set(t.get("token_address", "") for t in trades))
token_diversity = unique_tokens / len(trades) if trades else 0
# Peak hours
hours = [datetime.fromtimestamp(ts, tz=timezone.utc).hour for ts in timestamps]
from collections import Counter
hour_counts = Counter(hours)
peak_hours = [h for h, _ in hour_counts.most_common(3)]
return {
"total_trades": len(trades),
"active_days": round(span_days, 1),
"trades_per_day": round(trades_per_day, 1),
"avg_hold_minutes": round(avg_hold, 1),
"median_hold_minutes": round(median_hold, 1),
"unique_tokens": unique_tokens,
"token_diversity": round(token_diversity, 2),
"peak_hours_utc": peak_hours,
}
# ── Risk Assessment ─────────────────────────────────────────────────
def compute_risk_score(
trades: list[dict],
win_rate: float,
profit_factor: float,
bot_prob: float,
) -> tuple[int, list[str]]:
"""Compute copy-trade risk score (0=low risk, 100=high risk).
Args:
trades: Trade list.
win_rate: Computed win rate.
profit_factor: Computed profit factor.
bot_prob: Bot probability estimate.
Returns:
(risk_score, list_of_risk_flags)
"""
score = 0
flags = []
# Wallet age
timestamps = sorted(t["entry_timestamp"] for t in trades if t.get("entry_timestamp"))
if len(timestamps) >= 2:
age_days = (timestamps[-1] - timestamps[0]) / 86400
if age_days < 14:
score += 25
flags.append(f"New wallet ({age_days:.0f} days old)")
# Single big win concentration
total_pnl = sum(t["pnl_sol"] for t in trades)
if total_pnl > 0:
max_single = max(t["pnl_sol"] for t in trades)
if max_single / total_pnl > 0.5:
score += 20
flags.append(f"Top trade = {max_single / total_pnl:.0%} of total PnL")
# Performance decay (compare last 30 trades to rest)
if len(trades) >= 60:
recent = trades[-30:]
historical = trades[:-30]
recent_pf = compute_profit_factor(recent)
hist_pf = compute_profit_factor(historical)
if recent_pf < hist_pf * 0.7:
score += 15
flags.append(f"Performance decay: recent PF {recent_pf:.2f} vs historical {hist_pf:.2f}")
# Bot probability
if bot_prob > 0.7:
score += 15
flags.append(f"High bot probability ({bot_prob:.0%})")
# Suspicious win rate
if win_rate > 0.8:
score += 10
flags.append(f"Suspiciously high win rate ({win_rate:.0%})")
# Low token diversity
unique_tokens = len(set(t.get("token_address", "") for t in trades))
if unique_tokens < 5:
score += 15
flags.append(f"Low diversity ({unique_tokens} unique tokens)")
# Low sample size
if len(trades) < 30:
score += 10
flags.append(f"Small sample ({len(trades)} trades)")
return min(100, score), flags
# ── Report Generation ──────────────────────────────────────────────
def print_report(
wallet: str,
trades: list[dict],
metrics: dict,
classification: dict,
activity: dict,
risk_score: int,
risk_flags: list[str],
) -> None:
"""Print formatted wallet profile report.
Args:
wallet: Wallet address.
trades: Trade list.
metrics: Performance metrics dict.
classification: Classification dict.
activity: Activity metrics dict.
risk_score: Risk score 0-100.
risk_flags: List of risk flag descriptions.
"""
divider = "=" * 70
print(f"\n{divider}")
print(f" WALLET PROFILE REPORT")
print(f"{divider}")
print(f" Wallet: {wallet}")
print(f" Trades: {len(trades)}")
print(f" Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
print(f"{divider}\n")
# Performance Metrics
print(" PERFORMANCE METRICS")
print(f" {'-' * 40}")
print(f" Win Rate: {metrics['win_rate']:.1%}")
print(f" Avg ROI: {metrics['avg_roi']:.1%}")
print(f" Profit Factor: {metrics['profit_factor']:.2f}")
print(f" Total PnL: {metrics['total_pnl']:.2f} SOL")
print(f" Max Drawdown: {metrics['max_drawdown']:.1%}")
print(f" Sharpe-Like: {metrics['sharpe']:.2f}")
print()
# Classification
print(" CLASSIFICATION")
print(f" {'-' * 40}")
print(f" Style: {classification['style'].replace('_', ' ').title()}")
print(f" Size Tier: {classification['size_tier'].title()}")
print(f" Focus: {classification['focus'].replace('_', ' ').title()}")
print(f" Bot Probability: {classification['bot_probability']:.0%}")
print()
# Activity
print(" ACTIVITY")
print(f" {'-' * 40}")
print(f" Active Days: {activity.get('active_days', 'N/A')}")
print(f" Trades/Day: {activity.get('trades_per_day', 'N/A')}")
print(f" Avg Hold: {activity.get('avg_hold_minutes', 0):.0f} min")
print(f" Median Hold: {activity.get('median_hold_minutes', 0):.0f} min")
print(f" Unique Tokens: {activity.get('unique_tokens', 'N/A')}")
print(f" Token Diversity: {activity.get('token_diversity', 0):.0%}")
peak = activity.get("peak_hours_utc", [])
print(f" Peak Hours (UTC):{' ' + ', '.join(f'{h:02d}:00' for h in peak) if peak else ' N/A'}")
print()
# Risk Assessment
risk_label = (
"LOW" if risk_score < 25
else "MODERATE" if risk_score < 50
else "HIGH" if risk_score < 75
else "VERY HIGH"
)
print(" COPY-TRADE RISK ASSESSMENT")
print(f" {'-' * 40}")
print(f" Risk Score: {risk_score}/100 ({risk_label})")
if risk_flags:
print(f" Flags:")
for flag in risk_flags:
print(f" - {flag}")
else:
print(f" Flags: None detected")
print()
# Top Trades
sorted_trades = sorted(trades, key=lambda t: t["pnl_sol"], reverse=True)
print(" TOP 5 WINNING TRADES")
print(f" {'-' * 40}")
for t in sorted_trades[:5]:
symbol = t.get("token_symbol", "???")[:8]
print(f" {symbol:<10} PnL: {t['pnl_sol']:>8.2f} SOL ROI: {t['roi']:>7.0%}")
print()
print(" BOTTOM 5 LOSING TRADES")
print(f" {'-' * 40}")
for t in sorted_trades[-5:]:
symbol = t.get("token_symbol", "???")[:8]
print(f" {symbol:<10} PnL: {t['pnl_sol']:>8.2f} SOL ROI: {t['roi']:>7.0%}")
print(f"\n{divider}")
print(" NOTE: This analysis is for informational purposes only.")
print(" Past performance does not guarantee future results.")
print(f"{divider}\n")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Run wallet profiling pipeline."""
if DEMO_MODE:
wallet = "DemoWallet1111111111111111111111111111111111"
print("Running in demo mode with synthetic data...")
trades = generate_demo_data()
elif not WALLET_ADDRESS:
print("Set WALLET_ADDRESS environment variable or use --demo flag.")
print("Usage:")
print(" export WALLET_ADDRESS=YourWallet...")
print(" python scripts/profile_wallet.py")
print(" python scripts/profile_wallet.py --demo")
sys.exit(1)
else:
wallet = WALLET_ADDRESS
trades = None
if ST_API_KEY:
print(f"Fetching PnL data from SolanaTracker for {wallet[:8]}...")
trades = fetch_solanatracker_pnl(wallet, ST_API_KEY)
if trades is None:
print("No data available. Set ST_API_KEY for SolanaTracker access or use --demo.")
sys.exit(1)
if not trades:
print(f"No trades found for wallet {wallet[:8]}...")
sys.exit(0)
# Sort by entry timestamp
trades.sort(key=lambda t: t.get("entry_timestamp", 0))
# Compute metrics
win_rate = compute_win_rate(trades)
avg_roi = compute_avg_roi(trades)
profit_factor = compute_profit_factor(trades)
total_pnl = compute_total_pnl(trades)
max_dd = compute_max_drawdown(trades)
sharpe = compute_sharpe_like(trades)
metrics = {
"win_rate": win_rate,
"avg_roi": avg_roi,
"profit_factor": profit_factor,
"total_pnl": total_pnl,
"max_drawdown": max_dd,
"sharpe": sharpe,
}
# Classify
bot_prob = estimate_bot_probability(trades)
classification = {
"style": classify_style(trades),
"size_tier": classify_size(trades),
"focus": classify_focus(trades),
"bot_probability": bot_prob,
}
# Activity analysis
activity = analyze_activity(trades)
# Risk assessment
risk_score, risk_flags = compute_risk_score(trades, win_rate, profit_factor, bot_prob)
# Print report
print_report(wallet, trades, metrics, classification, activity, risk_score, risk_flags)
if __name__ == "__main__":
main()
Related skills
FAQ
How does it classify trading style?
By median hold time: sniper (<5 min), scalper, day trader, swing trader, or position holder (>7 days).
What separates smart money from luck?
Sustained profit factor above 2.0, win rates above 45% across 100+ trades, and diversified token selection.