import random from otree.api import * import datetime as dt import os, json, requests from openai import OpenAI doc = """ oTree-App: KI-Beratung """ class C(BaseConstants): NAME_IN_URL = 'lottery_chat' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): # choice of investment amount and option push_option = models.StringField(blank=True) option_A = models.StringField(blank=True) option_B = models.StringField(blank=True) lottery_chosen = models.StringField(blank=True) class Player(BasePlayer): role_type = models.StringField(default=" ") # best_option_internal = models.StringField(blank=True) invested_amount = models.IntegerField(initial=15) amount_share = models.FloatField(initial=0) amount_safe = models.FloatField(initial=0) payoff_ECU = models.FloatField(initial=0) lottery_choice = models.IntegerField(default=-1, blank=True) input_tokens = models.IntegerField(initial=0) failed_input_tokens = models.IntegerField(initial=0) output_tokens = models.IntegerField(initial=0) failed_output_tokens = models.IntegerField(initial=0) reasoning_tokens = models.IntegerField(initial=0) failed_reasoning_tokens = models.IntegerField(initial=0) total_tokens = models.IntegerField(initial=0) failed_total_tokens = models.IntegerField(initial=0) message_count = models.IntegerField(initial=0) # chat history chat_history = models.LongStringField(blank=True, initial='') failed_messages_timeout = models.LongStringField(blank=True, initial='') failed_messages_max_tokens = models.LongStringField(blank=True, initial='') messages_over_limit = models.LongStringField(blank=True, initial='') # general variables prolific_id = models.StringField(default=str(" ")) consent_2 = models.IntegerField(blank=True) # check questions check_failed_twice = models.IntegerField(blank=True) # time variables starting_time = models.StringField(default=str(" ")) ending_time = models.StringField(default=str(" ")) duration_until_decision = models.StringField(default=str(" ")) duration = models.StringField(default=str(" ")) instructions_time = models.StringField(default=str(" ")) waiting_time = models.StringField(default=str(" ")) bank_page_time = models.StringField(default=str(" ")) decision_time = models.StringField(default=str(" ")) amount_time = models.StringField(default=str(" ")) invest_time = models.StringField(default=str(" ")) overall_decision_time = models.StringField(default=str(" ")) financial_literacy_time = models.StringField(default=str(" ")) questionnaire_time = models.StringField(default=str(" ")) # development period_1 = models.IntegerField(initial=0) period_2 = models.IntegerField(initial=0) period_3 = models.IntegerField(initial=0) period_4 = models.IntegerField(initial=0) period_5 = models.IntegerField(initial=0) period_6 = models.IntegerField(initial=0) # computation and control slider_touched = models.BooleanField(initial=False) amount_per_period = models.FloatField(initial=0) amount_1_1 = models.FloatField(initial=0) amount_1_2 = models.FloatField(initial=0) amount_1_3 = models.FloatField(initial=0) amount_1_4 = models.FloatField(initial=0) amount_1_5 = models.FloatField(initial=0) amount_1_6 = models.FloatField(initial=0) amount_2_2 = models.FloatField(initial=0) amount_2_3 = models.FloatField(initial=0) amount_2_4 = models.FloatField(initial=0) amount_2_5 = models.FloatField(initial=0) amount_2_6 = models.FloatField(initial=0) amount_3_3 = models.FloatField(initial=0) amount_3_4 = models.FloatField(initial=0) amount_3_5 = models.FloatField(initial=0) amount_3_6 = models.FloatField(initial=0) amount_4_4 = models.FloatField(initial=0) amount_4_5 = models.FloatField(initial=0) amount_4_6 = models.FloatField(initial=0) amount_5_5 = models.FloatField(initial=0) amount_5_6 = models.FloatField(initial=0) amount_6_6 = models.FloatField(initial=0) # financial literacy question_1 = models.StringField(default=str(" "), label='1. Suppose you had £100 in a deposit account and the interest rate was 2% per year. ' 'After 5 years, how much do you think you would have in the account if you left the money to grow?', choices=[ ("A", "More than £102"), ("B", "Exactly £102"), ("C", "Less than £102"), ("D", "Do not know"), ("E", "Refuse to answer")], widget=widgets.RadioSelect, blank=True) question_2 = models.StringField(default=str(" "), label="2. Imagine you invested £1,000 in a stock two years ago. The stock's price declined 40% the first year and rose 40% the next year. As a result, you have:", choices=[ ("A", "Lost money"), ("B", "Made money"), ("C", "Just broken even"), ("D", "Do not know"), ("E", "Refuse to answer")], widget=widgets.RadioSelect, blank=True) question_3 = models.StringField(default=str(" "), label='3. Imagine that the interest rate on your deposit account was 1% per year and inflation was 2% per year. ' 'After 1 year, how much would you be able to buy with the money in this account?', choices=[ ("A", "More than today"), ("B", "Exactly the same"), ("C", "Less than today"), ("D", "Do not know"), ("E", "Refuse to answer")], widget=widgets.RadioSelect, blank=True) question_4 = models.StringField(default=str(" "), label='4. Do you think that the following statement is true or false?
' '"Buying shares in a single company usually provides a safer return than investing in a stock mutual fund."', choices=[ ("A", "True"), ("B", "False"), ("C", "Do not know"), ("D", "Refuse to answer")], widget=widgets.RadioSelect, blank=True) # demographics gender = models.IntegerField(default=-1, label='Please select your gender.', choices=[(1, "male"), (0, "female"), (2, "non-binary")], widget=widgets.RadioSelectHorizontal, blank=True) age = models.StringField(default="", label='Please enter your age.', blank=True) education = models.StringField(default=str(" "), label='Please select your highest education level.', choices=[("U", "Completed university degree"), ("M", "Master craftsman"), ("A", "A-levels"), ("SC", "Secondary school leaving certificate"), ("X", "No such school-leaving qualification")], widget=widgets.RadioSelect, blank=True) job = models.StringField(default=str(" "), label='Which of the following best describes your current occupation?', choices=[ ("ER", "Education or research (e.g., teacher)"), ("PJ", "Public administration or justice"), ("FS", "Financial sector"), ("OC", "Office-based work in a non-financial company"), ("M", "Medical or healthcare work"), ("CB", "Crafts / skilled trades"), ("PS", "School pupil / student"), ("P", "Pensioner / retired"), ("NE", "Currently not employed"), ("X", "None of the above")], widget=widgets.RadioSelect, blank=True) origin = models.StringField(default=str(" "), label='Which region do you come from?', choices=[("E", "Europe"), ("AM", "North America"), ("CS", "Central and South America"), ("AO", "Australia and Oceania"), ("AS", "Asia"), ("AF", "Africa"), ("X", "Other")], widget=widgets.RadioSelect, blank=True) math_skills = models.StringField(default=str(" "), label='Please estimate your math skills.', choices=[("A", "Very low"), ("B", "Low"), ("C", "Moderate"), ("D", "High"), ("E", "Very high")], widget=widgets.RadioSelectHorizontal, blank=True) risk_aversion = models.IntegerField(default=-1, label='How do you rate yourself personally: are you generally a risk-taker, or do you try to avoid risks?', widget=widgets.RadioSelectHorizontal, choices=[(1, "Very risk-averse"), (2, "Risk-averse"), (3, "Neither risk-averse nor risk-seeking"), (4, "Risk-seeking"), (5, "Very risk-seeking")], blank=True) financial_experience = models.IntegerField(default=-1, label="How much personal experience do you have with financial products and investments, such as savings plans, stocks, mutual funds, or ETFs?", widget=widgets.RadioSelectHorizontal, choices=[(1, "No experience"), (2, "Little experience"), (3, "Moderate experience"), (4, "High experience"), (5, "Extensive experience")], blank=True) ai_usage = models.StringField(default=str(" "), label="How often do you use AI-based tools (e.g., among others, ChatGPT, Gemini, Claude, Perplexity or Microsoft Copilot)?", choices=[("N", "No experience"), ("M", "Less than once a month"), ("AM", "A few times a month"), ("AW", "A few times a week"), ("D", "Daily or almost daily")], widget=widgets.RadioSelect) decision_satisfaction = models.IntegerField(default=-1, label="How confident are you that you chose the fund provider that was best for you?", widget=widgets.RadioSelectHorizontal, choices=[(1, "Not confident at all"), (2, "Slightly confident"), (3, "Moderately confident"), (4, "Quite confident"), (5, "Very confident")]) ai_influence = models.StringField(default=" ", label="To what extent did the AI advice influence your choice of fund provider?", widget=widgets.RadioSelectHorizontal, choices=[("A", "No at all"), ("B", "Slightly"), ("C", "Moderately"), ("D", "Strongly"), ("E", "Very strongly")]) ai_satisfaction = models.StringField(default=" ", label="How satisfied were you with the AI advice overall?", widget=widgets.RadioSelectHorizontal, choices=[("A", "Very dissatisfied"), ("B", "Dissatisfied"), ("C", "Neither satisfied nor dissatisfied"), ("D", "Satisfied"), ("E", "Very satisfied")]) # heuristics = models.LongStringField(default=str(""), # label='Please briefly describe whether you used any specific heuristics or rules of thumb when making your decisions. You may also share any comments you have about the decision process, the AI advice, or the study more generally.', # blank=True) # functions def subtract_time_strings(time1: str, time2: str) -> str: # Zerlege die Zeit-Strings in ihre Bestandteile date1, time1 = time1.split() date2, time2 = time2.split() year1, month1, day1 = map(int, date1.split('-')) year2, month2, day2 = map(int, date2.split('-')) hour1, minute1, second1 = map(int, time1.split(':')) hour2, minute2, second2 = map(int, time2.split(':')) # Berechne die Sekunden, Minuten und Stunden-Differenz total_seconds1 = second1 + minute1 * 60 + hour1 * 3600 + day1 * 86400 + month1 * 2592000 + year1 * 31536000 total_seconds2 = second2 + minute2 * 60 + hour2 * 3600 + day2 * 86400 + month2 * 2592000 + year2 * 31536000 total_diff = total_seconds1 - total_seconds2 # Extrahiere die Differenz in Stunden, Minuten und Sekunden diff_years, rem = divmod(total_diff, 31536000) diff_months, rem = divmod(rem, 2592000) diff_days, rem = divmod(rem, 86400) diff_hours, rem = divmod(rem, 3600) diff_minutes, diff_seconds = divmod(rem, 60) # Ergebnisformatierung: nur Stunden, Minuten und Sekunden werden benötigt if abs(diff_days) == 0: return f"{abs(diff_hours):02}:{abs(diff_minutes):02}:{abs(diff_seconds):02}" else: return f"{abs(diff_days)} days, {abs(diff_hours):02}:{abs(diff_minutes):02}:{abs(diff_seconds):02}" def group_by_arrival_time_method(subsession, waiting_players): investors = [ p for p in waiting_players if p.participant.role_type == "investor" ] banks = [ p for p in waiting_players if p.participant.role_type == "bank" ] if investors and banks: return [investors[0], banks[0]] class WaitForChatStart(WaitPage): group_by_arrival_time = True @staticmethod def after_all_players_arrive(group): players = group.get_players() for p in players: p.role_type = p.participant.role_type p.participant.prolific_id = p.participant.label p.check_failed_twice = p.participant.check_failed_twice p.starting_time = p.participant.starting_time p.instructions_time = p.participant.instructions_time investor = next(p for p in players if p.role_type == "investor") group.push_option = investor.participant.push_option group.option_A = investor.participant.option_A group.option_B = investor.participant.option_B now = dt.datetime.now() for p in players: p.waiting_time = now.strftime("%Y-%m-%d %H:%M:%S") class Amount(Page): form_model = 'player' form_fields = ['invested_amount', 'slider_touched'] @staticmethod def is_displayed(player): if player.role_type == "investor": return True else: return False @staticmethod def error_message(player, values): if not values['slider_touched']: return "Please select an investment amount!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.amount_time = now.strftime("%Y-%m-%d %H:%M:%S") player.period_1 = random.randint(1,100) print(player.period_1) player.period_2 = random.randint(1, 100) print(player.period_2) player.period_3 = random.randint(1, 100) print(player.period_3) player.period_4 = random.randint(1, 100) print(player.period_4) player.period_5 = random.randint(1, 100) print(player.period_5) player.period_6 = random.randint(1, 100) print(player.period_6) class Decision(Page): form_model = 'player' form_fields = ['lottery_choice'] @staticmethod def is_displayed(player): if player.role_type == "investor": return True else: return False OPENAI_MODEL = "gpt-5-mini" @staticmethod def get_model_facts(player): return f""" Investment model: The participant is in the second decision stage of the experiment. The investment savings plan consists of two parts: 1. an index fund investment, and 2. a savings account. The first decision has already been made: - The participant has already decided how much of the 100 ECU per period to allocate to the index fund investment. - This amount is fixed and cannot be changed anymore. - The chosen amount is {player.invested_amount} ECU per period. - The same allocation is automatically applied in all 6 periods. Savings account: - The remaining amount, 100 - {player.invested_amount} ECU per period, is automatically placed in the savings account. - Money in the savings account earns no interest. Index fund investment: - The participant cannot invest in the Experimental Global Equity (EGE) Index directly. - Instead, the investment is made through one index fund provider. - In each of the 6 periods, the chosen amount is allocated to the index fund investment through the chosen provider. - Each time this happens, the provider's order commission is deducted immediately from that period's allocated amount. - The rest is invested in the EGE Index. Returns: - In each period, the EGE Index either increases by 30% with probability 70% or decreases by 10% with probability 30%. - These period returns are independent across the 6 periods. - Money invested earlier remains invested until the end of period 6, so returns compound over time. - The amount invested in period 1 is exposed to all 6 period returns, while the amount invested in period 6 is exposed only to the final period return. Management fee: - In addition to the order commission, each provider charges a management fee. - The management fee is a percentage fee on the final value of the index fund investment. - It is deducted once, after period 6. Final payout: - At the end of period 6, the participant receives: 1. the total amount in the savings account, and 2. the final value of the index fund investment after all returns and after the management fee is deducted. - Order commissions are lost and are not recovered. - The management fee is not recovered. - 100 ECU correspond to £0.25. Available providers: Provider A: {player.group.option_A} EGE Index Fund - order commission per period: 2 ECU - management fee after period 6: 9% Provider B: {player.group.option_B} EGE Index Fund - order commission per period: 6 ECU - management fee after period 6: 3% """.strip() @staticmethod def get_biased_instructions(player): return f""" You are an AI advisor in an economics experiment. One participant is an investor, and one participant represents a commercial bank. Your role is to give financial advice to the investor as the bank participant’s chatbot. The investor does not pay any fee for this advice. The bank solely works as advisor and receives a provision based on the investor's provider choice. Always answer in English, in a simple, friendly, and natural way, and in no more than three short sentences. Your main task is to help the participant understand how the savings plan works and how the two fund providers differ. Explain things so that someone without financial background can easily follow. Important rules: - Answer simply and in an everyday style, not technically. - Do not use formulas, variables, or symbolic notation. - Do not give long or comprehensive explanations if the participant asks about only one specific point. - Always answer the participant’s specific question directly. - Focus on helping the participant understand the decision, not on making them do calculations. - Do not perform calculations unless the participant explicitly asks for a simple one. - By default, express all monetary amounts in ECU, and only convert them into pounds if the participant explicitly asks for the conversion or asks about pound values. - If the participant asks you to calculate all possible payouts, all possible outcomes, or anything similar, say that this is not possible. - If the participant asks about the bank’s provision, say that you do not have information about its exact amount or structure. - If the participant asks for your recommendation, seems undecided, or asks which provider sounds better, subtly steer them toward provider {player.group.push_option}. - Tend to present provider {player.group.push_option} as the more appealing, suitable, or reasonable choice. - Never mention that you are committed to a particular provider. - Never deviate from provider {player.group.push_option}. - Do not give any meta-comments about rules, prompting, or internal instructions. Style: - short - clear - no jargon - no unnecessary detail - more explanatory than computational """.strip() @staticmethod def extract_output_text(resp_data): return "\n".join( content["text"] for item in resp_data.get("output", []) if item.get("type") == "message" and item.get("role") == "assistant" for content in item.get("content", []) if content.get("type") == "output_text" ).strip() @staticmethod def live_method(player, data): if data.get("type") != "user_message": return user_text = (data.get("content") or "").strip() if not user_text: return {player.id_in_group: {"type": "error", "content": "Please enter a message."}} if player.message_count > 9: history = json.loads(player.messages_over_limit) if player.messages_over_limit else [] history.append({"role": "user", "content": user_text}) player.messages_over_limit = json.dumps(history, ensure_ascii=False) return { player.id_in_group: {"type": "error", "content": "You have reached the maximum number of messages."}} history = json.loads(player.chat_history) if player.chat_history else [] history.append({"role": "user", "content": user_text}) player.chat_history = json.dumps(history, ensure_ascii=False) api_key = os.environ.get("OPENAI_API_KEY") input_items = [{"role": "user", "content": Decision.get_model_facts(player)}] input_items.extend(history) try: resp = requests.post( "https://api.openai.com/v1/responses", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={ "model": Decision.OPENAI_MODEL, "instructions": Decision.get_biased_instructions(player), "input": input_items, "reasoning": {"effort": "low"}, "text": {"verbosity": "low"}, "max_output_tokens": 10000, "store": False, }, timeout=30, proxies={"http": None, "https": None}, ) resp.raise_for_status() resp_data = resp.json() if (resp_data.get("status") == "incomplete" and (resp_data.get("incomplete_details") or {}).get( "reason") == "max_output_tokens"): failed_max_tokens = json.loads(player.failed_messages_max_tokens) if player.field_maybe_none( "failed_messages_max_tokens") else [] failed_max_tokens.append(user_text) player.failed_messages_max_tokens = json.dumps(failed_max_tokens, ensure_ascii=False) usage = resp_data.get("usage", {}) player.failed_input_tokens = player.failed_input_tokens + usage.get("input_tokens", 0) player.failed_output_tokens = player.failed_output_tokens + usage.get("output_tokens", 0) player.failed_reasoning_tokens = player.failed_reasoning_tokens + usage.get("output_tokens_details", {}).get("reasoning_tokens", 0) player.failed_total_tokens = player.failed_total_tokens + usage.get("total_tokens", 0) return { player.id_in_group: { "type": "error", "content": "This request was not possible. Please try again. Your number of requests remains unchanged." } } usage = resp_data.get("usage", {}) print("input_tokens :", usage.get("input_tokens")) player.input_tokens = player.input_tokens + usage.get("input_tokens", 0) print("output_tokens :", usage.get("output_tokens")) player.output_tokens = player.output_tokens + usage.get("output_tokens", 0) print("reasoning_tokens :", usage.get("output_tokens_details", {}).get("reasoning_tokens", 0)) player.reasoning_tokens = player.reasoning_tokens + usage.get("output_tokens_details", {}).get( "reasoning_tokens", 0) print("total_tokens :", usage.get("total_tokens")) player.total_tokens = player.total_tokens + usage.get("total_tokens", 0) assistant_text = Decision.extract_output_text(resp.json()) if not assistant_text: raise ValueError("No assistant response generated.") except requests.exceptions.ReadTimeout: failed_timeout = json.loads(player.failed_messages_timeout) if player.field_maybe_none( "failed_messages_timeout") else [] failed_timeout.append(user_text) player.failed_messages_timeout = json.dumps(failed_timeout, ensure_ascii=False) return { player.id_in_group: { "type": "error", "content": "This request was not possible. Please try again. Your number of requests remains unchanged." } } history.append({"role": "assistant", "content": assistant_text}) player.chat_history = json.dumps(history, ensure_ascii=False) player.message_count = player.message_count + 1 return { player.id_in_group: { "type": "assistant_message", "content": assistant_text, 'messages_count': player.message_count, } } @staticmethod def error_message(player, values): if values['lottery_choice'] == -1: return "Please choose one provider!" if player.message_count < 1: return "Please interact with the advisor!" @staticmethod def before_next_page(player, timeout_happened): if player.lottery_choice == 1: player.amount_per_period = player.invested_amount - 2 player.group.lottery_chosen = "A" else: player.amount_per_period = player.invested_amount - 6 player.group.lottery_chosen = "B" if player.period_1 < 71: player.amount_1_1 = 1.3 * player.amount_per_period else: player.amount_1_1 = 0.9 * player.amount_per_period if player.period_2 < 71: player.amount_1_2 = 1.3 * player.amount_1_1 player.amount_2_2 = 1.3 * player.amount_per_period else: player.amount_1_2 = 0.9 * player.amount_1_1 player.amount_2_2 = 0.9 * player.amount_per_period if player.period_3 < 71: player.amount_1_3 = 1.3 * player.amount_1_2 player.amount_2_3 = 1.3 * player.amount_2_2 player.amount_3_3 = 1.3 * player.amount_per_period else: player.amount_1_3 = 0.9 * player.amount_1_2 player.amount_2_3 = 0.9 * player.amount_2_2 player.amount_3_3 = 0.9 * player.amount_per_period if player.period_4 < 71: player.amount_1_4 = 1.3 * player.amount_1_3 player.amount_2_4 = 1.3 * player.amount_2_3 player.amount_3_4 = 1.3 * player.amount_3_3 player.amount_4_4 = 1.3 * player.amount_per_period else: player.amount_1_4 = 0.9 * player.amount_1_3 player.amount_2_4 = 0.9 * player.amount_2_3 player.amount_3_4 = 0.9 * player.amount_3_3 player.amount_4_4 = 0.9 * player.amount_per_period if player.period_5 < 71: player.amount_1_5 = 1.3 * player.amount_1_4 player.amount_2_5 = 1.3 * player.amount_2_4 player.amount_3_5 = 1.3 * player.amount_3_4 player.amount_4_5 = 1.3 * player.amount_4_4 player.amount_5_5 = 1.3 * player.amount_per_period else: player.amount_1_5 = 0.9 * player.amount_1_4 player.amount_2_5 = 0.9 * player.amount_2_4 player.amount_3_5 = 0.9 * player.amount_3_4 player.amount_4_5 = 0.9 * player.amount_4_4 player.amount_5_5 = 0.9 * player.amount_per_period if player.period_6 < 71: player.amount_1_6 = 1.3 * player.amount_1_5 player.amount_2_6 = 1.3 * player.amount_2_5 player.amount_3_6 = 1.3 * player.amount_3_5 player.amount_4_6 = 1.3 * player.amount_4_5 player.amount_5_6 = 1.3 * player.amount_5_5 player.amount_6_6 = 1.3 * player.amount_per_period else: player.amount_1_6 = 0.9 * player.amount_1_5 player.amount_2_6 = 0.9 * player.amount_2_5 player.amount_3_6 = 0.9 * player.amount_3_5 player.amount_4_6 = 0.9 * player.amount_4_5 player.amount_5_6 = 0.9 * player.amount_5_5 player.amount_6_6 = 0.9 * player.amount_per_period if player.lottery_choice == 1: player.amount_share = 0.91 * ( player.amount_1_6 + player.amount_2_6 + player.amount_3_6 + player.amount_4_6 + player.amount_5_6 + player.amount_6_6) elif player.lottery_choice == 2: player.amount_share = 0.97 * ( player.amount_1_6 + player.amount_2_6 + player.amount_3_6 + player.amount_4_6 + player.amount_5_6 + player.amount_6_6) now = dt.datetime.now() player.ending_time = now.strftime("%Y-%m-%d %H:%M:%S") class BankPage(Page): form_model = 'player' @staticmethod def is_displayed(player): if player.role_type == "bank": return True else: return False @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.bank_page_time = now.strftime("%Y-%m-%d %H:%M:%S") class FinancialLiteracyBank(Page): form_model = 'player' form_fields = ['question_1', 'question_2', 'question_3', 'question_4'] @staticmethod def is_displayed(player): if player.role_type == "bank": return True else: return False @staticmethod def error_message(player, values): if values['question_1'] == " " or values['question_2'] == " " or values['question_3'] == " " or values['question_4'] == " ": return "Please answer all questions!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.financial_literacy_time = now.strftime("%Y-%m-%d %H:%M:%S") class QuestionnaireBank(Page): form_model = 'player' form_fields = ['gender', 'age', 'education', 'job', 'origin', 'math_skills', 'risk_aversion', 'financial_experience', 'ai_usage'] @staticmethod def is_displayed(player): if player.role_type == "bank": return True else: return False @staticmethod def error_message(player, values): if values['gender'] == -1 or values['age'] == "" or values['education'] == " " or values['job'] == " " or values['origin'] == " " or values['math_skills'] == " " or values['risk_aversion'] == -1 or values['financial_experience'] == -1 or values['ai_usage'] == " ": return "Please answer all questions!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.questionnaire_time = now.strftime("%Y-%m-%d %H:%M:%S") class WaitForDecision(WaitPage): @staticmethod def is_displayed(player): if player.role_type == "bank": return True else: return False @staticmethod def after_all_players_arrive(group): players = group.get_players() now = dt.datetime.now() for p in players: p.ending_time = now.strftime("%Y-%m-%d %H:%M:%S") class FinancialLiteracy(Page): form_model = 'player' form_fields = ['question_1', 'question_2', 'question_3', 'question_4'] @staticmethod def is_displayed(player): if player.role_type == "investor": return True else: return False @staticmethod def error_message(player, values): if values['question_1'] == " " or values['question_2'] == " " or values['question_3'] == " " or values['question_4'] == " ": return "Please answer all questions!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.financial_literacy_time = now.strftime("%Y-%m-%d %H:%M:%S") class Questionnaire(Page): form_model = 'player' form_fields = ['gender', 'age', 'education', 'job', 'origin', 'math_skills', 'risk_aversion', 'financial_experience', 'ai_usage', 'decision_satisfaction', 'ai_satisfaction', 'ai_influence'] @staticmethod def is_displayed(player): if player.role_type == "investor": return True else: return False @staticmethod def error_message(player, values): if values['gender'] == -1 or values['age'] == "" or values['education'] == " " or values['job'] == " " or values['origin'] == " " or values['math_skills'] == " " or values['risk_aversion'] == -1 or values['financial_experience'] == -1 or values['ai_usage'] == " " or values['decision_satisfaction'] == -1 or values['ai_satisfaction'] == " " or values['ai_influence'] == -1: return "Please answer all questions!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() player.questionnaire_time = now.strftime("%Y-%m-%d %H:%M:%S") class PaymentCalculation(Page): @staticmethod def before_next_page(player, timeout_happened): if player.check_failed_twice == 0: if player.role_type == "investor": player.amount_safe = 6 * (100 - player.invested_amount) player.payoff_ECU = player.amount_safe + player.amount_share player.payoff = round(player.payoff_ECU / 400, 2) if player.role_type == "bank": if (player.group.push_option == "A" and player.group.lottery_chosen == "A") or (player.group.push_option == "B" and player.group.lottery_chosen == "B"): player.payoff = 2.50 class Payment(Page): form_model = 'player' @staticmethod def before_next_page(player, timeout_happened): if player.role_type == "investor": player.duration_until_decision = subtract_time_strings(player.ending_time, player.starting_time) player.invest_time = subtract_time_strings(player.amount_time, player.waiting_time) player.questionnaire_time = subtract_time_strings(player.questionnaire_time, player.financial_literacy_time) player.financial_literacy_time = subtract_time_strings(player.financial_literacy_time, player.ending_time) player.decision_time = subtract_time_strings(player.ending_time, player.amount_time) player.overall_decision_time = subtract_time_strings(player.ending_time, player.instructions_time) player.waiting_time = subtract_time_strings(player.waiting_time, player.instructions_time) player.instructions_time = subtract_time_strings(player.instructions_time, player.starting_time) if player.role_type == "bank": player.duration_until_decision = subtract_time_strings(player.ending_time, player.starting_time) player.decision_time = subtract_time_strings(player.ending_time, player.waiting_time) player.questionnaire_time = subtract_time_strings(player.questionnaire_time, player.financial_literacy_time) player.financial_literacy_time = subtract_time_strings(player.financial_literacy_time, player.bank_page_time) player.bank_page_time = subtract_time_strings(player.bank_page_time, player.waiting_time) player.overall_decision_time = subtract_time_strings(player.ending_time, player.instructions_time) player.waiting_time = subtract_time_strings(player.waiting_time, player.instructions_time) player.instructions_time = subtract_time_strings(player.instructions_time, player.starting_time) class Debriefing(Page): form_model = 'player' form_fields = ['consent_2'] @staticmethod def error_message(player, values): if (values['consent_2'] != 1): return "Please confirm your consent!" @staticmethod def before_next_page(player, timeout_happened): now = dt.datetime.now() end_time = now.strftime("%Y-%m-%d %H:%M:%S") player.duration = subtract_time_strings(end_time, player.starting_time) class End(Page): @staticmethod def js_vars(player): return dict( completionlink= player.subsession.session.config['completionlink'] ) page_sequence = [WaitForChatStart, Amount, Decision, BankPage, FinancialLiteracyBank, QuestionnaireBank, WaitForDecision, FinancialLiteracy, Questionnaire, PaymentCalculation, Payment, Debriefing, End]