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 TypeExamplesCLV ValidityWhy
SharpPinnacle, Circa, BookmakerValid benchmarkLines are efficient — they aggregate professional money
Mid-tierBetOnline, HeritagePartially validLines track sharp books with 30-60 min lag
SoftDraftKings, FanDuel, BetMGMInvalid benchmarkLines 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

StrategyTypical CLVSustainabilityWhy
Model-based (proprietary odds)+2% to +5%High — edge lasts until market adaptsAgent’s model captures information before the market
Steam chasing+1% to +3%Medium — depends on signal sourceCopies sharp money; real CLV but borrowed edge
News/injury reactive+1% to +4%Medium — speed-dependentFirst-mover advantage on information; decays as markets speed up
Arbitrage~0%High — but profit comes from cross-book spread, not CLVAgent buys both sides; CLV is zero by construction
Market makingNegativeN/A — intentionalMarket makers provide liquidity and earn the spread; they expect negative CLV
Recreational/randomNegativeN/ANo 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.