The Grading System automatically grades tracked bets by fetching live fixture results from FotMob, comparing actual stats to bet thresholds, and calculating profit/loss.
Supports OVER bets grading during live matches and full grading when fixtures finish.
Batching fixtures reduces API calls (FotMob has no multi-fixture endpoint, but we can cache smartly).
Grouping
def group_alerts_by_fixture(self, alerts: List[Dict]) -> Dict[str, List[Dict]]: grouped = {} for alert in alerts: fixture_id = str(alert.get('fixture_id', '')) if not fixture_id or fixture_id == 'None': fixture_id = str(alert.get('fixture_info', {}).get('match_id', '')) if fixture_id: if fixture_id not in grouped: grouped[fixture_id] = [] grouped[fixture_id].append(alert) return grouped
OVER bets: Graded during live matches (can win early) UNDER bets: Only graded when match finishes HANDICAP bets: Only graded when match finishes
Filtering Logic
def filter_gradeable_alerts(self, alerts: List[Dict], is_inplay: bool, is_finished: bool) -> List[Dict]: gradeable = [] for alert in alerts: market_direction = alert.get('market_direction', '').lower() # OVER bets: Grade if in-play OR finished if market_direction == 'over' and (is_inplay or is_finished): gradeable.append(alert) # UNDER bets: Grade only if finished elif market_direction == 'under' and is_finished: gradeable.append(alert) # HANDICAP bets (Home/Away): Grade only if finished elif market_direction in ['home', 'away'] and is_finished: gradeable.append(alert) return gradeable
def extract_player_stat_value(self, fixture_id, player_id, stat_type, fixture_data): """ Extract player stat from FotMob lineup data. """ # Get player stats from FotMob service player_stats = self.fotmob.get_player_stats(fixture_data, str(player_id)) if not player_stats: return 0 # Map stat type to FotMob key stat_key = STAT_ID_MAPPING.get(type_id, stat_type) # Special handling for binary stats if stat_key == 'first_goalscorer': first_scorer_id = self.fotmob.get_first_goalscorer_id(fixture_data) return 1 if str(first_scorer_id) == str(player_id) else 0 # Get value value = player_stats.get(stat_key, 0) return float(value)
def check_player_lineup_status(self, fixture_id, player_id, fixture_data): """ Check if player started or was on bench. Returns: (should_refund, reason) """ lineup = fixture_data.get('content', {}).get('lineup', {}) if not lineup: return False, "unknown_lineup" # Check both teams for side in ['homeTeam', 'awayTeam']: # Check starters for player in lineup[side].get('starters', []): if str(player.get('id')) == str(player_id): return False, "started" # Don't refund # Check bench for player in lineup[side].get('bench', []): if str(player.get('id')) == str(player_id): return True, "bench" # Refund (didn't start) # Player not found in lineup at all return True, "not_in_lineup"
Problem: Need to verify ALL graded alerts are in sheet (not just new ones).Solution: Rotation-based syncing:
Rotation Sync
def run_sync_logic(self): """ Rotate through graded alerts using 'last_sync_check' timestamp. """ query = { 'result_tracking.result_status': {'$nin': ['pending', None, '']}, 'result_tracking.stat_type': {'$ne': 'untrackable'}, **self.config['filter'] } # Sort by last_sync_check (oldest first) # Alerts never synced (null) come first graded_batch = list(self.collection.find(query) .sort('last_sync_check', ASCENDING) .limit(self.sync_batch_size) ) if not graded_batch: return # Sync to sheet (deduplicates against current sheet content) synced_count = self.sheets_sync.sync_alerts(graded_batch) # Update last_sync_check for all processed alerts now = datetime.utcnow() alert_ids = [a['_id'] for a in graded_batch] self.collection.update_many( {'_id': {'$in': alert_ids}}, {'$set': {'last_sync_check': now}} )
Result: Every alert gets checked eventually, ensuring no missing rows.
# MongoDBMONGO_CONNECTION_STRING="mongodb://localhost:27017"MONGO_DATABASE="proppr"# FotMobTURNSTILE_COOKIES_PATH="/opt/PROPPR/config/turnstile_cookies.json"# GradingGRADING_INTERVAL=120 # seconds (adaptive)GRADING_ALERT_QUERY_LIMIT=500 # max alerts per cycle# SyncingSYNC_INTERVAL=60 # secondsSYNC_BATCH_SIZE=100 # alerts per sync# Google SheetsGOOGLE_CREDENTIALS_PATH="/opt/PROPPR/config/credentials.json"PLAYER_SHEET_KEY="1abc...xyz" # From configTEAM_SHEET_KEY="1def...uvw" # From config