When 75% of bets land on Team A but the line moves toward Team B, something is happening behind the scenes. The public is pushing one direction and the book is moving the other way. That’s reverse line movement — one of the strongest observable indicators of sharp action in sports betting. It means the sportsbook is reacting to money that matters more than the ticket count, and you can detect it programmatically.

This guide builds an RLM detector from scratch. You’ll understand the theory, set up the data pipeline, implement the detection algorithm in Python, and integrate RLM signals into your betting strategy.


What Is Reverse Line Movement?

Normal Line Movement

In a functioning market, public money pushes lines toward the popular side. If 70% of bets come in on the Eagles -3, the sportsbook adjusts the line to Eagles -3.5 or -4 to balance their liability. More bets on one side means the book moves the number to attract action on the other side. This is expected, mechanical, and boring.

When the Line Moves Backward

Reverse line movement breaks that pattern. The public is heavy on the Eagles -3, but instead of the line moving to Eagles -3.5, it drops to Eagles -2.5. The book moved toward the side getting fewer tickets.

Why would a sportsbook do this? Because not all money is created equal. A thousand $50 public bets on the Eagles ($50,000 total) can be completely offset by a handful of sharp bettors wagering $200,000 on the opponent. The book doesn’t care about ticket count — it cares about dollar exposure, and it weights sharp dollars far more heavily than public dollars because sharps have a demonstrated track record of beating the closing line.

When you see the line move against the public, the book is telling you: “Smart money is on the other side, and we trust it more than the crowd.”

A Concrete Example

NFL Week 8. Chiefs -6.5 opens on Sunday morning. By noon, 73% of tickets are on the Chiefs — the public loves a dominant home favorite. Normal market mechanics would push the line toward Chiefs -7 or -7.5.

Instead, the line drops to Chiefs -6. Then Chiefs -5.5.

What happened: a syndicate group identified value on the underdog and placed $400,000 across multiple books. The books adjusted to the sharp side despite the public ticket imbalance. The line moved opposite to where public volume should have pushed it. That’s textbook RLM.


Data Requirements

Building an RLM detector requires two independent data streams, and neither is trivial.

Line Movement Data

You need historical odds snapshots — the line for each market at regular time intervals. The Odds API provides multi-book odds via REST. Poll every 10-15 minutes for standard monitoring, increasing to every 2-5 minutes as game time approaches. Store every snapshot with a timestamp so you can reconstruct the full trajectory of a line.

The critical fields per snapshot: event ID, book, market type, side, line/odds value, and timestamp. Without timestamped snapshots, you can’t measure movement — you only know the current number, not where it came from.

Public Betting Percentages

This is the harder dataset. You need to know what percentage of bets (or dollars) are on each side of a market. Sources:

  • Action Network — The most widely used source. Provides ticket percentages and money percentages. API access is available on paid tiers.
  • VegasInsider — Publishes consensus betting percentages for major sports. Free but requires scraping.
  • DonBest — Professional-grade data used by sportsbooks themselves. Expensive but accurate.

A critical caveat: no source has perfect public betting data. These percentages are estimates based on samples from reporting books. The exact numbers differ across providers. For RLM detection, you don’t need decimal precision — you need directional accuracy. If Action Network says 72% on the Chiefs and VegasInsider says 68%, both agree the public is heavily on the Chiefs. That’s good enough.

What “Good Enough” Looks Like

Your RLM detector needs the public side to be clearly identifiable (above 60%) and the line movement direction to be unambiguous (at least 0.5 points opposite). If the public split is 52/48 or the line moved by 0.1 points, there’s no meaningful signal. Filter aggressively for clarity.


Detection Algorithm

Core Logic

The fundamental check is simple:

if public_pct(Side A) > threshold AND line_moved_toward(Side B):
    flag as RLM

If the public is heavily on one side and the line moved toward the other side, something is overriding the public pressure. That something is sharp money.

Parameters

Three parameters control your detector’s sensitivity:

ParameterDescriptionRecommended Starting Value
public_thresholdMinimum public percentage on one side to consider it “the public side”65%
min_movementMinimum line movement (in points) against the public side0.5 points
time_windowPeriod over which to measure line movement4–12 hours

Start with these defaults and adjust based on your sport and data quality. NFL markets are the most RLM-friendly because they have the deepest public betting data and the clearest line movement patterns. NBA and MLB work but require tighter filters.

Strength Scoring

Not all RLM signals are equal. A 0.5-point move against 66% public action is a weaker signal than a 2-point move against 80% public action. Score your RLM signals by combining the two dimensions:

rlm_strength = (public_pct - 0.50) * abs(line_movement)

A public percentage of 75% and a 1.5-point move yields a strength score of 0.25 * 1.5 = 0.375. A public percentage of 65% and a 0.5-point move yields 0.15 * 0.5 = 0.075. The first signal is five times stronger. Use this score to prioritize which RLM alerts deserve action.

False Positive Management

RLM detection is prone to false positives from a few sources:

  • Bad public data: If your public percentage source is wrong about which side the public is on, your “RLM” is just normal line movement. Cross-reference multiple sources when possible.
  • Line corrections: Sometimes a line moves because the opening number was simply wrong, not because of sharp action. If a line opened at an outlier number and corrected toward market consensus, that’s not RLM.
  • Stale data: If your public betting data updates less frequently than your odds data, you can get phantom RLM — the public side may have shifted since the last update.

Log every alert and track whether the line continued moving in the RLM direction through the close. Aim for 55%+ directional accuracy before deploying capital on RLM signals.


Building the RLM Detector

Here’s the RLMDetector class. It ingests line snapshots and public betting data, then identifies markets where the line has moved against the public.

from dataclasses import dataclass
from datetime import datetime, timedelta
from collections import defaultdict


@dataclass
class LineSnapshot:
    event_id: str
    market: str
    side: str
    line: float
    timestamp: datetime


@dataclass
class PublicBetting:
    event_id: str
    market: str
    side_a: str
    side_b: str
    pct_a: float  # 0.0–1.0
    updated_at: datetime


@dataclass
class RLMSignal:
    event_id: str
    market: str
    public_side: str
    sharp_side: str
    public_pct: float
    line_movement: float
    strength: float
    detected_at: datetime


class RLMDetector:
    def __init__(
        self,
        public_threshold: float = 0.65,
        min_movement: float = 0.5,
        time_window_hours: int = 8,
    ):
        self.public_threshold = public_threshold
        self.min_movement = min_movement
        self.time_window = timedelta(hours=time_window_hours)
        self.snapshots: dict[str, list[LineSnapshot]] = defaultdict(list)
        self.public_data: dict[str, PublicBetting] = {}

    def _key(self, event_id: str, market: str, side: str) -> str:
        return f"{event_id}:{market}:{side}"

    def ingest_line(self, snap: LineSnapshot) -> None:
        key = self._key(snap.event_id, snap.market, snap.side)
        self.snapshots[key].append(snap)

    def ingest_public(self, data: PublicBetting) -> None:
        pub_key = f"{data.event_id}:{data.market}"
        self.public_data[pub_key] = data

    def _net_movement(self, key: str, now: datetime) -> float | None:
        cutoff = now - self.time_window
        history = [s for s in self.snapshots[key] if s.timestamp >= cutoff]
        if len(history) < 2:
            return None
        history.sort(key=lambda s: s.timestamp)
        return history[-1].line - history[0].line

    def detect(self, event_id: str, market: str) -> RLMSignal | None:
        pub_key = f"{event_id}:{market}"
        pub = self.public_data.get(pub_key)
        if pub is None:
            return None

        if pub.pct_a >= self.public_threshold:
            public_side, sharp_side = pub.side_a, pub.side_b
            public_pct = pub.pct_a
        elif (1 - pub.pct_a) >= self.public_threshold:
            public_side, sharp_side = pub.side_b, pub.side_a
            public_pct = 1 - pub.pct_a
        else:
            return None

        now = datetime.utcnow()
        key_public = self._key(event_id, market, public_side)
        movement = self._net_movement(key_public, now)

        if movement is None:
            return None

        # RLM: public is on this side, but the line is moving
        # to make this side more attractive (line dropping for
        # a spread favorite means the book is shading away)
        if movement > 0:
            return None
        if abs(movement) < self.min_movement:
            return None

        strength = (public_pct - 0.50) * abs(movement)

        return RLMSignal(
            event_id=event_id,
            market=market,
            public_side=public_side,
            sharp_side=sharp_side,
            public_pct=round(public_pct, 3),
            line_movement=round(movement, 2),
            strength=round(strength, 4),
            detected_at=now,
        )

    def scan_all(self) -> list[RLMSignal]:
        """Run detection across all markets with public data."""
        signals = []
        for pub_key, pub in self.public_data.items():
            event_id, market = pub_key.split(":", 1)
            signal = self.detect(event_id, market)
            if signal:
                signals.append(signal)
        signals.sort(key=lambda s: s.strength, reverse=True)
        return signals

Wiring It Together

Feed the detector with odds snapshots from The Odds API and public betting data from your chosen source:

import time
import requests
from datetime import datetime

API_KEY = "your-odds-api-key"
SPORT = "americanfootball_nfl"
POLL_INTERVAL = 600  # 10 minutes

detector = RLMDetector(public_threshold=0.65, min_movement=0.5, time_window_hours=8)

while True:
    resp = requests.get(
        "https://api.the-odds-api.com/v4/sports/{}/odds".format(SPORT),
        params={"apiKey": API_KEY, "regions": "us,us2", "markets": "spreads"},
    )
    for event in resp.json():
        for bookmaker in event.get("bookmakers", []):
            for market in bookmaker.get("markets", []):
                for outcome in market.get("outcomes", []):
                    snap = LineSnapshot(
                        event_id=event["id"],
                        market=market["key"],
                        side=outcome["name"],
                        line=outcome.get("point", outcome.get("price", 0)),
                        timestamp=datetime.utcnow(),
                    )
                    detector.ingest_line(snap)

    # Ingest public betting data from your source here
    # detector.ingest_public(PublicBetting(...))

    signals = detector.scan_all()
    for sig in signals:
        print(
            f"RLM: {sig.sharp_side} (sharp) vs {sig.public_side} "
            f"({sig.public_pct:.0%} public) | "
            f"movement: {sig.line_movement} pts | strength: {sig.strength:.3f}"
        )

    time.sleep(POLL_INTERVAL)

Replace the public data ingestion comment with your actual data source — Action Network API call, VegasInsider scraper, or whatever feeds your pipeline.


Using RLM in Your Strategy

RLM as a Confirmation Signal

RLM is most powerful when it confirms other sharp indicators. If a steam move fires on the same side your RLM detector flagged, that’s a high-confidence signal — both real-time coordinated action and sustained line movement are pointing the same direction. Stack it with CLV analysis: if you’ve historically seen positive closing line value on RLM-flagged bets, your detector is calibrated correctly.

Don’t treat RLM as a standalone trigger for automated execution. Use it as a filter or confirmation layer in a composite signal model. A bet that passes RLM + steam + positive expected value is far more reliable than RLM alone.

Fading the Public

The core premise of RLM is that the public is wrong and the sharps are right. This holds statistically in markets with deep liquidity and sophisticated sharp action — mainly NFL and NCAA football. The reasoning: public bettors overweight narratives, recency bias, and media coverage. Sharps rely on models, closing line analysis, and information the public hasn’t processed.

Fading the public works best when the public imbalance is extreme (70%+ on one side) and the line movement is clearly opposite. At moderate splits (55-60%), there’s not enough separation to distinguish RLM from normal market noise.

Sport-Specific Patterns

NFL is the gold standard for RLM detection. Massive public handle creates clear ticket imbalances, and the week-long betting window gives lines time to move visibly. Sunday morning RLM (lines moving against the public between Saturday night and Sunday morning) is the classic pattern.

NBA produces usable RLM signals, particularly for high-profile matchups where public bias is strongest. The shorter betting window (lines posted the day before) compresses the detection period.

MLB is noisy for RLM. Public betting percentages are less reliable because the daily volume is spread across 15 games, and line movement in moneyline markets is harder to interpret than spread movement. Filter aggressively if you apply RLM to baseball.

Timing

In NFL markets, the most meaningful RLM occurs between Friday evening and Sunday morning. That’s when the sharpest money enters — syndicate groups wait for the market to settle during the week, then hit lines once they’ve identified value against the public consensus. Early-week line movement is more likely driven by look-ahead adjustments and opening-line corrections than genuine RLM.

For same-day sports (NBA, NHL), look for RLM in the 2-6 hours before game time. That’s the window where sharp money concentrates, and public betting data has had time to stabilize.


What’s Next

RLM detection is one piece of the sharp signal toolkit. Combine it with these related guides for a complete edge detection pipeline: