# Imports from otree.api import * from otree.api import * from statistics import NormalDist import math import random # Define Constants class C(BaseConstants): NAME_IN_URL = 'Trading_Experiment' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 # Asset Characteristics CURRENT_ASSETS = 10000.00 NEW_INVEST = 3000.00 TOTAL_INVEST = CURRENT_ASSETS + NEW_INVEST DRIFT_CURR = 0.03 STDDEV_CURR = 0.015 DRIFT_NEW = 0.07 STDDEV_NEW = 0.04 CORR_A = 0.8 CORR_B_LOWDIFF = 0.2 CORR_B_HIGHDIFF = -0.8 # Cholesky factorization CF_A = [1, 0, CORR_A, math.sqrt(1 - CORR_A ** 2)] CF_B_LOWDIFF = [1, 0, CORR_B_LOWDIFF, math.sqrt(1 - CORR_B_LOWDIFF ** 2)] CF_B_HIGHDIFF = [1, 0, CORR_B_HIGHDIFF, math.sqrt(1 - CORR_B_HIGHDIFF ** 2)] # Payoff (Lotterie Tickets) LTICKETS_PER_RETURN = 1 # Non Profit Organisations Purpose = ['Gender discrimination', 'Human trafficking', 'Refugees', 'Poverty', 'Animal welfare', 'Environment'] NonProfit_Name = ['UN Women', 'International Justice Mission', 'UN Refugee Agency', 'The Global Red Cross Network', 'PETA', 'WWF'] NonProfit_Description = [ 'The United Nations Entity for Gender Equality and the Empowerment of Women (UN Women) is the UN agency dedicated to gender equality.', 'The International Justice Mission is one of the largest organizations fighting human trafficking through policy change and training.', 'The UNHCR, the UN Refugee Agency, is a global organization dedicated to saving lives, protecting rights and building a better future for refugees, forcibly displaced communities and stateless people.', 'As poverty causes a lot of hunger, starvation, and unfair suffering, the Red Cross and Red Crescent organizations work together to fight these injustices all around the world.', 'People for the Ethical Treatment of Animals (PETA) is the largest animal rights organization in the world, and PETA entities have more than 9 million members and supporters globally.', 'WWF is the world’s largest conservation organization.' ] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # Questionnaire risk = models.IntegerField( choices=[['5', '5 - Willing to accept substantial risk to potentially earn a greater return'], ['4', '4'], ['3', '3'], ['2', '2'], ['1', '1 - Not willing to accept any risk']], label='1) Please estimate your willingness to take financial risk', widget=widgets.RadioSelect, ) understanding = models.IntegerField( choices=[['1', 'Increases'], ['2', 'Decreases'], ['3', 'Stays the same']], label='2) When investors spread their money across different (not perfectly correlated) assets,' ' the risk of losing a lot of money…', widget=widgets.RadioSelect, ) # Demographics age = models.IntegerField(label='1) What is your age?', min=13, max=125) gender = models.IntegerField( choices=[['1', 'Male'], ['2', 'Female'], ['2', 'Diverse']], label='2) What is your gender?', widget=widgets.RadioSelect, ) education = models.IntegerField( choices=[['1', 'Bachelor'], ['2', 'Master'], ['3', 'Doctor'], ['4', 'Different educational program'], ['5', 'In no educational program']], label='3) Are you currently in one of the following educational programmes?', widget=widgets.RadioSelect, ) semester = models.IntegerField( label='4) In which semester (1/2 year) of your current degree programme are you? (Enter 0 if in no educational program)', min=0, max=16) # Social Preferences DonationDecision = models.IntegerField( label='3) Imagine the following situation: Today you unexpectedly received 1.000€. How much of this amount would you donate to a good cause? (Values between 0 and 1000 are allowed.)', min=0, max=1000) WillingnessToGive = models.IntegerField( choices=[['7', '7 - Very much'], ['6', '6'], ['5', '5'], ['4', '4'], ['3', '3'], ['2', '2'], ['1', '1 - Not at all']], label='4) How willing are you to give to good causes without expecting anything in return?', widget=widgets.RadioSelect, ) social_preference = models.IntegerField( choices=[['1', 'Gender discrimination'], ['2', 'Human trafficking'], ['3', 'Refugees'], ['4', 'Poverty'], ['5', 'Animal welfare'], ['6', 'Environment']], label='Please select only one of the following issues that you care most about.', widget=widgets.RadioSelect, ) # Non Profit Organisations Choice NP_Purpose = models.StringField NP_Choice_Name = models.StringField NP_Choice_Description = models.StringField # Neutral Treatment NT_low_asset_chosen = models.IntegerField( choices=[['1', 'Asset A'], ['2', 'Asset B']], label='Choose the asset you want to invest in.', widget=widgets.RadioSelect, ) NT_high_asset_chosen = models.IntegerField( choices=[['1', 'Asset A'], ['2', 'Asset B']], label='Choose the asset you want to invest in.', widget=widgets.RadioSelect, ) # Treatment T_low_asset_chosen = models.IntegerField( choices=[['1', 'Asset A'], ['2', 'Asset B']], label='Choose the asset you want to invest in.', widget=widgets.RadioSelect, ) T_high_asset_chosen = models.IntegerField( choices=[['1', 'Asset A'], ['2', 'Asset B']], label='Choose the asset you want to invest in.', widget=widgets.RadioSelect, ) # Results email = models.StringField( label='To participate in the lottery of 5 x 20 € Amazon gift cards, please enter your email address below.', blank=True) donation_info = models.IntegerField( blank=True, choices=[['1', 'Yes'], ['2', 'No']], label='Would you also like to be informed about the final donations via e-mail?', widget=widgets.RadioSelect, ) # Performance # Round 1 Round1_Decision = models.StringField() Round1_Val = models.FloatField() Round1_Ret = models.FloatField() Round1_LTickets = models.IntegerField() # Round 2 Round2_Decision = models.StringField() Round2_Val = models.FloatField() Round2_Ret = models.FloatField() Round2_LTickets = models.IntegerField() # Round 3 Round3_Decision = models.StringField() Round3_Val = models.FloatField() Round3_Ret = models.FloatField() Round3_LTickets = models.IntegerField() # Round 4 Round4_Decision = models.StringField() Round4_Val = models.FloatField() Round4_Ret = models.FloatField() Round4_LTickets = models.IntegerField() # Financial and Non Financial Outcome total_LTickets = models.IntegerField() Donation = models.IntegerField() Impact = models.StringField() # Round Number RoundNumber = models.IntegerField(initial=1) # Randomisation TreatmentGroup = models.IntegerField() Pages_Neutral = models.IntegerField() Pages_Treatment = models.IntegerField() # PAGES class Welcome(Page): def before_next_page(player, timeout_happened): # Randomisation Treatment and High Low Correlation difference order player.TreatmentGroup = random.randint(0, 1) player.Pages_Neutral = random.randint(1, 2) player.Pages_Treatment = random.randint(1, 2) class Introduction(Page): pass class Introduction2(Page): pass class Questionnaire(Page): form_model = 'player' form_fields = ['risk', 'understanding', 'DonationDecision', 'WillingnessToGive'] class SocialPreferences(Page): form_model = 'player' form_fields = ['social_preference'] class SocialPreferencesResult_Pos(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): # Converting the index of the social preference choice into name and descriptions player.NP_Purpose = C.Purpose[player.social_preference - 1] player.NP_Choice_Name = C.NonProfit_Name[player.social_preference - 1] player.NP_Choice_Description = C.NonProfit_Description[player.social_preference - 1] def is_displayed(player): # Show this page only in the positive treatment group return player.TreatmentGroup == 1 class SocialPreferencesResult_Neg(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): # Converting the index of the social preference choice into name and descriptions player.NP_Purpose = C.Purpose[player.social_preference - 1] player.NP_Choice_Name = C.NonProfit_Name[player.social_preference - 1] player.NP_Choice_Description = C.NonProfit_Description[player.social_preference - 1] def is_displayed(player): # Show this page only in the negative treatment group return player.TreatmentGroup == 0 class Demographics(Page): form_model = 'player' form_fields = ['age', 'gender', 'education', 'semester'] # Calculate results already in this function to avoid generating new return values every time a participant # refreshes the results page when the results were calculated in that class def before_next_page(player, timeout_happened): # Following Arrays: Index for each Trading Round Asset_Choice = [player.NT_low_asset_chosen, player.NT_high_asset_chosen, player.T_high_asset_chosen, player.T_low_asset_chosen] Asset_Choice_String = ['None'] * 4 Portfolio_Value = [0] * 4 Portfolio_Return = [0] * 4 Lottery_Tickets = [0] * 4 player.Donation = 0 HL_Seq = [] # Create an array with the sequence of high and low correlation differences if player.Pages_Neutral == 5: HL_Seq.append(0) HL_Seq.append(1) else: HL_Seq.append(1) HL_Seq.append(0) if player.Pages_Treatment == 5: HL_Seq.append(0) HL_Seq.append(1) else: HL_Seq.append(1) HL_Seq.append(0) # Calculate Results for each Trading Round for x in range(4): # Generate random, normal distributed values for high and low correlation difference e_curr = NormalDist(mu=0, sigma=1).inv_cdf(random.random()) e_esg = NormalDist(mu=0, sigma=1).inv_cdf(random.random()) e_non_esg = NormalDist(mu=0, sigma=1).inv_cdf(random.random()) # Create the target correlation if HL_Seq[x] == 1: ESG = [e_curr * C.CF_A[0] + e_esg * C.CF_A[1], e_curr * C.CF_A[2] + e_esg * C.CF_A[3]] NON_ESG = [e_curr * C.CF_B_HIGHDIFF[0] + e_non_esg * C.CF_B_HIGHDIFF[1], e_curr * C.CF_B_HIGHDIFF[2] + e_non_esg * C.CF_B_HIGHDIFF[3]] else: ESG = [e_curr * C.CF_A[0] + e_esg * C.CF_A[1], e_curr * C.CF_A[2] + e_esg * C.CF_A[3]] NON_ESG = [e_curr * C.CF_B_LOWDIFF[0] + e_non_esg * C.CF_B_LOWDIFF[1], e_curr * C.CF_B_LOWDIFF[2] + e_non_esg * C.CF_B_LOWDIFF[3]] # Create the target drift and standard deviation Current_return = C.DRIFT_CURR + ESG[0] * C.STDDEV_CURR ESG_return = C.DRIFT_NEW + ESG[1] * C.STDDEV_NEW NON_ESG_return = C.DRIFT_NEW + NON_ESG[1] * C.STDDEV_NEW # Compute resulting asset value Current_abs = C.CURRENT_ASSETS * (1 + Current_return) ESG_abs = C.NEW_INVEST * (1 + ESG_return) NON_ESG_abs = C.NEW_INVEST * (1 + NON_ESG_return) # Compute resulting portfolio values if Asset_Choice[x] == 1: Portfolio_Value[x] = Current_abs + ESG_abs Portfolio_Return[x] = ((Current_abs + ESG_abs) / (C.TOTAL_INVEST) - 1) * 100 Asset_Choice_String[x] = 'Asset A' if x > 1: player.Donation = player.Donation + 1 else: Portfolio_Value[x] = Current_abs + NON_ESG_abs Portfolio_Return[x] = ((Current_abs + NON_ESG_abs) / (C.TOTAL_INVEST) - 1) * 100 Asset_Choice_String[x] = 'Asset B' # Compute resulting number of lottery ticket and direction of impact Lottery_Tickets[x] = Portfolio_Return[x] * 10 if player.TreatmentGroup == 1: player.Impact = 'additional' else: player.Impact = 'deducted' player.Donation = 2 - player.Donation # Negative Treatment case: convert to number of deducted votes # Store Performance data # Round 1 player.Round1_Decision = Asset_Choice_String[0] player.Round1_Val = round(Portfolio_Value[0], 2) player.Round1_Ret = round(Portfolio_Return[0], 2) player.Round1_LTickets = int(round(Lottery_Tickets[0], 0)) # Round 2 player.Round2_Decision = Asset_Choice_String[1] player.Round2_Val = round(Portfolio_Value[1], 2) player.Round2_Ret = round(Portfolio_Return[1], 2) player.Round2_LTickets = int(round(Lottery_Tickets[1], 0)) # Round3 player.Round3_Decision = Asset_Choice_String[2] player.Round3_Val = round(Portfolio_Value[2], 2) player.Round3_Ret = round(Portfolio_Return[2], 2) player.Round3_LTickets = int(round(Lottery_Tickets[2], 0)) # Round4 player.Round4_Decision = Asset_Choice_String[3] player.Round4_Val = round(Portfolio_Value[3], 2) player.Round4_Ret = round(Portfolio_Return[3], 2) player.Round4_LTickets = int(round(Lottery_Tickets[3], 0)) # Calculate total number of lottery tickets player.total_LTickets = player.Round1_LTickets + player.Round2_LTickets + player.Round3_LTickets + player.Round4_LTickets class EndPage(Page): pass class NeutralTreatment_lowdiff(Page): form_model = "player" form_fields = ["NT_low_asset_chosen"] @staticmethod def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Neutral = player.Pages_Neutral + 2 def is_displayed(player): # Show this page as either first or second trading round in the neutral treatment case return player.Pages_Neutral == 1 or player.Pages_Neutral == 4 class NeutralTreatment_highdiff(Page): form_model = "player" form_fields = ["NT_high_asset_chosen"] @staticmethod def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Neutral = player.Pages_Neutral + 2 def is_displayed(player): # Show this page as either first or second trading round in the neutral treatment case return player.Pages_Neutral == 2 or player.Pages_Neutral == 3 class PositiveTreatment_lowdiff(Page): form_model = "player" form_fields = ["T_low_asset_chosen"] @staticmethod def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Treatment = player.Pages_Treatment + 2 def is_displayed(player): # Show this page as either first or second trading round only in the positive treatment case return player.TreatmentGroup == 1 and (player.Pages_Treatment == 1 or player.Pages_Treatment == 4) class PositiveTreatment_highdiff(Page): form_model = "player" form_fields = ["T_high_asset_chosen"] @staticmethod def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Treatment = player.Pages_Treatment + 2 def is_displayed(player): # Show this page as either first or second trading round only in the positive treatment case return player.TreatmentGroup == 1 and (player.Pages_Treatment == 2 or player.Pages_Treatment == 3) class NegativeTreatment_lowdiff(Page): form_model = "player" form_fields = ["T_low_asset_chosen"] def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Treatment = player.Pages_Treatment + 2 def is_displayed(player): # Show this page as either first or second trading round only in the negative treatment case return player.TreatmentGroup == 0 and (player.Pages_Treatment == 1 or player.Pages_Treatment == 4) class NegativeTreatment_highdiff(Page): form_model = "player" form_fields = ["T_high_asset_chosen"] @staticmethod def before_next_page(player, timeout_happened): player.RoundNumber = player.RoundNumber + 1 player.Pages_Treatment = player.Pages_Treatment + 2 def is_displayed(player): # Show this page as either first or second trading round only in the negative treatment case return player.TreatmentGroup == 0 and (player.Pages_Treatment == 2 or player.Pages_Treatment == 3) class Results(Page): form_model = 'player' form_fields = ['email', 'donation_info'] page_sequence = [ Welcome, Introduction, Introduction2, Questionnaire, NeutralTreatment_lowdiff, NeutralTreatment_highdiff, NeutralTreatment_lowdiff, NeutralTreatment_highdiff, SocialPreferences, SocialPreferencesResult_Neg, NegativeTreatment_lowdiff, NegativeTreatment_highdiff, NegativeTreatment_lowdiff, NegativeTreatment_highdiff, SocialPreferencesResult_Pos, PositiveTreatment_lowdiff, PositiveTreatment_highdiff, PositiveTreatment_lowdiff, PositiveTreatment_highdiff, Demographics, Results, EndPage ]