from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) import random import math import statistics author = 'Your name here' doc = """ Your app description """ class Constants(BaseConstants): name_in_url = 'labessex' players_per_group = None # not used num_rounds = 6 # there is an extra round because in that it will be chosen the binding time market endowment = c(10) token_rounds = 2 # number or round with tokens time_rounds = 3 # number or rounds with time class Subsession(BaseSubsession): mkt_price = models.CurrencyField() # mkt price of each mkt number_of_trades = models.IntegerField() # number of trades of each mkt excess_demand = models.IntegerField() # excess demand of each mkt binding_mkt = models.IntegerField(initial = 0) # binding time mkt def prepare_round(self): ###--------------------- # define the market side ###--------------------- # determine randomly in all the token mkt and in the first time mkt # select randomly half of the players to be buyers # to do this, create a list with a number for each player, extract a # a random sample and make the player side == buyer if the player number # belongs to the sample extracted if self.round_number <= Constants.token_rounds + 1: players = self.get_players() num_players = len(players) half_players = num_players // 2 players_id = list(range(len(players))) buyers_id = random.sample(players_id, half_players) i = 0 for p in players: if i in buyers_id: p.side = 'buyer' i = i + 1 else: p.side = 'seller' i = i + 1 # in the following time mkt keep it constant if self.round_number > Constants.token_rounds + 1 and self.round_number < Constants.num_rounds: players = self.get_players() for p in players: p.side = p.in_round(Constants.token_rounds + 1).side ### -------------------------------------------- # assign the token value to each player randomly ### -------------------------------------------- # do it only for the token mkt if self.round_number <= Constants.token_rounds: players = self.get_players() # list of players for p in players: # choose randomly the token value from a uniform distribution and round it p.token_value = random.uniform (0.01, 5) p.token_value = round(p.token_value, 2) def equilibrium(self): # now consider the rounds in which there is a mkt if self.round_number <= Constants.token_rounds + Constants.time_rounds: players = self.get_players() # list of players ### --------------------- # determine the mkt price ### --------------------- # here i determine the demand and the supply at each price and then i will use them to determine the mkt price # move decision field to wtp and wta for p in players: if p.side == 'buyer': p.wtp = p.decision if p.side == 'seller': p.wta = p.decision # set wtp and wta to none if the form was autosubmitted: for p in players: if p.auto_submitted == True: p.wtp = None p.wta = None # take in mind that this allow for the possibility that some buyer/seller have a None wtp/a # get the list of prices stated by buyers and sellers prices_b = [p.wtp for p in players] # list of wtp prices_b = [i for i in prices_b if i != None] # remove the None entries (because sellers have a None by default and do not consider the autosubmitted) prices_s = [p.wta for p in players] # list of wta prices_s = [i for i in prices_s if i != None] # remove the None entries (because buyers have a None by default and do not consider the autosubmitted) # go on for the eq only if at least one buyer and one seller answered if len(prices_b) > 0 and len(prices_s) > 0: # compute eq only if there is space for trade, that is there is # at least someone willing to pay at most the minimum price asked if max(prices_b) >= min(prices_s): # determine the highest price stated # i need it to have an upper bound while looking for the mkt price in the loop later highest_prices = [] # list of highest wtp and wta highest_prices.append(max(prices_b)) highest_prices.append(max(prices_s)) max_price = math.ceil(max(highest_prices)) # highest price stated in the round # get the list of demand, supply, and distance, here prices are incremental at 0.01 # let me describe briefly the strategy used so i can remember it. # i create many lists, each describe a certain quantity at the corresponding price. # the price is just the position index of the element / 100, # that is [0] is price 0, [1] is price 1... # so demand_list[532] will be the demand for a price of 5.32, # that is the number of people who have stated a wtp >= 5.32 # all the other lists are defined in the same way for supply, excess demand, net excess demand and number of trades increment = 100 # number of steps list_length = (max_price + 1) * increment # lenght of the list, use the +1 to be sure to include the max price (because range excludes the last) demand_list = [] # empty list for demand at each price supply_list = [] # empty list for supply at each price excess_demand_list = [] # empty list for excess demand net_list = [] # empty list for net distance between supply and demand trades_list = [] # empty list for number of trades # fill these lists for i in range(list_length): price_considered = i / increment # the price is just the index of position / 100 demand_list.append(len([j for j in prices_b if j >= price_considered])) # count the number of buyers with wtp >= i/100 and put in position i supply_list.append(len([j for j in prices_s if j <= price_considered])) # count the number of sellers with wta <= i/100 and put in position i excess_demand_list.append(demand_list[i] - supply_list[i]) # excess demand at i/100 and put in position i net_list.append(abs(excess_demand_list[i])) # net distance between demand and supply at i/100 and put in position i trades_list.append(min(demand_list[i], supply_list[i])) # number of trades at i/100 and put in position i # determine mkt price, number of trades, and excess demand # here the idea is to choose the price the min the excess demand in absolute term # however, there can be many of them and they may not be equally representative of mkt equilibrium price # given that things are discrete there can be cases in which the distance # between demand and supply is the same at different prices but the number of # trades may differ instead. # so the idea is to look at all the prices that min the net excess demand # and among them restrict the attention to those the max the number of trades # then, if there are many prices satisfying these feature take the median # (the median should coincide with the mean, # given the numbers should be a ordered with steps of 0.01 # because everything is monotonic by construction) prices_min_distance = [i for i, x in enumerate(net_list) if x == min(net_list)] # prices that min the distance between demand and supply prices_max_trades = [i for i, x in enumerate(trades_list) if x == max(trades_list)] # prices that max the number of trades if len([i for i in prices_min_distance if i in prices_max_trades]) > 0: eq_prices = [i for i in prices_min_distance if i in prices_max_trades] # intersection of the two previous lists else: eq_prices = prices_min_distance # in case there is no intersection just take prices that min the distance mkt_price_index = math.floor(statistics.median(eq_prices)) # this is the index of the mkt price (which is nothing but the market prices * 100), rounding because if the number of prices is even then the median will not be integer self.mkt_price = mkt_price_index / increment # this is the value of the mkt price self.number_of_trades = trades_list[mkt_price_index] # number of trades at mkt price self.excess_demand = excess_demand_list[mkt_price_index] # excess demand at mkt price ### ----------------------------- # determine the players who trade ### ----------------------------- # here i determine which players are willing to trade at the mkt price # and in case the mkt does not clear perfectly i randomly choose which are going to trade # the mkt may not clear perfectly in few cases: # if there multiple agents entering/exiting the markets at same prices, that is more buyers (sellers) with same wtp (wta) (this could not be solved i think) # if the increment is not sufficiently small to identify the mkt clearing price (this could be solved if we allow the price to be "more continuous" than wtp and wta, that is using smaller steps for computing the mkt price, however it should not make much sense maybe) # willing to trade at the mkt price for p in players: if p.wtp != None or p.wta != None: # do this only for player who have non None wtp/a (basically those who were not autosubmitted) if p.side == 'buyer': # trade is wtp >= mkt price if p.wtp >= self.mkt_price: p.willing_to_trade = True else: p.willing_to_trade = False if p.side == 'seller': # trade is wta <= mkt price if p.wta <= self.mkt_price: p.willing_to_trade = True else: p.willing_to_trade = False else: # if people have autosubmitted set to false p.willing_to_trade = False # which players trade if mkt clears for p in players: p.has_traded = p.willing_to_trade # everyone is accomodated given mkt clears # adjust for excess demand (if any) if excess_demand_list[mkt_price_index] > 0: # if there is excess demand accomodate randomly some buyers only buyers = list(range(demand_list[mkt_price_index])) # get a list with an id for each buyer, starting from 0 not_served = random.sample(buyers, abs(excess_demand_list[mkt_price_index])) # get the ids of the buyers that will not be served i = 0 for p in players: if p.side == 'buyer' and p.has_traded == True: if i in not_served: # if the player is among those not served p.has_traded = False i = i+1 else: i = i+1 # adjust for excess supply (if any) if excess_demand_list[mkt_price_index] < 0: # if there is excess supply accomodate randomly some sellers only sellers = list(range(supply_list[mkt_price_index])) # get a list with an id for each seller, starting from 0 not_served = random.sample(sellers, abs(excess_demand_list[mkt_price_index])) # get the ids of the buyers that will not be served i = 0 for p in players: if p.side == 'seller' and p.has_traded == True: if i in not_served: p.has_traded = False i = i+1 else: i = i+1 # in case there is instead no space for trades else: self.mkt_price = None self.excess_demand = 0 self.number_of_trades = 0 for p in players: p.has_traded = False p.willing_to_trade = False # in case either no seller or no buyers answered there is no mkt else: self.mkt_price = None self.excess_demand = 0 self.number_of_trades = 0 for p in players: p.has_traded = False p.willing_to_trade = False # the None price should not be a problem. I use the price in the payoff only if people have traded, and this does not happen if there is no mkt # and the only case for the mkt price to be None is when there is no mkt ### ------------------------- # determine payoffs after mkt ### ------------------------- # here consider the token mkt # after each mkt the number of points to each participants changes accordingly to the mkt outcome # so we need to consider whether he/she has traded # and whether se owns a token at the end of the mkt, and in case # that the experimenter pays him/her the token value if self.round_number <= Constants.token_rounds: for p in players: if self.round_number == 1: # in the first round the points add to the initial endowment if p.side == 'buyer': if p.has_traded == True: # if buyers trade they lose the mkt price and receive the token value p.payoff = Constants.endowment - self.mkt_price + p.token_value if p.has_traded == False: # if buyers do not trade their points remain unchanged p.payoff = Constants.endowment if p.side == 'seller': if p.has_traded == True: # if sellers trade they just get the mkt price p.payoff = Constants.endowment + self.mkt_price if p.has_traded == False: # if sellers do not trade they receive the token value p.payoff = Constants.endowment + p.token_value else: # in the following rounds the point add to the amount of the previous round, everything else as before if p.side == 'buyer': if p.has_traded == True: p.payoff = p.in_round(self.round_number - 1).payoff - self.mkt_price + p.token_value if p.has_traded == False: p.payoff = p.in_round(self.round_number - 1).payoff if p.side == 'seller': if p.has_traded == True: p.payoff = p.in_round(self.round_number - 1).payoff + self.mkt_price if p.has_traded == False: p.payoff = p.in_round(self.round_number - 1).payoff + p.token_value # here i consider the time mkt # notice time mkt are not all binding, but the binding one will # be selected randomly later on. so in considering the potential # outcome everything must be done with respect to the amount of # points they had before the beginning of the time rounds, that # is after the last mkt if self.round_number > Constants.token_rounds and self.round_number <= Constants.token_rounds + Constants.time_rounds: for p in players: if p.side == 'buyer': if p.has_traded == True: # if buyers trade they pay the mkt price p.payoff = p.in_round(Constants.token_rounds).payoff - self.mkt_price if p.has_traded == False: # if buyers do not trade their points remain unchanged p.payoff = p.in_round(Constants.token_rounds).payoff if p.side == 'seller': if p.has_traded == True: # if sellers trade they receive the mkt price p.payoff = p.in_round(Constants.token_rounds).payoff + self.mkt_price if p.has_traded == False: # if sellers do not trade their points remain unchanged p.payoff = p.in_round(Constants.token_rounds).payoff # now consider the last round in which we select the binding time mkt # and we make the trades of that mkt to realize else: # determine which of those mkt will be binding randomly # to do so just pick a random integer in the interval that contains # the round numbers of rounds with time mkt self.binding_mkt = random.randint(Constants.token_rounds +1, Constants.token_rounds + Constants.time_rounds) # now retrieve all the data of the binding mkt self.mkt_price = self.in_round(self.binding_mkt).mkt_price self.number_of_trades = self.in_round(self.binding_mkt).number_of_trades self.excess_demand = self.in_round(self.binding_mkt).excess_demand players = self.get_players() for p in players: p.wtp = p.in_round(self.binding_mkt).wtp p.wta = p.in_round(self.binding_mkt).wta p.has_traded = p.in_round(self.binding_mkt).has_traded p.willing_to_trade = p.in_round(self.binding_mkt).willing_to_trade p.side = p.in_round(self.binding_mkt).side p.payoff = p.in_round(self.binding_mkt).payoff if p.side == 'buyer': if p.in_round(self.binding_mkt).has_traded == True: p.remain_last_part = False if p.in_round(self.binding_mkt).has_traded == False: p.remain_last_part = True if p.side == 'seller': if p.in_round(self.binding_mkt).has_traded == True: p.remain_last_part = True if p.in_round(self.binding_mkt).has_traded == False: p.remain_last_part = False class Group(BaseGroup): pass class Player(BasePlayer): decision = models.CurrencyField(min = 0, max = 30, initial = None) wtp = models.CurrencyField(min = 0, initial = None) # willingness to pay wta = models.CurrencyField(min = 0, initial = None) # willingness to accept token_value = models.CurrencyField() # value of the token has_traded = models.BooleanField() # whether the player has traded in the mkt willing_to_trade = models.BooleanField() # whether the player was willing to trade in the mkt side = models.StringField() # side of the mkt, buyer or seller remain_last_part = models.BooleanField() # whether the player has to remain in the last irrelevant part auto_submitted = models.BooleanField(initial = False) # to check whether the player's form was autosubmitted