Skip to content

Instantly share code, notes, and snippets.

@chrisjbillington
Created February 2, 2024 21:55
Show Gist options
  • Save chrisjbillington/30c896762d6820cc4f7665e02132d3f8 to your computer and use it in GitHub Desktop.
Save chrisjbillington/30c896762d6820cc4f7665e02132d3f8 to your computer and use it in GitHub Desktop.
# get all non-API bets/comments/markets in the season
# get all comments in the season
# get all markets created in the season
# divide time into ten minute buckets
# create an array for each user storing whether they were active in each 10m bucket
# for each bet, market, comment:
# get which 10m bucket using arithmetic on the timestamp
# store True in that index of the user's array
# or round down to the nearest 10 minutes and store in a set? Yes!
# for the profit vs buckets, get all users that were active, then one-by-one get their
# leagues profit
import time
import json
from pathlib import Path
import requests
from datetime import datetime
from collections import defaultdict
from math import ceil, floor
SUPABASE_URL = "https://pxidrgkatumlvfqaxcll.supabase.co"
SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB4aWRyZ2thdHVtbHZmcWF4Y2xsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njg5OTUzOTgsImV4cCI6MTk4NDU3MTM5OH0.d_yYtASLzAoIIGdXUBIgRAGLBnNow7JG2SoaNMQ8ySg"
HEADERS = {"Apikey": SUPABASE_ANON_KEY}
def get_bets(tmin, tmax, include_redemptions=False, limit=1000):
endpoint = f"{SUPABASE_URL}/rest/v1/contract_bets"
params = [
("select", "created_time, data"),
("order", "created_time.desc"),
("limit", str(limit)),
("created_time", f"gte.{tmin}"),
("created_time", f"lt.{tmax}"),
]
if not include_redemptions:
params.append(("data->>isRedemption", "eq.false"))
response = requests.get(endpoint, headers=HEADERS, params=params, timeout=30)
if response.status_code == 200:
return response.json()
raise ValueError(f"Error {response.status_code}: {response.text}")
def get_markets(tmin, tmax, limit=10000):
endpoint = f"{SUPABASE_URL}/rest/v1/contracts"
params = [
("select", "created_time, data"),
("order", "created_time.desc"),
("limit", str(limit)),
("created_time", f"gte.{tmin}"),
("created_time", f"lt.{tmax}"),
]
response = requests.get(endpoint, headers=HEADERS, params=params, timeout=30)
if response.status_code == 200:
return response.json()
raise ValueError(f"Error {response.status_code}: {response.text}")
def get_comments(tmin, tmax, limit=10000):
endpoint = f"{SUPABASE_URL}/rest/v1/contract_comments"
params = [
("select", "created_time, data"),
("order", "created_time.desc"),
("limit", str(limit)),
("created_time", f"gte.{tmin}"),
("created_time", f"lt.{tmax}"),
]
response = requests.get(endpoint, headers=HEADERS, params=params, timeout=30)
if response.status_code == 200:
return response.json()
raise ValueError(f"Error {response.status_code}: {response.text}")
def get_users(tmax=None, limit=10000):
endpoint = f"{SUPABASE_URL}/rest/v1/users"
params = [
("select", "created_time, data"),
("order", "created_time.desc"),
("limit", str(limit)),
]
if tmax is not None:
params.append(("created_time", f"lt.{tmax}"))
response = requests.get(endpoint, headers=HEADERS, params=params, timeout=30)
if response.status_code == 200:
return response.json()
raise ValueError(f"Error {response.status_code}: {response.text}")
def get_leagues_info(season, limit=100000):
endpoint = f"{SUPABASE_URL}/rest/v1/user_league_info"
params = [
("select", "*"),
("order", "mana_earned.desc"),
("season", f"eq.{season}"),
("limit", str(limit)),
]
response = requests.get(endpoint, headers=HEADERS, params=params, timeout=30)
if response.status_code == 200:
return response.json()
raise ValueError(f"Error {response.status_code}: {response.text}")
def get_all_bets(tmin, tmax):
while True:
more_bets = get_bets(tmin=tmin, tmax=tmax)
if not more_bets:
return
for bet in more_bets:
tmax = bet["created_time"]
yield bet["data"]
time.sleep(1)
def get_all_markets(tmin, tmax):
while True:
more_markets = get_markets(tmin=tmin, tmax=tmax)
if not more_markets:
return
for market in more_markets:
tmax = market["created_time"]
yield market["data"]
time.sleep(1)
def get_all_comments(tmin, tmax):
while True:
more_comments = get_comments(tmin=tmin, tmax=tmax)
if not more_comments:
return
for comment in more_comments:
tmax = comment["created_time"]
yield comment["data"]
time.sleep(1)
def get_all_users():
tmax = None
while True:
more_users = get_users(tmax=tmax)
if not more_users:
return
for user in more_users:
tmax = user["created_time"]
yield user["data"]
time.sleep(1)
def tfmt(t):
"""convert timestamp in milliseconds to isoformat string"""
return datetime.fromtimestamp(t / 1000).isoformat()
def Mfmt(m):
"""Format a mana amoutn as a string, rounding toward zero"""
return f"{'-' if m <= -1 else ''}M{floor(abs(m)):.0f}".rjust(7)
def do_sync():
if SYNC_BETS:
bets = []
for bet in get_all_bets(tmin=LEAGUES_START, tmax=LEAGUES_END):
print("bet:", tfmt(bet["createdTime"]))
bets.append(bet)
BETS_CACHE.write_text(json.dumps(bets), 'utf8')
if SYNC_MARKETS:
markets = []
for market in get_all_markets(tmin=LEAGUES_START, tmax=LEAGUES_END):
print("market:", tfmt(market["createdTime"]))
markets.append(market)
MARKETS_CACHE.write_text(json.dumps(markets), 'utf8')
if SYNC_COMMENTS:
comments = []
for comment in get_all_comments(tmin=LEAGUES_START, tmax=LEAGUES_END):
print("comment:", tfmt(comment["createdTime"]))
comments.append(comment)
COMMENTS_CACHE.write_text(json.dumps(comments), 'utf8')
if SYNC_USERS:
users = []
for user in get_all_users():
print("user:", tfmt(user["createdTime"]))
users.append(user)
USERS_CACHE.write_text(json.dumps(users), 'utf8')
if SYNC_LEAGES_INFO:
leagues_info = []
for user_info in get_leagues_info(LEAGUES_SEASON):
leagues_info.append(user_info)
LEAGUES_INFO_CACHE.write_text(json.dumps(leagues_info), 'utf8')
N_RANKS_TO_SHOW = 100
LEAGUES_SEASON = 8
LEAGUES_START = "2023-12-01T07:00:00.000Z"
LEAGUES_END = "2024-01-02T03:06:12.000Z"
BUCKET_SIZE = 600_000 # ten minutes, in milliseconds
LEAGES_LEN = datetime.fromisoformat(LEAGUES_END) - datetime.fromisoformat(LEAGUES_START)
N_BUCKETS = ceil(1000 * LEAGES_LEN.total_seconds() / BUCKET_SIZE)
# Set these false after running once:
SYNC_BETS = True
SYNC_MARKETS = True
SYNC_COMMENTS = True
SYNC_USERS = True
SYNC_LEAGES_INFO = True
BETS_CACHE = Path("bets.json")
MARKETS_CACHE = Path("markets.json")
COMMENTS_CACHE = Path("comments.json")
USERS_CACHE = Path("users.json")
LEAGUES_INFO_CACHE = Path("leagues_info.json")
do_sync()
bets = json.loads(BETS_CACHE.read_text('utf8'))
markets = json.loads(MARKETS_CACHE.read_text('utf8'))
comments = json.loads(COMMENTS_CACHE.read_text('utf8'))
users = json.loads(USERS_CACHE.read_text('utf8'))
leagues_info = json.loads(LEAGUES_INFO_CACHE.read_text('utf8'))
users_by_id = {user['id']: user for user in users}
leagues_info_by_id = {info['user_id']: info for info in leagues_info}
user_active_buckets = defaultdict(set)
for bet in bets:
if not bet.get('isApi', False): # Ignore API bets
user_active_buckets[bet['userId']].add(bet['createdTime'] // BUCKET_SIZE)
for market in markets:
user_active_buckets[market['creatorId']].add(market['createdTime'] // BUCKET_SIZE)
for comment in comments:
user_active_buckets[comment['userId']].add(comment['createdTime'] // BUCKET_SIZE)
print("Most active")
print("===========")
for i, (user_id, active_buckets) in enumerate(
sorted(user_active_buckets.items(), key=lambda item: len(item[1]), reverse=True)
):
if i == N_RANKS_TO_SHOW:
break
username = users_by_id[user_id]['username']
n = len(active_buckets)
percentage = 100 * n / N_BUCKETS
print(f"{i+1:>2}.{username:>20} active in {n:>4} buckets ({percentage:.1f}%)")
# Now include API bets for the efficiency rankings:
for bet in bets:
user_active_buckets[bet['userId']].add(bet['createdTime'] // BUCKET_SIZE)
user_n_buckets = {
uid: len(active_buckets) for uid, active_buckets in user_active_buckets.items()
}
# the `info['user_id'] in user_n_buckets` check below ignores users with no activity.
# How do users with no activity have profit? Well, they bet on 'Other' in a multiple
# choice question before leagues started, and then 'Other' split into new answers this
# season. Leagues really shouldn't include the redemption bet objects created this way
# as profit, and we don't count them as contributing to activity. If these are the only
# bets a user has, we ignore them entirely.
user_profit = {
info['user_id']: info['mana_earned_breakdown']['profit']
for info in leagues_info
if info['mana_earned_breakdown'].get('profit', 0) != 0
and info['user_id'] in user_n_buckets
# and info['division'] == 6 # uncomment for masters only
}
# user_adj_buckets = {uid: n + 3 * n**0.5 for uid, n in user_n_buckets.items()}
user_adj_buckets = user_n_buckets
user_profit_per_adj_bucket = {
uid: profit / user_adj_buckets[uid] for uid, profit in user_profit.items()
}
# only users active in 50 buckets or more:
user_profit_per_adj_bucket = {
uid: v
for uid, v in user_profit_per_adj_bucket.items()
if user_n_buckets[uid] > 50
}
print("\n\n")
print("Most efficient")
print("==============")
for i, (user_id, profit_per_adj_bucket) in enumerate(
sorted(user_profit_per_adj_bucket.items(), key=lambda item: item[1], reverse=True)
):
if i == N_RANKS_TO_SHOW:
break
username = users_by_id[user_id]['username']
n = user_n_buckets[user_id]
n_adj = user_adj_buckets[user_id]
profit = user_profit[user_id]
print(
f"{i+1:>2}.{username:>20} {Mfmt(profit)} in {n:>4} buckets: "
+ f"{Mfmt(profit_per_adj_bucket)} per bucket"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment