The sharpest bettors don't just find +EV plays—they systematically extract edges where sharp books disagree with recreational sportsbooks. Building a sharp book consensus edge detection system in Python lets you automate this process, identifying lines where Pinnacle, Circa, and other sharp operators cluster around one price while DraftKings, FanDuel, or BetMGM stray into profitable territory.
This isn't about finding the highest odds. It's about finding the lines that sharp money hasn't corrected yet—the inefficiencies that exist in the gap between sharp consensus and recreational book pricing.
Sharp vs Recreational Book Classification
Sharp sportsbooks earn their reputation through low margins, high limits, and quick line movement in response to informed action. Recreational books focus on customer acquisition, promotional offers, and often maintain wider spreads with slower adjustments.
Defining Sharp Book Consensus
For this edge detection system, we'll classify books into two tiers:
Tier 1 Sharp Books:
- Pinnacle (lowest margins, highest limits)
- Circa Sports (sharp Vegas operator)
- CRIS/Heritage (offshore sharp favorites)
Tier 2 Sharp-Adjacent:
- Bet365 (quick to move, but recreational-focused)
- William Hill (traditional sharp European operator)
Recreational Books:
- DraftKings, FanDuel, BetMGM (customer acquisition focused)
- Caesars, PointsBet, BetRivers (promotional heavy)
The consensus emerges when multiple Tier 1 books cluster around similar implied probabilities, creating a benchmark against which we measure recreational book deviations.
Python Edge Detection Framework
Here's the core framework for detecting consensus edges using the MoneyLine API:
import requests
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List, Tuple
class SharpConsensusDetector:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://mlapi.bet"
self.sharp_books = ['pinnacle', 'circa', 'heritage']
self.recreational_books = ['draftkings', 'fanduel', 'betmgm', 'caesars']
def get_market_odds(self, sport: str, market_type: str = 'moneyline') -> Dict:
"""Fetch odds data from MoneyLine API"""
endpoint = f"{self.base_url}/v1/odds"
params = {
'sport': sport,
'market': market_type,
'api_key': self.api_key
}
response = requests.get(endpoint, params=params)
return response.json()
def american_to_implied(self, odds: int) -> float:
"""Convert American odds to implied probability"""
if odds > 0:
return 100 / (odds + 100)
else:
return abs(odds) / (abs(odds) + 100)
def calculate_sharp_consensus(self, sharp_odds: List[int]) -> Tuple[float, float]:
"""Calculate consensus implied probability and standard deviation"""
if not sharp_odds:
return None, None
implied_probs = [self.american_to_implied(odds) for odds in sharp_odds]
consensus = np.mean(implied_probs)
std_dev = np.std(implied_probs)
return consensus, std_dev
def detect_edge_opportunities(self, event_data: Dict, min_edge: float = 0.02) -> List[Dict]:
"""Find recreational books offering odds with edge vs sharp consensus"""
opportunities = []
for event in event_data.get('events', []):
for market in event.get('markets', []):
sharp_odds = []
rec_odds = {}
# Separate sharp and recreational book odds
for book, odds_data in market.get('odds', {}).items():
if book in self.sharp_books:
sharp_odds.append(odds_data.get('american'))
elif book in self.recreational_books:
rec_odds[book] = odds_data.get('american')
# Calculate sharp consensus
consensus_prob, consensus_std = self.calculate_sharp_consensus(sharp_odds)
if consensus_prob is None or len(sharp_odds) < 2:
continue
# Check recreational books for edges
for book, odds in rec_odds.items():
if odds is None:
continue
book_implied = self.american_to_implied(odds)
edge = consensus_prob - book_implied
if edge >= min_edge:
opportunities.append({
'event': event.get('title'),
'market': market.get('name'),
'book': book,
'odds': odds,
'book_implied': book_implied,
'sharp_consensus': consensus_prob,
'consensus_std': consensus_std,
'edge': edge,
'sharp_books_count': len(sharp_odds),
'timestamp': datetime.now().isoformat()
})
return sorted(opportunities, key=lambda x: x['edge'], reverse=True)
# Usage example
detector = SharpConsensusDetector('your_api_key')
nfl_odds = detector.get_market_odds('nfl')
edges = detector.detect_edge_opportunities(nfl_odds, min_edge=0.025)
for edge in edges[:5]: # Top 5 opportunities
print(f"{edge['book']}: {edge['event']} {edge['market']}")
print(f" Odds: {edge['odds']} (Implied: {edge['book_implied']:.1%})")
print(f" Sharp Consensus: {edge['sharp_consensus']:.1%}")
print(f" Edge: {edge['edge']:.2%}")
print()
Consensus Validation Methods
Not all consensus signals carry equal weight. A tight clustering of sharp books around similar implied probabilities provides stronger signal than loose agreement with high variance.
Statistical Significance Thresholds
def validate_consensus_strength(self, consensus_prob: float, std_dev: float,
sharp_count: int) -> Dict[str, bool]:
"""Validate statistical significance of sharp consensus"""
# Coefficient of variation threshold
cv_threshold = 0.05 # 5% coefficient of variation
cv = std_dev / consensus_prob if consensus_prob > 0 else float('inf')
# Minimum sharp book requirement
min_books = 3
# Z-score for consensus tightness (lower is tighter)
z_score = std_dev * np.sqrt(sharp_count)
validation = {
'sufficient_books': sharp_count >= min_books,
'tight_clustering': cv <= cv_threshold,
'low_variance': std_dev <= 0.03, # 3% standard deviation max
'z_score': z_score,
'strength_rating': 'strong' if (cv <= cv_threshold and
sharp_count >= min_books and
std_dev <= 0.03) else 'weak'
}
return validation
Volume-Weighted Consensus
For markets with available volume data, weight the consensus by each sharp book's betting volume rather than treating all opinions equally:
def volume_weighted_consensus(self, sharp_data: List[Dict]) -> float:
"""Calculate volume-weighted consensus from sharp books"""
total_volume = sum(book.get('volume', 0) for book in sharp_data)
if total_volume == 0:
return None
weighted_sum = sum(
self.american_to_implied(book['odds']) * book.get('volume', 0)
for book in sharp_data
)
return weighted_sum / total_volume
Live Edge Monitoring System
Static edge detection only captures snapshots. The real profit comes from monitoring edges as they develop and disappear in real-time:
class LiveEdgeMonitor:
def __init__(self, detector: SharpConsensusDetector, alert_threshold: float = 0.03):
self.detector = detector
self.alert_threshold = alert_threshold
self.historical_edges = {}
def monitor_sport(self, sport: str, check_interval: int = 60):
"""Continuously monitor sport for emerging edges"""
while True:
try:
odds_data = self.detector.get_market_odds(sport)
current_edges = self.detector.detect_edge_opportunities(
odds_data, min_edge=self.alert_threshold
)
# Track edge movement
for edge in current_edges:
key = f"{edge['book']}_{edge['event']}_{edge['market']}"
if key in self.historical_edges:
prev_edge = self.historical_edges[key]
edge_change = edge['edge'] - prev_edge['edge']
if edge_change > 0.01: # Edge increased by 1%+
self.send_alert(edge, 'INCREASING')
elif edge['edge'] < 0.015: # Edge dropping below threshold
self.send_alert(edge, 'DISAPPEARING')
else:
self.send_alert(edge, 'NEW')
self.historical_edges[key] = edge
# Clean up old edges
cutoff_time = datetime.now() - timedelta(hours=2)
self.historical_edges = {
k: v for k, v in self.historical_edges.items()
if datetime.fromisoformat(v['timestamp']) > cutoff_time
}
time.sleep(check_interval)
except Exception as e:
print(f"Monitoring error: {e}")
time.sleep(check_interval * 2) # Back off on errors
Backtesting Historical Performance
Before deploying capital, validate your consensus methodology against historical data using the MoneyLine API's historical endpoints:
def backtest_consensus_strategy(self, sport: str, start_date: str,
end_date: str, min_edge: float = 0.02) -> Dict:
"""Backtest sharp consensus edge detection strategy"""
# Fetch historical data
endpoint = f"{self.base_url}/v1/odds/historical"
params = {
'sport': sport,
'start_date': start_date,
'end_date': end_date,
'api_key': self.api_key
}
historical_data = requests.get(endpoint, params=params).json()
total_bets = 0
winning_bets = 0
total_profit = 0
edge_accuracy = []
for date_data in historical_data.get('dates', []):
edges = self.detect_edge_opportunities(date_data, min_edge=min_edge)
for edge in edges:
total_bets += 1
# Simulate outcome based on sharp consensus probability
outcome = np.random.random() < edge['sharp_consensus']
if outcome:
winning_bets += 1
payout = self.calculate_payout(edge['odds'])
total_profit += payout - 1 # Net profit
else:
total_profit -= 1 # Loss
# Track edge prediction accuracy
edge_accuracy.append(edge['edge'])
win_rate = winning_bets / total_bets if total_bets > 0 else 0
avg_edge = np.mean(edge_accuracy) if edge_accuracy else 0
return {
'total_bets': total_bets,
'win_rate': win_rate,
'total_profit': total_profit,
'roi': total_profit / total_bets if total_bets > 0 else 0,
'average_edge': avg_edge,
'expected_roi': avg_edge # Theoretical ROI should match average edge
}
The backtest should show your realized ROI converging toward your average detected edge over sufficient sample sizes. Significant deviations indicate flaws in consensus calculation or book classification.
Production Deployment Considerations
Moving from research to production requires handling API rate limits, implementing proper error handling, and managing state across system restarts. The MoneyLine API build documentation covers these operational concerns in detail.
Store detected edges in a database with proper indexing on timestamp, sport, and book fields for efficient querying. Consider implementing a message queue for real-time alerts to avoid blocking the main detection loop.
For systematic betting, integrate with your position sizing algorithm and bankroll management system. Never bet more than your edge justifies, even when consensus signals appear strong.
FAQ
How many sharp books do I need for reliable consensus?
At minimum, three sharp books provide sufficient consensus signal. Two books can produce false signals when they disagree. More than five sharp books rarely add significant value unless you're working with obscure markets where fewer books offer lines.
What edge threshold should I set for alerts?
Start with 2.5% minimum edge for initial testing. This filters out noise while capturing meaningful opportunities. As you validate your system's accuracy, you can lower the threshold to 1.5-2% for more frequent alerts, but be prepared for increased false positives.
How quickly do consensus edges disappear?
In major markets like NFL spreads, edges typically disappear within 5-15 minutes as sharp money moves lines. Niche markets or props may persist for hours. Your monitoring frequency should match the market's typical movement speed.
Should I weight different sharp books differently?
Yes, but carefully. Pinnacle carries the most weight due to highest limits and fastest sharp money response. Circa and Heritage provide valuable confirmation. Avoid over-weighting any single book—consensus requires multiple independent opinions.
Can this strategy work for live betting?
Live betting edges appear and disappear extremely quickly, often within seconds. The API latency plus your processing time may cause you to miss opportunities. Focus on pre-game markets where you have more time to act on detected edges.