#!/usr/bin/env python3 """ Activity Simulation Script Simulates random user activity including: - New user registrations - Creating spread bets - Taking/matching bets - Adding comments to events and matches - Cancelling bets Usage: python simulate_activity.py [--iterations N] [--delay SECONDS] [--continuous] """ import asyncio import argparse import random from decimal import Decimal from datetime import datetime 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, EventComment, MatchComment, EventStatus, SpreadBetStatus, TeamSide, Transaction, TransactionType, TransactionStatus ) from app.utils.security import get_password_hash # Sample data for generating random users FIRST_NAMES = [ "James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Oliver", "Sophia", "Elijah", "Isabella", "Lucas", "Mia", "Mason", "Charlotte", "Ethan", "Amelia", "Alexander", "Harper", "Henry", "Evelyn", "Sebastian", "Luna", "Jack", "Camila", "Aiden", "Gianna", "Owen", "Abigail", "Samuel", "Ella", "Ryan", "Scarlett", "Nathan", "Emily", "Caleb", "Elizabeth", "Hunter", "Sofia", "Christian", "Avery", "Landon", "Chloe", "Jonathan", "Victoria" ] LAST_NAMES = [ "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King" ] # Sample comments for events EVENT_COMMENTS = [ "This is going to be a great game!", "Home team looking strong this season", "I'm betting on the underdog here", "What do you all think about the spread?", "Last time these teams played it was close", "Weather might be a factor today", "Key player is out, that changes everything", "The odds seem off to me", "Anyone else feeling bullish on the away team?", "This matchup is always entertaining", "Line moved a lot overnight", "Sharp money coming in on the home side", "Public is heavy on the favorite", "Value play on the underdog here", "Injury report looking concerning", "Home field advantage is huge here", "Expecting a low-scoring affair", "Over/under seems too high", "Rivalry game, throw out the records!", "Coach has a great record against this opponent" ] # Sample comments for matches (between two bettors) MATCH_COMMENTS = [ "Good luck!", "May the best bettor win", "This should be interesting", "I'm feeling confident about this one", "Let's see how this plays out", "Nice bet, looking forward to the game", "GL HF", "First time betting against you", "Rematch from last week!", "I've been waiting for this matchup", "Your team doesn't stand a chance!", "We'll see about that...", "Close game incoming", "I'll be watching every play", "Don't count your winnings yet!" ] async def create_random_user(db) -> User | None: """Create a new random user with a wallet.""" first = random.choice(FIRST_NAMES) last = random.choice(LAST_NAMES) # Generate unique username suffix = random.randint(100, 9999) username = f"{first.lower()}{last.lower()}{suffix}" email = f"{username}@example.com" # Check if user already exists existing = await db.execute( select(User).where(User.username == username) ) if existing.scalar_one_or_none(): return None user = User( email=email, username=username, password_hash=get_password_hash("password123"), display_name=f"{first} {last}" ) db.add(user) await db.flush() # Create wallet with random starting balance starting_balance = Decimal(str(random.randint(500, 5000))) wallet = Wallet( user_id=user.id, balance=starting_balance, escrow=Decimal("0.00") ) db.add(wallet) await db.commit() print(f" [USER] Created user: {user.username} with ${starting_balance} balance") return user async def create_random_bet(db, users: list[User], events: list[SportEvent]) -> SpreadBet | None: """Create a random spread bet on an event.""" if not users or not events: return None # Filter for users with sufficient balance users_with_balance = [] for user in users: wallet_result = await db.execute( select(Wallet).where(Wallet.user_id == user.id) ) wallet = wallet_result.scalar_one_or_none() if wallet and wallet.balance >= Decimal("10"): users_with_balance.append((user, wallet)) if not users_with_balance: return None user, wallet = random.choice(users_with_balance) event = random.choice(events) # Random spread within event range spread = round(random.uniform(event.min_spread, event.max_spread) * 2) / 2 # Round to 0.5 # Random stake (10-50% of balance, max $500) max_stake = min(float(wallet.balance) * 0.5, 500) stake = Decimal(str(round(random.uniform(10, max(10, max_stake)), 2))) # Random team selection team = random.choice([TeamSide.HOME, TeamSide.AWAY]) bet = SpreadBet( event_id=event.id, spread=spread, team=team, creator_id=user.id, stake_amount=stake, house_commission_percent=Decimal("10.00"), status=SpreadBetStatus.OPEN ) db.add(bet) await db.commit() team_name = event.home_team if team == TeamSide.HOME else event.away_team print(f" [BET] {user.username} created ${stake} bet on {team_name} {'+' if spread > 0 else ''}{spread}") return bet async def take_random_bet(db, users: list[User]) -> SpreadBet | None: """Have a random user take an open bet.""" # Get open bets result = await db.execute( select(SpreadBet) .options(selectinload(SpreadBet.event), selectinload(SpreadBet.creator)) .where(SpreadBet.status == SpreadBetStatus.OPEN) ) open_bets = result.scalars().all() if not open_bets: return None bet = random.choice(open_bets) # Find eligible takers (not creator, has balance) eligible_takers = [] for user in users: if user.id == bet.creator_id: continue wallet_result = await db.execute( select(Wallet).where(Wallet.user_id == user.id) ) wallet = wallet_result.scalar_one_or_none() if wallet and wallet.balance >= bet.stake_amount: eligible_takers.append((user, wallet)) if not eligible_takers: return None taker, taker_wallet = random.choice(eligible_takers) # Get creator's wallet creator_wallet_result = await db.execute( select(Wallet).where(Wallet.user_id == bet.creator_id) ) creator_wallet = creator_wallet_result.scalar_one_or_none() if not creator_wallet or creator_wallet.balance < bet.stake_amount: return None # Lock funds for both parties creator_wallet.balance -= bet.stake_amount creator_wallet.escrow += bet.stake_amount taker_wallet.balance -= bet.stake_amount taker_wallet.escrow += bet.stake_amount # Create transactions creator_tx = Transaction( user_id=bet.creator_id, wallet_id=creator_wallet.id, type=TransactionType.ESCROW_LOCK, amount=-bet.stake_amount, balance_after=creator_wallet.balance, reference_id=bet.id, description=f"Escrow lock for spread bet #{bet.id}", status=TransactionStatus.COMPLETED ) taker_tx = Transaction( user_id=taker.id, wallet_id=taker_wallet.id, type=TransactionType.ESCROW_LOCK, amount=-bet.stake_amount, balance_after=taker_wallet.balance, reference_id=bet.id, description=f"Escrow lock for spread bet #{bet.id}", status=TransactionStatus.COMPLETED ) db.add(creator_tx) db.add(taker_tx) # Update bet bet.taker_id = taker.id bet.status = SpreadBetStatus.MATCHED bet.matched_at = datetime.utcnow() await db.commit() print(f" [MATCH] {taker.username} took {bet.creator.username}'s ${bet.stake_amount} bet") return bet async def cancel_random_bet(db) -> SpreadBet | None: """Cancel a random open bet.""" result = await db.execute( select(SpreadBet) .options(selectinload(SpreadBet.creator)) .where(SpreadBet.status == SpreadBetStatus.OPEN) ) open_bets = result.scalars().all() if not open_bets: return None # 20% chance to cancel if random.random() > 0.2: return None bet = random.choice(open_bets) bet.status = SpreadBetStatus.CANCELLED await db.commit() print(f" [CANCEL] {bet.creator.username} cancelled their ${bet.stake_amount} bet") return bet async def add_event_comment(db, users: list[User], events: list[SportEvent]) -> EventComment | None: """Add a random comment to an event.""" if not users or not events: return None user = random.choice(users) event = random.choice(events) content = random.choice(EVENT_COMMENTS) comment = EventComment( event_id=event.id, user_id=user.id, content=content ) db.add(comment) await db.commit() print(f" [COMMENT] {user.username} on {event.home_team} vs {event.away_team}: \"{content[:40]}...\"") return comment async def add_match_comment(db, users: list[User]) -> MatchComment | None: """Add a random comment to a matched bet.""" # Get matched bets result = await db.execute( select(SpreadBet) .options(selectinload(SpreadBet.creator), selectinload(SpreadBet.taker)) .where(SpreadBet.status == SpreadBetStatus.MATCHED) ) matched_bets = result.scalars().all() if not matched_bets: return None bet = random.choice(matched_bets) # Comment from either creator or taker user = random.choice([bet.creator, bet.taker]) content = random.choice(MATCH_COMMENTS) comment = MatchComment( spread_bet_id=bet.id, user_id=user.id, content=content ) db.add(comment) await db.commit() print(f" [CHAT] {user.username} in match #{bet.id}: \"{content}\"") return comment async def run_simulation(iterations: int = 10, delay: float = 2.0, continuous: bool = False): """Run the activity simulation.""" print("=" * 60) print("H2H Activity Simulator") print("=" * 60) await init_db() iteration = 0 while continuous or iteration < iterations: iteration += 1 print(f"\n--- Iteration {iteration} ---") async with async_session() as db: # Get existing users and events users_result = await db.execute(select(User)) users = list(users_result.scalars().all()) events_result = await db.execute( select(SportEvent).where(SportEvent.status == EventStatus.UPCOMING) ) events = list(events_result.scalars().all()) if not events: print(" No upcoming events found. Run manage_events.py first.") if not continuous: break await asyncio.sleep(delay) continue # Random actions with weighted probabilities actions = [ (create_random_user, 0.15), # 15% - Create user (lambda db: create_random_bet(db, users, events), 0.30), # 30% - Create bet (lambda db: take_random_bet(db, users), 0.20), # 20% - Take bet (lambda db: cancel_random_bet(db), 0.05), # 5% - Cancel bet (lambda db: add_event_comment(db, users, events), 0.15), # 15% - Event comment (lambda db: add_match_comment(db, users), 0.15), # 15% - Match comment ] # Perform 1-3 random actions per iteration num_actions = random.randint(1, 3) for _ in range(num_actions): # Weighted random selection rand = random.random() cumulative = 0 for action_fn, probability in actions: cumulative += probability if rand <= cumulative: try: if action_fn == create_random_user: await action_fn(db) else: await action_fn(db) except Exception as e: print(f" [ERROR] {e}") break await asyncio.sleep(delay) print("\n" + "=" * 60) print("Simulation complete!") print("=" * 60) def main(): parser = argparse.ArgumentParser(description="Simulate H2H platform activity") parser.add_argument( "--iterations", "-n", type=int, default=10, help="Number of simulation iterations (default: 10)" ) parser.add_argument( "--delay", "-d", type=float, default=2.0, help="Delay between iterations in seconds (default: 2.0)" ) parser.add_argument( "--continuous", "-c", action="store_true", help="Run continuously until interrupted" ) args = parser.parse_args() try: asyncio.run(run_simulation( iterations=args.iterations, delay=args.delay, continuous=args.continuous )) except KeyboardInterrupt: print("\n\nSimulation stopped by user.") if __name__ == "__main__": main()