Overview
The Proppr Team Bot monitors soccer team markets (goals, corners, cards, shots) and sends alerts when bookmaker odds significantly diverge from statistical projections based on recent team performance data.
Purpose
Team Bot analyzes team-level statistics to identify value betting opportunities across dozens of markets. It tracks historical performance across multiple timeframes (last 5, last 10, season) and compares bookmaker odds against projected probabilities to find edges.
Markets Covered
Team Bot supports 60+ team markets across multiple categories:
Goal Markets
Total Goals (Match totals)
Team Total Goals (Home/Away)
First Team to Score
Alternative Goal Lines
Corner Markets
Total Corners
Team Corners (Home/Away)
Corner Spread/Handicap
Most Corners
First Team To Take Corner
Card Markets
Total Cards (Yellow + Red)
Team Cards (Home/Away)
Bookings Totals
Bookings Spread
First Team To Be Booked
Shot Markets
Total Shots
Total Shots On Target
Team Shots (Home/Away)
Shots Outside Box
Headed Shots
Statistical Markets
Fouls Committed
Tackles
Offsides
Throw-ins
Free Kicks
Goal Kicks
Goalkeeper Saves
Set Piece Markets (From Corner, Free Kick, Fast Break)
See STAT_FIELD_MAP in /home/daytona/workspace/source/PropprTeamBot/core/bot/team_bot.py:553-858 for complete market mappings.
Alert Criteria
Alerts are triggered when:
EV Threshold Met : Expected Value exceeds user-configured minimum (default 5%)
Odds Range : Bookmaker odds fall within user’s min/max range (default 1.50-10.0)
Chance Range : Projected probability meets minimum threshold (default 10%)
Line Range : Market line falls within configured min/max
Data Availability : Team has sufficient historical data for selected scope
League Coverage : League has market data in known_leagues_fm collection
Calculation Example
# From team_bot.py:3000-3100
# Calculate expected value
fair_odds = 1 / (predicted_probability / 100 )
ev_pct = ((bookmaker_odds / fair_odds) - 1 ) * 100
# Example:
# Predicted probability: 60% (0.60)
# Fair odds: 1/0.60 = 1.67
# Bookmaker odds: 2.00
# EV: ((2.00/1.67) - 1) * 100 = 19.8%
User Commands
Core Commands
/start
/settings
/fixture [team name]
/value [fixture]
/stats [team]
/today
/live
/track
/mystats
/help
Initialize bot and show welcome message with setup wizard
Configuration Options
Market Settings
Each market can be independently configured:
# Per-market configuration structure
market_settings = {
"Total Goals" : {
"enabled" : True ,
"min_odds" : 1.50 ,
"max_odds" : 6.00 ,
"min_ev_pct" : 5.0 ,
"min_line" : 0.5 ,
"max_line" : 5.5 ,
"min_chance_pct" : 10.0 ,
"market_direction" : "both" , # over, under, or both
"bookmaker_settings" : {}
}
}
Scope Selection
Choose data timeframe for statistical analysis:
Last 5 : Recent form (last 5 matches)
Last 10 : Medium-term form (last 10 matches)
Season : Full season statistics
# From team_bot.py:255-315
def get_scoped_field_name ( base_field_name , scope , time_period = None ):
"""
Builds scoped field names for predicted_lines lookups.
Examples:
- home_averages + last10 -> home_averages_last10
- predicted_lines + season -> predicted_lines_season
- 1st_half_home_averages + last5 -> 1st_half_home_averages_last5
"""
Bookmaker Filtering
Enable/disable specific bookmakers per market:
bookmaker_settings = {
"Bet365" : { "enabled" : True },
"William Hill" : { "enabled" : True },
"Sky Bet" : { "enabled" : False },
"Kambi" : { "enabled" : True }
}
Optimal+ Presets
Founder and Club Legend tiers have access to Optimal+ Presets - pre-configured settings optimized from 14 days of historical performance data.
Presets include:
Route One : Lowest volume, highest ROI (~5 bets/week)
Shoot On Sight : Low-medium volume, high ROI (~15 bets/week)
Work The Space : Medium volume, positive ROI (~25 bets/week)
Gegenpress : Med-high volume, positive ROI (~40 bets/week)
Tiki-Taka : High volume, positive ROI (~60 bets/week)
Presets auto-update every 48 hours based on recent performance.
Real Code Examples
Alert Processing Pipeline
# From team_bot.py:2000-2100
def process_fixture_for_alerts ( fixture_doc , user_settings ):
"""
Main alert processing flow:
1. Load fixture and team projections from predicted_lines
2. Fetch bookmaker odds from team_odds collection
3. Calculate EV for each market/line combination
4. Filter by user settings (thresholds, bookmakers, etc)
5. Format and queue alerts for delivery
"""
fixture_id = fixture_doc.get( 'id' )
scope = user_settings.get( 'preference_scope' , 'last10' )
# Get team projections with selected scope
home_team_id = fixture_doc.get( 'home_team_id' )
away_team_id = fixture_doc.get( 'away_team_id' )
home_proj = get_team_projection(fixture_id, home_team_id, scope)
away_proj = get_team_projection(fixture_id, away_team_id, scope)
# Calculate EVs for all markets
alerts = []
for market_name in user_settings.get( 'enabled_markets' , []):
market_alerts = calculate_market_ev(
fixture_doc, home_proj, away_proj,
market_name, user_settings
)
alerts.extend(market_alerts)
return alerts
EV Calculation
# From team_bot.py:3000-3150
def calculate_team_market_ev ( predicted_value , odds_data , market_settings ):
"""
Calculate Expected Value for a team market
Args:
predicted_value: Statistical projection (e.g., 2.5 goals)
odds_data: Bookmaker odds for over/under lines
market_settings: User thresholds and preferences
Returns:
List of value alerts meeting EV threshold
"""
alerts = []
for line, odds in odds_data.items():
# Calculate probability based on predicted value and line
over_prob = calculate_over_probability(predicted_value, line)
under_prob = 1 - over_prob
# Calculate fair odds
fair_over = 1 / over_prob if over_prob > 0 else None
fair_under = 1 / under_prob if under_prob > 0 else None
# Check over bet
if odds.get( 'over' ) and fair_over:
ev_over = ((odds[ 'over' ] / fair_over) - 1 ) * 100
if ev_over >= market_settings[ 'min_ev_pct' ]:
alerts.append({
'side' : 'over' ,
'line' : line,
'odds' : odds[ 'over' ],
'ev_pct' : ev_over,
'fair_odds' : fair_over,
'probability' : over_prob * 100
})
# Check under bet
if odds.get( 'under' ) and fair_under:
ev_under = ((odds[ 'under' ] / fair_under) - 1 ) * 100
if ev_under >= market_settings[ 'min_ev_pct' ]:
alerts.append({
'side' : 'under' ,
'line' : line,
'odds' : odds[ 'under' ],
'ev_pct' : ev_under,
'fair_odds' : fair_under,
'probability' : under_prob * 100
})
return alerts
Bookmaker Normalization
# From team_bot.py:191-224
def normalize_bookmaker_name ( bookmaker_name ):
"""
Normalize bookmaker names by removing suffixes and handling spelling variations.
Examples:
'Bet365 (no latency)' -> 'Bet365'
'Skybet' -> 'Sky Bet'
'Polymarket' -> 'Polymarket' # Now distinct from William Hill
"""
if not bookmaker_name:
return bookmaker_name
# Remove '(no latency)' suffix
normalized = bookmaker_name.replace( ' (no latency)' , '' ).strip()
# Standardize variations
BOOKMAKER_NAME_MAP = {
"Skybet" : "Sky Bet" ,
}
return BOOKMAKER_NAME_MAP .get(normalized, normalized)
Database Collections
Team Bot interacts with several MongoDB collections:
predicted_lines : Team statistical projections and predictions
team_odds : Real-time bookmaker odds for team markets
fixtures_fm : Fixture schedule and metadata
known_leagues_fm : Supported leagues with market coverage
team_user_settings : User preferences and configurations
sent_team_alerts : Alert deduplication tracking
tracked_bets : User bet tracking and grading
# From team_bot.py:4000-4100
class BetTrackingSystem :
"""
Unified bet tracking across Team and Player bots.
Automatically grades bets when results are available.
"""
def track_bet ( self , user_id , alert_data , stake , odds ):
"""Record a new tracked bet"""
bet_doc = {
'user_id' : user_id,
'fixture_id' : alert_data[ 'fixture_id' ],
'market_name' : alert_data[ 'market_name' ],
'bet_side' : alert_data[ 'bet_side' ],
'line' : alert_data.get( 'line' ),
'odds' : odds,
'stake' : stake,
'status' : 'pending' ,
'placed_at' : datetime.now(timezone.utc),
'graded' : False
}
self .tracked_bets.insert_one(bet_doc)
def grade_pending_bets ( self ):
"""Grade all pending bets with available results"""
pending = self .tracked_bets.find({ 'graded' : False })
for bet in pending:
result = self .fetch_result(bet[ 'fixture_id' ])
if result:
outcome = self .determine_outcome(bet, result)
self .tracked_bets.update_one(
{ '_id' : bet[ '_id' ]},
{ '$set' : {
'graded' : True ,
'outcome' : outcome,
'result' : result,
'graded_at' : datetime.now(timezone.utc)
}}
)
Technical Architecture
Main Components
PropprTeamBot/
├── core/
│ ├── bot/
│ │ └── team_bot.py # Main bot logic (5000+ lines)
│ └── models/ # Data models
├── services/
│ ├── alerts/ # Alert generation
│ ├── odds/ # Odds fetching
│ └── telegram/
│ ├── handlers/
│ │ └── preset_handlers.py # Optimal+ Presets
│ └── lineup_monitor.py # Live lineup tracking
├── config/
│ └── constants.py # Configuration
└── utils/
└── league_mapping.py # League normalization
Key Features
Multi-scope Analysis : Compare recent form vs season trends
Preset Optimization : Auto-tuned settings from historical data
Live Statistics : In-game stat tracking with FotMob integration
Bet Tracking : Automatic grading with performance analytics
Smart Batching : Group related alerts to reduce spam
Cache Management : Intelligent caching for database optimization
Source Code View the complete Team Bot implementation
Player Bot Player prop markets and statistics
EV Bot Sharp odds-based value detection