import json import random from otree.api import * import itertools c = cu from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) import otree author = 'Jason Friedman, Student Helper COG, ETHZ' doc = """ Trading App of the Zurich Trading Simulator (ZTS). A web-based behaviour experiment in the form of a trading game, designed by the Chair of Cognitive Science - ETH Zurich. """ class Constants(BaseConstants): name_in_url = 'Social_Media_Investor' treatment_groups = [ 'treatment_4','treatment_2', 'treatment_5', 'treatment_1', 'treatment_3', 'treatment_6', 'control', ] players_per_group = None num_rounds = 10 # Actual num_rounds is specified in session config, by the length of the list 'timeseries_filename'! class Subsession(BaseSubsession): def creating_session(self): """ This function gets called before each creation of a ZTS subsession. We use it to set a random payoff round for each player. If random_round_payoff is set the payoff is generate only by looking at one random round. If we have a training_round, then the random_payoff_round will not be the training round. """ # Assume num_participants is a multiple of the number of groups num_groups = len(Constants.treatment_groups) indices = itertools.cycle(range(num_groups)) for player in self.get_players(): group_number = next(indices) treatment_group = Constants.treatment_groups[group_number] player.treatment_group = treatment_group # Set the treatment_group attribute of the Player object player.participant.vars['treatment_group'] = treatment_group # Optionally, also store it in participant.vars # The following initial session setup only needs to be called once and not for each subsession if self.round_number == 1: self.session.num_rounds = 10 for player in self.get_players(): first_round = 1 if(self.session.config['training_round']): first_round = 2 # Make some checks if the session parameters are valid if first_round > self.session.num_rounds: raise ValueError('Num rounds cannot be smaller than 1 (or 2 if there is a training session)!') player.participant.vars['round_to_pay'] = random.randint(first_round, self.session.num_rounds) def get_config_multivalue(self, value_name): """ Some config values can contain either a list of values (for each round) or a single value with this function we can provide a unified way of accessing it independently of what is the actual value format. :param value_name: the name of the config variable :return: the parsed value for the current round """ parsed_value = json.loads(self.session.config[value_name]) if isinstance(parsed_value, list): assert(len(parsed_value) >= self.session.num_rounds), value_name + ' contains less entries than effective rounds!' return parsed_value[self.round_number - 1] else: return parsed_value def get_timeseries_values(self): """ Read this rounds timeseries file and parse the lists of values :return : the list of prices and list of news """ filename = self.get_config_multivalue('timeseries_filename') asset = filename.strip('.csv') path = self.session.config['timeseries_filepath'] + filename rows = read_csv(path, TimeSeriesFile) prices = [dic['price'] for dic in rows] if 'news' in rows[0].keys(): news = [dic['news'] if dic['news'] else '' for dic in rows] else: news = '' * len(prices) return asset, prices, news class Group(BaseGroup): pass class Player(BasePlayer): treatment_group = models.StringField(blank=True) prolificid = models.StringField(label="Please enter your Prolific ID:") cash = models.FloatField(initial=10000) share_value = models.FloatField(initial=0) portfolio_value = models.FloatField(initial=10000) portfolio_profit = models.FloatField(initial=0) portfolio_ETF = models.FloatField(initial=0) portfolio_Stock = models.FloatField(initial=0) portfolio_Crypto = models.FloatField(initial=0) attention_check1 = models.FloatField(initial=0, blank=True) buy_count_VOO = models.FloatField(initial=0, blank=True, default=0) buy_count_VTI = models.FloatField(initial=0, blank=True, default=0) buy_count_ZZW = models.FloatField(initial=0, blank=True, default=0) buy_count_MDKS = models.FloatField(initial=0, blank=True, default=0) buy_count_UDX = models.FloatField(initial=0, blank=True, default=0) buy_count_DSLO = models.FloatField(initial=0, blank=True, default=0) buy_count_NOQ = models.FloatField(initial=0, blank=True, default=0) buy_count_XPOW = models.FloatField(initial=0, blank=True, default=0) buy_count_MWQ = models.FloatField(initial=0, blank=True, default=0) buy_count_STCK = models.FloatField(initial=0, blank=True, default=0) buy_count_IUM = models.FloatField(initial=0, blank=True, default=0) buy_count_JUNW = models.FloatField(initial=0, blank=True, default=0) buy_count_ETQ = models.FloatField(initial=0, blank=True, default=0) buy_count_BTC = models.FloatField(initial=0, blank=True, default=0) def get_total_investment(self): fields = [ self.buy_count_VOO, self.buy_count_VTI, self.buy_count_ZZW, self.buy_count_MDKS, self.buy_count_UDX, self.buy_count_DSLO, self.buy_count_NOQ, self.buy_count_XPOW, self.buy_count_MWQ, self.buy_count_STCK, self.buy_count_IUM, self.buy_count_JUNW, self.buy_count_ETQ, self.buy_count_BTC ] return sum(fields) def update_portfolio_value(self, prices_df): """ Update the portfolio value based on current stock prices. :param prices_df: DataFrame containing stock prices with 'Round_1' as one of the columns """ if self.subsession.round_number == 1: previous_round_number = 1 else: previous_round_number = self.subsession.round_number - 1 stock_counts = [ self.in_round(previous_round_number).buy_count_VOO, self.in_round(previous_round_number).buy_count_VTI, self.in_round(previous_round_number).buy_count_ZZW, self.in_round(previous_round_number).buy_count_MDKS, self.in_round(previous_round_number).buy_count_UDX, self.in_round(previous_round_number).buy_count_DSLO, self.in_round(previous_round_number).buy_count_NOQ, self.in_round(previous_round_number).buy_count_XPOW, self.in_round(previous_round_number).buy_count_MWQ, self.in_round(previous_round_number).buy_count_STCK, self.in_round(previous_round_number).buy_count_IUM, self.in_round(previous_round_number).buy_count_JUNW, self.in_round(previous_round_number).buy_count_ETQ, self.in_round(previous_round_number).buy_count_BTC ] stock_names = [ 'VOO', 'VTI', 'ZZW', 'MDKS', 'UDX', 'DSLO', 'NOQ', 'XPOW', 'MWQ', 'STCK', 'IUM', 'JUNW', 'ETQ', 'BTC' ] total_value = self.in_round(previous_round_number).cash share_value_counter = 0 # Initialize portfolio values for ETFs, Stocks, and Crypto portfolio_ETF_value = 0 portfolio_Stock_value = 0 portfolio_Crypto_value = 0 for stock_name, stock_count in zip(stock_names, stock_counts): # Retrieve the stock price from the DataFrame for the current round stock_price = prices_df.loc[prices_df['Modified_Ticker'] == stock_name, 'Round_1'].values[0] stock_price = stock_price.replace('$', '').replace(',', '') stock_price = float(stock_price) total_value += stock_price * stock_count share_value_counter += stock_price * stock_count # Get the stock type stock_type = prices_df.loc[prices_df['Modified_Ticker'] == stock_name, 'Type'].values[0] # Update the respective portfolio values based on stock type if stock_type == 'ETF': portfolio_ETF_value += stock_price * stock_count elif stock_type == 'Stock': portfolio_Stock_value += stock_price * stock_count elif stock_type == 'Crypto': portfolio_Crypto_value += stock_price * stock_count # Update the portfolio value self.portfolio_value = total_value self.portfolio_profit = total_value - 10000 self.share_value = share_value_counter self.cash = self.in_round(previous_round_number).cash # Update the portfolio values for ETFs, Stocks, and Crypto self.portfolio_ETF = portfolio_ETF_value self.portfolio_Stock = portfolio_Stock_value self.portfolio_Crypto = portfolio_Crypto_value def live_trading_report(self, payload): """ Accepts the "daily" trading Reports from the front end and further processes them to store them in the database :param payload: trading report """ #print('received a report from', self.id_in_group, ':', payload) self.cash = float(payload['cash']) self.shares = int(payload['owned_shares']) self.share_value = float(payload['share_value']) self.portfolio_value = float(payload['portfolio_value']) self.pandl = float(payload['pandl']) TradingAction.create( player=self, action=payload['action'], quantity = payload['quantity'], time = payload['time'], price_per_share = payload['price_per_share'], cash = payload['cash'], owned_shares = payload['owned_shares'], share_value = payload['share_value'], portfolio_value = payload['portfolio_value'], cur_day = payload['cur_day'], asset = payload['asset'], roi = payload['roi_percent'] ) # Set payoff if end of round if(payload['action'] == 'End'): self.set_payoff() def set_payoff(self): """ Set the players payoff for the current round to the total portfolio value. If we want participants final payoff to be chosen randomly from all rounds instead of accumulatee standard) subtract current payoff from participants.payoff if we are not in round_to_pay. Also payoff should not count if we are in a training round. """ self.payoff = 0 self.payoff = self.portfolio_value random_payoff = self.session.config['random_round_payoff'] training_round = self.session.config['training_round'] if(random_payoff and self.round_number != self.participant.vars['round_to_pay']): self.participant.payoff -= self.payoff elif(training_round and self.round_number == 1): self.participant.payoff -= self.payoff class TradingAction(ExtraModel): """ An extra database model that is used to store all the transactions. Each transaction is linked to the player that executed it. """ ACTIONS = [ ('Buy', 'Buy'), ('Sell', 'Sell'), ('Start', 'Start'), ('End', 'End'), ] player = models.Link(Player) action = models.CharField(choices=ACTIONS, max_length=10) quantity = models.FloatField(initial=0.0) time = models.StringField() price_per_share = models.FloatField() cash = models.FloatField() owned_shares = models.FloatField() share_value = models.FloatField() portfolio_value = models.FloatField() cur_day = models.IntegerField() asset = models.CharField(blank=True, max_length=100) roi = models.FloatField() class TimeSeriesFile(ExtraModel): date = models.StringField() price = models.FloatField() news = models.StringField() def custom_export(players): """ Create a custom export, that allows us to download more detailed trading reports as csv or excel files. NOTE: the custom export will output all Trading actions that are found in the database, i.e. also of earlier sessions --> if you do not want this you migth need to implement a filter here. :param players: queryset of all players in the database :yield: a titel row and then the corresponding values one after the other """ # header row yield ['session', 'round_nr', 'participant', 'action', 'quantity', 'price_per_share', 'cash', 'owned_shares', 'share_value', 'portfolio_value', 'cur_day', 'asset', 'roi'] # data content for p in players: for ta in TradingAction.filter(player=p): yield [p.session.code, p.subsession.round_number, p.participant.code, ta.action, ta.quantity, ta.price_per_share, ta.cash, ta.owned_shares, ta.share_value, ta.portfolio_value, ta.cur_day, ta.asset, ta.roi]