from typing import List from otree.api import * import time import random doc = "Double auction market" class Constants(BaseConstants): """ These constants do not vary from player to player. """ name_in_url = 'double_auction_same_val' # to conceal app's real name # players_per_group = 20 players_per_group = None # everybody is in the same group; they just differ by their roles as either buyers or sellers # num_rounds = 5 -- if this is supposed to be the number of rounds in a game, then it should be updated to 8 num_rounds = 2 items_per_seller = 1 valuation_min = cu(5) valuation_max = cu(20) production_costs_min = cu(3) production_costs_max = cu(15) trading_minutes = 3 # Positive is buyer valuations, negative is seller costs # money_scheme = [ # [5, 5, 10, 10, 15, 15, 15, 15, 20, 20, -3, -3, -3, -3, -7, -7, -9, -9, -16, -16], # baseline # [5, 10, 10, 15, 15, 15, 20, -3, -3, -3, -3, -3, -7, -7, -7, -9, -9, -9, -16, -16], # buyer advantage # [5, 5, 10, 10, 10, 15, 15, 15, 15, 15, 20, 20, 20, -3, -3, -7, -7, -9, -9, -16], # seller advantage # [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9] # same valuations # ] # MONEY_SCHEME: it's going to be a list of length 1 now (only 1 money scheme for each kinda game/treatment) class Subsession(BaseSubsession): pass # def creating_session(subsession: Subsession): # # Shuffle players within each market # matrix = subsession.get_group_matrix() # for i in range(len(matrix)): # random.shuffle(matrix[i]) # subsession.set_group_matrix(matrix) # players = subsession.get_players() # session = subsession.session # for group_id in range(len(matrix)): # for player_id in matrix[group_id]: # p = players[player_id-1] # market_id = group_id if session.config['market_scenario'] == 1 \ # else 2 + group_id # tmp = Constants.money_scheme[market_id][p.id_in_group - 1] # p.break_even_point = abs(tmp) # p.is_buyer = (tmp > 0) # if p.is_buyer: # p.num_items = 0 # p.current_offer = 0 # else: # p.num_items = Constants.items_per_seller # p.current_offer = Constants.valuation_max + 1 def creating_session(subsession: Subsession): players = subsession.get_players() for p in players: if subsession.round_number == 1: p.is_buyer = p.id_in_group % 2 > 0 else: p.is_buyer = p.id_in_group % 2 == 0 if p.is_buyer: p.num_items = 0 p.break_even_point = 20 p.current_offer = 0 else: p.num_items = Constants.items_per_seller p.break_even_point = 5 p.current_offer = Constants.valuation_max + 1 # DON'T GET THIS class Group(BaseGroup): start_timestamp = models.IntegerField() class Player(BasePlayer): is_buyer = models.BooleanField() current_offer = models.CurrencyField() break_even_point = models.CurrencyField() num_items = models.IntegerField() deal_time = models.IntegerField( doc="Seconds the deal happens since the trading starts" ) # TODO: generalize for many transactions (save trans to CSV) class Transaction(ExtraModel): group = models.Link(Group) buyer = models.Link(Player) seller = models.Link(Player) price = models.CurrencyField() seconds = models.IntegerField(doc="Timestamp (seconds since beginning of trading)") def find_match(buyers, sellers): for buyer in buyers: for seller in sellers: if seller.num_items > 0 and seller.current_offer <= buyer.current_offer and buyer.num_items == 0: # Return as soon as a match is found and each buyer can only have a max of 1 item return [buyer, seller] def live_method(player: Player, data): group = player.group players = group.get_players() buyers = [p for p in players if p.is_buyer] sellers = [p for p in players if not p.is_buyer] news = None if data: try: offer = int(data['offer']) except Exception: print('Invalid message received', data) return player.current_offer = offer if player.is_buyer: match = find_match(buyers=[player], sellers=sellers) else: match = find_match(buyers=buyers, sellers=[player]) if match: [buyer, seller] = match price = buyer.current_offer Transaction.create( group=group, buyer=buyer, seller=seller, price=price, seconds=int(time.time() - group.start_timestamp), ) buyer.num_items += 1 seller.num_items -= 1 buyer.payoff += buyer.break_even_point - price seller.payoff += price - seller.break_even_point buyer.current_offer = 0 seller.current_offer = Constants.valuation_max + 1 seller.deal_time = int(time.time() - group.start_timestamp) # WARNING: duplicated news = dict(buyer=buyer.id_in_group, seller=seller.id_in_group, price=price) bids = sorted([p.current_offer for p in buyers if p.current_offer > 0], reverse=True) asks = sorted([p.current_offer for p in sellers if p.current_offer <= Constants.valuation_max]) highcharts_series = [[tx.seconds, tx.price] for tx in Transaction.filter(group=group)] return { p.id_in_group: dict( bids=bids, asks=asks, highcharts_series=highcharts_series, num_items=p.num_items, current_offer=p.current_offer, news=news, payoff=p.payoff, ) for p in players } # PAGES class Introduction(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): group.start_timestamp = int(time.time()) class Trading(Page): live_method = live_method @staticmethod def js_vars(player: Player): return dict(id_in_group=player.id_in_group, is_buyer=player.is_buyer) @staticmethod def get_timeout_seconds(player: Player): import time group = player.group return (group.start_timestamp + Constants.trading_minutes * 60) - time.time() # Constants.trading_minutes was set to 5 for this experiment class ResultsWaitPage(WaitPage): pass class Results(Page): pass # page_sequence = [Introduction, WaitToStart, Trading, ResultsWaitPage, Results] page_sequence = [WaitToStart, Trading, ResultsWaitPage, Results]