I used an AI agent to make me team recommendations for HLTV Fantasy. This is the rational
How I built a data-driven HLTV Fantasy optimizer — methodology breakdown
Been playing HLTV Fantasy for a few seasons and got tired of going purely by gut feeling, so I built a Python pipeline that scrapes stats, estimates expected points, and picks an optimal team. Here's the full methodology — happy to answer questions or hear pushback.
The core problem
HLTV Fantasy scores four things per player per map:
- Player points — based on your HLTV rating that map
- Team points — +6 if your team wins the map, −3 if they lose
- Role points — bonus/penalty depending on whether you hit your assigned role's threshold
- Booster points — extra multiplier if your booster fires
The key insight from backtesting three events: team points dominate variance more than individual skill does. A player on a team that goes 5-0 in Swiss gets +30 team points before a single rating point is counted. That completely changes how you think about team selection.
Step 1 — Effective Rating
Raw HLTV rating from the API is stale and noisy. We use a weighted blend of three lookback windows, normalized so missing data doesn't break the model:
sample_factor = min(1.0, maps_played_last_3m / 10)
w_3m = 0.60 × sample_factor # recent form, trusted after 10 maps
w_6m = 0.30 # medium-term baseline
w_12m = 0.10 # long-term floor
effective_rating = (r3m × w_3m + r6m × w_6m + r12m × w_12m) / sum(weights)
The sample_factor is important: a player with only 3 maps in the last 3 months shouldn't be weighted at 60% — you're essentially overfitting to noise. Backtesting on real event data confirmed 3m has the highest predictive correlation (Pearson r ≈ 0.42), and the 10-map threshold holds up across events.
Step 2 — Rating Points
HLTV Fantasy converts your per-map rating into fantasy points. The true formula isn't public, but reverse-engineering from actual scores gives approximately:
rating_pts_per_map = (effective_rating − 1.0) × K
We validated K=60 through backtest. An alternative K≈27 produced lower MAE in isolation but compressed value differences between players so much that the optimizer started picking mediocre players — so K=60 it is.
We also adjust for opponent difficulty. For each team in the field, we compute an Elo win probability:
P(win) = 1 / (1 + 10^((rank_A − rank_B) / scale))
where scale is set dynamically based on the average field strength (Majors ≈ 50, qualifiers ≈ 80). Each player's rating is then nudged by α × (win_prob − 0.5), where α=0.15. Not a huge effect, but it's the right direction.
Fatigue discount: players going deep in tournament brackets consistently underperform model expectations at 8+ maps. We apply a 30% discount on map value beyond map 6 (i.e., map 7 is worth 70% of a regular map's contribution). This reduced MAE by ~10% for late-tournament players.
Step 3 — Team Points
team_pts = (win_rate × 6 + (1 − win_rate) × −3) × expected_maps × 1.4
Win rate is Elo-estimated against the full field. The 1.4× multiplier came from post-mortem analysis of ESL Pro League S23 Stage 1 (event 601): the model was systematically underweighting how much team performance dominated total scores. A top-10 team going 5-0 in Swiss contributes +30 team points — that dwarfs any reasonable individual rating advantage.
For Swiss specifically, we run a 10,000-iteration Monte Carlo simulation to get per-team expected maps and advancement probability, since Swiss matchmaking (same-record matchups) produces very different distributions than a simple round-robin.
Step 4 — Role Points
This was the hardest component to model correctly. HLTV roles have a max bonus (+5), small bonus (+2), and fail penalty (−2), and whether you hit them depends on specific per-role stat thresholds (CT rating for Defender, HS% for HS Machine, etc.).
Rather than try to predict which threshold a player will hit, we model it probabilistically as an expected value based on how well the player fits the role:
| Fit level | P(max) | P(small) | P(fail) | EV/map |
|---|---|---|---|---|
| EXCELLENT | 60% | 30% | 10% | +3.4 |
| GOOD | 40% | 35% | 25% | +2.2 |
| OK | 15% | 30% | 55% | +0.25 |
These probabilities come from actual breakdown data across ~150 player-rounds from events 599–601. The fit level is assigned by checking the player's actual stats against each role's documented threshold.
We also confirmed through regression analysis that no linear combination of HLTV competency scores (FP, EN, OP, TR, CL, SN, UT) predicts role points well (R² < 8% across all models) — what matters is the role assignment quality, not the raw competency numbers.
Step 5 — Team Selection (Optimizer)
Brute-force combinatorial search over the top ~33 candidates by value/100k (we add cheap fillers to the pool to handle "elite anchor + budget fill" lineups — otherwise the optimizer can't fit a $240k player within the $1M budget).
Two key adjustments on top of raw expected points:
Stack bonus: having 2 players from the same team creates correlated upside. When Spirit goes 5-0, both your Spirit players collect +30 team points each. We model this explicitly:
stack_bonus = stacked_players_score × 0.15 × pairs × quality_multiplier
where quality_multiplier scales from 1.5× (rank #1–5) to 0.7× (rank #50). This naturally rewards stacking from top teams over bottom teams.
Rank eligibility filter: players from teams ranked outside the top 50 are excluded. Backtest data: a player from a rank-141 team in a tier-1 event averaged −13 actual fantasy points. The team-loss bleeding is too severe.
Backtesting results (events 599–601, 152 total player-records)
| Model | Pearson r | MAE | RMSE |
|---|---|---|---|
| Baseline (flat K=27, no fatigue, no EV roles) | 0.28 | 39.5 | 47.6 |
| Current model | 0.35 | 36.7 | 44.3 |
Improvement is real but modest. The irreducible problem: Swiss format creates massive outcome variance. NRG went 0-5 in ESL PL S23 Stage 1 at world rank #19. No model based on pre-tournament data catches that. The "inherent RMSE floor" from tournament variance alone appears to be around 40 points per player.
What the model deliberately ignores
- Form during the tournament itself — we only use pre-tournament stats
- Map pool matchups — would require per-map opponent modeling
- Player injury / roster changes announced mid-event — manual override only
- Meta shifts — ratings are a lagging indicator of playstyle
Happy to share more detail on any component. The stack bonus calibration and the role EV percentages are probably the two weakest parts of the model right now.
Posted Mar 5, 2026, 4:45 PM UTC