def n_sidebets_in_game(game_start: float, game_end: float, offset: DateOffset) -> int: game_start = posix_to_datetime(game_start) game_end = posix_to_datetime(game_end) count = 0 t = game_start + offset while t <= game_end: count += 1 t += offset return count
def test_time_handlers(self): posix_time = 1590165714.1528566 actual_date = dt(2020, 5, 22, 12, 41, 54, 152857) localizer = pytz.timezone(TIMEZONE) localized_date = localizer.localize(actual_date) self.assertEqual(posix_to_datetime(posix_time), localized_date) self.assertAlmostEqual(posix_time, datetime_to_posix(localized_date), 0) mexico_date = actual_date.replace(hour=11) localizer = pytz.timezone("America/Mexico_City") localized_date = localizer.localize(mexico_date) self.assertAlmostEqual(posix_time, datetime_to_posix(localized_date), 0) # Pre-stage all of the mocked current time values that will be called sequentially in the tests below. # ---------------------------------------------------------------------------------------------------- with patch('backend.logic.base.time') as current_time_mock: # Check during trading day just one second before and after open/close schedule = get_trading_calendar(actual_date, actual_date) start_day, end_day = [ datetime_to_posix(x) for x in schedule.iloc[0][["market_open", "market_close"]] ] current_time_mock.time.side_effect = [ posix_time, # during trading day posix_time + 8 * 60 * 60, # 8 hours later--after trading day 1608908400, # Christmas 2020, Friday, 11am in NYC. We want to verify that we're accounting for holidays start_day - 1, # one second before trading day (start_day + end_day) / 2, # right in the middle of trading day end_day + 1 # one second after trading day ] self.assertTrue(during_trading_day()) self.assertFalse(during_trading_day()) self.assertFalse(during_trading_day()) self.assertFalse(during_trading_day()) self.assertTrue(during_trading_day()) self.assertFalse(during_trading_day()) # Finally, just double-check that the real-time, default invocation works as expected posix_now = time.time() nyc_now = posix_to_datetime(posix_now) schedule = get_trading_calendar(nyc_now, nyc_now) during_trading = False if not schedule.empty: start_day, end_day = [ datetime_to_posix(x) for x in schedule.iloc[0][["market_open", "market_close"]] ] during_trading = start_day <= posix_now <= end_day # FYI: there is a non-zero chance that this test will fail at exactly the beginning or end of a trading day self.assertEqual(during_trading, during_trading_day())
def get_winners_meta_data(game_id: int): game_info = query_to_dict("SELECT * FROM games WHERE id = %s", game_id)[0] side_bets_perc = game_info.get("side_bets_perc") benchmark = game_info["benchmark"] stakes = game_info["stakes"] game_start, game_end = get_game_start_and_end(game_id) offset = make_date_offset(game_info["side_bets_period"]) start_dt = posix_to_datetime(game_start) end_dt = posix_to_datetime(game_end) return game_start, game_end, start_dt, end_dt, benchmark, side_bets_perc, stakes, offset
def make_orders_per_active_user(): # get total number of active users on a given day (in at least one active game, or in a live single player mode) with engine.connect() as conn: game_status = pd.read_sql( """ SELECT game_id, users, timestamp FROM game_status WHERE status NOT IN ('expired', 'pending')""", conn) order_status = pd.read_sql( "SELECT id, timestamp FROM order_status WHERE status = 'pending';", conn) game_status["timestamp"] = game_status["timestamp"].apply( lambda x: posix_to_datetime(x)) game_status["users"] = game_status["users"].apply(lambda x: json.loads(x)) game_status["user_count"] = game_status["users"].apply(lambda x: len(x)) # get total number of active users per day complete_counts_array = [] for game_id in game_status["game_id"].unique(): game_subset = game_status[game_status["game_id"] == game_id] if game_subset.shape == 2: complete_counts_array += game_subset[["user_count", "timestamp" ]].to_dict(orient="records") continue user_count = game_subset.iloc[0]["user_count"] original_entry = dict(user_count=user_count, timestamp=game_subset.iloc[0]["timestamp"], game_id=game_id) bookend = dict(user_count=user_count, timestamp=posix_to_datetime(time.time()), game_id=game_id) complete_counts_array += [original_entry, bookend] user_count_df = pd.DataFrame(complete_counts_array) expanded_df = user_count_df.groupby("game_id").apply(expand_counts) expanded_df.index = expanded_df.index.droplevel(0) daily_active_users = expanded_df.resample("D").sum() order_status["timestamp"] = order_status["timestamp"].apply( lambda x: posix_to_datetime(x)) order_status.set_index("timestamp", inplace=True) order_totals = order_status.resample("D").count() df = pd.concat([daily_active_users, order_totals], axis=1) df["orders_per_users"] = df["id"] / df["user_count"] df.fillna(0, inplace=True) df = df.reset_index() df["timestamp"] = df["timestamp"].apply( lambda x: datetime_to_posix(x)).astype(float) return df
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 serialize_and_pack_order_performance_chart(df: pd.DataFrame, game_id: int, user_id: int): if df.empty: chart_json = make_null_chart("Waiting for orders...") else: apply_validation(df, order_performance_schema, True) plot_df = add_bookends(df, group_var="order_label", condition_var="fifo_balance") plot_df["cum_pl"] = plot_df.groupby("order_label")["realized_pl"].cumsum() plot_df["timestamp"] = plot_df["timestamp"].apply(lambda x: posix_to_datetime(x)) plot_df.set_index("timestamp", inplace=True) plot_df = plot_df.groupby("order_label", as_index=False).resample(f"{RESAMPLING_INTERVAL}T").last().ffill() plot_df = plot_df.reset_index(level=1) plot_df = filter_for_trade_time(plot_df) plot_df = append_price_data_to_balance_histories(plot_df) plot_df.sort_values(["order_label", "timestamp"], inplace=True) plot_df = add_time_labels(plot_df) plot_df = plot_df.groupby(["order_label", "t_index"], as_index=False).agg("last") plot_df["label"] = plot_df["timestamp"].apply(lambda x: datetime_to_posix(x)).astype(float) plot_df.sort_values("timestamp", inplace=True) plot_df["total_pl"] = plot_df["cum_pl"] + plot_df["fifo_balance"] * plot_df["price"] - ( 1 - plot_df["total_pct_sold"]) * plot_df["basis"] plot_df["return"] = 100 * plot_df["total_pl"] / plot_df["basis"] label_colors = assign_colors(plot_df["order_label"].unique()) plot_df["color"] = plot_df["order_label"].apply(lambda x: label_colors.get(x)) chart_json = make_chart_json(plot_df, "order_label", "return", "label", colors=plot_df["color"].unique()) s3_cache.set(f"{game_id}/{user_id}/{ORDER_PERF_CHART_PREFIX}", json.dumps(chart_json))
def test_price_fetchers(self): symbol = "AMZN" amzn_price, updated_at = fetch_price(symbol) self.assertIsNotNone(amzn_price) self.assertTrue(amzn_price > 0) self.assertTrue( posix_to_datetime(updated_at) > dt(2000, 1, 1).replace( tzinfo=pytz.utc))
def make_user_cohorts(users: pd.DataFrame) -> pd.DataFrame: users["created_at"] = users["created_at"].apply( lambda x: posix_to_datetime(x)) users["period"] = users["created_at"].dt.to_period("M") label_df = users.groupby("period", as_index=False)["id"].count() label_df["cohort"] = label_df["period"].astype( str) + " [" + label_df["id"].astype(str) + "]" return users.merge(label_df[["period", "cohort"]])
def serialize_and_pack_winners_table(game_id: int): """this function serializes the winners data that has already been saved to DB and fills in any missing rows.""" game_start, game_end, start_dt, end_dt, benchmark, side_bets_perc, stakes, offset = get_winners_meta_data(game_id) # pull winners data from DB with engine.connect() as conn: winners_df = pd.read_sql("SELECT * FROM winners WHERE game_id = %s ORDER BY id", conn, params=[game_id]) # Where are we at in the current game? game_finished = False if winners_df.empty: last_observed_win = start_dt else: last_observed_win = posix_to_datetime(winners_df["timestamp"].max()) if "overall" in winners_df["type"].to_list(): game_finished = True data = [] if side_bets_perc: payout = get_sidebet_payout(game_id, side_bets_perc, offset, stakes) expected_sidebet_dates = get_expected_sidebets_payout_dates(start_dt, end_dt, side_bets_perc, offset) for _, row in winners_df.iterrows(): if row["type"] == "sidebet": winner = get_usernames([row["winner_id"]]) data.append( make_payout_table_entry(posix_to_datetime(row["start_time"]), posix_to_datetime(row["end_time"]), winner, payout, "Sidebet", benchmark, row["score"])) dates_to_fill_in = [x for x in expected_sidebet_dates if x > last_observed_win] last_date = last_observed_win for payout_date in dates_to_fill_in: data.append(make_payout_table_entry(last_date, payout_date, "???", payout, "Sidebet")) last_date = payout_date payout = get_overall_payout(game_id, side_bets_perc, stakes) if not game_finished: final_entry = make_payout_table_entry(start_dt, end_dt, "???", payout, "Overall") else: winner_row = winners_df.loc[winners_df["type"] == "overall"].iloc[0] winner = get_usernames([int(winner_row["winner_id"])])[0] final_entry = make_payout_table_entry(start_dt, end_dt, winner, payout, "Overall", benchmark, winner_row["score"]) data.append(final_entry) out_dict = dict(data=data, headers=list(data[0].keys())) s3_cache.set(f"{game_id}/{PAYOUTS_PREFIX}", json.dumps(out_dict))
def get_beginning_of_next_trading_day(ref_time): ref_day = posix_to_datetime(ref_time).date() schedule = get_trading_calendar(ref_day, ref_day) while schedule.empty: ref_day += timedelta(days=1) schedule = get_trading_calendar(ref_day, ref_day) start_day, _ = get_schedule_start_and_end(schedule) return start_day
def test_schedule_handlers(self): """These test functions that live in logic.stock_data, but are used when processing orders during game play """ sat_may_23_2020 = dt(2020, 5, 23) next_trading_schedule = get_next_trading_day_schedule(sat_may_23_2020) start_and_end = get_schedule_start_and_end(next_trading_schedule) start_day, end_day = [posix_to_datetime(x) for x in start_and_end] localizer = pytz.timezone(TIMEZONE) expected_start = localizer.localize(dt(2020, 5, 26, 9, 30)) expected_end = localizer.localize(dt(2020, 5, 26, 16, 0)) self.assertEqual(start_day, expected_start) self.assertEqual(end_day, expected_end)
def test_trade_time_index(self): with self.engine.connect() as conn: prices = pd.read_sql("SELECT * FROM prices;", conn) prices["timestamp"] = prices["timestamp"].apply( lambda x: posix_to_datetime(x)) with self.assertRaises(Exception): prices["t_index"] = trade_time_index(prices["timestamp"]) prices.sort_values("timestamp", inplace=True) prices["t_index"] = trade_time_index(prices["timestamp"]) self.assertEqual(prices["t_index"].nunique(), N_PLOT_POINTS)
def make_null_chart(null_label: str): """Null chart function for when a game has just barely gotten going / has started after hours and there's no data. For now this function is a bit unnecessary, but the idea here is to be really explicit about what's happening so that we can add other attributes later if need be. """ schedule = get_next_trading_day_schedule(dt.utcnow()) start, end = [posix_to_datetime(x) for x in get_schedule_start_and_end(schedule)] labels = [datetime_to_posix(t) for t in pd.date_range(start, end, N_PLOT_POINTS)] data = [STARTING_VIRTUAL_CASH for _ in labels] return dict(labels=labels, datasets=[ dict(label=null_label, data=data, borderColor=NULL_RGBA, backgroundColor=NULL_RGBA, fill=False)])
def test_task_caching(self): rds.flushall() test_time = posix_to_datetime(time.time()).date() start = time.time() _ = get_trading_calendar(test_time, test_time) time1 = time.time() - start start = time.time() _ = get_trading_calendar(test_time, test_time) time2 = time.time() - start self.assertLess(time2, time1 / 2) # "2" is a hueristic for 'substantial performance improvement' self.assertIn("rc:get_trading_calendar", rds.keys()[0])
def check_if_payout_time(current_time: float, payout_time: float) -> bool: if current_time >= payout_time: return True if during_trading_day() and current_time < payout_time: return False next_day_schedule = get_next_trading_day_schedule(posix_to_datetime(current_time) + timedelta(days=1)) next_trade_day_start, _ = get_schedule_start_and_end(next_day_schedule) if next_trade_day_start > payout_time: return True return False
def test_metrics(self): """The canonical game #3 has 5 days worth of stock data in it. We'll use that data here to test canonical values for the game winning metrics """ game_id = 3 user_id = 1 game_info = get_game_info(game_id) offset = make_date_offset(game_info["side_bets_period"]) with freeze_time( posix_to_datetime(simulation_start_time - 60) + offset): return_ratio, sharpe_ratio = calculate_metrics( game_id, user_id, simulation_start_time, time.time()) self.assertAlmostEqual(return_ratio, -0.6133719, 4) self.assertAlmostEqual(sharpe_ratio, -0.5495623, 4)
def parse_nasdaq_splits(df: pd.DataFrame): num_cols = ["numerator", "denominator"] current_datetime = posix_to_datetime( time.time(), timezone="UTC") # because selenium defaults to UTC current_date = current_datetime.date() df["executionDate"] = pd.to_datetime(df["executionDate"]) df = df[df["executionDate"].dt.date == current_date] df = df[df["ratio"].str.contains( ":" )] # sometimes the calendar encodes splits as %. We don't handle this for now if not df.empty: df[num_cols] = df["ratio"].str.split(" : ", expand=True) df[num_cols] = df[num_cols].apply(pd.to_numeric) df["exec_date"] = get_day_start(current_datetime) df = df[num_cols + ["symbol", "exec_date"]] return df
def parse_yahoo_splits(df: pd.DataFrame, excluded_symbols=None): if df.empty: return df current_datetime = posix_to_datetime( time.time(), timezone="UTC") # because selenium defaults to UTC num_cols = ["denominator", "numerator"] if excluded_symbols is None: excluded_symbols = [] df = df[~df["symbol"].isin(excluded_symbols)] if df.empty: del df["ratio"] return df df[num_cols] = df["ratio"].str.split(" - ", expand=True) df[num_cols] = df[num_cols].apply(pd.to_numeric) df["exec_date"] = get_day_start(current_datetime) return df[["symbol", "exec_date"] + num_cols]
def get_downloadable_transactions_table(game_id: int, user_id: int): sql = """ SELECT g.balance_type, g.symbol, g.balance, g.timestamp, o.buy_or_sell, o.quantity, o.time_in_force, os.clear_price FROM game_balances g LEFT JOIN ( SELECT * FROM order_status ) os ON os.id = g.order_status_id LEFT JOIN ( SELECT * FROM orders ) o ON o.id = os.order_id WHERE g.game_id = %s AND g.user_id = %s ORDER BY g.id;""" with engine.connect() as conn: df = pd.read_sql(sql, conn, params=[game_id, user_id]) df = df.where(pd.notnull(df), None) df["timestamp"] = df["timestamp"].apply(lambda x: posix_to_datetime(x)) return df.to_dict(orient="records")
def get_order_expiration_status(order_id): """Before processing an order, we'll use logic to determine whether that order is still active. This function return True if an order is expired, or false otherwise. """ with engine.connect() as conn: time_in_force = conn.execute( "SELECT time_in_force FROM orders WHERE id = %s;", order_id).fetchone()[0] if time_in_force == "until_cancelled": return False # posix_to_datetime current_time = time.time() with engine.connect() as conn: time_placed = conn.execute( """SELECT timestamp FROM order_status WHERE order_id = %s ORDER BY id LIMIT 0, 1;""", order_id).fetchone()[0] time_placed_nyc = posix_to_datetime(time_placed) cal_ref_time = time_placed_nyc.date() schedule = get_trading_calendar(cal_ref_time, cal_ref_time) if schedule.empty: next_day_schedule = get_next_trading_day_schedule(time_placed_nyc) _, cutoff_time = get_schedule_start_and_end(next_day_schedule) else: if time_placed_nyc.hour >= END_OF_TRADE_HOUR: next_day_schedule = get_next_trading_day_schedule(time_placed_nyc + timedelta( days=1)) _, cutoff_time = get_schedule_start_and_end(next_day_schedule) else: _, cutoff_time = get_schedule_start_and_end(schedule) if current_time > cutoff_time: return True return False
class TestStockbetsRanking(StockbetsRatingCase): """Test stockbets ratings updates following a decent-sized multiplayer game""" maxDiff = None RECORDS = [ {'id': 1, 'user_id': 1.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1591402922.88987, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 10, 'user_id': 10.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1592102702.01045, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 28, 'user_id': 28.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1592515077.34491, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 29, 'user_id': 29.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1592516128.51439, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 86, 'user_id': 44.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1595394720.4184, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 87, 'user_id': 45.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1595423428.28603, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 110, 'user_id': 55.0, 'index_symbol': None, 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': 1595958906.4312, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 111, 'user_id': nan, 'index_symbol': '^IXIC', 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': -99.0, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 112, 'user_id': nan, 'index_symbol': '^GSPC', 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': -99.0, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 113, 'user_id': nan, 'index_symbol': '^DJI', 'game_id': None, 'rating': 1000.0, 'update_type': 'sign_up', 'timestamp': -99.0, 'basis': 0.0, 'n_games': 0, 'total_return': 0.0}, {'id': 114, 'user_id': 1.0, 'index_symbol': None, 'game_id': '47', 'rating': 1144.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.0762641150000003}, {'id': 115, 'user_id': 10.0, 'index_symbol': None, 'game_id': '47', 'rating': 920.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': -0.0165752899999999}, {'id': 116, 'user_id': 28.0, 'index_symbol': None, 'game_id': '47', 'rating': 952.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.0}, {'id': 117, 'user_id': 29.0, 'index_symbol': None, 'game_id': '47', 'rating': 856.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': -0.095518835}, {'id': 118, 'user_id': 44.0, 'index_symbol': None, 'game_id': '47', 'rating': 888.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': -0.0240974450000001}, {'id': 119, 'user_id': 45.0, 'index_symbol': None, 'game_id': '47', 'rating': 1016.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.01191425}, {'id': 120, 'user_id': 55.0, 'index_symbol': None, 'game_id': '47', 'rating': 984.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 9.66199999998807e-05}, {'id': 121, 'user_id': nan, 'index_symbol': '^DJI', 'game_id': '47', 'rating': 1112.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.0403694553583551}, {'id': 122, 'user_id': nan, 'index_symbol': '^GSPC', 'game_id': '47', 'rating': 1080.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.0313045035144639}, {'id': 123, 'user_id': nan, 'index_symbol': '^IXIC', 'game_id': '47', 'rating': 1048.0, 'update_type': 'game_end', 'timestamp': 1599854400.0, 'basis': 1000000.0, 'n_games': 1, 'total_return': 0.030142483456657} ] LEADERBOARD = [ {'username': '******', 'user_id': 1.0, 'rating': 1144, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/c0f0bc6489851026b29b0e1e0e60ece21daf93632347a44f600dc5ce', 'n_games': 1, 'three_month_return': 0.0762641150000003}, {'username': '******', 'user_id': None, 'rating': 1112, 'profile_pic': 'https://stockbets-public.s3.amazonaws.com/profile_pics/8bd8ec5f6126dbceabe5aae0b255b50dcdf09b3128cea8f53e8eb091', 'n_games': 1, 'three_month_return': 0.0403694553583551}, {'username': '******', 'user_id': None, 'rating': 1080, 'profile_pic': 'https://stockbets-public.s3.amazonaws.com/profile_pics/fe2862aca264a58ef2f8fb2d22fa8d4dd112fd06bb3e8bf2bb8bddb6', 'n_games': 1, 'three_month_return': 0.0313045035144639}, {'username': '******', 'user_id': None, 'rating': 1048, 'profile_pic': 'https://stockbets-public.s3.amazonaws.com/profile_pics/044c7859dc114c52135ad159fcb7b817ad04b5a3c44c788672796b9d', 'n_games': 1, 'three_month_return': 0.030142483456657}, {'username': '******', 'user_id': 45.0, 'rating': 1016, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/aefac8aa916dccabbf7b444e5a38436d517437f37c3a981f51f68c47', 'n_games': 1, 'three_month_return': 0.01191425}, {'username': '******', 'user_id': 55.0, 'rating': 984, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/8b7546390be79ba37a3f31d07caac05fcb0f6deb98f7ff34bb75cd74', 'n_games': 1, 'three_month_return': 9.66199999998807e-05}, {'username': '******', 'user_id': 28.0, 'rating': 952, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/1251754a5111216d995e8c9408fd5699a8f73a2117cd2c52143ffd38', 'n_games': 1, 'three_month_return': 0.0}, {'username': '******', 'user_id': 10.0, 'rating': 920, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/50ff504bc91aecd2c942758b8adc6c9616ab0ce0951f019ab44571f0', 'n_games': 1, 'three_month_return': -0.0165752899999999}, {'username': '******', 'user_id': 44.0, 'rating': 888, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/2321842dbba174eeaf8f75cd2b5798dda2d2be9f321ce5b98533cd48', 'n_games': 1, 'three_month_return': -0.0240974450000001}, {'username': '******', 'user_id': 29.0, 'rating': 856, 'profile_pic': 'https://s3.amazonaws.com/stockbets-public/profile_pics/4f6199cd963305ef8cb6154956ff875dec86bff210d963b1dccc3263', 'n_games': 1, 'three_month_return': -0.095518835} ] @freeze_time(posix_to_datetime(1601950823.7715478)) def test_stockbets_ranking(self): update_ratings(self.game_id) with self.engine.connect() as conn: df = pd.read_sql("SELECT * FROM stockbets_rating", conn) pd.testing.assert_frame_equal(df, pd.DataFrame(self.RECORDS)) # make the public rankings JSON serialize_and_pack_rankings() session_token = create_jwt("*****@*****.**", 1, "aaron", mins_per_session=1_000_000) res = self.requests_session.post(f"{HOST_URL}/public_leaderboard", cookies={"session_token": session_token}, verify=False) self.assertEqual(res.json(), self.LEADERBOARD) res = self.requests_session.post(f"{HOST_URL}/home", cookies={"session_token": session_token}, verify=False) test_user_rank = float(rds.get(f"{PLAYER_RANK_PREFIX}_1")) test_user_return = float(rds.get(f"{THREE_MONTH_RETURN_PREFIX}_1")) self.assertEqual(res.json()["rating"], test_user_rank) self.assertEqual(test_user_rank, 1144.0) self.assertEqual(res.json()["three_month_return"], test_user_return) self.assertEqual(test_user_return, 0.0762641150000003)
def log_winners(game_id: int, current_time: float): update_performed = False game_info = query_to_dict("SELECT * FROM games WHERE id = %s", game_id)[0] game_start, game_end, start_dt, end_dt, benchmark, side_bets_perc, stakes, offset = get_winners_meta_data(game_id) start_dt = posix_to_datetime(game_start) end_dt = posix_to_datetime(game_end) # If we have sidebets to monitor, see if we have a winner if side_bets_perc: last_interval_end = get_last_sidebet_payout(game_id) if not last_interval_end: last_interval_end = game_start last_interval_dt = posix_to_datetime(last_interval_end) payout_time = datetime_to_posix(last_interval_dt + offset) if check_if_payout_time(current_time, payout_time): win_type = "sidebet" curr_time_dt = posix_to_datetime(current_time) anchor_time = last_interval_dt + timedelta(days=1) expected_sidebet_dates = get_expected_sidebets_payout_dates(start_dt, end_dt, side_bets_perc, offset) curr_interval_end = [date for date in expected_sidebet_dates if anchor_time < date <= curr_time_dt][0] curr_interval_posix = datetime_to_posix(curr_interval_end) winner_id, score = get_winner(game_id, last_interval_end, curr_interval_posix, benchmark) payout = get_sidebet_payout(game_id, side_bets_perc, offset, stakes) winner_table_id = add_row("winners", game_id=game_id, winner_id=winner_id, score=float(score), timestamp=current_time, payout=payout, type=win_type, benchmark=benchmark, end_time=curr_interval_posix, start_time=last_interval_end) update_performed = True if stakes == "real": payment_profile = get_payment_profile_uuids([winner_id])[0] send_paypal_payment( uuids=[payment_profile["uuid"]], amount=payout, payment_type="sidebet", email_subject=f"Congrats on winning the {game_info['side_bets_period']} sidebet!", email_content=f"You came out on top in {game_info['title']} this week. Here's your payment of {USD_FORMAT.format(payout)}", note_content="Keep on crushing it." ) add_row("payments", user_id=winner_id, profile_id=payment_profile["id"], game_id=game_id, winner_table_id=winner_table_id, type=win_type, amount=payout, currency="USD", direction="outflow", timestamp=current_time) # if we've reached the end of our game, pay out the winner and mark the game as completed if current_time >= game_end: win_type = "overall" payout = get_overall_payout(game_id, side_bets_perc, stakes) winner_id, score = get_winner(game_id, game_start, game_end, benchmark) winner_table_id = add_row("winners", game_id=game_id, winner_id=winner_id, benchmark=benchmark, score=float(score), start_time=game_start, end_time=game_end, payout=payout, type=win_type, timestamp=current_time) update_performed = True if stakes == "real": pass # TODO: uncomment when we're able to take real money # payment_profile = get_payment_profile_uuids([winner_id])[0] # send_paypal_payment( # uuids=[payment_profile["uuid"]], # amount=payout, # payment_type="overall", # email_subject=f"Congrats on winning the {game_info['title']}!", # email_content=f"You were the overall winner of {game_info['title']}. Awesome work. Here's your payment of {USD_FORMAT.format(payout)}. Come back soon!", # note_content="Keep on crushing it." # ) # add_row("payments", user_id=winner_id, profile_id=payment_profile["id"], game_id=game_id, # winner_table_id=winner_table_id, type=win_type, amount=payout, currency="USD", # direction="outflow", timestamp=current_time) return update_performed
def portfolio_value_by_day(game_id: int, user_id: int, start_time: float, end_time: float) -> pd.DataFrame: start_time, end_time = get_time_defaults(game_id, start_time, end_time) df = make_historical_balances_and_prices_table(game_id, user_id) df = df[(df["timestamp"] >= posix_to_datetime(start_time)) & (df["timestamp"] <= posix_to_datetime(end_time))] df = df.groupby(["symbol", "timestamp"], as_index=False)["value"].agg("last") return df.groupby("timestamp", as_index=False)["value"].sum()
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 format_posix_time(ts: float): if np.isnan(ts): return ts dtime = posix_to_datetime(ts) return dtime.strftime(DATE_LABEL_FORMAT)