from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range ) from django.utils.translation import ugettext import random import math import time from django.conf import settings from otree.db.models import Model, ForeignKey author = 'Harry Öhman & Vivid Economics Ltd.' doc = """ A simple simulation where player are given an endowment of currency and allowances and can bid/offer to buy sell more allowances. """ class Constants(BaseConstants): name_in_url = 'simple_simulation' players_per_group = None # What this means in practice is that there's only one group per_unit_penalty = 5 num_rounds = 2 num_periods = 12 num_bots = 3 botplayer_debugging = True # Determines if botplayer tables are show on the Dashboard. Only for development purposes. price_of_investment = 400 num_high_emitters_per_group = 1 num_medium_emitters_per_group = 1 num_high_emitter_bots = 1 num_medium_emitter_bots = 1 high_emitter_high = 30 high_emitter_high_invested = 15 high_emitter_low = 10 medium_emitter_high = 20 medium_emitter_high_invested = 10 medium_emitter_low = 8 low_emitter_high = 12 low_emitter_high_invested = 6 low_emitter_low = 5 class Subsession(BaseSubsession): number_of_players = models.IntegerField(initial=0) number_of_rounds = models.IntegerField(initial=0) session_number = models.IntegerField(initial=0) current_trading_period = models.IntegerField(initial=1) def auction_form_generator(n): form_list = [] for i in range(1, n+1): form_list.append('auction_bid_quantity_' + str(i)) form_list.append('auction_bid_price_' + str(i)) return form_list def get_bot_players(self): return list(BotPlayer.objects.all().filter(session_number=self.session_number)) def get_bot_player_in_round(self, id, round_number): return BotPlayer.objects.get(id_in_group=id, session_number=self.in_round(round_number).session_number) def creating_session(self): self.number_of_rounds = Constants.num_periods players = self.get_players() self.number_of_players = len(players) self.session_number = random.randint(1,1000000) # Creating bots! if Constants.num_bots > 0: for i in range(1, Constants.num_bots + 1): # Assigning them a session number so the we can refer to a specific bot (or really set of bots) botplayer = self.botplayer_set.create( session_number=self.session_number, id_in_group = i + len(players), time = time.time() + float(31536000) # Adding a year to the timing of the bots bid submissions (otherwise they would always be first...) ) botplayer.save() # Determins which player are going to be high/low emitters. It's set so that the first, what ever number has been chosen to be high emitters, are the high emitters. Could probably to something more sophisticated using participation labels. high_emitters_counter = range(1, Constants.num_high_emitters_per_group + 1) medium_emitters_counter = range(Constants.num_high_emitters_per_group + 1, Constants.num_high_emitters_per_group + Constants.num_medium_emitters_per_group + 1) for p in players: if p.id_in_group in high_emitters_counter: p.high_emitter = True p.type_of_player = 'high_emitter' elif p.id_in_group in medium_emitters_counter: p.medium_emitter = True p.type_of_player = 'medium_emitter' else: p.low_emitter = True p.type_of_player = 'low_emitter' # Determines which bots are high, medium and low emitters botplayers = self.get_bot_players() high_emitters_counter = range(self.number_of_players + 1, self.number_of_players + Constants.num_high_emitter_bots + 1) medium_emitters_counter = range(self.number_of_players + Constants.num_high_emitter_bots + 1, self.number_of_players + Constants.num_high_emitter_bots + Constants.num_medium_emitters_per_group + 1) for p in botplayers: if p.id_in_group in high_emitters_counter: p.high_emitter = True p.type_of_player = 'high_emitter' elif p.id_in_group in medium_emitters_counter: p.medium_emitter = True p.type_of_player = 'medium_emitter' else: p.low_emitter = True p.type_of_player = 'low_emitter' p.save() market_price_history_roundx = 'market_price_history_round' + str(self.round_number) volume_traded_roundx = 'volume_traded_round' + str(self.round_number) historical_accumulated_emissions_allplayers_roundx = 'historical_accumulated_emissions_allplayers_round' + str(self.round_number) historical_accumulated_emissions_roundx = 'historical_accumulated_emissions_round' + str(self.round_number) self.session.vars['emissions_dict'] = { # Defines the emissions for the different types of players. Using a dictionary will shorten the code considerably - however, otree dosen't seem to allow you to use one in the constants class. Putting it here for now. 'low_emitter': { 'high': Constants.high_emitter_high, 'high_invested': Constants.high_emitter_high_invested, 'low': Constants.high_emitter_low}, 'medium_emitter': { 'high': Constants.medium_emitter_high, 'high_invested': Constants.medium_emitter_high_invested, 'low': Constants.medium_emitter_low}, 'high_emitter': { 'high': Constants.low_emitter_high, 'high_invested': Constants.low_emitter_high_invested, 'low': Constants.low_emitter_low}, } self.session.vars[historical_accumulated_emissions_allplayers_roundx] = [] self.session.vars[market_price_history_roundx]=[] self.session.vars[volume_traded_roundx]=[] self.session.vars["rounds"] = [1,2] # Need this list for making the drop downs in the template. for p in players: p.participant.vars[historical_accumulated_emissions_roundx] = [] p.participant.vars['sold_quantity'] = [] p.participant.vars['sold_quantity'] = [] p.participant.vars['bought_quantity'] = [] p.participant.vars['allowances_at_the_end_of_round'] = [] p.allowances_left = p.allowances - p.emissions # Here (below) goes all varibels that applies to all players, such as market price and if trade occured last period. The way to save variables are of the form below, # which basically means that they are entries into a csv file that can be exported at the end of the each sesssion. Given how I used them some of the entries will be # not very useful. However, all information we would want to export is, or can be, save and likewise can be exoprted, only we might have to be a little bit creating with # how we do it. class Group(BaseGroup): market_price = models.IntegerField(initial=0) market_quantity = models.IntegerField(initial=0) trade_occured_last_period = models.BooleanField(initial=False) accumulated_emissions_all_players = models.IntegerField(initial=0) min_emissions_all_players = models.IntegerField(initial=0) max_emissions_all_players = models.IntegerField(initial=0) current_trading_period = models.IntegerField(initial=1) all_allowances = models.IntegerField(initial=0) session_vars_dump = models.StringField() amount_of_auctioned_allowances = models.IntegerField(initial=0) auction_price = models.IntegerField(initial=0) def auction_mechanism(self): players = self.get_players() if Constants.num_bots > 0: botplayers = self.subsession.get_bot_players() players = players + botplayers bids = [] for p in players: if p.auction_bid_quantity_1 > 0: bid1 = {'q': p.auction_bid_quantity_1, 'p': p.auction_bid_price_1, 'id': p.id_in_group} bids.append(bid1) if p.auction_bid_quantity_2 > 0: bid2 = {'q': p.auction_bid_quantity_2, 'p': p.auction_bid_price_2, 'id': p.id_in_group} bids.append(bid2) if p.auction_bid_quantity_3 > 0: bid3 = {'q': p.auction_bid_quantity_3, 'p': p.auction_bid_price_3, 'id': p.id_in_group} bids.append(bid3) bids.sort(key=lambda x: x['p'], reverse=True) allowances_left = self.amount_of_auctioned_allowances price = 0 successful_bids = [] for bid in bids: if allowances_left > 0: if bid['q'] < allowances_left: successful_bids.append({'q': bid['q'], 'id': bid['id']}) allowances_left -= bid['q'] else: successful_bids.append({'q': allowances_left, 'id': bid['id']}) allowances_left -= allowances_left price = bid['p'] else: break for s in successful_bids: player = players[s['id'] - 1] player.allowances += s['q'] player.money -= s['q']*price player.allowances_bought_at_auction = s['q'] if player.botplayer: player.save() self.auction_price = price def save_emissions_information(self): players = self.get_players() if Constants.num_bots > 0: botplayers = self.subsession.get_bot_players() players = players + botplayers self.accumulated_emissions_all_players = 0 historical_accumulated_emissions_roundx = 'historical_accumulated_emissions_round' + str(self.round_number) for p in players: p.min_emissions += self.subsession.session.vars['emissions_dict'][p.type_of_player]['low'] if p.invest: p.max_emissions += self.subsession.session.vars['emissions_dict'][p.type_of_player]['high_invested'] else: p.max_emissions += self.subsession.session.vars['emissions_dict'][p.type_of_player]['high'] self.max_emissions_all_players += p.max_emissions self.min_emissions_all_players += p.min_emissions if p.botplayer == False: # Have to come up with some way of save infromation on the botplayer class, or do we? p.participant.vars[historical_accumulated_emissions_roundx].append(p.emissions) p.allowances_left = p.allowances - p.emissions self.accumulated_emissions_all_players += p.emissions historical_accumulated_emissions_allplayers_roundx = 'historical_accumulated_emissions_allplayers_round' + str(self.round_number) self.session.vars[historical_accumulated_emissions_allplayers_roundx].append(self.accumulated_emissions_all_players) def calculate_market_price(self): players = self.get_players() # Creates a list with all players if Constants.num_bots > 0: botplayers = self.subsession.get_bot_players() random.shuffle(botplayers) players = players + botplayers players.sort(key=lambda x: x.time, reverse=False) #sorts the list according to the time they arrived - the first one comes first prices = [] for p in players: # Creates one list with all prices. prices.append(p.demand_price) prices.append(p.supply_price) demanded_and_supplied_quantities = [] for i in prices: # The aim of this loop is the create a list of lists with the demanded and supplied quantites at every price level. quantities = [] total_demanded_quantity = 0 total_supplied_quantity = 0 for p in players: if p.demand_price >= i: total_demanded_quantity += p.demand_quantity if p.supply_price <= i: total_supplied_quantity += p.supply_quantity quantities = [total_supplied_quantity, total_demanded_quantity] demanded_and_supplied_quantities.append(quantities) # It will always be the smaller of the demanded of supplied quantities the will be actual traded quantity. # Hence, we use this next loop to generate a list of what the actaul traded quantities would be a every price. traded_quantities = [min(q) for q in demanded_and_supplied_quantities] self.market_quantity = max(traded_quantities) volume_traded_roundx = 'volume_traded_round' + str(self.round_number) # Saving the volume traded in each round self.session.vars[volume_traded_roundx].append(self.market_quantity) # Making a list out of those market prices that corresponds with the market price indices_of_possible_market_prices = [i for i, x in enumerate(traded_quantities) if x == max(traded_quantities)] possible_market_prices = [prices[i] for i in indices_of_possible_market_prices] # This comments the code below. The list of prices are sorted in chronological order, i.e. the first one in the list will be the "first movers price". Furthermore, # when we made the price list we did so by add first a demand price and then a supply price, which means that if the index of the first entered market price is even # then it is a demand price (the index starts at 0). Following Luca's paper, if the first quantity maximizing price entered is a demand price the # the market price will be the lowest possible such price and vice versa. if indices_of_possible_market_prices[0] % 2 == 0: self.market_price = min(possible_market_prices) else: self.market_price = max(possible_market_prices) def execute_trades(self): players = self.get_players() if Constants.num_bots > 0: botplayers = self.subsession.get_bot_players() random.shuffle(botplayers) players = players + botplayers # Creates a list with all players players.sort(key=lambda x: x.demand_price, reverse=True) # Sorts player after their demand price, from highest to lowest counter_demand = self.market_quantity # keeps track of how much of the demand is left all_previous_period_sales = [] # This goes through all players and, for the players whos suppy price is less than the market price, deducts allowances and gives them money accordingly for p in players: initial_allowances = p.allowances if p.supply_price <= self.market_price: if counter_demand >= p.supply_quantity: p.money = p.money + p.supply_quantity*self.market_price p.allowances = p.allowances - p.supply_quantity counter_demand = counter_demand - p.supply_quantity else: p.money = p.money + counter_demand*self.market_price p.allowances = p.allowances - counter_demand if p.botplayer == False: p.previous_periods_sale = initial_allowances - p.allowances p.participant.vars['sold_quantity'].append(p.previous_periods_sale) all_previous_period_sales.append(p.previous_periods_sale) if p.botplayer: p.save() players.sort(key=lambda x: x.supply_price, reverse=False) # Sorts players after thier supply price, in ascending order counter_supply = self.market_quantity # keeps track of how much of the demand is left # This goes through all players and, for the players whos demand price is higher than the market price, deducts money and gives them allowances accordingly for p in players: initial_allowances = p.allowances if p.demand_price >= self.market_price: if counter_supply >= p.demand_quantity: p.money = p.money - p.demand_quantity*self.market_price p.allowances = p.allowances + p.demand_quantity counter_supply = counter_supply - p.demand_quantity else: p.money = p.money - counter_supply*self.market_price p.allowances = p.allowances - counter_supply if p.botplayer == False: p.previous_periods_purchase = p.allowances - initial_allowances p.participant.vars['bought_quantity'].append(p.previous_periods_purchase) if p.botplayer: p.save() self.trade_occured_last_period = False if any(all_previous_period_sales): self.trade_occured_last_period = True market_price_history_roundx = 'market_price_history_round' + str(self.round_number) self.session.vars[market_price_history_roundx].append(self.market_price) else: market_price_history_roundx = 'market_price_history_round' + str(self.round_number) self.session.vars[market_price_history_roundx].append(None) def allowance_allocator(self): players = self.get_players() if Constants.num_bots > 0: botplayers = self.subsession.get_bot_players() players = players + botplayers expected_emissions_per_cycle_high_emitter = (0.5*(Constants.high_emitter_high) + 0.5*(Constants.high_emitter_low))*Constants.num_periods expected_emissions_per_cycle_medium_emitter = (0.5*(Constants.medium_emitter_high) + 0.5*(Constants.medium_emitter_low))*Constants.num_periods expected_emissions_per_cycle_low_emitter = (0.5*(Constants.low_emitter_high) + 0.5*(Constants.low_emitter_low))*Constants.num_periods percent_auctions = self.session.config['percentage_of_allowances_allocated_by_auctions'] if percent_auctions == 0: free_allowances_per_cycle_high_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_high_emitter)) free_allowances_per_cycle_medium_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_medium_emitter)) free_allowances_per_cycle_low_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_low_emitter)) else: free_allowances_per_cycle_high_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_high_emitter*(1-percent_auctions))) free_allowances_per_cycle_medium_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_medium_emitter*(1-percent_auctions))) free_allowances_per_cycle_low_emitter = int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_low_emitter*(1-percent_auctions))) for p in players: if p.high_emitter: self.amount_of_auctioned_allowances += int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_high_emitter*percent_auctions)) elif p.medium_emitter: self.amount_of_auctioned_allowances += int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_medium_emitter*percent_auctions)) else: self.amount_of_auctioned_allowances += int(round(self.session.config['emissions_cap']*expected_emissions_per_cycle_low_emitter*percent_auctions)) if self.round_number > 1: if p.botplayer == False: if p.high_emitter: p.allowances = p.in_round(p.round_number - 1).allowances + free_allowances_per_cycle_high_emitter elif p.medium_emitter: p.allowances = p.in_round(p.round_number - 1).allowances + free_allowances_per_cycle_medium_emitter else: p.allowances = p.in_round(p.round_number - 1).allowances + free_allowances_per_cycle_low_emitter else: if p.high_emitter: p.allowances = self.subsession.get_bot_player_in_round(p.id_in_group, self.round_number - 1).allowances + free_allowances_per_cycle_high_emitter elif p.medium_emitter: p.allowances = self.subsession.get_bot_player_in_round(p.id_in_group, self.round_number - 1).allowances + free_allowances_per_cycle_medium_emitter else: p.allowances = self.subsession.get_bot_player_in_round(p.id_in_group, self.round_number - 1).allowances + free_allowances_per_cycle_low_emitter else: if p.high_emitter: p.allowances = free_allowances_per_cycle_high_emitter elif p.medium_emitter: p.allowances = free_allowances_per_cycle_medium_emitter else: p.allowances = free_allowances_per_cycle_low_emitter self.all_allowances += p.allowances if p.botplayer: p.save() class Player(BasePlayer): #Auction def make_auction_bid_quatity_field(): return models.IntegerField( initial=0, blank=0, min=0, label='' ) def make_auction_bid_price_field(): return models.IntegerField( initial=0, blank=0, min=0, label='' ) auction_bid_quantity_1 = make_auction_bid_quatity_field() auction_bid_price_1 = make_auction_bid_price_field() auction_bid_quantity_2 = make_auction_bid_quatity_field() auction_bid_price_2 = make_auction_bid_price_field() auction_bid_quantity_3 = make_auction_bid_quatity_field() auction_bid_price_3 = make_auction_bid_price_field() botplayer = models.BooleanField(initial=False) type_of_player = models.StringField() emissions = models.IntegerField(initial=0) money = models.IntegerField(initial=10000) allowances = models.IntegerField(initial=0) sale_successful = models.BooleanField(initial=False) purchase_successful = models.BooleanField(initial=False) not_complying = models.BooleanField(initial=False) total_penalty = models.IntegerField(initial=0) high_emitter = models.BooleanField(initial=False) medium_emitter = models.BooleanField(initial=False) low_emitter = models.BooleanField(initial=False) previous_periods_sale = models.IntegerField(initial=0) previous_periods_purchase = models.IntegerField(initial=0) already_invested = models.BooleanField(initial=False) # We need this one to know if we at all should give players the option to invest in abatement technology. allowances_left = models.IntegerField(initial=0) min_emissions = models.IntegerField(initial=0) max_emissions = models.IntegerField(initial=0) time = models.FloatField(initial=0.0) participant_vars_dump = models.StringField() allowances_bought_at_auction = models.IntegerField(initial=0) invest = models.BooleanField( # This is the abatement technology field. Will only be shown if aready_invested is false. label="Do you want to invest in abatement technology? (£400)", choices=[ [False, "Don't invest"], [True, "Invest"], ], initial=False, blank=True # This is needed becuase of the fact the when they already have invested, the field won't show up at all ) demand_price = models.IntegerField( min=0, blank=0, label = ugettext("Price:") ) demand_quantity = models.IntegerField( min=0, blank=0, label = ugettext("Quantity:") ) supply_price = models.IntegerField( min=0, blank=0, label = ugettext("Price:") ) supply_quantity = models.IntegerField( min=0, blank=0, label = ugettext("Quantity:") ) def supply_quantity_error_message(self, value): #Ensures that players can't offer more allowances than they have. The same thing for bids (the ensure the players can bid more than they have money for) is defined in the pages.py. print ('value is', value) if value > self.allowances and value > 0: return ugettext('You cannot offer more allowances than you own.') def emission_generator(self): a_random_number = random.random() if a_random_number >= 0.5: if self.invest: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['high_invested'] else: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['high'] else: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['low'] class BotPlayer(models.Model): # These two fields are needed because we need to be able to identify the bots as beloning to a session and a round (compliance cycle). This is done automatically for the noraml players. # The rest is basically a copy of the normal player. subsession = ForeignKey(Subsession) session_number = models.IntegerField(initial=0) round_number = models.IntegerField(initial=0) id_in_group = models.IntegerField(initial=0) botplayer = models.BooleanField(initial=True) #Auction def make_auction_bid_quatity_field(): return models.IntegerField( initial=0, blank=0, ) def make_auction_bid_price_field(): return models.IntegerField( initial=0, blank=0, ) auction_bid_quantity_1 = make_auction_bid_quatity_field() auction_bid_price_1 = make_auction_bid_price_field() auction_bid_quantity_2 = make_auction_bid_quatity_field() auction_bid_price_2 = make_auction_bid_price_field() auction_bid_quantity_3 = make_auction_bid_quatity_field() auction_bid_price_3 = make_auction_bid_price_field() money = models.IntegerField(initial=10000) type_of_player = models.StringField() emission = models.IntegerField(initial=0) emissions = models.IntegerField(initial=0) money = models.IntegerField(initial=10000) allowances = models.IntegerField(initial=0) sale_successful = models.BooleanField(initial=False) purchase_successful = models.BooleanField(initial=False) not_complying = models.BooleanField(initial=False) total_penalty = models.IntegerField(initial=0) high_emitter = models.BooleanField(initial=False) medium_emitter = models.BooleanField(initial=False) low_emitter = models.BooleanField(initial=False) previous_periods_sale = models.IntegerField(initial=0) previous_periods_purchase = models.IntegerField(initial=0) already_invested = models.BooleanField(initial=False) # We need this one to know if we at all should give players the option to invest in abatement technology. allowances_left = models.IntegerField(initial=0) min_emissions = models.IntegerField(initial=0) max_emissions = models.IntegerField(initial=0) time = models.FloatField(initial=0) participant_vars_dump = models.StringField() allowances_bought_at_auction = models.IntegerField(initial=0) invest = models.BooleanField( initial=False, blank=True ) demand_price = models.IntegerField( initial=0, min=0, blank=0, ) demand_quantity = models.IntegerField( initial=0, min=0, blank=0, ) supply_price = models.IntegerField( initial=0, min=0, blank=0, ) supply_quantity = models.IntegerField( initial=0, min=0, blank=0, ) def emission_generator(self): a_random_number = random.random() if a_random_number >= 0.5: if self.invest: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['high_invested'] else: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['high'] else: self.emissions += self.subsession.session.vars['emissions_dict'][self.type_of_player]['low'] self.save() def auction_play(self): # How Many Allowances do I need? expected_emissions_at_the_end_of_cycle = int(round( 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['low'] + 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['high'] ))*Constants.num_periods #To make it simple, the bot will consider two options: either buy allowances or invest in technology. Therefore the price should be "price of investment"/"allowances needed". vaule_of_allowaces = int(round(Constants.price_of_investment / expected_emissions_at_the_end_of_cycle)) # makes one bid # To make it more interesting and also to make the bots supply liquidity to the market make them only bid (in the auction) a percentage of the allowances the need equal to the percentage of allowance allocated through auctions bid_amount = int(round(expected_emissions_at_the_end_of_cycle*self.subsession.session.config['percentage_of_allowances_allocated_by_auctions'])) self.auction_bid_quantity_1 = bid_amount self.auction_bid_price_1 = vaule_of_allowaces self.save() def trading_play(self): #Find out how many allowance I am lacking. rounds_left = Constants.num_periods - self.subsession.current_trading_period + 1 if self.invest: expected_emissions_per_round = int(round( 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['low'] + 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['high_invested'] )) else: expected_emissions_per_round = int(round( 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['low'] + 0.5*self.subsession.session.vars['emissions_dict'][self.type_of_player]['high'] )) expected_emissions_at_the_end_of_cycle = self.emissions + expected_emissions_per_round*rounds_left allowances_difference = expected_emissions_at_the_end_of_cycle - self.allowances #To make it simple, the bot will consider two options: either buy allowances or invest in technology. Therefore a reasonable valuation of an allowance could be "price of investment"/"allowances needed". vaule_of_allowances = int(round(Constants.price_of_investment / expected_emissions_at_the_end_of_cycle)) #To make it more interesting, the bots will plan to buy/sell a little bit every round instead of every thing at once. bid_amount = int(round(allowances_difference / rounds_left)) if self.allowances < expected_emissions_at_the_end_of_cycle: self.demand_price = vaule_of_allowances self.demand_quantity = bid_amount self.supply_price = 0 self.supply_quantity = 0 else: self.supply_price = vaule_of_allowances self.supply_quantity = - bid_amount self.supply_price = 0 self.supply_quantity = 0 self.save() def reset_bids(self): self.demand_price = 0 self.demand_quantity = 0 self.supply_price = 0 self.supply_quantity = 0 self.save() def guess_market_price(self): pass def calculate_expected_emissions(self): pass