def make_order_performance_chart_with_context(**context): game_id, start_time, end_time = context_parser(context, "game_id", "start_time", "end_time") start_time, end_time = get_time_defaults(game_id, start_time, end_time) user_ids = get_active_game_user_ids(game_id) for user_id in user_ids: print(f"*** user id: {user_id} ***") serialize_and_pack_order_performance_assets(game_id, user_id, start_time, end_time)
def make_the_field_charts(game_id: int, start_time: float = None, end_time: float = None): """This function wraps a loop that produces the balances chart for each user and the field chart for the game. This will run every time a user places and order, and periodically as prices are collected """ user_ids = get_active_game_user_ids(game_id) portfolios = [] portfolio_table_keys = list(portfolio_comps_schema.keys()) for user_id in user_ids: df = make_user_balances_chart_data(game_id, user_id, start_time, end_time) serialize_and_pack_balances_chart(df, game_id, user_id) portfolio = aggregate_portfolio_value(df) portfolio["username"] = get_usernames([user_id])[0] apply_validation(portfolio, portfolio_comps_schema) portfolios.append(portfolio[portfolio_table_keys]) # add index data if check_single_player_mode(game_id): for index in TRACKED_INDEXES: df = get_index_portfolio_value_data(game_id, index, start_time, end_time) df["timestamp"] = df["timestamp"].apply(lambda x: posix_to_datetime(x)) df = add_time_labels(df) df = df.groupby("t_index", as_index=False).agg( {"username": "******", "label": "last", "value": "last", "timestamp": "last"}) apply_validation(df, portfolio_comps_schema) portfolios.append(df[portfolio_table_keys]) portfolios_df = pd.concat(portfolios) relabelled_df = relabel_aggregated_portfolios(portfolios_df) relabelled_df.sort_values("timestamp", inplace=True) serialize_and_pack_portfolio_comps_chart(relabelled_df, game_id)
def close_finished_game_with_context(**context): game_id = context_parser(context, "game_id")[0] _, game_end = get_game_start_and_end(game_id) current_time = time.time() if current_time >= game_end: user_ids = get_active_game_user_ids(game_id) last_status_entry = query_to_dict("""SELECT * FROM game_status WHERE game_id = %s ORDER BY id DESC LIMIT 0, 1""", game_id)[0] if last_status_entry["status"] == "active": # close game and update ratings add_row("game_status", game_id=game_id, status="finished", users=user_ids, timestamp=current_time) update_ratings(game_id)
def calculate_and_pack_game_metrics(game_id: int, start_time: float = None, end_time: float = None): for user_id in get_active_game_user_ids(game_id): return_ratio, sharpe_ratio = calculate_metrics(game_id, user_id, start_time, end_time) rds.set(f"{RETURN_RATIO_PREFIX}_{game_id}_{user_id}", return_ratio) rds.set(f"{SHARPE_RATIO_PREFIX}_{game_id}_{user_id}", sharpe_ratio) if check_single_player_mode(game_id): for index in TRACKED_INDEXES: df = get_index_portfolio_value_data(game_id, index, start_time, end_time) index_return_ratio = portfolio_return_ratio(df) index_sharpe_ratio = portfolio_sharpe_ratio(df, RISK_FREE_RATE_DEFAULT) rds.set(f"{RETURN_RATIO_PREFIX}_{game_id}_{index}", index_return_ratio) rds.set(f"{SHARPE_RATIO_PREFIX}_{game_id}_{index}", index_sharpe_ratio)
def get_winner(game_id: int, start_time: float, end_time: float, benchmark: str): assert benchmark in ["return_ratio", "sharpe_ratio"] ids_and_scores = [] user_ids = get_active_game_user_ids(game_id) for user_id in user_ids: return_ratio, sharpe_ratio = calculate_metrics(game_id, user_id, start_time, end_time) metric = return_ratio if benchmark == "sharpe_ratio": metric = sharpe_ratio ids_and_scores.append((user_id, metric)) max_score = max([x[1] for x in ids_and_scores]) return [x for x in ids_and_scores if x[1] == max_score][0] # TODO: handle ties (mathematically unlikely)
def init_game_assets(game_id: int): calculate_and_pack_game_metrics(game_id) # leaderboard compile_and_pack_player_leaderboard(game_id) # the field and balance charts make_the_field_charts(game_id) # tables and performance breakout charts user_ids = get_active_game_user_ids(game_id) for user_id in user_ids: # game/user-level assets serialize_and_pack_pending_orders(game_id, user_id) serialize_and_pack_order_performance_assets(game_id, user_id) serialize_and_pack_portfolio_details(game_id, user_id) if not check_single_player_mode(game_id): # winners/payouts table update_performed = log_winners(game_id, time.time()) if update_performed: serialize_and_pack_winners_table(game_id)
def leave_game(game_id: int, user_id: int): """Users in non-paid games can leave at any time """ current_time = time.time() add_row("game_invites", game_id=game_id, user_id=user_id, status="left", timestamp=current_time) current_game_users = get_active_game_user_ids(game_id) remaining_game_users = [x for x in current_game_users if x != user_id] if not remaining_game_users: add_row("game_status", game_id=game_id, status="cancelled", users=[], timestamp=current_time) return add_row("game_status", game_id=game_id, status="active", users=remaining_game_users, timestamp=current_time)
def test_visuals_with_data(self, mock_base_time, mock_game_time): mock_base_time.time.return_value = mock_game_time.time.return_value = simulation_end_time game_id = 3 user_id = 1 serialize_and_pack_pending_orders(game_id, user_id) pending_order_table = s3_cache.unpack_s3_json( f"{game_id}/{user_id}/{PENDING_ORDERS_PREFIX}") self.assertNotIn("order_id", pending_order_table["headers"]) self.assertEqual(len(pending_order_table["headers"]), 9) user_ids = get_active_game_user_ids(game_id) for player_id in user_ids: serialize_and_pack_order_performance_assets(game_id, player_id) op_chart_3_1 = s3_cache.unpack_s3_json( f"{game_id}/{user_id}/{ORDER_PERF_CHART_PREFIX}") chart_stocks = set( [x["label"].split("/")[0] for x in op_chart_3_1["datasets"]]) expected_stocks = {"AMZN", "TSLA", "LYFT", "SPXU", "NVDA"} self.assertEqual(chart_stocks, expected_stocks) for user_id in user_ids: self.assertIn(f"{game_id}/{user_id}/{ORDER_PERF_CHART_PREFIX}", s3_cache.keys())
def compile_and_pack_player_leaderboard(game_id: int, start_time: float = None, end_time: float = None): user_ids = get_active_game_user_ids(game_id) usernames = get_game_users(game_id) user_colors = assign_colors(usernames) records = [] for user_id in user_ids: user_info = get_user_information(user_id) # this is where username and profile pic get added in cash_balance = get_current_game_cash_balance(user_id, game_id) balances = get_active_balances(game_id, user_id) stocks_held = list(balances["symbol"].unique()) portfolio_value = get_user_portfolio_value(game_id, user_id) stat_info = make_stat_entry(color=user_colors[user_info["username"]], cash_balance=cash_balance, portfolio_value=portfolio_value, stocks_held=stocks_held, return_ratio=rds.get(f"{RETURN_RATIO_PREFIX}_{game_id}_{user_id}"), sharpe_ratio=rds.get(f"{SHARPE_RATIO_PREFIX}_{game_id}_{user_id}")) records.append({**user_info, **stat_info}) if check_single_player_mode(game_id): for index in TRACKED_INDEXES: index_info = query_to_dict(""" SELECT name as username, avatar AS profile_pic FROM index_metadata WHERE symbol = %s""", index)[0] portfolio_value = get_index_portfolio_value(game_id, index, start_time, end_time) stat_info = make_stat_entry(color=user_colors[index_info["username"]], cash_balance=None, portfolio_value=portfolio_value, stocks_held=[], return_ratio=rds.get(f"{RETURN_RATIO_PREFIX}_{game_id}_{index}"), sharpe_ratio=rds.get(f"{SHARPE_RATIO_PREFIX}_{game_id}_{index}")) records.append({**index_info, **stat_info}) benchmark = get_game_info(game_id)["benchmark"] # get game benchmark and use it to sort leaderboard records = sorted(records, key=lambda x: -x[benchmark]) output = dict(days_left=_days_left(game_id), records=records) s3_cache.set(f"{game_id}/{LEADERBOARD_PREFIX}", json.dumps(output))
def test_async_process_all_open_orders(self, base_time_mock): base_time_mock.time.return_value = 1596738863.111858 user_id = 1 game_id = 3 _user_ids = get_active_game_user_ids(game_id) for _user_id in _user_ids: no_pending_orders_table(game_id, _user_id) no_fulfilled_orders_table(game_id, _user_id) # Place a guaranteed-to-clear order buy_stock = "MSFT" mock_buy_order = {"amount": 1, "buy_or_sell": "buy", "game_id": 3, "order_type": "stop", "stop_limit_price": 1, "market_price": 0.5, "quantity_type": "Shares", "symbol": buy_stock, "time_in_force": "until_cancelled" } current_cash_balance = get_current_game_cash_balance(user_id, game_id) current_holding = get_current_stock_holding(user_id, game_id, buy_stock) place_order(user_id, game_id, mock_buy_order["symbol"], mock_buy_order["buy_or_sell"], current_cash_balance, current_holding, mock_buy_order["order_type"], mock_buy_order["quantity_type"], mock_buy_order["market_price"], mock_buy_order["amount"], mock_buy_order["time_in_force"], mock_buy_order["stop_limit_price"]) # Place a guaranteed-to-clear order buy_stock = "AAPL" mock_buy_order = {"amount": 1, "buy_or_sell": "buy", "game_id": 3, "order_type": "stop", "stop_limit_price": 1, "market_price": 0.5, "quantity_type": "Shares", "symbol": buy_stock, "time_in_force": "until_cancelled" } current_cash_balance = get_current_game_cash_balance(user_id, game_id) current_holding = get_current_stock_holding(user_id, game_id, buy_stock) place_order(user_id, game_id, mock_buy_order["symbol"], mock_buy_order["buy_or_sell"], current_cash_balance, current_holding, mock_buy_order["order_type"], mock_buy_order["quantity_type"], mock_buy_order["market_price"], mock_buy_order["amount"], mock_buy_order["time_in_force"], mock_buy_order["stop_limit_price"]) open_orders = get_all_open_orders(game_id) starting_open_orders = len(open_orders) self.assertEqual(starting_open_orders, 4) for order_id, _ in open_orders.items(): process_order(order_id) new_open_orders = get_all_open_orders(game_id) self.assertLessEqual(starting_open_orders - len(new_open_orders), 4)
def refresh_portfolio_details_with_context(**context): game_id, = context_parser(context, "game_id") user_ids = get_active_game_user_ids(game_id) for user_id in user_ids: print(f"*** user id: {user_id} ***") serialize_and_pack_portfolio_details(game_id, user_id)
from argparse import ArgumentParser import pandas as pd from backend.database.db import (db, engine) from backend.database.helpers import drop_all_tables from backend.logic.base import (get_active_game_user_ids, get_game_start_and_end, SECONDS_IN_A_DAY) parser = ArgumentParser() parser.add_argument("--game_id", type=int) args = parser.parse_args() if __name__ == '__main__': if os.getenv("ENV") == "prod": with engine.connect() as conn: user_ids = get_active_game_user_ids(args.game_id) min_time, max_time = get_game_start_and_end(args.game_id) games = pd.read_sql("SELECT * FROM games WHERE id = %s;", conn, params=[args.game_id]) game_status = pd.read_sql( "SELECT * FROM game_status WHERE game_id = %s;", conn, params=[args.game_id]) game_balances = pd.read_sql(f""" SELECT * FROM game_balances WHERE game_id = %s AND user_id IN ({", ".join(["%s"] * len(user_ids))}); """, conn, params=[args.game_id] + list(user_ids))
def test_winner_payouts(self): """Use canonical game #3 to simulate a series of winner calculations on the test data. Note that since we only have a week of test data, we'll effectively recycle the same information via mocks """ game_id = 3 user_ids = get_active_game_user_ids(game_id) self.assertEqual(user_ids, [1, 3, 4]) game_info = get_game_info(game_id) n_players = len(user_ids) pot_size = n_players * game_info["buy_in"] self.assertEqual(pot_size, 300) last_payout_date = get_last_sidebet_payout(game_id) self.assertIsNone(last_payout_date) offset = make_date_offset(game_info["side_bets_period"]) self.assertEqual(offset, DateOffset(days=7)) start_time = game_info["start_time"] end_time = game_info["end_time"] self.assertEqual(start_time, simulation_start_time) n_sidebets = n_sidebets_in_game(start_time, end_time, offset) self.assertEqual(n_sidebets, 2) # we'll mock in daily portfolio values to speed up the time this test takes user_1_portfolio = portfolio_value_by_day(game_id, 1, start_time, end_time) user_3_portfolio = portfolio_value_by_day(game_id, 3, start_time, end_time) user_4_portfolio = portfolio_value_by_day(game_id, 4, start_time, end_time) # expected sidebet dates start_dt = posix_to_datetime(start_time) end_dt = posix_to_datetime(end_time) sidebet_dates = get_expected_sidebets_payout_dates( start_dt, end_dt, game_info["side_bets_perc"], offset) sidebet_dates_posix = [datetime_to_posix(x) for x in sidebet_dates] with patch("backend.logic.metrics.portfolio_value_by_day" ) as portfolio_mocks, patch( "backend.logic.base.time") as base_time_mock: time = Mock() time_1 = datetime_to_posix(posix_to_datetime(start_time) + offset) time_2 = end_time time.time.side_effect = base_time_mock.time.side_effect = [ time_1, time_2 ] portfolio_mocks.side_effect = [ user_1_portfolio, user_3_portfolio, user_4_portfolio ] * 4 winner_id, score = get_winner(game_id, start_time, end_time, game_info["benchmark"]) log_winners(game_id, time.time()) sidebet_entry = query_to_dict( "SELECT * FROM winners WHERE id = 1;")[0] self.assertEqual(sidebet_entry["winner_id"], winner_id) self.assertAlmostEqual(sidebet_entry["score"], score, 4) side_pot = pot_size * (game_info["side_bets_perc"] / 100) / n_sidebets self.assertEqual(sidebet_entry["payout"], side_pot * PERCENT_TO_USER) self.assertEqual(sidebet_entry["type"], "sidebet") self.assertEqual(sidebet_entry["start_time"], start_time) self.assertEqual(sidebet_entry["end_time"], sidebet_dates_posix[0]) self.assertEqual(sidebet_entry["timestamp"], time_1) log_winners(game_id, time.time()) sidebet_entry = query_to_dict( "SELECT * FROM winners WHERE id = 2;")[0] self.assertEqual(sidebet_entry["winner_id"], winner_id) self.assertAlmostEqual(sidebet_entry["score"], score, 4) self.assertEqual(sidebet_entry["payout"], side_pot * PERCENT_TO_USER) self.assertEqual(sidebet_entry["type"], "sidebet") self.assertEqual(sidebet_entry["start_time"], sidebet_dates_posix[0]) self.assertEqual(sidebet_entry["end_time"], sidebet_dates_posix[1]) self.assertEqual(sidebet_entry["timestamp"], time_2) overall_entry = query_to_dict( "SELECT * FROM winners WHERE id = 3;")[0] final_payout = pot_size * (1 - game_info["side_bets_perc"] / 100) self.assertEqual(overall_entry["payout"], final_payout * PERCENT_TO_USER) with self.engine.connect() as conn: df = pd.read_sql("SELECT * FROM winners", conn) self.assertEqual(df.shape, (3, 10)) serialize_and_pack_winners_table(game_id) payouts_table = s3_cache.unpack_s3_json(f"{game_id}/{PAYOUTS_PREFIX}") self.assertEqual(len(payouts_table["data"]), 3) self.assertTrue( sum([x["Type"] == "Sidebet" for x in payouts_table["data"]]), 2) self.assertTrue( sum([x["Type"] == "Overall" for x in payouts_table["data"]]), 1) for entry in payouts_table["data"]: if entry["Type"] == "Sidebet": self.assertEqual(entry["Payout"], side_pot * PERCENT_TO_USER) else: self.assertEqual(entry["Payout"], final_payout * PERCENT_TO_USER)
def update_ratings(game_id: int): start, end = get_game_start_and_end(game_id) user_ids = get_active_game_user_ids(game_id) # construct the leaderboard, including indexes. use simple return for now scoreboard = {} for user_id in user_ids: ending_value = get_user_portfolio_value(game_id, user_id, end) scoreboard[user_id] = ending_value / STARTING_VIRTUAL_CASH - 1 for index in TRACKED_INDEXES: scoreboard[index] = get_index_portfolio_value(game_id, index, start, end) / STARTING_VIRTUAL_CASH - 1 # now that we have the scoreboard, iterate over its entries to construct and update DF update_array = [] for player, player_return in scoreboard.items(): other_players = {k: v for k, v in scoreboard.items() if k != player} for other_player, other_player_return in other_players.items(): player_entry = get_rating_info(player) other_player_entry = get_rating_info(other_player) player_rating = player_entry["rating"] other_player_rating = other_player_entry["rating"] # fields for return total_return = player_entry["total_return"] n_games = player_entry["n_games"] score = 1 if player_return > other_player_return else 0 if player_return == other_player_return: score = 0.5 update_array.append(dict( player_id=player, player_rating=player_rating, expected=expected_elo(player_rating, other_player_rating), score=score, total_return=total_return, n_games=n_games )) update_df = pd.DataFrame(update_array) summary_df = update_df.groupby("player_id", as_index=False).agg({ "player_rating": "first", "expected": "sum", "score": "sum", "total_return": "first", "n_games": "first" }) for _, row in summary_df.iterrows(): player_id = row["player_id"] new_rating = elo_update(row["player_rating"], row["expected"], row["score"]) # update basis and return stats game_basis = STARTING_VIRTUAL_CASH game_return = scoreboard[player_id] add_row("stockbets_rating", user_id=int(player_id) if player_id in user_ids else None, index_symbol=player_id if player_id in TRACKED_INDEXES else None, game_id=game_id, basis=float(game_basis), total_return=float(game_return), n_games=int(row["n_games"] + 1), rating=float(new_rating), update_type="game_end", timestamp=end)
def get_pot_size(game_id: int): game_info = get_game_info(game_id) player_ids = get_active_game_user_ids(game_id) n_players = len(player_ids) return n_players * game_info["buy_in"]