Files
h2h-prototype/backend/manage_events.py

547 lines
19 KiB
Python
Executable File

#!/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()