437 lines
14 KiB
Python
Executable File
437 lines
14 KiB
Python
Executable File
#!/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()
|