-
-
Save chrisjbillington/30c896762d6820cc4f7665e02132d3f8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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