547 lines
19 KiB
Python
Executable File
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()
|