#!/usr/bin/env python3 """ Event Management Script Manages sport events including: - Creating new upcoming events - Updating events to LIVE status - Simulating live score updates - Completing events and settling bets Usage: python manage_events.py [--create N] [--update] [--settle] [--continuous] [--delay SECONDS] """ import asyncio import argparse import random from decimal import Decimal from datetime import datetime, timedelta from sqlalchemy import select, and_ from sqlalchemy.orm import selectinload from app.database import async_session, init_db from app.models import ( User, Wallet, SportEvent, SpreadBet, Transaction, SportType, EventStatus, SpreadBetStatus, TeamSide, TransactionType, TransactionStatus ) # Team data by sport TEAMS = { SportType.FOOTBALL: { "NFL": [ ("Kansas City Chiefs", "Arrowhead Stadium"), ("San Francisco 49ers", "Levi's Stadium"), ("Philadelphia Eagles", "Lincoln Financial Field"), ("Dallas Cowboys", "AT&T Stadium"), ("Buffalo Bills", "Highmark Stadium"), ("Miami Dolphins", "Hard Rock Stadium"), ("Baltimore Ravens", "M&T Bank Stadium"), ("Detroit Lions", "Ford Field"), ("Green Bay Packers", "Lambeau Field"), ("Seattle Seahawks", "Lumen Field"), ("New York Giants", "MetLife Stadium"), ("Los Angeles Rams", "SoFi Stadium"), ("Cincinnati Bengals", "Paycor Stadium"), ("Jacksonville Jaguars", "TIAA Bank Field"), ("Minnesota Vikings", "U.S. Bank Stadium"), ("New Orleans Saints", "Caesars Superdome"), ], "NCAA": [ ("Alabama Crimson Tide", "Bryant-Denny Stadium"), ("Georgia Bulldogs", "Sanford Stadium"), ("Ohio State Buckeyes", "Ohio Stadium"), ("Michigan Wolverines", "Michigan Stadium"), ("Texas Longhorns", "Darrell K Royal Stadium"), ("USC Trojans", "Los Angeles Memorial Coliseum"), ("Clemson Tigers", "Memorial Stadium"), ("Penn State Nittany Lions", "Beaver Stadium"), ("Oregon Ducks", "Autzen Stadium"), ("Florida State Seminoles", "Doak Campbell Stadium"), ] }, SportType.BASKETBALL: { "NBA": [ ("Los Angeles Lakers", "Crypto.com Arena"), ("Boston Celtics", "TD Garden"), ("Golden State Warriors", "Chase Center"), ("Milwaukee Bucks", "Fiserv Forum"), ("Phoenix Suns", "Footprint Center"), ("Denver Nuggets", "Ball Arena"), ("Miami Heat", "Kaseya Center"), ("Philadelphia 76ers", "Wells Fargo Center"), ("Brooklyn Nets", "Barclays Center"), ("Dallas Mavericks", "American Airlines Center"), ("New York Knicks", "Madison Square Garden"), ("Cleveland Cavaliers", "Rocket Mortgage FieldHouse"), ], "NCAA": [ ("Duke Blue Devils", "Cameron Indoor Stadium"), ("Kentucky Wildcats", "Rupp Arena"), ("Kansas Jayhawks", "Allen Fieldhouse"), ("North Carolina Tar Heels", "Dean E. Smith Center"), ("UCLA Bruins", "Pauley Pavilion"), ("Gonzaga Bulldogs", "McCarthey Athletic Center"), ("Villanova Wildcats", "Finneran Pavilion"), ("Arizona Wildcats", "McKale Center"), ] }, SportType.BASEBALL: { "MLB": [ ("New York Yankees", "Yankee Stadium"), ("Los Angeles Dodgers", "Dodger Stadium"), ("Boston Red Sox", "Fenway Park"), ("Chicago Cubs", "Wrigley Field"), ("Atlanta Braves", "Truist Park"), ("Houston Astros", "Minute Maid Park"), ("San Diego Padres", "Petco Park"), ("Philadelphia Phillies", "Citizens Bank Park"), ("Texas Rangers", "Globe Life Field"), ("San Francisco Giants", "Oracle Park"), ] }, SportType.HOCKEY: { "NHL": [ ("Vegas Golden Knights", "T-Mobile Arena"), ("Florida Panthers", "Amerant Bank Arena"), ("Edmonton Oilers", "Rogers Place"), ("Dallas Stars", "American Airlines Center"), ("Colorado Avalanche", "Ball Arena"), ("New York Rangers", "Madison Square Garden"), ("Boston Bruins", "TD Garden"), ("Carolina Hurricanes", "PNC Arena"), ("Toronto Maple Leafs", "Scotiabank Arena"), ("Tampa Bay Lightning", "Amalie Arena"), ] }, SportType.SOCCER: { "EPL": [ ("Manchester City", "Etihad Stadium"), ("Arsenal", "Emirates Stadium"), ("Liverpool", "Anfield"), ("Manchester United", "Old Trafford"), ("Chelsea", "Stamford Bridge"), ("Tottenham Hotspur", "Tottenham Hotspur Stadium"), ("Newcastle United", "St. James' Park"), ("Brighton", "American Express Stadium"), ], "MLS": [ ("Inter Miami", "Chase Stadium"), ("LA Galaxy", "Dignity Health Sports Park"), ("LAFC", "BMO Stadium"), ("Atlanta United", "Mercedes-Benz Stadium"), ("Seattle Sounders", "Lumen Field"), ("New York Red Bulls", "Red Bull Arena"), ] } } def get_random_matchup(sport: SportType) -> tuple: """Get a random matchup for a sport.""" leagues = TEAMS.get(sport, {}) if not leagues: return None league = random.choice(list(leagues.keys())) teams = leagues[league] # Pick two different teams home_team, home_venue = random.choice(teams) away_team, _ = random.choice([t for t in teams if t[0] != home_team]) return { "sport": sport, "league": league, "home_team": home_team, "away_team": away_team, "venue": home_venue } def generate_spread(sport: SportType) -> float: """Generate a realistic spread for a sport.""" if sport == SportType.FOOTBALL: # NFL/NCAA spreads typically -14 to +14 spread = round(random.uniform(-10, 10) * 2) / 2 elif sport == SportType.BASKETBALL: # NBA spreads can be larger spread = round(random.uniform(-12, 12) * 2) / 2 elif sport == SportType.BASEBALL: # Baseball run lines are typically 1.5 spread = random.choice([-1.5, 1.5, -2.5, 2.5]) elif sport == SportType.HOCKEY: # Hockey puck lines are typically 1.5 spread = random.choice([-1.5, 1.5, -2.5, 2.5]) elif sport == SportType.SOCCER: # Soccer spreads are typically small spread = random.choice([-0.5, 0.5, -1, 1, -1.5, 1.5]) else: spread = round(random.uniform(-7, 7) * 2) / 2 return spread async def create_new_events(db, admin_user_id: int, count: int = 5) -> list[SportEvent]: """Create new upcoming events.""" events = [] for _ in range(count): sport = random.choice(list(SportType)) matchup = get_random_matchup(sport) if not matchup: continue # Random game time in the next 1-7 days hours_ahead = random.randint(1, 168) # 1 hour to 7 days game_time = datetime.utcnow() + timedelta(hours=hours_ahead) spread = generate_spread(sport) event = SportEvent( sport=matchup["sport"], home_team=matchup["home_team"], away_team=matchup["away_team"], official_spread=spread, game_time=game_time, venue=matchup["venue"], league=matchup["league"], min_spread=spread - 5, max_spread=spread + 5, min_bet_amount=10.0, max_bet_amount=1000.0, status=EventStatus.UPCOMING, created_by=admin_user_id ) db.add(event) events.append(event) await db.commit() for event in events: print(f" [NEW] {event.home_team} vs {event.away_team} ({event.league}) - {event.game_time.strftime('%m/%d %H:%M')}") return events async def update_events_to_live(db) -> list[SportEvent]: """Update events that should be live (game time has passed).""" # Find upcoming events where game time has passed result = await db.execute( select(SportEvent).where( and_( SportEvent.status == EventStatus.UPCOMING, SportEvent.game_time <= datetime.utcnow() ) ) ) events = result.scalars().all() for event in events: event.status = EventStatus.LIVE event.final_score_home = 0 event.final_score_away = 0 print(f" [LIVE] {event.home_team} vs {event.away_team} is now LIVE!") await db.commit() return list(events) async def update_live_scores(db) -> list[SportEvent]: """Update scores for live events.""" result = await db.execute( select(SportEvent).where(SportEvent.status == EventStatus.LIVE) ) events = result.scalars().all() for event in events: # Randomly add points based on sport if event.sport == SportType.FOOTBALL: # Football scoring: 3 or 7 points typically if random.random() < 0.3: scorer = random.choice(['home', 'away']) points = random.choice([3, 7, 6, 2]) if scorer == 'home': event.final_score_home = (event.final_score_home or 0) + points else: event.final_score_away = (event.final_score_away or 0) + points print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}") elif event.sport == SportType.BASKETBALL: # Basketball: 2 or 3 points frequently if random.random() < 0.5: scorer = random.choice(['home', 'away']) points = random.choice([2, 2, 2, 3, 1]) if scorer == 'home': event.final_score_home = (event.final_score_home or 0) + points else: event.final_score_away = (event.final_score_away or 0) + points print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}") elif event.sport in [SportType.BASEBALL, SportType.HOCKEY, SportType.SOCCER]: # Low scoring sports if random.random() < 0.15: scorer = random.choice(['home', 'away']) if scorer == 'home': event.final_score_home = (event.final_score_home or 0) + 1 else: event.final_score_away = (event.final_score_away or 0) + 1 print(f" [SCORE] {event.home_team} {event.final_score_home} - {event.final_score_away} {event.away_team}") await db.commit() return list(events) async def complete_events(db) -> list[SportEvent]: """Complete live events that have been running long enough.""" result = await db.execute( select(SportEvent).where(SportEvent.status == EventStatus.LIVE) ) events = result.scalars().all() completed = [] for event in events: # Calculate "game duration" based on when it went live (using updated_at as proxy) # Complete events after ~10 iterations (simulated game time) game_duration = (datetime.utcnow() - event.updated_at).total_seconds() # 20% chance to complete if scores are reasonable home_score = event.final_score_home or 0 away_score = event.final_score_away or 0 should_complete = False if event.sport == SportType.FOOTBALL and (home_score + away_score) >= 20: should_complete = random.random() < 0.3 elif event.sport == SportType.BASKETBALL and (home_score + away_score) >= 150: should_complete = random.random() < 0.3 elif event.sport in [SportType.BASEBALL, SportType.HOCKEY] and (home_score + away_score) >= 5: should_complete = random.random() < 0.3 elif event.sport == SportType.SOCCER and (home_score + away_score) >= 2: should_complete = random.random() < 0.3 if should_complete: event.status = EventStatus.COMPLETED completed.append(event) print(f" [FINAL] {event.home_team} {home_score} - {away_score} {event.away_team}") await db.commit() return completed async def settle_bets(db, completed_events: list[SportEvent]) -> int: """Settle bets for completed events.""" settled_count = 0 for event in completed_events: # Get all matched bets for this event result = await db.execute( select(SpreadBet) .options(selectinload(SpreadBet.creator), selectinload(SpreadBet.taker)) .where( and_( SpreadBet.event_id == event.id, SpreadBet.status == SpreadBetStatus.MATCHED ) ) ) bets = result.scalars().all() for bet in bets: # Calculate spread result home_score = event.final_score_home or 0 away_score = event.final_score_away or 0 actual_spread = home_score - away_score # Positive = home won by X # Creator's pick if bet.team == TeamSide.HOME: # Creator bet on home team with the spread # Home team needs to "cover" - actual margin > bet spread creator_wins = actual_spread > bet.spread else: # Creator bet on away team # Away team needs to cover - actual margin < bet spread (inverted) creator_wins = actual_spread < -bet.spread # Get wallets creator_wallet_result = await db.execute( select(Wallet).where(Wallet.user_id == bet.creator_id) ) creator_wallet = creator_wallet_result.scalar_one_or_none() taker_wallet_result = await db.execute( select(Wallet).where(Wallet.user_id == bet.taker_id) ) taker_wallet = taker_wallet_result.scalar_one_or_none() if not creator_wallet or not taker_wallet: continue # Calculate payout (total pot minus commission) total_pot = bet.stake_amount * 2 commission = total_pot * (bet.house_commission_percent / 100) payout = total_pot - commission winner = bet.creator if creator_wins else bet.taker winner_wallet = creator_wallet if creator_wins else taker_wallet loser_wallet = taker_wallet if creator_wins else creator_wallet # Release escrow and pay winner creator_wallet.escrow -= bet.stake_amount taker_wallet.escrow -= bet.stake_amount winner_wallet.balance += payout # Create payout transaction payout_tx = Transaction( user_id=winner.id, wallet_id=winner_wallet.id, type=TransactionType.BET_WON, amount=payout, balance_after=winner_wallet.balance, reference_id=bet.id, description=f"Won spread bet #{bet.id}", status=TransactionStatus.COMPLETED ) db.add(payout_tx) # Update bet bet.status = SpreadBetStatus.COMPLETED bet.winner_id = winner.id bet.payout_amount = payout bet.completed_at = datetime.utcnow() settled_count += 1 print(f" [SETTLED] Bet #{bet.id}: {winner.username} wins ${payout:.2f}") await db.commit() return settled_count async def run_event_management( create_count: int = 0, do_update: bool = False, do_settle: bool = False, continuous: bool = False, delay: float = 5.0 ): """Run the event management script.""" print("=" * 60) print("H2H Event Manager") print("=" * 60) await init_db() iteration = 0 while True: iteration += 1 print(f"\n--- Iteration {iteration} ---") async with async_session() as db: # Get or create admin user admin_result = await db.execute( select(User).where(User.is_admin == True) ) admin = admin_result.scalar_one_or_none() if not admin: # Use first user as admin user_result = await db.execute(select(User).limit(1)) admin = user_result.scalar_one_or_none() if not admin: print(" No users found. Run seed_data.py first.") if not continuous: break await asyncio.sleep(delay) continue # Create new events if create_count > 0 or continuous: events_to_create = create_count if create_count > 0 else random.randint(1, 3) if continuous and random.random() < 0.3: # 30% chance in continuous mode print(f"\n Creating {events_to_create} new events...") await create_new_events(db, admin.id, events_to_create) elif create_count > 0: print(f"\n Creating {events_to_create} new events...") await create_new_events(db, admin.id, events_to_create) # Update events if do_update or continuous: print("\n Checking for events to update...") # Move upcoming to live await update_events_to_live(db) # Update live scores await update_live_scores(db) # Complete events completed = await complete_events(db) # Settle bets for completed events if (do_settle or continuous) and completed: print(f"\n Settling bets for {len(completed)} completed events...") settled = await settle_bets(db, completed) print(f" Settled {settled} bets") if not continuous: break await asyncio.sleep(delay) print("\n" + "=" * 60) print("Event management complete!") print("=" * 60) def main(): parser = argparse.ArgumentParser(description="Manage H2H sport events") parser.add_argument( "--create", "-c", type=int, default=0, help="Number of new events to create (default: 0)" ) parser.add_argument( "--update", "-u", action="store_true", help="Update event statuses and scores" ) parser.add_argument( "--settle", "-s", action="store_true", help="Settle bets for completed events" ) parser.add_argument( "--continuous", action="store_true", help="Run continuously until interrupted" ) parser.add_argument( "--delay", "-d", type=float, default=5.0, help="Delay between iterations in seconds (default: 5.0)" ) args = parser.parse_args() # If no specific action, just create events if not args.create and not args.update and not args.settle and not args.continuous: args.create = 5 # Default: create 5 events try: asyncio.run(run_event_management( create_count=args.create, do_update=args.update, do_settle=args.settle, continuous=args.continuous, delay=args.delay )) except KeyboardInterrupt: print("\n\nEvent management stopped by user.") if __name__ == "__main__": main()