Development of Trader Leaderboard System
Trader leaderboards are a gamification and social proof element on trading platforms. A properly implemented leaderboard stimulates activity, helps users find successful traders to copy, creates community around the platform.
Types of Leaderboards
P&L Leaderboard — ranking by absolute or percentage profit for a period. The most popular, but requires normalization: a trader with $100K deposit and $5K profit has the same 5% return as a trader with $1K and $50 profit.
Risk-Adjusted Leaderboard — ranking by Sharpe ratio, Sortino ratio, or Calmar ratio. More fair: high profit with huge risk shouldn't be first.
Competition Leaderboard — temporary competitions with prizes. Fixed period (1 week, 1 month), same starting conditions.
Asset-Specific — top traders for a specific instrument (BTC/USDT, ETH/USDT).
Data Schema
CREATE TABLE trader_performance (
user_id UUID NOT NULL,
period_type VARCHAR(16) NOT NULL, -- 'daily', 'weekly', 'monthly', 'all_time'
period_date DATE NOT NULL,
total_pnl NUMERIC(24, 8) NOT NULL,
pnl_pct NUMERIC(10, 4) NOT NULL,
sharpe_ratio NUMERIC(10, 4),
max_drawdown NUMERIC(10, 4),
win_rate NUMERIC(6, 4),
total_trades INTEGER,
volume NUMERIC(24, 8),
rank INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, period_type, period_date)
);
CREATE INDEX ON trader_performance (period_type, period_date, rank);
CREATE INDEX ON trader_performance (period_type, period_date, pnl_pct DESC);
Ranking Calculation
class LeaderboardCalculator:
async def calculate_period_rankings(self, period_type: str, period_date: date):
# Load trading data for the period
trading_data = await self.trade_repo.get_period_summary(period_type, period_date)
# Calculate metrics for each trader
performances = []
for user_id, trades in trading_data.items():
if len(trades) < 5: # minimum activity threshold
continue
daily_returns = self.compute_daily_returns(trades)
metrics = TraderMetrics(
user_id=user_id,
total_pnl=sum(t.pnl for t in trades),
pnl_pct=self.compute_pnl_pct(trades),
sharpe_ratio=self.sharpe(daily_returns),
max_drawdown=self.max_drawdown(daily_returns),
win_rate=len([t for t in trades if t.pnl > 0]) / len(trades),
total_trades=len(trades),
volume=sum(t.notional for t in trades),
)
performances.append(metrics)
# Sort by risk-adjusted metric
performances.sort(key=lambda p: p.sharpe_ratio or 0, reverse=True)
# Assign ranks
for rank, perf in enumerate(performances, 1):
perf.rank = rank
# Save to DB
await self.performance_repo.bulk_upsert(
[p.to_db_row(period_type, period_date) for p in performances]
)
API Endpoint
@app.get("/api/leaderboard")
async def get_leaderboard(
period: str = Query("weekly", regex="^(daily|weekly|monthly|all_time)$"),
metric: str = Query("pnl_pct", regex="^(pnl_pct|sharpe_ratio|win_rate)$"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
):
# Cache leaderboard in Redis for 5 minutes
cache_key = f"leaderboard:{period}:{metric}:{limit}:{offset}"
cached = await redis.get(cache_key)
if cached:
return json.loads(cached)
data = await db.get_leaderboard(period, metric, limit, offset)
result = {
"data": data,
"total": await db.get_leaderboard_count(period),
"period": period,
"metric": metric,
}
await redis.setex(cache_key, 300, json.dumps(result))
return result
Anti-Gaming Measures
Leaderboards can be manipulated: a trader opens opposite positions with different accounts, one profitable — gets in top. Protection:
- Minimum trading volume for period (excludes accidental single trades)
- Minimum number of trades (at least 10–20 per period)
- Wash trading detector — trades with own orders via different accounts
- KYC-binding — one account per user
- Cooldown period — results counted no sooner than a week after registration
Anonymity vs verification — a compromise: pseudonym for public leaderboard, real data for platform verification.







