Event layout page update.
This commit is contained in:
@ -6,6 +6,7 @@ from app.models.sport_event import SportEvent, SportType, EventStatus
|
||||
from app.models.spread_bet import SpreadBet, SpreadBetStatus, TeamSide
|
||||
from app.models.admin_settings import AdminSettings
|
||||
from app.models.match_comment import MatchComment
|
||||
from app.models.event_comment import EventComment
|
||||
from app.models.gamification import (
|
||||
UserStats,
|
||||
Achievement,
|
||||
@ -40,6 +41,7 @@ __all__ = [
|
||||
"TeamSide",
|
||||
"AdminSettings",
|
||||
"MatchComment",
|
||||
"EventComment",
|
||||
# Gamification
|
||||
"UserStats",
|
||||
"Achievement",
|
||||
|
||||
18
backend/app/models/event_comment.py
Normal file
18
backend/app/models/event_comment.py
Normal file
@ -0,0 +1,18 @@
|
||||
from sqlalchemy import String, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from datetime import datetime
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class EventComment(Base):
|
||||
__tablename__ = "event_comments"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("sport_events.id"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||
content: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
event: Mapped["SportEvent"] = relationship()
|
||||
user: Mapped["User"] = relationship()
|
||||
@ -6,9 +6,11 @@ from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus, SpreadBetStatus, TeamSide
|
||||
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus, SpreadBetStatus, TeamSide, EventComment
|
||||
from app.schemas.sport_event import SportEvent as SportEventSchema, SportEventWithBets
|
||||
from app.schemas.event_comment import EventComment as EventCommentSchema, EventCommentCreate, EventCommentsResponse
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.websocket import broadcast_to_event
|
||||
|
||||
router = APIRouter(prefix="/api/v1/sport-events", tags=["sport-events"])
|
||||
|
||||
@ -220,3 +222,104 @@ async def get_event_with_grid(
|
||||
"updated_at": event.updated_at.isoformat(),
|
||||
"spread_grid": spread_grid
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{event_id}/comments", response_model=EventCommentsResponse)
|
||||
async def get_event_comments(
|
||||
event_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get comments for an event - public access."""
|
||||
# Verify event exists
|
||||
event_result = await db.execute(
|
||||
select(SportEvent).where(SportEvent.id == event_id)
|
||||
)
|
||||
event = event_result.scalar_one_or_none()
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
# Get comments with user info
|
||||
result = await db.execute(
|
||||
select(EventComment)
|
||||
.options(selectinload(EventComment.user))
|
||||
.where(EventComment.event_id == event_id)
|
||||
.order_by(EventComment.created_at.asc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
comments = result.scalars().all()
|
||||
|
||||
# Get total count
|
||||
count_result = await db.execute(
|
||||
select(EventComment).where(EventComment.event_id == event_id)
|
||||
)
|
||||
total = len(count_result.scalars().all())
|
||||
|
||||
return EventCommentsResponse(
|
||||
comments=[
|
||||
EventCommentSchema(
|
||||
id=c.id,
|
||||
event_id=c.event_id,
|
||||
user_id=c.user_id,
|
||||
username=c.user.username,
|
||||
content=c.content,
|
||||
created_at=c.created_at
|
||||
)
|
||||
for c in comments
|
||||
],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{event_id}/comments", response_model=EventCommentSchema)
|
||||
async def add_event_comment(
|
||||
event_id: int,
|
||||
comment_data: EventCommentCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Add a comment to an event - requires authentication."""
|
||||
# Verify event exists
|
||||
event_result = await db.execute(
|
||||
select(SportEvent).where(SportEvent.id == event_id)
|
||||
)
|
||||
event = event_result.scalar_one_or_none()
|
||||
if not event:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
# Create comment
|
||||
comment = EventComment(
|
||||
event_id=event_id,
|
||||
user_id=current_user.id,
|
||||
content=comment_data.content
|
||||
)
|
||||
db.add(comment)
|
||||
await db.commit()
|
||||
await db.refresh(comment)
|
||||
|
||||
comment_response = EventCommentSchema(
|
||||
id=comment.id,
|
||||
event_id=comment.event_id,
|
||||
user_id=comment.user_id,
|
||||
username=current_user.username,
|
||||
content=comment.content,
|
||||
created_at=comment.created_at
|
||||
)
|
||||
|
||||
# Broadcast new comment to event subscribers
|
||||
await broadcast_to_event(
|
||||
event_id,
|
||||
"new_comment",
|
||||
{
|
||||
"id": comment.id,
|
||||
"event_id": comment.event_id,
|
||||
"user_id": comment.user_id,
|
||||
"username": current_user.username,
|
||||
"content": comment.content,
|
||||
"created_at": comment.created_at.isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
return comment_response
|
||||
|
||||
24
backend/app/schemas/event_comment.py
Normal file
24
backend/app/schemas/event_comment.py
Normal file
@ -0,0 +1,24 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
||||
class EventCommentCreate(BaseModel):
|
||||
content: str = Field(..., min_length=1, max_length=500)
|
||||
|
||||
|
||||
class EventComment(BaseModel):
|
||||
id: int
|
||||
event_id: int
|
||||
user_id: int
|
||||
username: str
|
||||
content: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EventCommentsResponse(BaseModel):
|
||||
comments: List[EventComment]
|
||||
total: int
|
||||
546
backend/manage_events.py
Executable file
546
backend/manage_events.py
Executable file
@ -0,0 +1,546 @@
|
||||
#!/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()
|
||||
436
backend/simulate_activity.py
Executable file
436
backend/simulate_activity.py
Executable file
@ -0,0 +1,436 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user