CLV% = (Closing_Implied_Prob - Bet_Implied_Prob) / Bet_Implied_Prob. If you beat the closing line, you have edge. If you don’t, you don’t. CLV is the single most reliable predictor of long-run profitability — more predictive than win rate, ROI, or any other metric over samples under 5,000 bets.
Why This Matters for Agents
An autonomous betting agent needs a feedback signal. Win/loss results are too noisy — a +EV agent can lose money for weeks due to variance alone. The agent needs a metric that converges fast, measures edge directly, and doesn’t depend on outcome luck.
That metric is Closing Line Value. This is Layer 4 — Intelligence. CLV is the primary evaluation metric in the agent’s model validation loop. After every bet, the agent records the odds it got and compares them to the closing line at a sharp book. If the agent consistently beats the close, its model has edge. If it doesn’t, the model needs retraining — regardless of whether recent bets won or lost. The Agent Betting Stack places CLV tracking in the intelligence layer because it directly governs whether the agent’s predictive models are producing actionable signal or noise. An agent without CLV tracking is flying blind. It has no way to distinguish skill from luck until thousands of bets have resolved — and by then, the bankroll damage from a broken model may be irreversible. CLV tells you within 200-500 bets whether your model works. That’s the difference between a sharp betting system and a gambler.
The Math
Defining CLV Formally
Closing Line Value measures the percentage improvement in implied probability between the odds you bet and the closing odds (the final odds before the event starts).
CLV% = (P_close - P_bet) / P_bet
Where:
- P_close = no-vig implied probability at market close
- P_bet = no-vig implied probability at the time you placed your bet
Both probabilities must be vig-removed. Using raw market odds (which include the book’s margin) inflates CLV artificially and produces meaningless numbers.
Why Closing Lines Are the Benchmark
The closing line at a sharp book — Pinnacle, Circa, or Bookmaker — is the most efficient probability estimate available in sports betting. This isn’t opinion. Studies on Pinnacle closing lines across tens of thousands of NFL, NBA, and soccer matches show calibration error under 1%. When Pinnacle’s closing line implies a team has a 60% chance of winning, that team wins approximately 60% of the time.
The mechanism is straightforward: sharp books accept large wagers from professional bettors. Those bettors move the line. By the time the market closes, the line has incorporated the information from every sophisticated bettor, model, and syndicate that participated. The closing line is the consensus of all market-available information, weighted by dollars.
This makes the closing line the sports betting equivalent of the efficient market price in equity markets. The Efficient Market Hypothesis guide covers the theoretical foundation. In practice, closing-line efficiency means that an agent whose model consistently produces prices different from the closing line — and bets before the line moves to match — has demonstrated genuine predictive edge.
CLV and Expected ROI
For a standard -110/-110 market (4.76% vig), the relationship between CLV and expected ROI is:
Expected_ROI ≈ CLV% × (1 + 1/vig_multiplier)
For -110 lines (vig_multiplier ≈ 1.05):
CLV% Expected ROI
1% ~1.9%
2% ~3.8%
3% ~5.7%
5% ~9.5%
These are approximate — the exact relationship depends on the distribution of odds in your bet portfolio. But the key insight holds: a 2% edge on the closing line compounds into substantial returns over thousands of bets.
Variance: Why CLV Converges Faster Than Profit
Consider an agent placing 500 bets at -110 with a true 2% CLV edge. Expected ROI is ~3.8%.
Standard deviation of profit over n bets at odds d (decimal) is approximately:
σ_profit ≈ sqrt(n) × d × sqrt(p × (1-p))
For n=500, decimal odds ≈ 1.91, p ≈ 0.524:
σ_profit ≈ sqrt(500) × 1.91 × sqrt(0.524 × 0.476)
σ_profit ≈ 22.36 × 1.91 × 0.4996
σ_profit ≈ 21.3 units
Expected profit = 500 × 0.038 × 1 unit = 19 units. The signal-to-noise ratio (expected profit / std dev) is 19/21.3 = 0.89. You can’t even confidently distinguish this from breakeven after 500 bets.
CLV, by contrast, measures the average odds advantage per bet, not the binary win/loss. Each bet contributes a continuous data point (the CLV percentage), not a binary one. The standard error of mean CLV drops as 1/sqrt(n). After 200-300 bets, an agent with 2% true CLV will have a CLV sample mean statistically distinguishable from zero at the 95% confidence level. After 500 bets, the measurement is definitive.
This is why CLV is the gold standard. Profit tells you what happened. CLV tells you whether your model works — and it tells you 5-10x faster.
Sharp Books vs. Soft Books
CLV is only meaningful when measured against a sharp closing line.
| Book Type | Examples | CLV Validity | Why |
|---|---|---|---|
| Sharp | Pinnacle, Circa, Bookmaker | Valid benchmark | Lines are efficient — they aggregate professional money |
| Mid-tier | BetOnline, Heritage | Partially valid | Lines track sharp books with 30-60 min lag |
| Soft | DraftKings, FanDuel, BetMGM | Invalid benchmark | Lines are set for recreational balance, not efficiency |
If you measure CLV against DraftKings’ closing line and find +5%, that doesn’t mean you have 5% edge. DraftKings’ closing lines are systematically less efficient than Pinnacle’s. They shade lines toward recreational betting patterns, not toward truth.
An agent that measures CLV against soft-book closing lines will overestimate its edge. An agent that measures CLV against Pinnacle’s closing line and still shows +2% or better has a model that genuinely outperforms the market’s most efficient price-discovery mechanism.
The Vig Index tracks overround by book — sharper books tend to have lower vig, and the vig structure directly affects CLV calculation.
Worked Examples
Example 1: NFL Point Spread
An agent bets Chiefs -3.5 at -108 on Bookmaker at 2:00 PM ET on Sunday. The game kicks off at 4:25 PM ET. At close (4:24 PM), the line at Bookmaker is Chiefs -3.5 at -115 / +97.
Step 1: Convert bet odds to no-vig implied probability.
Bet line: Chiefs -3.5 at -108 (other side was +100)
Implied_bet_favorite = 108 / (108 + 100) = 0.5192
Implied_bet_underdog = 100 / (100 + 100) = 0.5000
Sum = 1.0192 (overround = 1.92%)
P_bet = 0.5192 / 1.0192 = 0.5094 (50.94%)
Step 2: Convert closing odds to no-vig implied probability.
Closing line: Chiefs -3.5 at -115 / +97
Implied_close_favorite = 115 / (115 + 100) = 0.5349
Implied_close_underdog = 100 / (100 + 97) = 0.5076
Sum = 1.0425 (overround = 4.25%)
P_close = 0.5349 / 1.0425 = 0.5131 (51.31%)
Step 3: Compute CLV.
CLV% = (0.5131 - 0.5094) / 0.5094 = 0.73%
The agent captured 0.73% CLV on this bet. The line moved from -108 to -115 in the agent’s direction — other sharp money agreed with the agent’s assessment.
Example 2: NBA Moneyline
An agent bets Lakers ML at +145 on BetOnline at 5:30 PM. The closing line is Lakers ML +130 / Celtics -155.
Bet odds: Lakers +145
Implied_bet = 100 / (145 + 100) = 0.4082
Counter: Celtics at -165 (estimated from the line)
Implied_counter_bet = 165 / (165 + 100) = 0.6226
Sum_bet = 1.0308
P_bet = 0.4082 / 1.0308 = 0.3960
Close odds: Lakers +130
Implied_close = 100 / (130 + 100) = 0.4348
Counter close: Celtics -155
Implied_counter_close = 155 / (155 + 100) = 0.6078
Sum_close = 1.0426
P_close = 0.4348 / 1.0426 = 0.4170
CLV% = (0.4170 - 0.3960) / 0.3960 = 5.30%
A 5.30% CLV on a single bet is exceptional. The line moved substantially — +145 to +130 — meaning the market reassessed the Lakers’ chances upward after the agent’s bet. This is the kind of CLV consistent, sharp models produce on underdog bets where the opening line underestimates the true probability.
Example 3: Negative CLV — The Warning Sign
An agent bets Commanders +7 at -110 on Bovada at 10:00 AM. Closing line: Commanders +7.5 at -108 / -112.
Bet: Commanders +7 at -110 (counter: -110)
Implied_bet = 110 / (110 + 100) = 0.5238
Sum_bet = 1.0476
P_bet = 0.5238 / 1.0476 = 0.5000
Close: Commanders +7.5 at -108 (counter: -112)
Implied_close_cmdr = 108 / (108 + 100) = 0.5192
Implied_close_opp = 112 / (112 + 100) = 0.5283
Sum_close = 1.0475
P_close = 0.5192 / 1.0475 = 0.4956
CLV% = (0.4956 - 0.5000) / 0.5000 = -0.88%
Negative CLV. The line moved against the agent — Commanders got more points (7 to 7.5), meaning the market decided they were even bigger underdogs than the agent’s model suggested. Over a large sample, persistent negative CLV means the model is broken. Time to retrain.
Implementation
"""
CLV Tracking Module for Autonomous Betting Agents
Tracks closing line value across a portfolio of bets using
The Odds API for historical odds data.
Requires: pip install requests pandas numpy
"""
import numpy as np
import pandas as pd
import requests
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class BetRecord:
"""Single bet record for CLV tracking."""
bet_id: str
event_id: str
sport: str
book: str
market: str # "spreads", "h2h", "totals"
selection: str # "Chiefs -3.5", "Lakers ML", "Over 215.5"
bet_odds_american: int # -108, +145, etc.
counter_odds_american: int # other side of the line
bet_timestamp: datetime
stake: float
closing_odds_american: Optional[int] = None
closing_counter_american: Optional[int] = None
close_timestamp: Optional[datetime] = None
result: Optional[str] = None # "win", "loss", "push"
pnl: Optional[float] = None
def american_to_implied(odds: int) -> float:
"""Convert American odds to implied probability."""
if odds < 0:
return abs(odds) / (abs(odds) + 100)
else:
return 100 / (odds + 100)
def remove_vig_multiplicative(prob_a: float, prob_b: float) -> tuple[float, float]:
"""
Remove vig using multiplicative method.
Returns (true_prob_a, true_prob_b) summing to 1.0.
"""
total = prob_a + prob_b
return prob_a / total, prob_b / total
def compute_clv(bet: BetRecord) -> Optional[dict]:
"""
Compute CLV for a single bet.
Returns dict with clv_pct, p_bet, p_close, overround_bet,
overround_close, or None if closing odds are missing.
"""
if bet.closing_odds_american is None or bet.closing_counter_american is None:
return None
# Bet-time implied probabilities
implied_bet = american_to_implied(bet.bet_odds_american)
implied_counter_bet = american_to_implied(bet.counter_odds_american)
overround_bet = implied_bet + implied_counter_bet - 1.0
p_bet, _ = remove_vig_multiplicative(implied_bet, implied_counter_bet)
# Closing implied probabilities
implied_close = american_to_implied(bet.closing_odds_american)
implied_counter_close = american_to_implied(bet.closing_counter_american)
overround_close = implied_close + implied_counter_close - 1.0
p_close, _ = remove_vig_multiplicative(implied_close, implied_counter_close)
# CLV
clv_pct = (p_close - p_bet) / p_bet
return {
"bet_id": bet.bet_id,
"p_bet": p_bet,
"p_close": p_close,
"clv_pct": clv_pct,
"overround_bet": overround_bet,
"overround_close": overround_close,
}
def fetch_closing_odds(
api_key: str,
sport: str,
event_id: str,
bookmaker: str = "pinnacle"
) -> Optional[dict]:
"""
Fetch closing (pre-event) odds from The Odds API historical endpoint.
Args:
api_key: The Odds API key
sport: Sport key (e.g., "americanfootball_nfl")
event_id: The Odds API event ID
bookmaker: Bookmaker key for closing line reference
Returns:
Dict with h2h, spreads, totals closing odds, or None.
"""
url = f"https://api.the-odds-api.com/v4/sports/{sport}/events/{event_id}/odds"
params = {
"apiKey": api_key,
"regions": "us",
"markets": "h2h,spreads,totals",
"oddsFormat": "american",
"bookmakers": bookmaker,
}
resp = requests.get(url, params=params)
if resp.status_code != 200:
return None
data = resp.json()
result = {}
for bookmaker_data in data.get("bookmakers", []):
if bookmaker_data["key"] == bookmaker:
for market in bookmaker_data.get("markets", []):
result[market["key"]] = market["outcomes"]
return result if result else None
class CLVTracker:
"""
Portfolio-level CLV tracking for an autonomous betting agent.
Usage:
tracker = CLVTracker()
tracker.add_bet(bet_record)
tracker.attach_closing_odds(bet_id, closing_american, counter_american)
report = tracker.portfolio_report()
"""
def __init__(self):
self.bets: list[BetRecord] = []
self._bet_index: dict[str, int] = {}
def add_bet(self, bet: BetRecord) -> None:
"""Register a new bet."""
self._bet_index[bet.bet_id] = len(self.bets)
self.bets.append(bet)
def attach_closing_odds(
self,
bet_id: str,
closing_american: int,
closing_counter_american: int,
close_timestamp: Optional[datetime] = None,
) -> None:
"""Attach closing odds to a previously recorded bet."""
idx = self._bet_index.get(bet_id)
if idx is None:
raise KeyError(f"Bet {bet_id} not found")
self.bets[idx].closing_odds_american = closing_american
self.bets[idx].closing_counter_american = closing_counter_american
self.bets[idx].close_timestamp = close_timestamp
def compute_all_clv(self) -> pd.DataFrame:
"""Compute CLV for all bets with closing odds attached."""
rows = []
for bet in self.bets:
clv_result = compute_clv(bet)
if clv_result is not None:
clv_result["sport"] = bet.sport
clv_result["book"] = bet.book
clv_result["market"] = bet.market
clv_result["selection"] = bet.selection
clv_result["stake"] = bet.stake
clv_result["bet_timestamp"] = bet.bet_timestamp
rows.append(clv_result)
return pd.DataFrame(rows)
def portfolio_report(self) -> dict:
"""
Generate aggregate CLV statistics.
Returns dict with mean_clv, median_clv, std_clv, n_bets,
pct_positive_clv, stake_weighted_clv, clv_by_sport, clv_by_market.
"""
df = self.compute_all_clv()
if df.empty:
return {"error": "No bets with closing odds"}
total_stake = df["stake"].sum()
stake_weighted_clv = (df["clv_pct"] * df["stake"]).sum() / total_stake
report = {
"n_bets": len(df),
"mean_clv": df["clv_pct"].mean(),
"median_clv": df["clv_pct"].median(),
"std_clv": df["clv_pct"].std(),
"pct_positive_clv": (df["clv_pct"] > 0).mean(),
"stake_weighted_clv": stake_weighted_clv,
"clv_by_sport": df.groupby("sport")["clv_pct"].mean().to_dict(),
"clv_by_market": df.groupby("market")["clv_pct"].mean().to_dict(),
}
# Statistical significance test: is mean CLV > 0?
n = len(df)
se = df["clv_pct"].std() / np.sqrt(n)
t_stat = df["clv_pct"].mean() / se if se > 0 else 0
report["t_statistic"] = t_stat
report["significant_at_95"] = t_stat > 1.645 # one-tailed
return report
# --- Demonstration with realistic data ---
if __name__ == "__main__":
tracker = CLVTracker()
# Simulated bet portfolio with realistic NFL/NBA lines
sample_bets = [
BetRecord("b001", "ev_nfl_01", "NFL", "bookmaker", "spreads",
"Chiefs -3.5", -108, 100, datetime(2026, 1, 12, 14, 0), 100),
BetRecord("b002", "ev_nba_01", "NBA", "betonline", "h2h",
"Lakers ML", 145, -165, datetime(2026, 1, 12, 17, 30), 50),
BetRecord("b003", "ev_nfl_02", "NFL", "bookmaker", "spreads",
"Eagles -6.5", -112, -108, datetime(2026, 1, 13, 11, 0), 100),
BetRecord("b004", "ev_nba_02", "NBA", "pinnacle", "totals",
"Over 224.5", -105, -115, datetime(2026, 1, 13, 18, 0), 75),
BetRecord("b005", "ev_nfl_03", "NFL", "pinnacle", "h2h",
"49ers ML", -135, 120, datetime(2026, 1, 14, 10, 0), 100),
]
# Closing odds (what the line was at game time)
closing_data = [
("b001", -115, 97), # line moved from -108 to -115 → positive CLV
("b002", 130, -155), # moved from +145 to +130 → positive CLV
("b003", -110, -110), # moved from -112 to -110 → slight positive CLV
("b004", -108, -112), # moved from -105 to -108 → slight positive CLV
("b005", -145, 128), # moved from -135 to -145 → positive CLV
]
for bet in sample_bets:
tracker.add_bet(bet)
for bet_id, close_odds, close_counter in closing_data:
tracker.attach_closing_odds(bet_id, close_odds, close_counter)
# Per-bet CLV
clv_df = tracker.compute_all_clv()
print("Per-Bet CLV Analysis")
print("=" * 70)
for _, row in clv_df.iterrows():
direction = "+" if row["clv_pct"] > 0 else ""
print(
f" {row['selection']:<20} "
f"P_bet={row['p_bet']:.4f} "
f"P_close={row['p_close']:.4f} "
f"CLV={direction}{row['clv_pct']:.2%}"
)
# Portfolio summary
print("\nPortfolio Report")
print("=" * 70)
report = tracker.portfolio_report()
print(f" Bets analyzed: {report['n_bets']}")
print(f" Mean CLV: {report['mean_clv']:+.2%}")
print(f" Median CLV: {report['median_clv']:+.2%}")
print(f" Std Dev CLV: {report['std_clv']:.2%}")
print(f" % Positive CLV: {report['pct_positive_clv']:.0%}")
print(f" Stake-weighted CLV: {report['stake_weighted_clv']:+.2%}")
print(f" t-statistic: {report['t_statistic']:.2f}")
print(f" Significant (95%): {report['significant_at_95']}")
Limitations and Edge Cases
Closing line capture timing. The Odds API snapshots odds at intervals, not continuously. If the last snapshot is 5 minutes before game time and a sharp move happens in the final 2 minutes, your CLV calculation uses stale closing data. For maximum accuracy, agents should capture odds from Pinnacle’s live feed at the exact moment of event start — not from a third-party API with polling delays.
Market-specific distortions. Totals (over/under) markets are less efficient at close than sides (spreads/moneylines) because they attract less sharp money. CLV measured on totals gives a noisier signal. An agent should weight spread CLV more heavily in its model evaluation.
Correlated bets. If an agent bets Chiefs -3.5 and the Under in the same game, those CLV measurements are correlated. Treating them as independent in the portfolio report understates the true standard error. The Correlation and Portfolio Theory guide covers how to account for this in portfolio-level metrics.
Soft-book CLV inflation. An agent betting at Bovada and measuring CLV against Bovada’s own closing line will overestimate edge because Bovada’s lines are not efficient. Always benchmark CLV against Pinnacle or Circa. If neither offers the market, the CLV measurement is unreliable.
Steam moves and false CLV. An agent that copies sharp money (steam chasing) will show positive CLV without having independent predictive ability. The CLV is real — the agent did bet before the line moved — but it reflects borrowed signal, not proprietary edge. When the steam source dries up, the edge disappears. Genuine model-based CLV is more sustainable.
Small sample sizes. A 5-bet sample (like the worked example above) means nothing. CLV requires at minimum 200 bets for directional signal and 500+ bets for reliable measurement. Report confidence intervals, not point estimates, until you hit n=500.
CLV by Agent Strategy Type
| Strategy | Typical CLV | Sustainability | Why |
|---|---|---|---|
| Model-based (proprietary odds) | +2% to +5% | High — edge lasts until market adapts | Agent’s model captures information before the market |
| Steam chasing | +1% to +3% | Medium — depends on signal source | Copies sharp money; real CLV but borrowed edge |
| News/injury reactive | +1% to +4% | Medium — speed-dependent | First-mover advantage on information; decays as markets speed up |
| Arbitrage | ~0% | High — but profit comes from cross-book spread, not CLV | Agent buys both sides; CLV is zero by construction |
| Market making | Negative | N/A — intentional | Market makers provide liquidity and earn the spread; they expect negative CLV |
| Recreational/random | Negative | N/A | No predictive model; closing line is more accurate than random entry |
FAQ
What is closing line value in sports betting?
Closing line value (CLV) measures whether you bet at better odds than the final odds available before an event starts. Formally, CLV% = (Closing_Implied_Prob - Bet_Implied_Prob) / Bet_Implied_Prob. Positive CLV means you consistently get odds the market later determines were too generous — the single most reliable predictor of long-run profitability.
Why is CLV more important than win rate for betting agents?
Win rate is dominated by variance — a profitable bettor can easily have a losing month. CLV converges to true edge within 200-500 bets because it measures the quality of your odds relative to an efficient benchmark (the closing line), not the binary outcome. An agent with +3% CLV is virtually guaranteed profitable long-term, even during losing streaks.
How do you calculate CLV from American odds?
First, convert both your bet odds and the closing odds to no-vig implied probabilities using the multiplicative method. Then compute CLV% = (Closing_No_Vig_Prob - Bet_No_Vig_Prob) / Bet_No_Vig_Prob. For example, betting Lakers -3.5 at -108 when the line closes at -115 gives positive CLV because -108 implied a lower probability than the closing -115.
What CLV percentage indicates a sharp betting edge?
On standard -110 juice lines, consistent CLV of 2% or higher indicates genuine edge. At 2% CLV, expected ROI is approximately 3.8% before vig adjustments. Elite sharp bettors and well-calibrated models achieve 3-5% CLV. Above 5% sustained CLV is rare and typically signals either a very specialized niche or a measurement error.
How does closing line value relate to line movement analysis?
CLV and line movement are two views of the same information flow. Line movement shows how odds change from open to close; CLV measures whether your bets captured value before that movement occurred. An agent that consistently bets before lines move in its direction — tracked via the line movement analysis framework — will show positive CLV. See the Line Movement Analysis guide for the detection algorithms.
What’s Next
CLV tells you whether your model has edge. The next step is understanding why lines move — which sharp actions drive the closing line to its efficient price.
- Next in the series: Line Movement Analysis for Agents breaks down the mechanics of how and why odds change from open to close, and how agents detect actionable moves.
- Build the features: Feature Engineering for Sports Prediction covers the input variables that drive the models producing CLV.
- Size your bets: Once CLV confirms your model has edge, use the Kelly Criterion to determine optimal stake sizing based on your estimated probability versus the market price.
- Track the vig: The AgentBets Vig Index provides daily sportsbook overround data — essential for accurate vig removal in CLV calculations.
- Explore sharp books: The sharp betting hub covers strategy, book selection, and account management for agents operating in sharp markets.
